mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
8a21c0cc9f
Signed-off-by: Casey Gowrie <ctgowrie@gmail.com>
631 lines
31 KiB
Plaintext
631 lines
31 KiB
Plaintext
---
|
|
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.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. Ordinary `defineTool` also accepts `execution: "background"`, but its executor still runs
|
|
inside the initiating step and cannot suspend at workflow waits. 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,
|
|
}); // { optionId?: string; text?: string }
|
|
```
|
|
|
|
`ctx.ask` publishes an `input.requested` event on the session — rendered the way channels render
|
|
`ask_question` and tool approvals — and returns an awaitable answer. Awaiting it suspends the run until a response arrives.
|
|
It 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 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.
|
|
- 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 while a request is pending 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 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-4.8",
|
|
});
|
|
```
|
|
|
|
```ts title="agent/subagents/researcher/agent.ts"
|
|
import { defineAgent } from "eve";
|
|
|
|
export default defineAgent({
|
|
description: "Investigation, analysis, and explanation",
|
|
model: "anthropic/claude-opus-4.8",
|
|
tool: false,
|
|
});
|
|
```
|
|
|
|
```ts title="agent/subagents/operator/agent.ts"
|
|
import { defineAgent } from "eve";
|
|
|
|
export default defineAgent({
|
|
description: "Execution and operational changes",
|
|
model: "openai/gpt-5.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. Ordinary yields report progress in both execution
|
|
modes. 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. | Emits stream-only task progress; does not request a parent-agent turn. |
|
|
| `yield task.postMessage(message)` | Unavailable; there is no `task` argument. | Sends a message requesting a parent-agent turn; the task stays open. |
|
|
| `return value` | Settles the tool call with its output. | Completes the task and delivers its output in a later notification. |
|
|
| 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 reports progress, sends the parent one message, and then suspends:
|
|
|
|
```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 and report progress before waiting.",
|
|
inputSchema: z.object({ note: z.string(), delay: z.string() }),
|
|
execution: "background",
|
|
async *execute({ note, delay }, ctx, task) {
|
|
"use workflow";
|
|
yield { status: "preparing reminder" }; // Stream-only progress.
|
|
yield task.postMessage(`Reminder scheduled for ${delay}.`); // Parent-agent message.
|
|
await sleep(delay); // Durable suspension while the timer is pending.
|
|
return { reminder: note }; // Task completion notification.
|
|
},
|
|
});
|
|
```
|
|
|
|
Calling `task.postMessage` constructs a descriptor; yielding it sends the message. It does not
|
|
wait for a reply. Use [`ctx.ask`](#ask-a-human-ctxask) when the workflow needs a human answer.
|
|
Removing `execution: "background"` requires removing the `task` argument and message yield; the
|
|
ordinary progress yield and `await sleep(delay)` still work, but the model waits for the final
|
|
reminder as the tool result. See [yield and return](/docs/tools#yield-and-return) for the different
|
|
final-output rules of ordinary `defineTool` generators.
|
|
|
|
## 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.
|
|
|
|
## 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.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.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; everything after arrives as
|
|
separate session input.
|
|
|
|
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.
|
|
|
|
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 run.
|
|
|
|
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.
|