How the HITL flow works
Tool suspends the run
A tool calls
meta.requestApproval(payload) (or throws ApprovalRequiredError directly), passing a payload that describes what needs approval.Run is checkpointed
The agent runtime catches the error, saves an
AgentCheckpoint with status: 'suspended' and the pending payload, and emits a suspended event to the response stream.Reviewer inspects and decides
The
/_anvil dashboard shows the pending run in the HITL queue with its payload. The reviewer approves or rejects from the UI — or your application calls resumeAgent programmatically.Triggering HITL from a tool
Usemeta.requestApproval inside a tool’s execute function. It is a typed shorthand that throws ApprovalRequiredError with the supplied payload:
ApprovalRequiredError directly if you need more control:
ApprovalRequiredError fields
The tool call ID that triggered the suspension. Anvil uses this to inject the approval result back into the message history when the run resumes.
Arbitrary data describing the pending action. This is what appears in the
/_anvil dashboard and what your approval handler receives to make its decision. Keep it human-readable — it is the reviewer’s primary signal.Wiring the checkpoint store
HITL requires durable checkpointing. Pass acheckpoint option to runAgent or streamAgent so the suspended state is persisted:
Resuming with an approval decision
CallresumeAgent with the runId and the reviewer’s approval value. The approval is injected as the result of the suspended tool call, and the agent loop continues:
approval is undefined or carries a rejection value, the tool should inspect it in its execute body and handle accordingly — for example by returning an error string instead of calling the payment provider.
resumeAgent options
resumeAgent accepts all the same options as runAgent, minus messages, approvals, and resumeState (which come from the checkpoint), plus two additional fields:
The same
StateStore used when the run was originally started.The stable run identifier used to locate the saved checkpoint.
The human reviewer’s decision. Injected as the result of the tool call that was pending when the run suspended. The receiving tool can inspect this value in its
execute body on the next invocation.Using toolPolicy for zero-code HITL
You can require approval for any tool without modifying the tool’sexecute function. Add toolPolicy to guardrails with the tool name in requireApproval:
issueRefund or deleteAccount, the guardrail returns an approve decision before the tool’s execute function runs. The runtime suspends the run exactly as if the tool had thrown ApprovalRequiredError. This is useful for adding approval requirements to third-party tools you cannot modify.
The HITL approval queue in /_anvil
All runs withstatus: 'suspended' appear in the /_anvil dashboard approval queue. Each entry shows:
- The
runIdand the trace name - The
pending.payload— the description of the action needing approval - The accumulated cost and token usage at suspension time
- Controls to approve or reject, which call
resumeAgenton your behalf
The dashboard HITL queue reads suspended checkpoints from the
StateStore. Make sure your dashboardMiddleware and your agent routes share the same SQLite file (.anvil/state.db) so the queue reflects live data.Handling rejection
When a reviewer rejects a pending action, callresumeAgent and pass a rejection value as approval. On resume, the tool’s execute function runs again from the top with the suspended call fenced — the agent loop supplies the approval value as the tool result directly. Design your tool to check the approval value before performing the side effect:
approval to resumeAgent. The approval value is injected as the tool result seen by the model. The model then generates a new response based on that result — for example, explaining to the user that the refund was rejected:
{ approved: false, reason: '...' } as the tool result and crafts an appropriate response for the user.
meta.requestApproval() is typed as never — it always throws ApprovalRequiredError and never returns. Any code written after it in the same execute function body is unreachable and will not run on the initial invocation or on resume.