Approvals
Human-in-the-loop for sensitive agent actions. When an agent attempts a configured action, it pauses, snapshots its state, fires a webhook, and waits for an approve or deny decision.
How it works
- 1.Agent executes a task and reaches a configured action (e.g.
send_email). - 2.Theazo intercepts the action, pauses the agent, and creates a snapshot of its full state.
- 3.An
approval.requestedwebhook fires to your server with the action name and parameters. - 4.Your team reviews and calls
theazo.approvals.approve(id)ortheazo.approvals.deny(id). - 5.On approval, the agent resumes from the snapshot. On denial, it receives an error and can handle it gracefully.
Configuring approvals on an agent
Pass an approvals config when creating the agent. List the action names that require gating.
import { Theazo } from 'theazo'
const theazo = new Theazo({ apiKey: 'th_live_...' })
const session = await theazo.sessions.forUser('user_123')
const agent = await session.agents.create({
tools: ['exec_code', 'send_email', 'update_crm'], // send_email/update_crm are custom tools you register
approvals: {
require: ['send_email', 'update_crm'],
notifyVia: 'webhook', // required — how reviewers are notified
timeout: '24h', // how long to wait before defaultAction fires
defaultAction: 'deny', // 'deny' | 'approve' — what happens on timeout
},
})
// Agent runs normally; approval-gated actions pause automatically
const result = await agent.run('Draft and send a follow-up email to the Stripe lead')
void resultApproval config
The approvals config fields. This table is generated from the @theazo/contracts schema — it can't drift from the SDK.
| Field | Type | Required | Description |
|---|---|---|---|
require | string[] | yes | Action names that require human approval before running. |
notifyVia | 'webhook' | yes | How reviewers are notified. Only webhook is implemented today. |
timeout | string | yes | How long to wait for a decision before defaultAction, e.g. '24h'. |
defaultAction | 'approve' | 'deny' | yes | Applied if the timeout elapses with no decision. |
Approval webhooks
Configure a webhook endpoint in your dashboard or via API. Theazo will POST to it whenever an approval event occurs.
approval.requestedAn agent hit a gated action and is waiting for a decision. Payload includes approvalId, agentId, action name, and the full parameters the agent intends to use.
approval.approvedA human approved the action. The agent has resumed execution.
approval.deniedA human denied the action (or it timed out with defaultAction: deny). The agent received the denial and can continue or stop.
approval.timeoutThe approval window expired. defaultAction was applied automatically.
// Example: Next.js route handler
export async function POST(req: Request) {
const event = await req.json()
if (event.type === 'approval.requested') {
const { approvalId, agentId, action, params } = event.data
// Send Slack message, email, etc.
await slack.send({
channel: '#agent-approvals',
text: `Agent ${agentId} wants to ${action}:`,
blocks: buildApprovalBlocks(approvalId, action, params),
})
}
return new Response('ok')
}Listing pending approvals
// All pending approvals for your platform
const pending = await theazo.approvals.list({ status: 'pending' })
// Filter to a specific session
const sessionPending = await theazo.approvals.list({
status: 'pending',
sessionId: 'ses_abc123',
})// Approval object shape:
// {
// id: 'apr_abc123',
// agentId: 'agt_xyz',
// action: 'send_email',
// params: { to: 'ceo@acme.com', subject: '...', body: '...' },
// status: 'pending', // 'pending' | 'approved' | 'denied' | 'expired'
// requestedAt: '2024-01-15T10:23:00Z',
// expiresAt: '2024-01-16T10:23:00Z',
// decidedAt: undefined, // set once approved/denied
// decidedBy: undefined, // set once approved/denied
// }Approve and deny
Approve
Approve with optional modifications to override the parameters the agent intended to use. This lets reviewers correct values before the action executes.
// Approve as-is
await theazo.approvals.approve('apr_abc123')
// Approve with modifications — override the params the agent will use
await theazo.approvals.approve('apr_abc123', {
modifications: {
to: 'approvedrecipient@acme.com',
subject: '[Reviewed] ' + originalSubject,
},
})| Field | Type | Required | Description |
|---|---|---|---|
modifications | Record<string, unknown> | — | Overrides merged into the action params on approval. |
decidedBy | string | — | Identifier of the human who approved. |
Deny
await theazo.approvals.deny('apr_abc123', {
reason: 'Email tone is too aggressive. Revise before sending.',
})
// The agent receives the denial reason and can adapt:
// "I was denied because: Email tone is too aggressive..."
// The agent may then revise and try again, or stop.| Field | Type | Required | Description |
|---|---|---|---|
reason | string | — | Why the action was denied (surfaced to the agent). |
decidedBy | string | — | Identifier of the human who denied. |
Bulk decisions
For high-volume workflows, approve or deny multiple approvals in a single call.
// Approve a batch
await theazo.approvals.bulkApprove([
'apr_001',
'apr_002',
'apr_003',
])
// Deny a batch
await theazo.approvals.bulkDeny([
'apr_004',
'apr_005',
])End-to-end example
import { Theazo } from 'theazo'
const theazo = new Theazo({ apiKey: 'th_live_...' })
const session = await theazo.sessions.forUser('user_456')
// Create agent with approval gates
const agent = await session.agents.create({
tools: ['exec_code', 'read_file', 'update_crm', 'send_email'],
approvals: {
require: ['update_crm', 'send_email'],
notifyVia: 'webhook',
timeout: '8h',
defaultAction: 'deny',
},
})
// Start the agent — it will pause when it tries to update_crm or send_email
const runPromise = agent.run(
'Research Acme Corp and update their CRM record, then send a follow-up email to their CEO'
)
// Meanwhile, your webhook handler received 'approval.requested'
// Your UI shows the approval to the reviewer
// Reviewer clicks approve:
const pending = await theazo.approvals.list({
status: 'pending',
sessionId: session.id,
})
for (const approval of pending) {
console.log(`Action: ${approval.action}`)
console.log(`Params: ${JSON.stringify(approval.params, null, 2)}`)
// Approve each one — agent resumes after last approval resolves
await theazo.approvals.approve(approval.id)
}
// Agent resumes and completes
const result = await runPromise
console.log(result.output)
console.log(result.cost) // { amount: 340, currency: 'usd' }API reference
theazo.approvals.list({ sessionId, status })Promise<Approval[]>List approvals. Filter by status (pending | approved | denied | expired) and optionally by sessionId.theazo.approvals.get(approvalId)Promise<Approval>Fetch a single approval by ID.theazo.approvals.approve(id, { modifications? })Promise<void>Approve an action. Optionally override params before execution.theazo.approvals.deny(id, { reason? })Promise<void>Deny an action. The reason is passed to the agent.theazo.approvals.bulkApprove(ids)Promise<void>Approve multiple approvals in one call.theazo.approvals.bulkDeny(ids)Promise<void>Deny multiple approvals in one call.