Overview
A synchronous tool stops the conversation until it returns. That’s fine for a weather lookup. It’s a problem when the tool renders a video, runs a data pipeline, or waits days for a legal review. Async tools let the model keep working while the tool finishes:- The model gets a pending placeholder for the call right away and moves on.
- When the work finishes, the SDK injects the result as a
tool_task_resultmessage at the next turn boundary. External work resolves whenever another process reports it. - While a task runs, the model can check progress, read logs, steer, or cancel it through one built-in
tasktool.
Lifecycles
Every tool is written the same way: arun handler (an async function or async generator) plus a lifecycle that controls how it executes.
run is a generator, each yield becomes a log entry. Logs feed check-ins, tool.preliminary_result events, and transcripts (optionally validated by eventSchema). The return value is the tool’s result.
If run is a plain async function, log with ctx.log() instead.
lifecycle: 'background' and 'deferred' require an outputSchema. The result arrives later, so the SDK must be able to validate it without the original call.Background Tools
A background tool’srun executes in the same process, but the round doesn’t wait for it:
- If the work finishes within the grace window (
graceMs, default 250ms), it behaves like a plain sync call and no placeholder is created. - Otherwise the model receives a pending placeholder (including a
taskId) and the loop continues. - When the task finishes, the SDK injects the result as a
tool_task_resultmessage before the next model turn.
When the Run Would End First
If the model finishes its answer while background work is still in flight,asyncTools.onRunEnd decides what happens:
'drain'(default): wait for running tasks and give the model extra no-tool turns so the final answer includes the results.'detach': return immediately. Tasks keep running, and results are dropped (persisted asorphanedwhen aStateAccessoris configured).'cancel': abort in-flight tasks and finish.
Deferred Tools
Deferred tools hand work to an external system, such as a human review queue, a batch pipeline, or a webhook-driven service. Therun handler registers the work and returns ctx.defer(taskId). The conversation pauses (status: 'awaiting_async_tools') until any process completes the task.
StateAccessor (see Tool Approval & State) so the paused conversation can be found and resumed from another process.
Typed Completion from Any Process
Completion methods live on the tool, so the output is typechecked against itsoutputSchema:
legalReview.resolve(...): deliver a successful result.legalReview.fail(...): deliver an error.legalReview.cancel(...): cancel the task.- Omit
runto record the result only; it’s delivered on the nextcallModel({ state }). - A task settles once. A replayed webhook throws
ToolTaskAlreadySettledErrorinstead of delivering the result twice.
resumeToolResults(client, { state, results, ... }) covers batches and tools you don’t have a reference to.
Checking On Running Tasks
When any long-running tool is registered, the SDK adds one built-intask tool to the request. It’s a single fixed definition no matter how many async tools you register, so your tools’ schemas stay untouched and the prompt cost stays flat.
The pending placeholder tells the model how to use it:
Custom Check Handlers
Add acheck config to control what the model sees when it checks on your tool:
check, the SDK answers the three views itself (status / logs / transcript, truncated to asyncTools.maxTranscriptChars, default 20,000 characters).
The SDK treats task-tool calls as internal: they’re exempt from doom-loop detection, skip per-tool concurrency and timeout limits, and never fire
PreToolUse/PostToolUse hooks.asyncTools: { checkins: false }. Placeholders then tell the model not to call the tool again, and results still arrive automatically.
The name task is reserved: tool({ name: 'task' }) throws. If a tool list built without tool() includes a tool named task, the SDK disables the built-in and logs a warning.
After a process restart, deferred tasks answer status from persisted state (including a bounded lastLog). Full logs and transcripts live in memory only, so those views report a short note explaining that instead.
Steering Running Tasks
Three ways to send guidance into a running task:ctx.onMessage(handler) and queued until a handler registers, so no messages are lost. Deferred tasks throw on sendToTask because their work runs in an external system the SDK can’t reach.
Agent Tools (Subagents)
tool.agent() creates a tool whose work is a child callModel conversation, running as a background task:
- The parent keeps working while children run; several children can run concurrently under the background pool.
- The child’s conversation is the check-in transcript, each child turn is a log entry, and
statusreportsturnsCompletedandcurrentActivity. - Steering (
sendToTaskortask({ action: 'steer' })) lands in the child as a user message at its next turn boundary. cancelTask(taskId), parent abort, ortimeoutMscancels the child.
task tool.
Children run in-memory (no
StateAccessor) and don’t inherit the parent’s hooks; pass child hooks explicitly in the agent spec if needed. A child that pauses (HITL, approval, or deferred tools inside it) fails the task with a clear error.Observing Async Tasks
From Code
From the Event Stream
Async tasks emit dedicated events ongetFullResponsesStream():
Options Reference
Run-level configuration, all optional:tool() / tool.agent():
Next Steps
- Tools - The
tool()helper, schemas, and tool types - Tool Approval & State -
StateAccessorsetup for deferred tools and cross-process resume - Streaming - Consuming the full event stream
- Lifecycle Hooks - Observing tool execution