Approval Methods
Human-in-the-loop approval workflows for TypeScript agents
Approval Methods
Human-in-the-loop approval workflows for TypeScript agents
The ApprovalClient class provides methods to pause agent execution and wait for a human decision before continuing. When you call approval.requestApproval(), the execution transitions to a waiting state. It resumes only after a human approves or rejects the request through your review interface.
Approval state is persisted by the AgentField control plane. If your process restarts while waiting, the execution remains paused and can be resumed once the human responds.
Basic Usage
import { ApprovalClient } from '@agentfield/sdk';
const approval = new ApprovalClient({
baseURL: 'http://localhost:8080',
nodeId: 'loan-processor',
apiKey: process.env.AGENTFIELD_API_KEY,
});
// Inside your agent handler
async function processLoan(executionId: string, applicationId: string, amount: number) {
if (amount > 50000) {
// Pause for human review
await approval.requestApproval(executionId, {
projectId: 'loans',
title: `Loan Application ${applicationId}`,
description: `High-value loan of $${amount} requires manual review`,
expiresInHours: 24,
});
// Wait for human decision
const status = await approval.waitForApproval(executionId, {
timeoutMs: 24 * 60 * 60 * 1000, // 24 hours in ms
});
if (status.status !== 'approved') {
return { status: 'rejected', reason: status.response };
}
}
return { status: 'approved', applicationId };
}Constructor
import { ApprovalClient } from '@agentfield/sdk';
const approval = new ApprovalClient({
baseURL: 'http://localhost:8080',
nodeId: 'loan-processor',
apiKey: process.env.AGENTFIELD_API_KEY,
});Options
Prop
Type
approval.requestApproval()
Submits an approval request and transitions the execution to waiting state. The execution stays paused until a human responds or the request expires.
const result = await approval.requestApproval(executionId, {
projectId: 'content-review',
title: 'Review order #42',
description: 'High-value order requires manual approval before fulfillment',
expiresInHours: 48,
});
console.log(result.approvalRequestId);
console.log(result.approvalRequestUrl);Parameters
requestApproval(executionId, payload)
Prop
Type
Returns
Promise<ApprovalRequestResponse>
Prop
Type
approval.getApprovalStatus()
Fetches the current status of an approval request without blocking. Use this for one-off checks or when building your own polling logic.
const status = await approval.getApprovalStatus(executionId);
if (status.status === 'approved') {
console.log('Approved. Reviewer data:', status.response);
} else if (status.status === 'rejected') {
console.log('Rejected. Reason:', status.response);
}Parameters
getApprovalStatus(executionId)
Prop
Type
Returns
Promise<ApprovalStatusResponse>
Prop
Type
approval.waitForApproval()
Polls the approval status with exponential backoff until the request is resolved (approved, rejected, or expired) or the timeout is reached. This is the recommended way to block execution until a human responds.
const status = await approval.waitForApproval(executionId, {
pollIntervalMs: 5000, // Start polling every 5 seconds
maxIntervalMs: 60000, // Cap at 60 seconds between polls
timeoutMs: 86400000, // Give up after 24 hours
});
switch (status.status) {
case 'approved':
return { proceed: true, note: status.response };
case 'rejected':
return { proceed: false, reason: status.response };
case 'expired':
throw new Error('Approval request expired before a decision was made');
}Parameters
waitForApproval(executionId, opts?)
Prop
Type
Returns
Promise<ApprovalStatusResponse> — same shape as getApprovalStatus().
If timeoutMs is reached before the human responds, waitForApproval() throws
an error. Wrap it in a try/catch if you want to handle timeouts gracefully.
Error Handling
import { ApprovalClient } from '@agentfield/sdk';
const approval = new ApprovalClient({
baseURL: process.env.AGENTFIELD_BASE_URL!,
nodeId: 'content-reviewer',
apiKey: process.env.AGENTFIELD_API_KEY,
});
async function reviewContent(executionId: string, contentId: string) {
try {
await approval.requestApproval(executionId, {
projectId: 'content',
title: `Review content ${contentId}`,
expiresInHours: 8,
});
const status = await approval.waitForApproval(executionId, {
timeoutMs: 8 * 60 * 60 * 1000,
});
if (status.status === 'approved') {
return { published: true, contentId };
}
return {
published: false,
reason: status.status === 'expired'
? 'No reviewer responded in time'
: status.response,
};
} catch (err) {
// Network errors, control plane unavailable, timeout exceeded, etc.
console.error('Approval flow failed:', err);
throw err;
}
}Examples
Multi-step Approval with Context
Pass context to reviewers by embedding structured data in the payload field of the request.
import { ApprovalClient } from '@agentfield/sdk';
const approval = new ApprovalClient({
baseURL: process.env.AGENTFIELD_BASE_URL!,
nodeId: 'expense-processor',
apiKey: process.env.AGENTFIELD_API_KEY,
});
async function processExpense(
executionId: string,
employeeId: string,
amount: number,
description: string,
) {
await approval.requestApproval(executionId, {
projectId: 'expenses',
title: `Expense request from ${employeeId}`,
description: `$${amount} — ${description}`,
payload: {
employee: employeeId,
amount,
description,
submittedAt: new Date().toISOString(),
},
expiresInHours: 72,
});
const decision = await approval.waitForApproval(executionId, {
timeoutMs: 72 * 60 * 60 * 1000,
});
return {
approved: decision.status === 'approved',
reviewerNote: decision.response,
respondedAt: decision.respondedAt,
};
}Tiered Approval Based on Risk
import { ApprovalClient } from '@agentfield/sdk';
const approval = new ApprovalClient({
baseURL: process.env.AGENTFIELD_BASE_URL!,
nodeId: 'trade-executor',
apiKey: process.env.AGENTFIELD_API_KEY,
});
async function executeTrade(executionId: string, tradeId: string, value: number) {
// Low-value trades skip approval
if (value < 10000) {
return { executed: true, tradeId, autoApproved: true };
}
// High-value trades require senior approval with a shorter window
const expiresInHours = value > 500000 ? 4 : 24;
await approval.requestApproval(executionId, {
projectId: 'trading',
title: `Trade ${tradeId} — $${value}`,
description: value > 500000
? 'High-value trade requires senior approval'
: 'Trade requires standard approval',
payload: { tradeId, value },
expiresInHours,
});
const status = await approval.waitForApproval(executionId, {
timeoutMs: expiresInHours * 60 * 60 * 1000,
});
if (status.status !== 'approved') {
return {
executed: false,
tradeId,
reason: status.status,
};
}
return { executed: true, tradeId, autoApproved: false };
}Polling Without Blocking
Use getApprovalStatus() directly when you want to check status as part of a larger loop rather than blocking on a single execution.
import { ApprovalClient } from '@agentfield/sdk';
const approval = new ApprovalClient({
baseURL: process.env.AGENTFIELD_BASE_URL!,
nodeId: 'batch-processor',
apiKey: process.env.AGENTFIELD_API_KEY,
});
async function processBatch(baseExecutionId: string, items: string[]) {
const pendingApprovals: string[] = [];
// Submit all approval requests
for (const item of items) {
const executionId = `${baseExecutionId}-${item}`;
await approval.requestApproval(executionId, {
projectId: 'batch',
title: `Approve item ${item}`,
});
pendingApprovals.push(item);
}
// Poll until all are resolved
const results: Record<string, string> = {};
while (pendingApprovals.length > 0) {
for (const item of [...pendingApprovals]) {
const status = await approval.getApprovalStatus(`${baseExecutionId}-${item}`);
if (status.status !== 'pending') {
results[item] = status.status;
pendingApprovals.splice(pendingApprovals.indexOf(item), 1);
}
}
if (pendingApprovals.length > 0) {
await new Promise(resolve => setTimeout(resolve, 10000));
}
}
return { results };
}Related
- Configuration - Agent setup and options
- ctx.memory - Store context for reviewers to fetch
- ReasonerContext - Full context reference
- ctx.call() - Calling other agents in a workflow