Agent Workflows
A composite tool calls other tools and subagents in code. Use it when an operation needs a fixed sequence, parallel checks, or a structured result. Durable execution adds recorded steps and pause/resume support.
When To Use A Workflow
Use a workflow when several required steps must run together, such as loading a request, validating fields, obtaining a policy review, and saving findings. Use a single tool for a self-contained calculation or API action.
The Authoring Model
| Part | Purpose |
|---|---|
| Tool handler | Deterministic calculation, data transformation, or API action. |
| Composite handler | Execution order and aggregation of child results. |
| Subagent | A bounded task requiring model judgment. |
| Trigger | Starts the tool from chat, a predicate, or an external entry point. |
| Durable execution | Records steps and resumes after operator input or a scheduled pause. |
Composite Tools
Use handler.ts with tool.json. Composition declarations live in tool.json:
amodal/tools/review_request/
├── tool.json
└── handler.ts{
"name": "review_request",
"description": "Load a request, review completeness and policy, and save the findings.",
"parameters": {
"type": "object",
"properties": { "request_id": { "type": "string" } },
"required": ["request_id"]
},
"uses": {
"tools": ["load_request", "persist_review_findings"],
"subagents": ["prescreen-reviewer", "policy-reviewer"]
}
}export default async function (params, ctx) {
const request = await ctx.callTool('load_request', {
request_id: params.request_id,
});
const [completeness, policy] = await Promise.all([
ctx.callSubagent('prescreen-reviewer', 'Check field completeness.', request),
ctx.callSubagent('policy-reviewer', 'Review policy requirements.', request),
]);
const findings = { completeness, policy };
await ctx.callTool('persist_review_findings', {
request_id: params.request_id,
findings,
});
return { request_id: params.request_id, findings };
}The example requires the two named child tools and two authored subagents. Subagent results are text; parse and validate them in a child tool if your product needs structured fields.
Do not add tool.ts to this directory. Its presence selects that layout and ignores the sibling tool.json; that layout does not bind composition methods.
Declaring Composition (uses)
Declare every child tool in uses.tools and every specialist in uses.subagents. Calls to undeclared names fail on both inline and durable paths. The runtime also checks that declared capabilities can be resolved.
Composition Semantics
ctx.callTool(name, params)executes a declared child tool and returns its result.ctx.callSubagent(name, task, input?, opts?)runs a specialist's agent loop and returns its text.- Nested tool calls produce progress events and nested cards in the chat UI.
- A hard subagent failure throws. Tool errors within a specialist are returned to its model to handle.
- Recursion is bounded by runtime composition and delegation limits.
- Durable child calls are journaled. A completed call returns its recorded result when the parent resumes.
Nested tool calls do not re-run the outer loop's permission or hook checks. Inline composites refuse tools marked requiresConfirmation and tools in the connection category, including request. They do not open an approval dialog for those children.
Durable child execution does not apply that confirmation check. Use an explicit durable approval before a write that needs an operator decision. A uses declaration limits reachability; child code and downstream APIs must enforce authorization.
Fanning Out Attachments
Top-level handlers receive the current turn's uploads in ctx.attachments. Pass selected files in the fourth argument to a specialist:
export default async function (params, ctx) {
const results = await Promise.all(
(ctx.attachments ?? []).map((file) =>
ctx.callSubagent(
'document-extractor',
"Extract the document's facts and cite the supporting text.",
{ filename: file.filename },
{ attachments: [file] },
),
),
);
return { results };
}Declare document-extractor in uses.subagents. Attachments become file parts on the specialist's user turn. They are omitted, with a warning, for providers that do not support them.
Durable Workflows
Use durable execution when a tool must pause for input, approval, or a future time. The handler runs again on resume. Recorded runtime calls return their saved results instead of executing again.
Durable Tools
Set "execution": "durable" in tool.json:
{
"name": "release_to_environment",
"description": "Ask for a target environment and release to it.",
"execution": "durable",
"parameters": { "type": "object", "properties": {} },
"uses": { "tools": ["run_release"] }
}export default async function (params, ctx) {
const target = await ctx.requestInput({
question: 'Where should this release run?',
inputType: 'choice',
options: [
{ value: 'staging', label: 'Staging', recommended: true },
{ value: 'production', label: 'Production', sub: 'Makes the release live' },
],
});
const result = await ctx.callTool('run_release', { target: target.value });
return { target: target.value, result };
}The example requires a run_release child tool that validates the target and performs the release.
| Method | Contract |
|---|---|
ctx.requestInput(opts) | Pause for an answer. Supports question, context, inputType, options, payload, allowEdits, eligibleAnswerers, and excludeAnswerers. Returns {value, respondedBy, edits?}. |
ctx.waitForApproval(prompt) | Yes/no form of requestInput; returns {approved, respondedBy?, note?}. |
ctx.step(name, fn) | Run a checkpoint and record its JSON result. On replay, return that result without calling fn. Repeated names are distinguished by call order. |
ctx.sleepUntil(when) | Pause until a time, then resume through the scheduler. |
ctx.now() | Recorded clock value. |
ctx.random() | Recorded random value. |
inputType accepts text, choice, or yesno. Choice options have a value and label; recommended and sub affect presentation. The returned value is the posted JSON value, so validate it before using it for a sensitive action.
Use ctx.step for handler-local work whose result must survive replay:
const record = await ctx.step('parse-input', () => JSON.parse(params.document_json));Return plain data from checkpoints. A Date returns as an ISO string; a class instance returns as a plain object. Mutations to variables outside fn are not restored on replay.
Route external I/O through declared child tools. Durable contexts do not provide raw ctx.request or ctx.store. Time, randomness, and expensive local work outside journaled calls run again on resume.
A paused run appears as awaiting-input. In live chat, the model receives an asynchronous-start result and can continue. Persistence across restarts requires a persistent session store; local amodal dev stores are in memory.
Answering a Pause
Answer the parked durable session, whose ID differs from the originating chat session:
curl "$RUNTIME/sessions/pending-approvals" \
-H "Authorization: Bearer $RUNTIME_TOKEN"
curl -X POST "$RUNTIME/api/sessions/$DURABLE_SESSION_ID/answer" \
-H "Authorization: Bearer $RUNTIME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "staging"}'With allowEdits, include an edits field containing the edited payload. The handler receives it alongside the answer.
The endpoint enforces eligible and excluded answerers. An ineligible caller receives 403 not_eligible_answerer. An already answered pause returns 409 session_not_awaiting_input with answeredBy.
The runtime event bus publishes session_updated events for answer, cancel, and resume transitions. Subscribe through GET /api/events to update other viewers.
In the React SDK, useSessionActions(id).answer(value, edits?) answers from an inbox or custom screen. ChatWidget renders inline choice, yes/no, and text cards. Answering a chat card also sends a follow-up turn so the agent can acknowledge it. Another viewer's answer updates the card through the event bus.
Triggers
| Entry point | Use |
|---|---|
| LLM tool selection | The model chooses a tool in response to a user message. |
| Predicate trigger | A configured command or pattern starts the tool deterministically. |
| Operator action | Your app invokes the tool from a button or form. |
| Schedule or event | A configured automation or external integration starts the workflow. |
Entry points need their own authentication and invocation configuration. A composite manifest alone does not expose a button, schedule, or webhook.
Example: Document Packet To Workspace
A document-ingestion workflow can:
- Extract facts and source references from uploaded documents.
- Match extracted facts to product records.
- Validate required fields in code.
- Ask a specialist to review ambiguous items.
- Present proposed records for operator review.
- Save approved records and their supporting evidence.
Product State Beats Chat Memory
Load current records from the product API when the workflow runs. Request context can identify the selected record; verify permissions and mutable state on the server before writing.
Approval And Audit
Record the input, source records, findings, proposed changes, approval decision, and applied writes. Use the records to explain who approved an action and which evidence supported it.
Design Checklist
Before deploying, verify the workflow's success and failure paths with evals and tests for deterministic calculations. Include denied approval, missing data, child-tool failure, and replay after a pause. Test direct tool behavior separately from model-driven selection and approval gates.