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 };
}