Tasks
A task is one request to one agent, written to the database before any model is called. Everything else in the execution layer hangs off it: the Temporal workflow that runs it, the event feed you stream, the artifacts it produces, and the governance policy it was launched under.The problem
Without a persisted task, an agent run is a request-scoped side effect. It ends when the HTTP connection ends, there is nothing to resume after a worker restart, nothing to attach a spend limit to, no row to join against for an audit question, and no stable identity for a follow-up message to address. Every capability the platform sells — durability, governance, streaming, audit — requires that the run has an identity that outlives the process running it.How AgentArea approaches it
A task is created before it is dispatched, and creation is where the expensive decisions get made.TaskService.create_task_with_policy resolves an effective
governance policy for the workspace, agent, user, and task; checks the
workspace’s month-to-date spend against the policy cap and raises
BudgetCapExceededError if it is already exhausted; verifies the agent exists
and has a model configured; and only then writes the row.
REST, A2A, and the MCP toolset all funnel through the same entry point,
create_and_execute_task_with_workflow, so none of them can drift into a
different set of defaults.
Identity
The status machine
Eleven values pass validation (BaseTaskService._validate_task). Ten are
written by some path today:
A twelfth value,
routed, is returned to the caller but never stored. When a
channel message carries a chat_id that matches a live workflow for the same
agent, the message is signalled into that workflow and the existing task comes
back marked routed — no new task is created and no new policy is resolved.
Terminal states depend on which surface you ask
This is the part that trips people up, because the two answers differ. On the task row, four states end it:completed, failed, blocked,
cancelled. AgentTask.is_completed() returns true for exactly these.
On the event feed, three types end it: task.completed, task.failed,
task.cancelled. There is no task.blocked event. A blocked task writes
blocked to its row and emits task.failed on the feed, because the workflow
picks the event type from state.success and blocked runs are not successful.
A consumer watching the stream sees a failure; a consumer reading the row sees
the more specific reason. failure_reason carries the machine-readable code in
both cases — capability_unavailable, validation_failed, iteration_limit,
budget_exceeded, missing_final_response, task_unsuccessful.
Completion is gated, not asserted
An agent does not finish by saying it finished. When it calls the completion tool, the workflow runs an artifact validation activity against the committed task workspace and branches on the result:- passed — the task succeeds, the row is written
completedimmediately, and the final response is stored. - failed — the agent gets structured repair feedback as a tool result and
tries again. After two repair attempts the task ends
failedwithfailure_reason: validation_failed. - unavailable — the validator itself could not run. The task ends
blockedwithfailure_reason: capability_unavailable. It does not fall through to success.
completed does not mean the workflow exited
After the gate passes, the workflow sets the row to completed and then stays
alive for 30 minutes waiting for a follow-up message. A follow-up clears the
terminal state, returns the row to running, and continues the same
conversation in the same workflow. Only delegation children skip this — their
parent is blocked awaiting their result, so sitting in a wait would deadlock it.
This is why a live Temporal status of running is never allowed to overwrite a
persisted completed: the workflow is legitimately alive after the task is
done.
Continuation
Hitting the iteration limit or the budget ceiling is not a failure yet. The workflow records the reason, writeswaiting_for_continuation, emits
task.awaiting_continuation, and idles for up to 24 hours.
POST /v1/tasks/{task_id}/continue grants more iterations, more budget, or
both, as a Temporal update that returns whether the grant was accepted. The
grant must match the reason: additional iterations for iteration_limit,
additional budget for budget_exceeded. A mismatched or late grant returns 409.
If the window expires, the task ends failed.
Why not derive status from Temporal
The obvious simplification is to delete the status column and ask Temporal. AgentArea does not, for three reasons. Temporal’s status vocabulary is coarse — running, completed, failed, cancelled — and cannot expressblocked, waiting_for_input, or
waiting_for_continuation, which are the states users actually need to act on.
Temporal’s retention is bounded, so history older than the retention window
stops answering. And a workflow status cannot be joined against a
workspace-scoped SQL query, which is what every list view and every audit
question needs.
The cost of keeping both is a reconciliation rule, and AgentArea states it
explicitly: the database is the source of truth, and Temporal is consulted only
as a recovery oracle for terminal states. _enrich_task_with_workflow_status
upgrades a stale row when Temporal reports a terminal status and ignores
Temporal otherwise.
Limits
- Cancel does not write the row.
DELETE /v1/agents/{agent_id}/tasks/{task_id}cancels the Temporal workflow and returns. It does not persistcancelled.GET /v1/agents/{agent_id}/tasks/{task_id}enriches from Temporal and will reportcancelled; the list endpoint does not enrich, so a cancelled task can still readrunningin a list view and in the database. - There is no wall-clock timeout on a task.
AgentExecutionRequestcarries an optionaltimeout_secondsfield and the workflow never reads it. The only termination conditions are goal achieved, iteration limit, and budget exceeded. A task that keeps making progress runs until one of those trips. - Transitions are unvalidated. Validation checks that a status is a member of the allowed set, not that the move from the previous status is legal. Any status can follow any other.
- The iteration budget has no runtime fallback. The resolved governance
snapshot must provide
execution.max_model_turns. A typedexecution.max_model_turnsrequest may tighten that ceiling and is persisted into the task policy; missing execution policy rejects the run. - Two tasks are not isolated from each other’s spend. The month-to-date cap is checked once at creation. A task already running is not stopped when the workspace crosses the cap; only new task creation is refused.
Related
- Durable execution — what Temporal contributes to the states above, and what it costs.
- Events — the feed those terminal types end.
- Artifacts — what the completion gate validates.