Skip to main content
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

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 completed immediately, 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 failed with failure_reason: validation_failed.
  • unavailable — the validator itself could not run. The task ends blocked with failure_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, writes waiting_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) for AGENTAREA_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 express blocked, 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 persist cancelled. GET /v1/workspaces/{workspace}/agents/{agent_id}/tasks/{task_id} enriches from Temporal and will report cancelled; the list endpoint does not enrich, so a cancelled task can still read running in a list view and in the database.
  • There is no wall-clock timeout on a task. AgentExecutionRequest carries an optional timeout_seconds field 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 typed execution.max_model_turns request 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.

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
Last modified on October 20, 2026