Workflows as Tools
A workflow tool is a tool whose calls can wait for a person, a webhook, or a timer without holding
compute. Define one with defineWorkflowTool from eve/tools under agent/tools/. The model sees
it under its path-derived name, such as deploy.
Use a workflow tool when a call must wait for input, delegate work to subagents, or run retryable
steps over a long period. Each call starts a Workflow run that survives restarts. For a tool that
returns in one step, use defineTool instead.
A workflow tool defines exactly one entry point, which decides how its calls run:
| Entry point | How a call runs |
|---|---|
execute(input, ctx) | The turn waits until the workflow finishes, then gets its output as the tool result. Waits hold no compute. |
task(input, ctx) | A task: the model gets a receipt at once, and the result arrives later. |
serve(receive, ctx) | A resumable task that the model can send more calls by its taskId. |
See Tasks for choosing between them and for what the model sees while tasks work.
Workflow tools use the Workflow SDK: "use workflow", "use step",
createHook, createWebhook, sleep, retries, and replay. eve adds ctx.ask to ask a person a
question and ctx.agent to open a session with another agent. ctx.agents lists the agents the
call can open.
Define a workflow tool
The turn waits while the person reads the plan. When they answer, minutes or days later, the run continues, deploys, and returns one tool result.
Rules
- Default-export
defineWorkflowTool({ ... })with exactly one entry point:execute,task, orserve. Defining none, or more than one, throws when the tool is defined. - Write the entry point inline, or reference a top-level
async functionin the same module or an imported application module.executeandtaskcan be async generators;servecan't. - Start the entry point with
"use workflow". A missing directive is a build error, even if another function in the module has one. "use step"marks a top-levelasync functionin the tool module, or any module it imports, as a step. The workflow body replays and must stay deterministic, so put side effects, clocks, randomness,process.env, and Node.js APIs in steps. Keep values you need after a wait in local variables in the body.- Import durable primitives such as
createHook,sleep, andFatalErrorfromeve/workflowin the body. Callstart,getRun, andresumeHookfromeve/workflow/apiin routes or steps, not in the body. Both use eve's bundled Workflow SDK, so your app needs noworkflowdependency. See Workflow imports. - The tool's input must be a JSON object.
- Workflow bodies are for static tools under
agent/tools/, not tools adefineDynamicfunction returns."use workflow"indefineTool, 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 or schedule handler APIs. - Workflow imports resolve
pathsaliases fromtsconfig.jsonorjsconfig.json, including in a workspace package. An agent workspace member can import workflow and step modules from the shared root package, such aslib/. An unresolved workflow import fails the build and names the missing import.
The context depends on where your code runs:
| Where | Type | Members |
|---|---|---|
execute or task body | WorkflowToolContext | session, callId, toolName, abortSignal, channel, agent, agents, ask |
serve body | WorkflowServeContext | session, toolName, channel, agent, agents, ask, reply; each received call carries callId and abortSignal |
"use step" helper that gets ctx | WorkflowStepToolContext | session, callId, toolName, abortSignal, channel, getSandbox, getToken, requireAuth, to |
Call ctx.agent() and ctx.ask() and read ctx.agents in the workflow body. Pass a step only the
serializable values it needs. Call getSandbox, getToken, requireAuth, and to in a step, because
the workflow body throws when it calls them. Ordinary tools, channel handlers, and schedule handlers
don't receive agent, agents, or ask. Import WorkflowStepToolContext from eve/tools to type
step helpers.
Workflow imports
Import durable primitives from eve/workflow, and run and hook operations from eve/workflow/api:
| Export | Entry | What it does |
|---|---|---|
createHook<T>({ token?, metadata? }) | eve/workflow | Returns a WorkflowHook<T>. Await it for the next payload, or iterate it with for await. metadata is JSON stored with the hook. |
createWebhook({ respondWith? }) | eve/workflow | Returns a WorkflowWebhook whose url resumes it with the incoming Request. With respondWith: "manual", answer each WorkflowWebhookRequest with request.respondWith(response). |
sleep(duration) | eve/workflow | Waits for a duration such as "1h" or a number of milliseconds, or until a Date. |
getWorkflowMetadata() | eve/workflow | Returns the run's workflowName, workflowRunId, workflowStartedAt, and url. |
FatalError, RetryableError | eve/workflow | Throw FatalError to fail without a retry. Throw RetryableError, with an optional { retryAfter }, to retry a step. |
start(workflow, args) | eve/workflow/api | Starts a "use workflow" function on the agent's own workflow queue and deployment. It takes no options; omit args when the function takes none. |
getRun(runId) | eve/workflow/api | Returns a WorkflowRun<T> with runId, status, returnValue, and cancel(). start returns the same type. |
resumeHook(token, payload) | eve/workflow/api | Sends a payload to the hook with that token. |
A hook's token resumes it. Pass a fixed token to createHook when a route must find the hook later. await hook.getConflict() registers the hook before you hand out its token, and resolves to the run that already holds the token, or null. Call dispose(), or declare the hook with using, to release the token.
Import WorkflowHook, WorkflowHookOptions, WorkflowWebhook, WorkflowWebhookOptions, WorkflowWebhookRequest, and WorkflowMetadata from eve/workflow for explicit types.
Only these exports are public. A bare workflow or workflow/* import in an application module fails the build with an error that names these entries.
Wait for the result
An execute call works like any other tool call. The turn waits for the body to finish and gets its
output as the tool result. A wait inside the body pauses the workflow while the call stays pending.
Cancelling the turn cancels the run, and a new message aborts ctx.abortSignal; see
Stop early for a new message.
To keep the conversation going while a call works, define task instead.
How suspension works
A workflow pauses, holding no compute, when it awaits an unresolved ctx.ask, hook, webhook, or
sleep. When the answer, event, or timer arrives, the workflow replays, reuses recorded step
results, and continues past the wait. Put side effects in "use step" functions so replay doesn't
repeat them.
In this body, the yield reports progress and the await pauses:
The tool call stays pending until the answer arrives and the body returns. An ordinary Promise or Node.js timer inside a step doesn't pause the workflow or survive a restart.
Run calls as tasks: task
Define task(input, ctx) in place of execute when the conversation should continue while a call
works. Examples are a twenty-minute deploy or research the model may not need right away. Waiting
alone doesn't need a task, because a question, sleep, or approval in an execute call already holds
no compute.
A task body gets the same WorkflowToolContext as an execute body. Returning finishes the call,
a throw fails it, and an async *task generator can yield progress. Steering never aborts a task.
Each call is a task with its own id, <tool>-<6 characters>:
- Receipt: the call's tool result is a receipt, such as
Started task deploy-4hd8sa. Its result will arrive in a <task_result> message., and the model continues at once. - Result: the body's return value or its error arrives later as a
<task_result>block in atask.resultmessage. eve adds the message before the model's next step.toModelOutputapplies to the result, not the receipt.
The model waits for results with eve__task_wait and stops a task with eve__task_cancel. A turn
can't end while tasks are working. See Cancel and clean up
for what aborts a task, and Tasks for what the model sees, held turns, events,
and limits.
Resumable tasks: serve
Define serve(receive, ctx) when the model should be able to send a running task more input, such
as a revision request. A serve body runs once per task and gets its calls from receive():
A serve tool's calls are tasks, with the receipts and results
described above. Tasks covers waiting, the turn
rule, who can continue a task, and the task limit.
Receive calls and reply
eve adds an optional taskId to the tool's model input and explains it in the description. The
build fails if inputSchema declares its own taskId. A call without taskId starts a new task. Its
receipt ends with
To send it another message, call release-plan again with taskId release-plan-7k2m9q. A call with
taskId goes to that task. Its receipt starts Sent to task release-plan-7k2m9q. The
taskId must name an unfinished task this tool started, or the call fails with UNKNOWN_TASK.
Any caller in the session can send it.
receive()resolves with the next call,{ input, callId, abortSignal }, withinputvalidated byinputSchema. The first call is the one that started the task. Later calls arrive in order.- eve keeps calls that arrive while the body isn't waiting. A second
receive()while one is pending returns the same promise. - A call counts as received once its promise resolves, even if it lost a
Promise.race. The nextctx.reply()answers it, so keep a racedreceive()promise until you read the call. ctx.reply(output)answers every call received so far. Each call reports its owncall.settled, the model receives the reply once, and the task goes idle until its next call.- A reply with no call waiting is dropped, and
ctx.askthrows while no call is waiting. - A reply withdraws the open questions of the calls it answers. It doesn't abort their
abortSignal, so work after the reply keeps running. - Returning answers the calls still waiting and ends the task; a throw fails them.
serveisn't a generator, so it can'tyieldprogress.
Working, idle, and cancelled tasks
- A resumable task is working while one of its calls has no result, and idle otherwise. Only a working task holds the turn and counts toward the task limit.
- One stretch of work runs from a call reaching an idle task until a reply or cancel ends its
calls. Calls in a stretch share one
abortSignal. The next stretch gets a new one, so a task can be cancelled any number of times. eve__task_cancel,session.cancel(), and a failed turn end the waiting calls ascancelled, abort the stretch's signal, and make the task idle.- To stay available after a cancel, the body must catch the abort and return to
receive()within 30 seconds, as the example does. Otherwise the task finishes. A body that ignores the signal keeps working, and its reply is dropped. - The task finishes when its body returns or throws, or when the session ends. The end of the
session aborts the current signal and rejects a pending
receive().
Calls from several callers
A later call can come in a later turn, from a different caller than the one that started the task.
Once receive() resolves with a call, the context describes that call:
ctx.session.turnis the call's turn, andctx.session.authis the auth the session accepted the call with. Steps that receivectxsee the same values, sogetToken,requireAuth, and connections use that call's auth.ctx.agentslists the agents the call may open, including dynamic agents selected in its turn.- The call's
call.startedandcall.settledevents carry itscallIdand turn. So do the questions and sign-ins the body opens while serving it. - A session the body opens with
ctx.agentbelongs to that call. Itssession.parentnames the call'scallIdand turn, even after later calls. A remote receiver accepts that parent only from a trusted forwarder. - Each message to that child uses the call's auth. Remote identity follows the forwarding rules.
- When trace context is available, a local child's first turn uses the call's tool span as its
parent. A remote child's first turn starts its own trace, linked to that span with
eve.link.type=agent.dispatch. Later child turns start new traces.
Variables the body keeps between calls, such as plan, can hold another caller's output. Key
per-caller data on these ctx.session.auth.current fields together: authenticator, issuer,
principalType, and principalId. A principalId is unique only within its authenticator and
issuer. Anonymous callers, whose current is null or whose principalType is "anonymous",
share one identity, so don't keep per-person data for them. Any caller in the session can continue
or cancel the task; see Tasks.
Every agent tool, including the built-in agent tool and remote agents, is a
serve tool. Each agent task holds one session with the agent and replies with the final response
of each agent turn. Unlike other serve tasks, cancelling an agent task ends it, so start a new
task to delegate again.
Ask a human: ctx.ask
ctx.ask opens a question on the session, reported as interaction.opened, and channels show it
like a tool approval. Awaiting the answer pauses the run. The provided ask_question tool is built
on ctx.ask.
status | Meaning |
|---|---|
answered | The person picked an option (optionId) or typed a reply (text). |
cancelled | The question was withdrawn before anyone answered, because signal or the call's abortSignal aborted or a serve body replied to the call. The stream reports interaction.settled as withdrawn, so clients stop offering it. |
unavailable | The session cannot ask for input, such as a scheduled run. ctx.ask returns this immediately. |
A plain follow-up message answers the question when it is the only one open. The message must match an option, or the question must allow free text. Answer a question from a subagent with a structured response.
Race ctx.ask against sleep for a deadline:
- The question belongs to the run, not the turn. It stays open until someone answers it, it is withdrawn, or the run ends by returning, throwing, or cancellation.
- Each question is answered once, so ask again for the next answer. Several questions can be open at once.
- A response carries only the option
id, so choose ids that read well on their own, such asapprove. If a person answers after the question ended, for example from an old Slack menu, the agent receives theidas a new message. - A response never steers. A new message that answers no open question follows the session's
turnPolicy. Steering withdraws the questions of theexecutecalls the turn waits on, such asask_question's, but not a task's. See Stop early for a new message. - An aborted
signalasks the session to withdraw the question. If a person's answer reached the session first, the ask resolves asansweredand the channel shows it answered. Branch onstatus, not on the signal.
Compare the approval policy, which gates the call before execute runs
and can only show the model's input. Use both together: approval before the run, ctx.ask inside
it.
Delegate work: ctx.agent
ctx.agent(name, options?) returns a session with an agent, shaped like the TypeScript client's
session. The session opens on its first send, and each response.result() resolves when that
message's turn ends:
Set model to run a local child on a different AI Gateway model. Every message sent through the
returned session uses it:
The override changes only the model. The child keeps the target agent's instructions, tools, skills, connections, sandbox behavior, limits, reasoning, and explicit compaction model. Pass a selection object to override model-specific settings too:
Omit modelContextWindowTokens to read the context window from the AI Gateway model catalog
recorded at build time. Set it for a model the catalog doesn't list; otherwise the first send
rejects.
The selection must contain an AI Gateway model id, because a live AI SDK LanguageModel can't be
saved across waits. A remote agent rejects model, because its deployment owns its model
configuration.
The name is an available agent target:
- On the root agent,
"agent"starts a copy of the root agent. Delegated root copies and declared subagents can't open that copy. - A declared subagent uses its path-derived name, and stays callable when hidden with
tool: falseordisableTool(). - The name
agentis reserved, so a declared subagent can't use it. - An unavailable name rejects the first
send.
send(message, options?) accepts text or MessageContent<string | URL>: up to 64 text and file
parts. File data must be an HTTP(S) URL, as a string or URL object. eve rejects inline bytes,
base64, and data: URLs before sending, so upload a local file first and send its URL:
| Option | Description |
|---|---|
outputSchema | Structured output for the turn, as a Standard JSON Schema such as a Zod schema, or a JSON Schema object. |
signal | Aborting it cancels the turn the message started or joined. The turn still reports, as cancelled. |
A message sent while the child's turn is running joins that turn, and its response resolves with that turn's result. If the turn ends before the agent reads the message, the message starts the next turn. Its response, and those of earlier messages still waiting, resolve with that turn's result. A message sent while the session is idle starts the next turn. The session keeps its conversation between turns.
result() returns:
| Field | Description |
|---|---|
data | The structured value, when the turn requested outputSchema. |
reply | The agent's final reply text, when available. |
status | "completed", "failed", or "cancelled". Completion does not by itself mean the session ended. |
sessionEnded | Whether the session ended with this turn. |
error | { message } with the reason the turn failed. Required when status is "failed" and absent otherwise. |
Each ctx.agent(name, options?) call returns a new session that only this workflow run can send to.
The model sees only what the tool returns. The session ends when the run finishes, which cancels any
turn still running.
- Local child: runs with the caller's auth.
- Remote child: its identity comes from the outbound request, not the caller's session. To
forward the caller's identity, set
forwardPrincipal: trueon the remote agent and configure the receiver'strustedForwarders. See Forwarding the caller identity. - Questions and sign-ins: a person answers the child's questions and sign-ins at the root session, as for any subagent.
The parent stream announces each session with child.opened. The event carries the agent name,
the child's sessionId, its stream path, and its owner. owner is the task whose run opened
the session, or the tool call when the call doesn't run as a task. Pass the event to
session.agent(opened).stream() to follow the
child.
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. ctx.agents and ctx.agent() still reach the specialists.
If the router may select the root-copy agent, give the root a description that says which work should stay with a copy of the root. Without one, ctx.agents.agent.description is an empty string, which agentRouter() skips.
Define the model-visible router under agent/tools/. Build the Jev criteria from the subagents' descriptions, then pass its typed choice to ctx.agent():
The decision runs in a step, so replay reuses it instead of repeating the routing request. The parent model sees only agent-router, and the selected subagent's reply becomes its tool result.
ctx.agents holds agent metadata as it was when the session accepted the call, and replay doesn't change it. In a serve task, it follows the call being served. It includes agents hidden with tool: false or disableTool(), but no model definitions, credentials, or callbacks. In a top-level root workflow, ctx.agents.agent always exists as the root-copy target. A delegated root copy omits agent, so agentRouter() can't select another root copy. ctx.agent() still checks each target's availability.
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:
Its input is { message: string }. agentRouter() considers only targets with a non-empty description. With two or more, Jev picks one from the message and descriptions; a sole target gets the message directly. The router sends the message to a new session with that agent. Each call runs as a task, so the reply reaches the model as the task's result. 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.
Add a runtime-generated workflow tool
Export the provided workflow factory from agent/tools/workflow.ts when the model should write the
JavaScript program at runtime:
The model supplies an async function body through the tool's js input. Each call runs as a
task, so the program's return value reaches the model in a
task.result message.
The program can only call ctx.agent(name, { message, outputSchema? }). It sends the message to a
new session with that agent, and resolves to the reply or, when outputSchema is set, to the
structured value. The program can call subagents hidden with tool: false or a same-named
disableTool() file, so list their path-derived names in the tool description or agent
instructions:
Omit maxSubagents to allow 100 agent calls; set an integer from 1 through 128. The program
can't use the workflow context, session state, credentials, imports, or ordinary tools. Each
ctx.agent call starts a new child session, so a program can't continue a child from an earlier
call. Agent authorization, questions, approvals, and cancellation work as they do for sessions an
authored workflow tool opens with ctx.agent.
The program continues after every pending call in the current batch finishes, so Promise.all
supports fan-out followed by fan-in. A ctx.agent call still pending when the program returns
fails the workflow, such as one never awaited or one that loses a Promise.race or Promise.any.
A failed child turn, an invalid call, or a call over the maxSubagents budget throws a catchable
WorkflowProgramAgentError at that ctx.agent call. The error message names the cause.
The return value and each ctx.agent input pass through JSON.stringify, so a Date becomes a
string, and functions and undefined members drop. A return value that converts to nothing, such
as undefined, becomes null.
| Limit | Value |
|---|---|
Return value or ctx.agent input as JSON | 1 MiB |
| Child reply as JSON | 4 MiB |
| Program source | 256 KiB |
| JavaScript time per start or resume | 30 seconds |
| Total time per start or resume | 5 minutes |
Time spent waiting for child agents does not count toward either time limit.
Start channel sessions: ctx.to
A workflow tool reaches the agent's channels with ctx.to(channel, target), as an ordinary tool does. Call it in a "use step" helper that receives ctx, like ctx.getSandbox(); the workflow body throws when it calls it:
A failed step retries, so pass an idempotencyKey that the retry repeats. Include ctx.session.id, because the target session dedupes keys from every sender. Add a label for each send, because ctx.callId is the same for every step of the call. Create the channel in its own step, so a retried send step reuses the saved channelId instead of creating another channel.
ctx.channel holds the kind, continuationToken, and metadata of the channel serving the calling session, in the workflow body and in steps, as in ordinary tools.
Report progress: yield
A workflow body can be an async generator whose yields report progress. A yield doesn't pause the body; await a workflow operation to pause.
| Operation | Behavior |
|---|---|
yield value | Emits a call.progress snapshot for the pending tool call. |
return value | Ends the tool call with its output. |
| No return value | Uses the last yield as output, or null if there were no yields. |
An explicit return null also falls back to the last yield, so return an object when progress
snapshots and the final result have different shapes. Each snapshot replaces the previous one and
stays out of the model's history, unless it becomes the final output.
See yield and return for the final-output rules.
Cancel and clean up: ctx.abortSignal
ctx.abortSignal aborts when the call is cancelled by session.cancel(). For a task, it also aborts
on eve__task_cancel, a failed turn, or the end of the session. For an execute call, a steering
message aborts it too; see Stop early for a new message. The signal
survives replay, and a step that receives it sees the abort. Pass it into the steps that should
stop, and clean up in try/finally:
After a cancel, the run waits up to 30 seconds for the body to stop, then ends as cancelled whether
or not it did. A body paused on a hook or a sleep doesn't see the signal. eve abandons it when
the 30 seconds end, so clean up in steps that received the signal.
The caller waits up to 35 seconds for a cancelled run to report its outcome, then forces the run to stop.
Stop early for a new message
A steering message is one sent with turnPolicy: "steer" (the default) that answers no open
question. It aborts the ctx.abortSignal of each execute call the turn waits on. A message that
answers a question never stops a call, and steering never touches a
task.
Steering does not cancel the call or end the turn. The call ends with what the body returns, and the model reads the new message after the step's workflow tool calls finish:
- Questions: an open
ctx.askquestion is withdrawn and resolves ascancelled, unless a person's answer reached the session first. - Rejections: a body that rejects after the abort, such as one whose step received the signal,
ends as
{ interrupted: true }instead of failing. - Paused bodies: a body paused on a hook or a
sleepdoesn't see the signal, and it keeps the turn waiting until it finishes.
The body doesn't need to tell a steering message from a cancel: stop and return what you have.
After a cancel, eve discards the result. The provided sleep tool races its timer against the
signal:
To keep a question or long work going while the conversation continues, define
task instead of execute.
Authorize inside a step
Pass ctx directly to a step helper and call ctx.getToken(provider) there, with the provider
declaration and the API request inside the step. User-scoped providers resolve as the person whose
call started the workflow tool, even if another person speaks in the session while it waits:
The workflow body calls await readRepository(ctx, repository).
Keep tokens out of step results
eve records step results in the workflow's history, so do not return the token from the helper. The step input is recorded too and, after sign-in, carries the provider's callback parameters, such as a one-time authorization code. Bearer tokens are never written.
When sign-in is required, the step attempt ends and the channel shows the sign-in prompt. The workflow waits for the callback without holding compute. After the callback, eve retries the whole interrupted step, resolves the token, and continues. Completed steps don't run again.
- Resolve auth before other side effects in that step, and make operations before
requireAuthsafe to retry. - A token rejected immediately after sign-in fails instead of prompting again.
- Provider declarations can be shared imports. Pass
ctxdirectly, not nested inside another argument or captured in a closure. - If the step fails and retries after a successful sign-in, eve reads the token from the provider instead of reusing the callback. The provider must therefore persist the grant or token.
- Cancelling an authorization wait withdraws its callback.
Use the session sandbox in a step
Pass ctx directly to a step helper and call ctx.getSandbox() there. The step opens the
sandbox that the calling session's turns use. That includes a sandbox shared with
a parent session, as in a copy started by the built-in agent tool. The step and the session see
each other's files:
- The workflow body can't open the sandbox, because it replays.
ctx.getSandbox()in the body throws and tells you to passctxto a step helper. - The step works with any sandbox provider. If the session recorded its sandbox before it called
the tool, the step resumes it. Otherwise, or if that sandbox is gone, the step runs your
defineSandbox()definition as the session's first turn would. Later turns open the same sandbox. - Your definition can run once for each caller that opens the sandbox before the session records it, so make its setup safe to repeat.
- The sandbox belongs to the session, not the step. When the step ends, even by throwing, the
sandbox keeps running.
sandbox.stop()andsandbox.delete()in a step fail without a retry, because the session's turns may be using the sandbox. - The session removes the sandbox when it ends, even when only a step opened it. A step still running at that point can open the sandbox again, and nothing removes the new one.
- When the eve server shuts down, it stops the sandboxes its sessions opened, but not one that only a step opened. With Docker, that container keeps running until the session ends or you stop it.
- A step can use the sandbox while the session's turns use it, as subagents that share a sandbox do. Give each one its own files to write.
- Steps run at least once and retry after a failure, so make sandbox writes safe to repeat. Write whole files instead of appending, or check for earlier output first.
- When the session shares its parent's sandbox, the step opens it with the parent's sandbox definition, and the parent removes it when the parent ends.
- A step runs wherever the workflow runs. A sandbox on one machine's disk, such as just-bash, is reachable from a step only on that machine, as in local development.
Workflow tool examples
Approve with a deadline and an escalation
One question stays on the channel the whole time. As a task, the call doesn't hold the
conversation, and steering doesn't withdraw the question. pageOnCall is a step, and returning
withdraws the question.
Wait for an external system to call back
createWebhook mints a URL under /.well-known/workflow/v1/webhook/ that eve serves. The external
system posts to it when it is done, and nothing runs in between. Use createHook with resumeHook
for a deterministic token. To customize the HTTP response, pass respondWith: new Response(...) to
createWebhook.
Ask now, act when answered
The turn waits while the approval card stays on the channel. When someone answers, the refund runs and the agent receives the outcome as the tool result.
Semantics
A workflow tool call ends once, with the return value, the error, or a cancellation.
While an execute call runs, the turn waits, and a message sent with turnPolicy: "queue" waits
for it. A task doesn't hold the turn. See Stop early for a new message
for steer messages.
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 call.
An interrupted dispatch can start a second run for the same call, and both runs may execute. Use an application idempotency key for side effects that must happen only once.
The workflow id comes from the entry point's module path and function name. Inline entry points use
the tool module and the name execute, task, or serve. Imported ones 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.