eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
663 lines (527 loc) • 33 kB
text/mdx
---
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.