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
Twelve values pass validation (BaseTaskService._validate_task). Eleven 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, and, when a
governance gate refuses the model call, the gate’s code (no_credits,
model_unpriced, billing_unavailable, … or governance_denied).
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.
A follow-up after the window has closed still continues the conversation. When
a completed run ends, it stores where its conversation stands in the task’s
conversation_resume metadata; the follow-up starts a new run of the same
workflow id that adopts that position, keeps the iteration, token, tool-call
and spend totals, and runs under freshly resolved policy (the next governance
snapshot revision). Tasks completed before the snapshot existed are rebuilt
from their conversation log. Cancelled, failed and delegated tasks are not
resumed; a follow-up to them returns 409.
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/workspaces/{workspace}/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.
Waiting for a service
An activity that cannot reach a service it needs retries with backoff (2 s growing to 60 s) forAGENTAREA_TASK_OUTAGE_RETRY_WINDOW, 5 minutes by
default; a short outage goes unnoticed. If the service is still down, the
workflow writes waiting_for_infrastructure, emits
task.awaiting_infrastructure with the service and the error, and retries
after each step of AGENTAREA_TASK_OUTAGE_WAIT_BACKOFF (1, 5, then every 10
minutes). When the service answers it emits task.infrastructure_recovered,
writes running and goes on by itself; the service answering with a final
error (the skill was deleted meanwhile) ends the wait the same way, and the
error goes to the model. Resuming the task retries at once and lifts a pause; a
paused task does not retry, and the time it spends paused does not count toward
the deadline; cancelling stops the wait. Once
AGENTAREA_TASK_OUTAGE_WAIT_DEADLINE (24 hours) has passed, or ten minutes
before the run’s own execution timeout (a delegated agent’s is six hours,
counted from its first run across continue-as-new),
the task ends failed with failure reason dependency_unavailable and a
message naming the service; a retry continues it from there. Only an
unreachable service is waited out (object storage, the database, GitHub); any
other error is final at once. A 24-hour wait adds about 1,700 events to the
run’s history. Skill activation is the one call that waits this way today.
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/workspaces/{workspace}/agents/{agent_id}/tasks/{task_id}cancels the Temporal workflow and returns. It does not persistcancelled.GET /v1/workspaces/{workspace}/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