Tasks
Run agent and workflow tool calls as tasks that keep working while the conversation continues, and control how the model waits for, corrects, and cancels them.
A task is a tool call that returns a receipt right away and keeps working while the conversation
continues. Every call to an agent is a task, and so is every call to a
workflow tool that defines task(input, ctx) or serve(receive, ctx).
A task's result reaches the model later, in a task.result message, and a turn can't end while
tasks are still working.
This page covers when to use a task, how to order work that depends on a result, the signals a workflow tool body receives, and what the model and the stream see.
Choose how a call runs
A workflow tool defines exactly one entry point, and the entry point answers one question: should the conversation continue while the call works?
Defining none of the three, or more than one, throws when the tool is defined.
| Call | Runs as | Result | A new message during the call |
|---|---|---|---|
execute workflow tool, including sleep and ask_question | Tool call; the turn waits | The tool result | Aborts the call's abortSignal |
task workflow tool, including agentRouter() and the workflow tool | Task | A receipt, then a task.result message | Nothing |
serve workflow tool, including every agent tool | Resumable task | A receipt, then a task.result message per reply | Nothing |
task_wait | Wait inside the turn | Which tasks settled | Ends the wait |
| Plain and MCP tools | Inside the model step | The tool result | Applied at the next step boundary |
Waiting alone doesn't need a task. A question, a sleep, or an approval inside an execute call
parks the turn durably and holds no compute, and a new message stops the wait. Use a task when the
model should keep talking while the work runs, such as a twenty-minute deploy or research the model
may not need right away, or when a question should stay open through new messages. A long
execute tool holds the conversation until it settles or a new message stops it.
Agents have no choice to make: every agent tool is a serve tool, so every agent call is a
resumable task, because only the model can tell from the conversation whether it needs an agent's
answer now. The turn rule guarantees the answer reaches the model
before the turn ends.
See Run calls as tasks for writing a task body
and Resumable tasks for serve bodies,
receive(), and ctx.reply().
Order work that depends on a result
Because agent calls are tasks, the model decides when to wait for them. An instruction such as
"publish only after the review approves" holds only as long as the model follows it. When an order
must always hold, write a workflow tool that runs the first step through
ctx.agent and then acts, and don't expose the
side effect as a separate tool:
The publish step can only run after the review returns. Set tool: false on the reviewer
subagent if the model shouldn't call it directly, and define task in place of execute if the
conversation should continue while the review runs.
Pass signals to the work
A workflow tool body receives its signals from its context or, in a serve body, from each call
receive() resolves:
abortSignalaborts when the call's work should stop: bytask_cancel,session.cancel(), a cancelled or failed turn, or the end of the session, and, for anexecutecall, by a steering message that arrives while the turn waits on it.executeandtaskbodies read it asctx.abortSignal. In aservebody, each call fromreceive()carries its ownabortSignal; every call in one stretch of work shares one signal, and the next stretch gets a new one. Pass it to the steps andctx.agentsends that should stop.ctx.ask(request)withdraws the question when the call'sabortSignalaborts, andctx.ask(request, { signal })also whensignaldoes. In aservebody,ctx.reply()also withdraws the questions still pending for the calls it settles. The answer resolves as{ status: "cancelled" }, and the stream reportsinput.resolvedwithoutcome: "cancelled"so channels stop offering the question. A person's answer that reached the session first still wins: the ask resolves asanswered, so branch onstatus, not on the signal.
A body doesn't need to tell a steering message from a cancel: in both cases, stop and return what
you have. After a steering message the call settles with what the body returns, or with
{ interrupted: true } if the body rejects, and the model reads the message next. After a cancel,
eve discards the result. So a question in an execute call lapses once the conversation moves on,
as the provided ask_question tool's does, while a question in a task stays open:
See Stop early for a new message for how the
provided sleep tool races its timer against the signal.
What the model sees
eve adds task_wait, task_cancel, and a short system prompt block that explains tasks whenever
the agent has an agent tool or a task or serve workflow tool. The block tells the model to call
task_wait when it has nothing to say until a result arrives, and to reply when the person should
hear from it first, such as a confirmation that work is underway. In a child session or a
schedule's session, where only the final reply reaches the caller, it tells the model to call
task_wait instead of replying while tasks work.
Receipts. A call that starts a task returns a receipt as its tool result. Task ids are the tool
name and six characters. A resumable task's receipt tells the model how to reach it again, and a
call that reaches it by taskId returns a short receipt of its own:
Results. Each result arrives once, as a <task_result> block in a task.result message that
eve appends at the next step boundary, right after task_wait returns, or when a held turn
resumes. The body is the tool's toModelOutput projection of the output, or the error message for
a failed call. All results in one message share a budget of 50 KB and 2,000 lines, after which the
text is cut and marked [truncated].
task_wait({ timeoutSeconds? }) controls when the model replies. The model should call it
sparingly, only when it deliberately wants to withhold a message from the user while waiting for
a task result. Tasks keep running and their results reach the model without this call; the model
can reply now if the user should hear from it.
The call parks the turn until any task has a result, a new message arrives, or timeoutSeconds
pass. timeoutSeconds is a whole number of at least 1, in seconds like sleep. It returns at once
when a result is already waiting. Waiting never stops a task. The
model reads which tasks settled and which are still working, for example:
task_cancel({ taskId }) stops a task's current work and says so. A resumable task's answer
also tells the model how to give it new work. When the task has no work to stop, because it already
finished or is an idle resumable task, task_cancel says that instead:
[Tasks] note. When the session's tasks change, eve adds a note at the next step boundary that
lists the working tasks and the 10 most recently used idle resumable tasks. The note returns after
compaction, so the model can always find a task id:
Errors. Two error codes are specific to tasks:
| Code | When |
|---|---|
UNKNOWN_TASK | A call's taskId names no unfinished resumable task that the same tool started |
UNKNOWN_TASK | task_cancel's taskId names no task; a finished or idle one has no work to stop |
TOO_MANY_TASKS | A call would start a task, or make an idle one work, while 32 tasks are working |
A final_output call made while tasks are working returns an error that names them.
Turns wait for their tasks
No turn ends while any task is working. When the model ends a turn early, eve waits as
task_wait does: it parks until one of the tasks settles or the turn's own caller steers it,
appends the results, and calls the model again in the same turn. No result ever starts a turn on
its own. An idle resumable task isn't working, so it doesn't hold the turn.
A held turn stays open. Each time it parks, when eve holds it or when task_wait has no result
yet, the stream emits turn.waiting with the turn's turnId. The next step.started for the
same turnId means the turn resumed, and session.waiting comes only after the turn ends. The
model's text before the wait depends on the session:
- Root sessions: the text completes as an ordinary message. The person can keep writing, and the turn resumes under the same id.
- Child sessions and a schedule's sessions: the held text step reports
finishReason: "tool-calls"onmessage.completed, so channels post only the final reply.
A question or sign-in from a task, such as an agent's question, parks the turn the same way: the
stream emits input.requested or authorization.required, then turn.waiting, and the turn
continues once the person answers or signs in.
In every session, turn.completed comes only when the turn really ends. The TypeScript client's
send(...).result() and the MCP channel's agent_get read past turn.waiting and report the
final reply rather than the text written before the wait. result() stops at turn.waiting only
while a question is pending or the turn waits on a sign-in or approval, and returns
status: "waiting"; respond() then reads the same turn to its end. See Aggregate a turn.
Channels such as Slack post a root session's text before the wait as an
ordinary reply, then post the reply after the results as another message. Slack also shows each
turn's tasks in a live task card.
A steering message from the turn's own caller, one sent with turnPolicy: "steer", the default,
ends a task_wait and aborts the abortSignal of any execute call the turn waits on, but it
never interrupts a task. The model reads the message and decides whether to keep each task,
correct an agent by calling it again with its taskId, or stop a task with task_cancel. A
"queue" message, or a message from another caller, waits for the turn to end.
Cancel a task
task_cancelstops one task's current work.session.cancel()cancels the turn, theexecutecalls it waits on, and every working task in the session.- A failed turn cancels every working task.
- The end of the session ends every task.
Cancelled work never reports back. A task tool's task is then finished; its run waits up to 30
seconds for the body to finish unwinding, then ends as cancelled. A serve tool's task stays
available instead: its waiting calls settle as cancelled, the stretch's abortSignal aborts, and
the task becomes idle, as long as the body catches that abort and returns to receive(). For an
agent, that cancels the agent's current turn and keeps its conversation, so a later call with the
same taskId continues where it left off. A serve body that doesn't return to receive() within
30 seconds of a cancel ends, and the task finishes.
Stream events
| Event | When | Data |
|---|---|---|
task.started | A call starts a task, or reaches a resumable task by its taskId | taskId, callId, turnId, the tool name, and kind |
task.settled | A reply, return, failure, or cancel settles one call | taskId, callId, turnId, name, kind, status, and output, error, or cancel |
agent.started | A workflow run, including an agent tool's, opens a session with an agent | callId, turnId, taskId, name, sessionId, streamPath |
turn.waiting | An open turn parks on its tasks, a task_wait, a question, a sign-in, or an approval | turnId, sequence, and on: "input" when a person must act, otherwise "tasks" |
task.started and task.settled come once each per call, and (taskId, callId) identifies the
call. Both carry that call's turnId, which for a resumable task's later call can be a later turn
than the one that started the task, and the task's tool name and kind. kind is "agent" for
an agent tool's call and "tool" otherwise. Events recorded by earlier eve versions omit name
and kind on task.settled; match those to their task.started by callId. status is "completed", "failed", or "cancelled". A completed call carries
output, a failed call carries error, and a cancelled call carries cancel.reason:
"task_cancel" when the model called task_cancel, "turn_cancelled" when someone cancelled
the turn, or the working tasks between turns, or "turn_ended" when the turn ended, such as by
failing, while the task still worked. cancel is absent when the task's run stopped on its own, and on events recorded by
earlier eve versions.
agent.started names the call and turn whose run opened the session, and its taskId is absent
when an execute call opened it. Results reach the model as a message in its history, not as a
stream event, so read outcomes from task.settled. An input.requested,
authorization.required, or authorization.completed event from a task's run carries its
taskId. Hooks subscribe to the same events. The stream also carries the model's task_wait and
task_cancel calls as ordinary actions.requested tool calls, so evals can assert on them. The
eve dev terminal UI and Slack typing indicators leave those calls out; the terminal UI shows each
task's start and end instead. See
Sessions, runs, and streaming and
Follow a subagent.
Ownership and limits
- Ownership. Tasks belong to the session's open turn, not to the caller that started them. A
session has at most one open turn, so
task_wait,task_cancel, task results, and the[Tasks]note cover every task in the session. Any later turn can continue an idle resumable task by itstaskId, and that call runs with its own caller's auth. Only the turn's own caller steers it; anonymous callers share one identity. eve doesn't check which caller started a task: in a session with several people, such as a shared Slack thread, the model working for one person can continue ortask_cancela task another person started. A continuedservetask keeps the state its body built for earlier calls, so key per-caller data on the principal and enforce per-person access inside the tool. - Working tasks. A session runs at most 32 working tasks at once. An idle resumable task doesn't count, and a call that makes it work again does.
- No time limits. Tasks have no timeout of their own. The session lifetime,
limits.sessionTimeoutMs(30 days by default), bounds all work in a session. A body that needs a deadline racessleepagainst its work.
Upgrade from background tasks
Tasks replace background tasks, execution: "background", and agentId continuation. See
Upgrade to Tasks for every breaking change.