Skip to main content

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_result message 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 task tool.

Lifecycles

Every tool is written the same way: a run handler (an async function or async generator) plus a lifecycle that controls how it executes.
If 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’s run executes in the same process, but the round doesn’t wait for it:
  1. If the work finishes within the grace window (graceMs, default 250ms), it behaves like a plain sync call and no placeholder is created.
  2. Otherwise the model receives a pending placeholder (including a taskId) and the loop continues.
  3. When the task finishes, the SDK injects the result as a tool_task_result message 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 as orphaned when a StateAccessor is 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. The run handler registers the work and returns ctx.defer(taskId). The conversation pauses (status: 'awaiting_async_tools') until any process completes the task.
Deferred tools require a 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 its outputSchema:
  • legalReview.resolve(...): deliver a successful result.
  • legalReview.fail(...): deliver an error.
  • legalReview.cancel(...): cancel the task.
  • Omit run to record the result only; it’s delivered on the next callModel({ state }).
  • A task settles once. A replayed webhook throws ToolTaskAlreadySettledError instead of delivering the result twice.
The lower-level resumeToolResults(client, { state, results, ... }) covers batches and tools you don’t have a reference to.
.resolve() injects a value the model treats as a tool result. Authenticate the webhook before calling it; the SDK can’t do that for you. Outputs are validated against outputSchema at runtime as well as compile time.

Checking On Running Tasks

When any long-running tool is registered, the SDK adds one built-in task 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:
The SDK intercepts these calls and routes each one to the tool that owns the task. The model sees one tool; each of your tools decides how to answer.

Custom Check Handlers

Add a check config to control what the model sees when it checks on your tool:
Without a custom 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.
Disable check-ins entirely with 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:
Messages are delivered to the run body’s 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:
Everything above works the same way for agent tools:
  • 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 status reports turnsCompleted and currentActivity.
  • Steering (sendToTask or task({ action: 'steer' })) lands in the child as a user message at its next turn boundary.
  • cancelTask(taskId), parent abort, or timeoutMs cancels the child.
By default, the full child transcript stays out of the parent’s context. The parent receives the mapped result, and the model pulls transcript detail on demand via the 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 on getFullResponsesStream():

Options Reference

Run-level configuration, all optional:
Per-tool configuration on tool() / tool.agent():

Next Steps