UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

663 lines (527 loc) 33 kB
--- title: "Workflows as Tools" description: "Define durable workflow tools that wait for people, webhooks, or timers without holding compute." url: /tools/workflows --- A workflow tool is a static tool defined with `defineWorkflowTool` from `eve/tools`, with `"use workflow"` as the first statement of its executor. Each call starts a durable Workflow run. Use one when a tool must wait for a person, webhook, or timer, delegate work to subagents, or coordinate retryable steps over a long period. Durable suspension and background execution are independent. `defineWorkflowTool` lets the body suspend at durable waits without holding compute. `execution: "background"` determines whether the parent agent receives a task receipt and continues before the body finishes. A workflow can suspend in either execution mode; running in the background does not require a suspension. Workflow tools use the [Workflow SDK](https://workflow-sdk.dev): `"use workflow"`, `"use step"`, `createHook`, `createWebhook`, `sleep`, retries, and replay. eve provides `ctx.ask` for questions answered through the session's channel, `ctx.agents` for effective callable-agent descriptions, and `ctx.agent` for durable agent delegation. Use `yield` to report progress and `await` on a workflow operation to wait durably. Values needed after a durable wait stay in local variables in the workflow body. A workflow tool runs code you wrote and appears to the model under its path-derived tool name, such as `deploy`. ## Define a workflow tool ```ts title="agent/tools/deploy.ts" import { defineWorkflowTool } from "eve/tools"; import { z } from "zod"; import { computePlan, runDeploy, type DeployPlan } from "../lib/deploy"; export default defineWorkflowTool({ description: "Deploy a service to production. Pauses for a human to approve the plan.", inputSchema: z.object({ service: z.string() }), async execute({ service }, ctx) { "use workflow"; const plan = await planDeploy(service); const answer = await ctx.ask({ prompt: `Deploy ${service}?\n\n${plan.summary}`, display: "confirmation", options: [ { id: "approve", label: "Deploy", style: "primary" }, { id: "cancel", label: "Cancel" }, ], }); if (answer.status !== "answered" || answer.optionId !== "approve") { return { deployed: false, reason: "rejected" }; } return { deployed: true, url: await applyDeploy(plan) }; }, }); async function planDeploy(service: string) { "use step"; return computePlan(service); } async function applyDeploy(plan: DeployPlan) { "use step"; return runDeploy(plan); } ``` The model calls `deploy`. The turn parks while the human reads the plan. When they answer, minutes or days later, the run resumes, deploys, and returns. The model sees one tool result. ### Rules - Export `defineWorkflowTool({ ... })` as the default export. Its `execute` must be an async function or async generator, written inline or referenced as a top-level `async function` in the same module or an imported application module. Start the executor with `"use workflow"` as its first statement. A missing directive is a build error, even if another function in the module has one. - `"use step"` marks a top-level `async function` in the tool module, or any module it imports, as a step. Side effects, clocks, randomness, `process.env`, and Node.js APIs belong in steps; the body is replayed and must stay deterministic. - Import `createHook`, `createWebhook`, `sleep`, and `FatalError` from `workflow` in the body. `start`, `getRun`, and `resumeHook` from `workflow/api` belong in steps. Your app does not install the SDK; for types, new projects list `eve/workflow-modules` in the tsconfig `types`. - In the body, `ctx` has `session`, `callId`, `toolName`, `abortSignal`, `agent`, `agents`, and `ask`. A `"use step"` helper that receives `ctx` directly gets a restricted `WorkflowStepToolContext` with `session`, `callId`, `toolName`, `abortSignal`, `getToken`, and `requireAuth`. Read `ctx.agents`, call `ctx.agent()`, and call `ctx.ask()` in the workflow body; pass only the serializable values the step needs. `getSandbox` and `getSkill` remain unavailable. - The tool's input must be a JSON object. Workflow bodies are for static tools under `agent/tools/`, not tools returned from `defineDynamic` resolvers. `ctx.agent`, `ctx.agents`, and `ctx.ask` are available only on `WorkflowToolContext` in the workflow body. Ordinary tools, channel handlers, schedule handlers, and workflow steps do not receive these methods. Type step helpers with `WorkflowStepToolContext` from `eve/tools` when they need step-safe context capabilities. Workflow executors require `defineWorkflowTool`. Adding `"use workflow"` to `defineTool`, a bare tool object, a channel handler, or a schedule handler fails the build. To start a session from a channel or schedule, use the [channel operations](/docs/channels/custom#channel-operations-and-session-handles) or [schedule handler](/docs/schedules#handler-form-run) APIs. Workflow imports resolve `paths` aliases from your application's `tsconfig.json` or `jsconfig.json`, including when the application is a workspace package. eve includes only workflow and step modules reachable from the agent's runtime modules. Unrelated workflows in the host application stay outside the agent bundle. An unresolved workflow import fails the build with the missing import in the error. ### Add a runtime-generated workflow tool Create `agent/tools/workflow.ts` and export the provided `workflow` factory when the model should supply the JavaScript program at runtime: ```ts title="agent/tools/workflow.ts" import { workflow } from "eve/tools/workflow"; export default workflow({ maxSubagents: 20 }); ``` The model supplies an async function body through the tool's `js` input. Its only host capability is `ctx.agent(name, { message, agentId?, outputSchema? })`. This can invoke subagents configured with `tool: false` or hidden by a same-named `disableTool()` file. Include those path-derived names in the workflow tool description or agent instructions when the model writes the program. For example: ```js const [triage, review] = await Promise.all([ ctx.agent("ticket-triage", { message: JSON.stringify(tickets) }), ctx.agent("ticket-review", { message: JSON.stringify(tickets) }), ]); return { triage, review }; ``` `maxSubagents` defaults to `100` and must be an integer from `1` through `128`. The generated program cannot access the workflow context, session state, credentials, imports, or ordinary tools. Agent authorization, questions, approvals, cancellation, and `agentId` continuation use the same owner-managed behavior as direct `ctx.agent` calls. The sandbox resumes after every pending call in the current batch settles. `Promise.all` supports fan-out followed by fan-in, but `Promise.race` does not resume after only the first child settles. A child failure is thrown at the corresponding `ctx.agent` call, so generated code can catch it. The program must return a JSON-serializable value. ### Migrate from `experimental_workflow` The experimental uppercase `Workflow` framework tool and its exports have been removed. Replace the old sentinel: ```diff -import { experimental_workflow } from "eve/tools/workflow"; - -export default experimental_workflow({ maxSubagents: 20 }); ``` with the lowercase factory: ```ts import { workflow } from "eve/tools/workflow"; export default workflow({ maxSubagents: 20 }); ``` The path gives the authored tool the model-facing name `workflow`. eve does not discover or inject an agent catalog. Calls use the same target resolution, availability, and authorization checks as `ctx.agent` in any other authored workflow tool. ### Migrate an authored workflow tool Replace `defineTool` with `defineWorkflowTool`, keep the executor's `"use workflow"` directive, and replace `agent(ctx, input)` and `ask(ctx, request)` with `ctx.agent(target, input)` and `ctx.ask(request)`. The `eve/workflow` entry point has been removed. Import `WorkflowToolContext`, `AgentInput`, `ToolInputRequest`, and `ToolInputResponse` from `eve/tools` when you need explicit types. ## Wait or run in the background Both modes support the same durable waits. Choose the execution mode based on when the parent agent should receive a tool result: | | Default execution | `execution: "background"` | | ---------------------------- | --------------------------------------------------- | -------------------------------------------------------------------- | | Tool result | The workflow's output after it finishes. | `{ status: "working", taskId }` before the body finishes. | | Parent turn | Waits for this tool call to finish. | Continues after receiving the receipt. | | Durable wait inside the body | Suspends the workflow; the tool call stays pending. | Suspends the workflow; the parent can continue independently. | | When the run ends | Settles the pending tool call. | Sends a task completion or failure notification to the parent agent. | | Cancel | Cancelling the turn cancels the run. | `task_cancel`, or the session ending. | Use default execution when the model needs the answer to continue. Use background execution when the conversation should continue while the task is pending. Background tools need no root-agent flag. `defineTool` does not accept background execution. See the [tool execution comparison](/docs/tools#background-execution). ### How suspension works Suspension happens when a workflow must wait for an unresolved durable operation, such as `ctx.ask`, an awaited hook or webhook, or `sleep`. The runtime persists the wait and releases the workflow's compute. When the answer, event, or timer arrives, the runtime replays the workflow, reuses recorded step results, and continues past the wait. Put side effects in `"use step"` functions so replay does not repeat completed effects. Consider a body that reports progress and then asks for approval: ```ts async *execute(input, ctx) { "use workflow"; yield { status: "awaiting approval" }; const answer = await ctx.ask({ prompt: "Continue?", display: "confirmation" }); return { answer }; } ``` The yield reports a snapshot, and eve advances the generator to `ctx.ask`. Awaiting the unanswered request is the durable wait. In default execution, the original tool call remains pending until the answer arrives and the body returns. With `execution: "background"`, the original call has already returned a task receipt, and the workflow can suspend while the conversation continues. Answering resumes the body in either mode. `yield` itself does not wait for approval or switch a tool into background execution. An ordinary Promise or Node.js timer inside a step also does not create a durable workflow suspension; use the workflow operations for waits that must survive a restart. ## Authorize inside a step Pass `ctx` directly to a step helper and call `ctx.getToken(provider)` there. User-scoped providers resolve as whoever launched this workflow tool, even if another person speaks in the session while it waits. The provider declaration and the API request both stay inside the step: ```ts import { connect } from "@vercel/connect/eve"; import type { WorkflowStepToolContext } from "eve/tools"; async function readRepository(ctx: WorkflowStepToolContext, repository: string) { "use step"; const provider = connect("github/my-agent"); const { token } = await ctx.getToken(provider); const response = await fetch(`https://api.github.com/repos/${repository}`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.status === 401) ctx.requireAuth(provider); if (!response.ok) throw new Error(`GitHub returned ${response.status}`); return response.json(); } ``` Connections discovered through `connection_search`, ordinary tools, and workflow steps use the same authorization machinery for requester identity, token caching, callback completion, and rejection after sign-in. The execution runtime owns the wait: an agent returns to its model after authorization, while a workflow retries the interrupted step and continues the authored body. The workflow body calls `await readRepository(ctx, repository)`. <Callout type="warning" title="Keep tokens out of step results"> Step results enter the workflow's durable history, so do not return the token from the helper. eve's token cache stays inside the step. The step input is recorded too and, after sign-in, carries the provider's callback parameters, such as a one-time authorization code. This matches how agent turns record callbacks. Bearer tokens are never written. </Callout> When sign-in is required, the step attempt ends and the workflow waits on its own callback hook, without holding compute. The channel renders the sign-in challenge. After the callback, eve retries **the whole interrupted step**, resolves the token, and continues. Previously completed steps are not rerun. Resolve auth before other side effects in that step, and make operations before `requireAuth` safe to retry. A token rejected immediately after sign-in fails instead of prompting again. Provider declarations can be shared imports, but context must be passed directly, not nested inside another argument or captured in a closure. After a successful callback exchange, eve records a completion marker before returning to authored code. If that code later fails and the step retries, eve reads the token from the provider instead of exchanging the same callback again. The provider must persist the grant or token; the marker itself contains no credentials. A background task becomes `input_required` during sign-in. The callback resumes that task; it does not rely on the launching agent turn still being active. Cancelling an authorization wait withdraws its callback. Cancellation uses the existing turn or task cancellation path rather than a separate authorization completion event. Background workflow authorization requires a new session after upgrading. In older sessions, `ctx.getToken` and `ctx.requireAuth` fail immediately with an instruction to start a new session. Blocking workflows and background workflows without auth are unaffected. ## Ask a human: `ctx.ask` ```ts const answer = await ctx.ask({ prompt: string, display?: "confirmation" | "select" | "text", options?: { id: string; label: string; description?: string; style?: "primary" | "danger" | "default" }[], allowFreeform?: boolean, dismissible?: boolean, }); // | { status: "answered"; optionId?: string; text?: string } // | { status: "dismissed" } // | { status: "unavailable" } ``` `ctx.ask` publishes an `input.requested` event on the session — rendered the way channels render tool approvals — and returns an awaitable answer. Awaiting it suspends the run until a response arrives. The `ask_question` tool is a workflow tool built on `ctx.ask`. The answer's `status` tells the body what happened: - `answered`: the person picked an option (`optionId`) or typed a reply (`text`). - `dismissed`: the request set `dismissible: true` and the person sent a message that did not answer it. The message reaches the agent as a normal turn. Only blocking tools can be dismissed this way. - `unavailable`: the session cannot request input, such as a scheduled run. `ctx.ask` returns this immediately instead of waiting for an answer no one can give. A plain follow-up message answers the request when it is the only pending question and the message matches an option or the request allows free text. This applies to blocking tools only: answer a question from a background tool or a subagent with a structured response. `ctx.ask` composes with the SDK's own constructs; race it against a deadline: ```ts const pending = ctx.ask({ prompt: `Deploy ${service}?`, options: APPROVE_OR_CANCEL }); const answer = await Promise.race([pending, sleep("4h")]); if (answer === undefined) return { deployed: false, reason: "timed out" }; ``` - The request belongs to the run, not the turn. It stays answerable until it is answered, dismissed, or the run ends. In a background tool that means long after the turn that started it. - A request is answered once. Ask again for the next answer. - Choose option `id`s that read well on their own, such as `approve` or the option's label. A response carries only the `id`. If a person answers after the request has already resolved, for example from an old Slack menu, the agent receives the `id` as a new message, not the label. - Ending the run, by returning, throwing, or cancellation, withdraws its pending requests. - Several requests may be outstanding at once. - A response never steers. A new human message that does not answer a pending request follows the session's normal `turnPolicy`. Compare the [`approval`](/docs/human-in-the-loop) policy, which gates the call before `execute` runs and can only show the model's input. Both compose: `approval` before the run, `ctx.ask` inside it. ## Delegate work: `ctx.agent` Workflow tools can call a subagent and wait for its result: ```ts const result = await ctx.agent("reviewer", { message: "Review the deployment plan for security risks.", outputSchema: { type: "object", properties: { findings: { type: "array", items: { type: "string" } }, }, required: ["findings"], }, }); ``` The first argument is an available agent target. On the root agent, `"agent"` starts or continues a copy of the root agent. A declared subagent uses its path-derived name and remains callable when hidden with `tool: false` or `disableTool()`. Delegated root copies and declared subagent contexts do not expose the built-in root-copy target. The invocation name `agent` is reserved, so a declared subagent cannot shadow it. eve assigns each call a replay-stable invocation identity, including repeated and parallel calls to the same target. Pass `agentId` to continue an existing child. An inline `outputSchema` requires structured output and determines the return type, so `result` in the example is typed as `{ findings: string[] }`. ### Route to a hidden subagent with Jev Use an authored workflow tool when the parent model should decide to delegate, but [Jev](https://vercel.com/i/what-is-jev) should choose the specialist. Set `tool: false` on each specialist so the parent model sees only the routing tool. The specialists remain available through `ctx.agents` and callable through `ctx.agent()`. If the router may select the root-copy `agent`, author a root `description` that explains which work should stay with a copy of the root. `ctx.agents.agent` always exists in a top-level root workflow, but its `description` is an empty string when omitted from `agent.ts`. `agentRouter()` considers only entries with a non-empty description, so it does not route to an undescribed root copy. ```ts title="agent/agent.ts" import { defineAgent } from "eve"; export default defineAgent({ description: "Coordinate cross-specialist tasks that need the root agent's complete toolset.", model: "anthropic/claude-opus-5.5", }); ``` ```ts title="agent/subagents/researcher/agent.ts" import { defineAgent } from "eve"; export default defineAgent({ description: "Investigation, analysis, and explanation", model: "anthropic/claude-opus-5.5", tool: false, }); ``` ```ts title="agent/subagents/operator/agent.ts" import { defineAgent } from "eve"; export default defineAgent({ description: "Execution and operational changes", model: "openai/gpt-6-sol", tool: false, }); ``` Define the model-visible router under `agent/tools/`. Build the Jev criteria from the subagents' effective descriptions, then pass its typed choice directly to `ctx.agent()`: ```ts title="agent/tools/agent-router.ts" import { evaluate } from "eve/ai"; import { defineWorkflowTool } from "eve/tools"; import { z } from "zod"; async function chooseTarget( task: string, criteria: Record<"researcher" | "operator", string>, abortSignal: AbortSignal, ) { "use step"; const result = await evaluate({ abortSignal, state: { task }, questions: { route: { type: "choice", instructions: "Which specialist should handle this task?", criteria, }, }, }); return result.answers.route.choice; } export default defineWorkflowTool({ description: "Route a task to the appropriate specialist.", inputSchema: z.object({ task: z.string().min(1).max(8000) }), async execute({ task }, ctx) { "use workflow"; const target = await chooseTarget( task, { researcher: ctx.agents.researcher.description, operator: ctx.agents.operator.description, }, ctx.abortSignal, ); return ctx.agent(target, { message: task }); }, }); ``` The evaluation runs in a step so workflow replay records its result instead of making the routing request again. The parent model receives `agent-router`, but not `researcher` or `operator`. Jev returns the typed `"researcher" | "operator"` choice, and the selected subagent's result becomes the `agent-router` tool result. `ctx.agents` is a replay-stable metadata snapshot taken when the workflow starts. In a top-level root workflow, `ctx.agents.agent` always represents the root-copy target and carries the root's authored `description` when provided. The snapshot also includes declared agents hidden from the parent model with `tool: false` or `disableTool()`, but exposes no model definitions, credentials, or callbacks. A delegated root copy omits `agent`, so `agentRouter()` cannot select another root copy recursively. Invocation still checks each target's availability through `ctx.agent()`. #### Route across every agent target To replace the model-facing `agent` tool with a router across every target in `ctx.agents`, export `agentRouter()` from the `agent` slot: ```ts title="agent/tools/agent.ts" import { agentRouter } from "eve/tools/agent-router"; export default agentRouter(); ``` Its input is `{ message: string, outputSchema?: object }`. `agentRouter()` ignores entries without a non-empty description. With two or more described targets, it sends the message and effective descriptions to Jev, then invokes the selected name through `ctx.agent()`. It invokes a sole target without evaluation and forwards an optional `outputSchema` unchanged. In a top-level root session, the candidates include the root-copy `agent`; delegated root copies and declared subagent contexts include only their declared targets. When an authored tool accepts a JSON Schema supplied by the model, represent that input with a permissive object such as `z.looseObject({})`. Avoid `z.record(z.string(), z.json())`: its generated JSON Schema uses `propertyNames`, which OpenAI does not support. Validate the supplied value at the point where your tool consumes it. This pattern controls specialist selection, not whether the parent model delegates at all. Route before the parent model runs if every incoming request must go through Jev. ## Report progress: `yield` A workflow body may be an async generator. In default execution, yields report progress. Background execution consumes yielded values without publishing them or retaining a task-progress stream. After processing a yield, eve advances the generator; use an awaited workflow operation when the body needs to suspend. | Operation | Default execution | `execution: "background"` | | --------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `yield value` | Emits an `action.partial` snapshot for the pending tool call. | Consumes the value without publishing progress or requesting a parent-agent turn. | | `return value` | Settles the tool call with its output. | Completes the task and contributes to its cohort's terminal report. | | No return value | Uses the last yield as output, or `null` if there were no yields. | Completes with `null`; yielded progress is not the task output. | For default execution, an explicit `return null` also falls back to the last yield. Prefer an explicit object return when progress snapshots and the final result have different shapes. Progress snapshots are last-write-wins by tool call id and do not enter model history as intermediate tool results. A snapshot used as the final output does enter history as the tool result. This background workflow consumes an intermediate yield, then suspends until its timer expires: ```ts title="agent/tools/remind_with_progress.ts" import { defineWorkflowTool } from "eve/tools"; import { sleep } from "workflow"; import { z } from "zod"; export default defineWorkflowTool({ description: "Schedule a reminder.", inputSchema: z.object({ note: z.string(), delay: z.string() }), execution: "background", async *execute({ note, delay }) { "use workflow"; yield { status: "preparing reminder" }; // Consumed without publishing progress in background mode. await sleep(delay); // Durable suspension while the timer is pending. return { reminder: note }; // Task completion notification. }, }); ``` Use [`ctx.ask`](#ask-a-human-ctxask) when the workflow needs a human answer. Removing `execution: "background"` makes the yield visible as progress and keeps the durable sleep; the model waits for the final reminder as the tool result. See [yield and return](/docs/tools#yield-and-return) for the final-output rules. ## Cancel and clean up: `ctx.abortSignal` `ctx.abortSignal` aborts when the run is cancelled: explicit turn cancellation for a waiting tool, `task_cancel` or the session ending for a background one. It is durable — it survives replay, and a step that receives it observes the abort. Pass it into the steps that should stop, and clean up in `try/finally`: ```ts async execute({ projectId }, ctx) { "use workflow"; const jobId = await submitRender(projectId); try { return await waitForRender(jobId, ctx.abortSignal); } finally { if (ctx.abortSignal.aborted) await abortRender(jobId); } } ``` After the signal fires, the run waits up to 30 seconds for the body to finish unwinding, then ends as cancelled whether or not it did. A body parked on a hook or a `sleep` does not observe the signal; it is abandoned when the grace period ends. Steps that received the signal are how you clean up first. The caller waits up to 35 seconds for a cooperatively cancelled run to settle before forcing it to stop. This includes time for the run to publish its outcome after body cleanup. A background task records its cancelled status before cleanup finishes, so that status alone does not mean its work has stopped. ## Workflow tool examples ### Approve with a deadline and an escalation ```ts async execute({ service }, ctx) { "use workflow"; const plan = await planDeploy(service); const pending = ctx.ask({ prompt: `Deploy ${service}?`, display: "confirmation", options: APPROVE_OR_CANCEL }); let answer = await Promise.race([pending, sleep("4h")]); if (answer === undefined) { await pageOnCall(service); answer = await Promise.race([pending, sleep("20h")]); } if (answer === undefined) return { deployed: false, reason: "timed out" }; if (answer.status !== "answered" || answer.optionId !== "approve") { return { deployed: false, reason: "rejected" }; } return { deployed: true, url: await applyDeploy(plan) }; } ``` One request stays on the channel the whole time. `sleep` is the deadline, `pageOnCall` is a step, and returning withdraws the request. ### Wait for an external system to call back ```ts title="agent/tools/render_video.ts" import { defineWorkflowTool } from "eve/tools"; import { createWebhook, FatalError } from "workflow"; import { z } from "zod"; import { submitRender } from "../lib/render"; export default defineWorkflowTool({ description: "Render a video. Returns the URL once the render farm finishes.", inputSchema: z.object({ projectId: z.string() }), async execute({ projectId }) { "use workflow"; const done = createWebhook(); const jobId = await submitRender(projectId, done.url); const callback = await done; const { status, url } = await callback.json(); if (status !== "ok") throw new FatalError(`Render ${jobId} failed: ${status}`); return { url }; }, }); ``` `createWebhook` mints a URL under `/.well-known/workflow/v1/webhook/` that eve serves. The external system posts to it when it is done. Nothing runs in between. Webhook tokens are generated for you; use `createHook` with `resumeHook` if you need a deterministic token. To customize the HTTP response, pass `respondWith: new Response(...)` to `createWebhook`. ### Ask now, act when answered ```ts title="agent/tools/refund_order.ts" import { defineWorkflowTool } from "eve/tools"; import { z } from "zod"; import { issueRefund } from "../lib/refunds"; export default defineWorkflowTool({ description: "Request approval to refund an order, then issue the refund once approved.", inputSchema: z.object({ orderId: z.string(), amount: z.number() }), execution: "background", async execute({ orderId, amount }, ctx) { "use workflow"; const decision = await ctx.ask({ prompt: `Refund $${amount} on order ${orderId}?`, display: "confirmation", options: [ { id: "approve", label: "Refund", style: "primary" }, { id: "deny", label: "Deny" }, ], }); if (decision.status !== "answered" || decision.optionId !== "approve") { return { refunded: false }; } return { refunded: true, receipt: await issueRefund(orderId, amount) }; }, }); ``` The model reports that approval is pending and the conversation continues. The approval card stays on the channel. When it is answered, the refund runs and the agent is woken with the outcome. ### Remind me later ```ts title="agent/tools/remind.ts" import { defineWorkflowTool } from "eve/tools"; import { sleep } from "workflow"; import { z } from "zod"; export default defineWorkflowTool({ description: "Remind the user about something after a delay.", inputSchema: z.object({ note: z.string(), delay: z.string() }), execution: "background", async execute({ note, delay }) { "use workflow"; await sleep(delay); return { reminder: note }; }, }); ``` The session parks between the receipt and the wake. The agent receives the return value and relays it. ## Semantics One call, one result. A waiting tool's call resolves once, with the return value, the error, or a cancellation. A background tool's call resolves once, with the receipt. Its terminal outcome arrives in one automatic report after every task in its cohort is terminal. A cohort includes overlapping background work in the same session, including tasks launched in later user turns while earlier work remains open. While a waiting tool runs, the turn is parked. A `queue` message waits for it. A `steer` message cancels the turn, which cancels the run, which withdraws its requests. Input responses never steer. Background runs belong to the session. They survive turn completion and cancellation, appear in the session's task index, can be cancelled with `task_cancel`, and are cancelled when the session ends. The parent session records each background task's outcome when it processes the child's notification. The first terminal outcome it records is final: a late child result cannot replace a recorded cancellation. Task completion does not require the wrapper workflow to have exited. Errors follow the SDK. A thrown error in a step retries per the step's policy; `FatalError` does not. An error that escapes the body fails the tool invocation. Starting a waiting workflow tool can also be retried. If dispatch is interrupted after starting a run, its retry starts another run, and both may execute. The parent tracks the run returned by the successful dispatch attempt. Use an application idempotency key for side effects that must happen only once. The workflow id derives from the executor's module path and function name. Inline executors use the tool module and the name `execute`; imported executors use their declaring module and name. Renaming or moving that function creates a new workflow. Runs in flight finish on the deployment that started them; a run that resumes on a deployment without its tool fails with an error naming the missing workflow.