The A2A task lifecycle, state by state
Every A2A v1.0 task state, which ones are final, how multi-turn input works, how to follow a task, and the errors you hit, with a live exchange.
An A2A task is the unit of work a remote agent creates in response to a message. It has a server-generated id, a contextId that groups related work, and a status whose state is one of eight TASK_STATE_* values. Four of them are terminal: COMPLETED, FAILED, CANCELED and REJECTED. Once a task reaches one, it never changes again. Two are interrupted: INPUT_REQUIRED and AUTH_REQUIRED, where the agent waits for the client. SUBMITTED and WORKING mean the task is in progress.
This guide walks through each state, the transitions the A2A v1.0 specification allows, multi-turn exchanges on one taskId, reading and canceling tasks, the three ways to follow progress, the error codes you meet, and the v0.3 names. It ends with a real multi-turn exchange against a live agent.
Tasks, messages and contexts
SendMessage returns either a Task or a plain Message. A Message suits a quick answer that needs no tracking. A Task suits anything with state: work that takes time, asks follow-up questions, or produces outputs. The A2A documentation describes three agent styles: message-only agents, agents that always create tasks, and hybrids that negotiate with messages and then create a task.
A Task in v1.0 has these fields:
| Field | Notes |
|---|---|
id |
Required. Generated by the server; clients cannot choose it for a new task |
contextId |
Groups related tasks and messages |
status |
Required. Holds state, an optional agent message, and a timestamp |
artifacts |
The task’s outputs, made of parts |
history |
Messages exchanged during the task, as far as the agent keeps them |
metadata |
Free-form key/value data |
The specification separates communication from results. Status messages explain progress or ask for input. Outputs belong in artifacts.
The states
| State | Meaning | Category |
|---|---|---|
TASK_STATE_UNSPECIFIED |
Zero value of the enum: state unknown or indeterminate | None |
TASK_STATE_SUBMITTED |
Accepted and acknowledged, not yet being processed | In progress |
TASK_STATE_WORKING |
The agent is processing the task | In progress |
TASK_STATE_INPUT_REQUIRED |
The agent needs more input from the client to continue | Interrupted |
TASK_STATE_AUTH_REQUIRED |
The agent needs authorization to continue | Interrupted |
TASK_STATE_COMPLETED |
Finished successfully | Terminal |
TASK_STATE_FAILED |
Finished with an error | Terminal |
TASK_STATE_CANCELED |
Canceled before completion | Terminal |
TASK_STATE_REJECTED |
The agent decided not to perform it, at creation or later | Terminal |
In JSON, states appear as these exact strings. The specification uses ProtoJSON, which writes enum values by their names in the proto.
Legal transitions
The specification does not publish a transition table. Its rules still pin the lifecycle down:
- A task in a terminal state accepts no further messages, cannot be canceled, and cannot be subscribed to. The A2A documentation says a finished task cannot restart; follow-up work becomes a new task in the same context.
- A task in an interrupted state accepts new messages on the same
taskId. - A blocking
SendMessagereturns when the task reaches a terminal or interrupted state. - An agent may reject a task when it is created or later.
- Cancellation applies only to tasks that have not reached a terminal state.
SendMessage (new task)
|
v
SUBMITTED ----> WORKING ----> COMPLETED, FAILED, CANCELED or REJECTED
| ^ (terminal: the task never changes again)
v |
INPUT_REQUIRED or AUTH_REQUIRED
(interrupted: waits for the client's next message on the same
taskId, or for a credential delivered out of band)
Any non-terminal state can also move straight to a terminal state:
CancelTask leads to CANCELED, and the agent may fail or reject at any point.
| From | Next states | Caused by |
|---|---|---|
| No task yet | Usually SUBMITTED or WORKING; an agent may also finish, reject or interrupt in the same call |
The client’s first message |
SUBMITTED |
WORKING, an interrupted state, or a terminal state |
The agent starts, pauses or ends the work; the client cancels |
WORKING |
An interrupted state or a terminal state | Same as above |
INPUT_REQUIRED |
WORKING or whatever the new input leads to; CANCELED, FAILED, REJECTED |
A client message on the same taskId; a cancel request |
AUTH_REQUIRED |
Same as INPUT_REQUIRED |
A credential arrives, the client sends a message, or the client cancels |
COMPLETED, FAILED, CANCELED, REJECTED |
None | The task is final |
Multi-turn tasks
Input required
When the agent needs something, it sets TASK_STATE_INPUT_REQUIRED and explains what it needs in status.message. The client answers with a new message that carries the same taskId and contextId. Rules from section 3.4:
- The
taskIdin a client message must refer to an existing task, or the agent returnsTaskNotFoundError. - If the client sends only
taskId, the agent infers thecontextIdfrom the task. - If the client sends both and they don’t match, the agent must reject the message.
- A message with a
contextIdand notaskIdstarts a new task in that conversation.
Auth required
TASK_STATE_AUTH_REQUIRED (section 7.6) is how an agent asks the client to supply authorization mid-task, such as an OAuth token or a person’s approval before a destructive action. The agent must track the work as a task, move it to this state, and explain the requirement in a status message, unless the details were agreed out of band or through an extension.
By default the credential travels out of band, directly to the agent. The agent may then resume without any follow-up message from the client. A client with no open stream could miss that, so the specification says it should subscribe, register a webhook, or poll. Agents should still accept messages on the task while it waits, so the client can negotiate, correct or decline the request. A client that is itself an agent may pass the request up by putting its own task into AUTH_REQUIRED, forming a chain.
The protocol does not define the scope, format, validity or revocation of the resulting credential. Section 7.6.4 also says an agent must not treat the state change itself as authorization for any operation. Implementations or extensions have to define that.
After a task ends
A terminal task is immutable. To refine a result, send a new message in the same contextId and list the earlier task in referenceTaskIds. The agent creates a new task.
Reading a task: GetTask and historyLength
GetTask takes the task id and an optional historyLength. The same parameter appears in SendMessage as configuration.historyLength.
historyLength |
Result |
|---|---|
| Not set | The server’s default amount of history, which may be all of it |
0 |
No history; the specification says the history field should be omitted |
| A positive number | At most that many of the most recent messages |
The server must not return more messages than requested but may return fewer. The agent also decides which messages to persist at all, so clients should not treat history as a reliable delivery channel.
Canceling: CancelTask
CancelTask asks the agent to stop a task and returns its updated state. Success is not guaranteed. A task that is already COMPLETED, FAILED, CANCELED or REJECTED returns TaskNotCancelableError (-32002). Cancel is idempotent: repeating it has the same effect, though a repeat may return TaskNotFoundError if the agent has already purged the task.
Following progress: polling, streaming and push
| Mechanism | Operations | Requires | Best for |
|---|---|---|---|
| Polling | GetTask |
Nothing extra | Simple clients, infrequent updates, restrictive networks |
| Streaming | SendStreamingMessage, SubscribeToTask |
capabilities.streaming: true |
Interactive clients and live progress |
| Push notifications | CreateTaskPushNotificationConfig plus get, list and delete |
capabilities.pushNotifications: true |
Long-running, server-to-server work |
Streaming over the JSON-RPC binding uses Server-Sent Events (text/event-stream). Each event is a JSON-RPC response whose result holds exactly one of task, message, statusUpdate or artifactUpdate. A task stream starts with the Task and closes when the task reaches a terminal state. v1.0 removed the old final flag; the stream closing marks the end. SubscribeToTask also sends the current Task first, so a client that just called GetTask misses nothing. Several streams may watch one task, and all receive the same events in the same order.
Push notifications are HTTP POSTs to a client webhook with the same StreamResponse shape. Agents must attempt delivery at least once and may retry, so webhook handlers should be idempotent and reply with a 2xx status.
Execution mode matters too. By default SendMessage blocks until the task is terminal or interrupted. With configuration.returnImmediately: true, it returns right away with an in-progress task, and the client follows up by polling, streaming or push.
If a client calls a streaming operation on an agent without the capability, the agent must return UnsupportedOperationError. Push configuration calls return PushNotificationNotSupportedError instead.
Errors in task handling
| Code | Error | Typical cause |
|---|---|---|
-32001 |
TaskNotFoundError |
Unknown, inaccessible or purged task ID in GetTask, CancelTask, SubscribeToTask or a message’s taskId |
-32002 |
TaskNotCancelableError |
CancelTask on a terminal task |
-32003 |
PushNotificationNotSupportedError |
Push configuration on an agent without the capability |
-32004 |
UnsupportedOperationError |
A message or subscription to a terminal task; streaming on an agent without it |
-32005 |
ContentTypeNotSupportedError |
A part’s media type the agent or skill does not accept |
-32009 |
VersionNotSupportedError |
An A2A-Version the agent does not serve |
-32602 |
Invalid params | A missing or malformed required field |
A2A errors in JSON-RPC carry a data array. Its entries use the google.rpc.ErrorInfo type with a reason and the domain a2a-protocol.org. The specification also says servers should not reveal whether a task exists when the caller cannot access it, so an unknown task, a deleted task and someone else’s task all look like -32001. A real one from the agent used below:
{
"jsonrpc": "2.0",
"id": 6,
"error": {
"code": -32001,
"message": "Task not found",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "TASK_NOT_FOUND",
"domain": "a2a-protocol.org"
}
]
}
}
v0.3 names
A2A v1.0 renamed the states and methods. The specification tells agents to treat a request with no A2A-Version value as v0.3, so v1.0 clients must send the header.
| v0.3 | v1.0 |
|---|---|
submitted |
TASK_STATE_SUBMITTED |
working |
TASK_STATE_WORKING |
input-required |
TASK_STATE_INPUT_REQUIRED |
auth-required |
TASK_STATE_AUTH_REQUIRED |
completed |
TASK_STATE_COMPLETED |
failed |
TASK_STATE_FAILED |
canceled |
TASK_STATE_CANCELED |
rejected |
TASK_STATE_REJECTED |
unknown |
No listed mapping; the v1.0 zero value is TASK_STATE_UNSPECIFIED |
message/send, message/stream |
SendMessage, SendStreamingMessage |
tasks/get, tasks/cancel, tasks/resubscribe |
GetTask, CancelTask, SubscribeToTask |
Roles user, agent |
ROLE_USER, ROLE_AGENT |
v1.0 also dropped the kind discriminator (the JSON member name now identifies the type) and added ListTasks.
Worked example: a multi-turn signup on a live agent
Emissar runs a public A2A agent at https://emissar.ai/a2a/v1 (JSON-RPC binding, A2A 1.0). Its Agent Card declares streaming and pushNotifications as false, so polling with GetTask is how a client follows a task. Its request-early-access skill returns TASK_STATE_INPUT_REQUIRED when the email address is missing. The calls below ran on 2026-09-25; IDs and timestamps are real, and responses are formatted for reading. The address is a documentation placeholder. The agent registers whatever address you send.
1. Ask without an email
curl -s https://emissar.ai/a2a/v1 \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"messageId": "smoke-test-docs-1",
"parts": [{ "text": "Please sign up Example Docs Co for Emissar early access." }]
}
}
}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"task": {
"id": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287",
"status": {
"state": "TASK_STATE_INPUT_REQUIRED",
"timestamp": "2026-09-25T20:50:43.975Z",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Which email address should I register? Send it as text, or as a data part like {\"email\": \"name@company.com\"}. You can also include name, company, role, side, program and useCase."
}
],
"messageId": "c3f6dfc3-68ee-44fd-a6e4-1af9ccb774fd",
"taskId": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287"
}
},
"artifacts": [],
"history": [
{
"role": "ROLE_USER",
"parts": [
{
"text": "Please sign up Example Docs Co for Emissar early access."
}
],
"messageId": "smoke-test-docs-1",
"taskId": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287"
}
],
"metadata": {
"skill": "request-early-access"
}
}
}
}
The agent created a task and stopped in an interrupted state. The question is in status.message, and artifacts is empty because nothing has been produced yet. This agent always answers with a Task, even for simple questions.
2. Read the task
curl -s https://emissar.ai/a2a/v1 \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-d '{"jsonrpc": "2.0", "id": 2, "method": "GetTask",
"params": {"id": "b2cd8521-5746-40f3-bc0e-fe195348908c", "historyLength": 0}}'
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287",
"status": {
"state": "TASK_STATE_INPUT_REQUIRED",
"timestamp": "2026-09-25T20:50:43.975Z",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Which email address should I register? Send it as text, or as a data part like {\"email\": \"name@company.com\"}. You can also include name, company, role, side, program and useCase."
}
],
"messageId": "c3f6dfc3-68ee-44fd-a6e4-1af9ccb774fd",
"taskId": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287"
}
},
"artifacts": [],
"metadata": {
"skill": "request-early-access"
}
}
}
GetTask returns the Task directly, without a task wrapper. With historyLength set to 0, the history field is left out, as the specification says it should be.
3. Answer on the same task
curl -s https://emissar.ai/a2a/v1 \
-H 'Content-Type: application/json' \
-H 'A2A-Version: 1.0' \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"messageId": "smoke-test-docs-2",
"taskId": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287",
"parts": [{ "text": "docs-example@example.com" }]
},
"configuration": { "historyLength": 2 }
}
}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"task": {
"id": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287",
"status": {
"state": "TASK_STATE_COMPLETED",
"timestamp": "2026-09-25T20:50:55.916Z"
},
"artifacts": [
{
"artifactId": "7c09674c-67a2-4e1c-be2e-9efd2e7a6ebb",
"name": "Registration",
"parts": [
{
"text": "Registered docs-example@example.com for early access. A person at Emissar will follow up by email. This registration arrived over A2A, which is exactly the kind of exchange Emissar is built for."
},
{
"data": {
"registered": true,
"email": "docs-example@example.com",
"program": "early_access"
},
"mediaType": "application/json"
}
]
}
],
"history": [
{
"role": "ROLE_USER",
"parts": [
{
"text": "Please sign up Example Docs Co for Emissar early access."
}
],
"messageId": "smoke-test-docs-1",
"taskId": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287"
},
{
"role": "ROLE_USER",
"parts": [
{
"text": "docs-example@example.com"
}
],
"messageId": "smoke-test-docs-2",
"taskId": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287"
}
],
"metadata": {
"skill": "request-early-access"
}
}
}
}
Same task ID, now TASK_STATE_COMPLETED. The result is an artifact with a text part and a structured data part. The history holds the two client messages but not the agent’s earlier question: the agent decides which messages to persist, and this one keeps only incoming messages.
4. What a terminal state blocks
Canceling the finished task (CancelTask with "params": {"id": "b2cd8521-5746-40f3-bc0e-fe195348908c"}):
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32002,
"message": "Task cannot be canceled because it has already ended",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "TASK_NOT_CANCELABLE",
"domain": "a2a-protocol.org"
}
]
}
}
Sending another message to it (SendMessage with the same taskId and messageId smoke-test-docs-3):
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32004,
"message": "This task has already ended. Send a new message without taskId to start another.",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "UNSUPPORTED_OPERATION",
"domain": "a2a-protocol.org"
}
]
}
}
Both follow the specification: -32002 for canceling a terminal task, and -32004 for messaging one. A follow-up here would be a new message without taskId, which starts a new task.
The same task over v0.3
A tasks/get call with no A2A-Version header is treated as v0.3. The same task comes back with a kind field and the lowercase state (artifacts omitted here):
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"kind": "task",
"id": "b2cd8521-5746-40f3-bc0e-fe195348908c",
"contextId": "7efc23bc-5cbc-4c1e-b1e0-ec93f9e18287",
"status": {
"state": "completed",
"timestamp": "2026-09-25T20:50:55.916Z"
},
"metadata": {
"skill": "request-early-access"
}
}
}
Questions
- Can a completed task be reopened?
- No. COMPLETED, FAILED, CANCELED and REJECTED are final. A message sent to a finished task gets an UnsupportedOperationError (-32004). To follow up, send a new message with the same contextId, optionally listing the old task in referenceTaskIds; the agent starts a new task.
- What is the difference between taskId and contextId?
- The server generates a taskId for one unit of work. A contextId groups related tasks and messages into one conversation. To answer an interrupted task, send its taskId (and contextId). To start related new work, send only the contextId.
- Should a client poll, stream or register a webhook?
- Check the Agent Card first. Streaming needs capabilities.streaming set to true, and push notifications need capabilities.pushNotifications set to true. Polling with GetTask works with every agent. Streaming suits interactive clients; webhooks suit long-running, server-to-server work.
Sources
- A2A Protocol Specification (sections 3, 5.4, 7.6 and 9) (accessed )
- A2A protocol buffer definition (a2a.proto) (accessed )
- Life of a Task (A2A documentation) (accessed )
- What's new in A2A v1.0 (A2A documentation) (accessed )
- A2A v0.3.0 type definitions (TaskState) (accessed )
- Emissar Agent Card (live example agent) (accessed )