Delegate over A2A
AGLedger is an A2A server that speaks A2A 1.0 and 0.3, so independent agents run by different teams, vendors, or platforms can delegate tasks through it. The ledger keeps the ask, the acceptance, the result and the approval on the record, and each record is an A2A Task.
This page takes a delegating agent from the agent card to a delegated task approved by that agent. Each agent needs its own agent API key on the Server (the same agl_ key it uses for REST, see Authentication), and the remote agent must be registered as an agent in the same org. Delegation between organizations, each on its own Server, runs over federation instead.
Fetch the agent card
The card is public and needs no key:
curl -s "$AGLEDGER_API_URL/.well-known/agent-card.json"
{
"name": "AGLedger",
"url": "https://agledger.example.com/a2a",
"version": "<server version>",
"protocolVersion": "0.3",
"preferredTransport": "JSONRPC",
"supportedInterfaces": [
{ "url": "https://agledger.example.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0", "tenant": "" },
{ "url": "https://agledger.example.com/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "0.3", "tenant": "" }
],
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": false,
"extensions": [
{ "uri": "https://agledger.ai/a2a/ext/idempotency-key/v1", "required": false },
{ "uri": "https://agledger.ai/a2a/ext/actions/v1", "required": false }
]
},
"securitySchemes": {
"bearerAuth": { "type": "http", "scheme": "Bearer" }
},
"security": [{ "bearerAuth": [] }],
"defaultInputModes": ["application/json"],
"defaultOutputModes": ["application/json"]
}
The response above is abbreviated. The full card carries a description on every extension and scheme, a skills list with one entry per action, and securityRequirements, the v1.0 spelling of security. The ext/actions/v1 extension lists every action this page uses, with its required and optional fields. The same card is also served at /.well-known/agent.json, the older draft path.
Everything else is one endpoint, POST /a2a, authenticated with Authorization: Bearer <agent key>. It takes a JSON-RPC 2.0 request, or a batch of up to 100.
Choose a dialect
The A2A-Version request header picks the dialect for each request. No header means 0.3. Send A2A-Version: 1.0 for 1.0; anything else is refused rather than downgraded. The response echoes the dialect it served in A2A-Version and sets Vary: A2A-Version. The card itself answers the same header: under 1.0 it drops url, preferredTransport, protocolVersion and security, and lists only the 1.0 interface.
Method names, params, ids and agledger:* metadata are identical in both. What changes is the response:
| 0.3 (no header) | 1.0 | |
|---|---|---|
| Task state | "working", "completed" | "TASK_STATE_WORKING", "TASK_STATE_COMPLETED" |
| Message role | "user", "agent" | "ROLE_USER", "ROLE_AGENT" |
kind field on Task, Message, Part | present | absent |
SendMessage result | the Task, as result | the Task under result.task |
GetTask, CancelTask and ListTasks return their payload directly in both dialects. Wherever a request takes a task state (the ListTasks status param and its filter), either spelling is accepted regardless of the header. The examples below use 0.3; a 1.0 client adds the header and can drop kind from its parts.
Create a record
Every write is a SendMessage whose message carries a data part, and the action field of that part names what to do. create_record takes type and criteria plus any other field POST /v1/records accepts except orgId, validated against the same schema.
Delegation starts from a record the delegating agent performs, so create one first. Agent A names itself as performer and activates the record in the same call. The procurement.po-fulfilled.v1 Type is the example with a completion phase from Define Custom Types; any registered Type with a completionSchema works.
curl -s -X POST "$AGLEDGER_API_URL/a2a" \
-H "Authorization: Bearer $AGENT_A_KEY" -H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "SendMessage",
"params": {
"message": {
"kind": "message",
"role": "user",
"messageId": "a-0001",
"parts": [{
"kind": "data",
"data": {
"action": "create_record",
"type": "procurement.po-fulfilled.v1",
"criteria": { "po_number": "PO-4821", "quantity_ordered": 500 },
"performerAgentId": "019e7a10-3c2d-7a41-8e5f-0b1c2d3e4f50",
"autoActivate": true
}
}]
}
}
}'
{
"jsonrpc": "2.0",
"result": {
"kind": "task",
"id": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"contextId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"status": {
"state": "working",
"message": {
"kind": "message",
"role": "agent",
"messageId": "status-1790352000000",
"contextId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"taskId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"parts": [{ "kind": "text", "text": "Record is active. Submit completion evidence when work is complete." }]
},
"timestamp": "2026-09-25T16:00:00.000Z"
},
"metadata": {
"agledger:type": "procurement.po-fulfilled.v1",
"agledger:platform": "a2a",
"agledger:status": "ACTIVE",
"agledger:performerAgentId": "019e7a10-3c2d-7a41-8e5f-0b1c2d3e4f50",
"agledger:supersededByCount": 0
},
"artifacts": []
},
"id": "req-1"
}
The Task id is the record id. contextId is the root of the delegation chain the record belongs to, so on a root record it is the record's own id. agledger:status is the record's status, and the full metadata also carries agledger:nextSteps, agledger:nextActions and agledger:validTransitions, which name the calls that are valid next.
An agent key is always the principal of the records it creates, so naming any other agent as principalAgentId is refused. To give work to another agent, name it as performerAgentId.
Delegate to a remote agent
In the calls below, agent A (019e7a10-3c2d-...) delegates part of its record to remote agent B (019e7a10-4d3e-...). Every call is a SendMessage like the one above; only the data part and the key change, so the next call is shown in full and the rest show the data part. Responses from here on are abbreviated.
1. Agent A creates the child record. parentRecordId links it to A's record, performerAgentId names B, and gateMode: "principal" holds the completion for A's verdict instead of settling it automatically:
curl -s -X POST "$AGLEDGER_API_URL/a2a" \
-H "Authorization: Bearer $AGENT_A_KEY" -H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-2",
"method": "SendMessage",
"params": {
"message": {
"kind": "message",
"role": "user",
"messageId": "a-0002",
"parts": [{
"kind": "data",
"data": {
"action": "create_record",
"type": "procurement.po-fulfilled.v1",
"criteria": { "po_number": "PO-4821-B", "quantity_ordered": 200 },
"parentRecordId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"performerAgentId": "019e7a10-4d3e-7b52-9f60-1c2d3e4f5061",
"gateMode": "principal"
}
}]
}
}
}'
{
"jsonrpc": "2.0",
"result": {
"kind": "task",
"id": "019e7a11-2b3c-7d4e-9f50-617283940a1b",
"contextId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"status": { "state": "submitted", "timestamp": "2026-09-25T16:01:00.000Z" },
"metadata": {
"agledger:status": "CREATED",
"agledger:performerAgentId": "019e7a10-4d3e-7b52-9f60-1c2d3e4f5061",
"agledger:parentRecordId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"agledger:chainDepth": 1
}
},
"id": "req-2"
}
The child's contextId is the parent's id: every record in one delegation chain shares the root's contextId. A child's principal must be the parent's performer, so only agent A can delegate from its record, and the parent must be ACTIVE or RECORDED.
2. Agent A proposes it (key $AGENT_A_KEY):
{ "action": "transition", "recordId": "019e7a11-2b3c-7d4e-9f50-617283940a1b", "transition": "propose" }
The Task stays submitted and agledger:status becomes PROPOSED.
3. Agent B accepts (key $AGENT_B_KEY). message is optional; reason and notes are accepted as aliases:
{ "action": "accept_proposal", "recordId": "019e7a11-2b3c-7d4e-9f50-617283940a1b", "message": "Can deliver 200 by Friday." }
B can answer with reject_proposal instead, which ends the Task as rejected.
4. Agent A activates it (key $AGENT_A_KEY):
{ "action": "transition", "recordId": "019e7a11-2b3c-7d4e-9f50-617283940a1b", "transition": "activate" }
The Task moves to working. Steps 2 to 4 are the consent handshake, and they are optional: pass "autoActivate": true on the create in step 1 and the child starts ACTIVE, with the chain stating that B never accepted.
5. Agent B submits its completion (key $AGENT_B_KEY). The evidence must validate against the Type's completionSchema:
{ "action": "submit_completion", "recordId": "019e7a11-2b3c-7d4e-9f50-617283940a1b", "evidence": { "quantity_delivered": 200 } }
The response is the Task, still working because the verdict is A's, with the completion attached:
{
"jsonrpc": "2.0",
"result": {
"kind": "task",
"id": "019e7a11-2b3c-7d4e-9f50-617283940a1b",
"contextId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"status": { "state": "working", "timestamp": "2026-09-25T16:20:00.000Z" },
"metadata": { "agledger:status": "PROCESSING" },
"artifacts": [{
"artifactId": "019e7a12-3c4d-7e5f-8061-728394a5b6c7",
"name": "completion-019e7a12-3c4d-7e5f-8061-728394a5b6c7",
"parts": [{ "kind": "data", "data": { "quantity_delivered": 200 } }],
"metadata": { "agledger:completionId": "019e7a12-3c4d-7e5f-8061-728394a5b6c7" }
}]
},
"id": "req-5"
}
6. Agent A renders the verdict (key $AGENT_A_KEY). The completionId is the artifact's artifactId, which A reads with GetTask (next section). verdict is accept or reject, and notes is optional:
{ "action": "submit_verdict", "recordId": "019e7a11-2b3c-7d4e-9f50-617283940a1b", "completionId": "019e7a12-3c4d-7e5f-8061-728394a5b6c7", "verdict": "accept", "notes": "Received and counted." }
The Task ends completed and agledger:status is FULFILLED. A reject ends it failed.
The child's principal is always the parent's performer, here agent A, and a verdict is the principal's call (or an org admin's), so agent B's key is refused if it tries to approve its own result.
Read state
GetTask returns one Task with its completions as artifacts and its message history. Pass the id as id, taskId, or name (tasks/<id>, the 1.0 form); historyLength keeps the most recent N messages:
curl -s -X POST "$AGLEDGER_API_URL/a2a" \
-H "Authorization: Bearer $AGENT_A_KEY" -H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": "req-7", "method": "GetTask",
"params": { "id": "019e7a11-2b3c-7d4e-9f50-617283940a1b" } }'
ListTasks returns the tasks the key is a party to, newest first. Delegation is walked with the filter param, which takes the AGLedger term parentRecordId = "<id>" for the direct children of one record:
curl -s -X POST "$AGLEDGER_API_URL/a2a" \
-H "Authorization: Bearer $AGENT_A_KEY" -H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": "req-8", "method": "ListTasks",
"params": {
"filter": "parentRecordId = \"019e7a11-0a1b-7c2d-8e3f-405162738495\" AND status.state = \"working\"",
"pageSize": 20
} }'
{
"jsonrpc": "2.0",
"result": {
"tasks": [{
"kind": "task",
"id": "019e7a11-2b3c-7d4e-9f50-617283940a1b",
"contextId": "019e7a11-0a1b-7c2d-8e3f-405162738495",
"status": { "state": "working", "timestamp": "2026-09-25T16:20:00.000Z" }
}],
"nextPageToken": "",
"pageSize": 20,
"totalSize": 1
},
"id": "req-8"
}
The filter grammar is a subset of AIP-160: status.state = "<state>", status.state IN ("<state>", ...), createTime and updateTime compared with =, >, >=, < or <= against an ISO 8601 timestamp, and the AGLedger terms parentRecordId = "<id>", type = "<type>" and superseded = false, joined with AND. OR and functions are refused. The other params are status, contextId (every record in one delegation chain), pageSize (default 50, maximum 100), pageToken, historyLength, includeArtifacts and statusTimestampAfter. An unrecognized param is refused rather than ignored, so limit returns an error that names pageSize.
Task states
A Task's state comes from the record's status, the value in agledger:status:
| Record status | A2A 0.3 | A2A 1.0 |
|---|---|---|
CREATED | submitted | TASK_STATE_SUBMITTED |
PROPOSED | submitted | TASK_STATE_SUBMITTED |
ACTIVE | working | TASK_STATE_WORKING |
PROCESSING | working | TASK_STATE_WORKING |
REVISION_REQUESTED | working | TASK_STATE_WORKING |
DISPUTED | working | TASK_STATE_WORKING |
FULFILLED | completed | TASK_STATE_COMPLETED |
RECORDED | completed | TASK_STATE_COMPLETED |
FAILED | failed | TASK_STATE_FAILED |
REMEDIATED | failed | TASK_STATE_FAILED |
EXPIRED | failed | TASK_STATE_FAILED |
CANCELLED | canceled | TASK_STATE_CANCELED |
REJECTED | rejected | TASK_STATE_REJECTED |
RECORDED is a notarize-only record, which is complete the moment it is created. No record is ever input-required or auth-required. Several statuses share one Task state, so branch on agledger:status when the difference matters (for example PROCESSING, a completion waiting on its verdict, against ACTIVE, still waiting on a completion).
Errors and limits
Branch on error, not on the HTTP status. Once a request is dispatched, every answer is HTTP 200, including errors. error.data is an array holding one google.rpc.ErrorInfo, whose metadata values are all strings (lists arrive JSON-encoded):
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": [{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "INVALID_PARAMS",
"domain": "a2a-protocol.org",
"metadata": {
"detail": "Unknown action 'create_task'. Supported actions: create_record, submit_completion, get_status, transition, submit_verdict, accept_proposal, reject_proposal, open_dispute.",
"errorCode": "VALIDATION_ERROR",
"requestId": "5b0e2c1f-8a4d-4e27-9c61-3f7a9d20b8e4"
}
}]
},
"id": "req-9"
}
A rejection caused by record state also carries currentState, allowedActions, recoveryHint and refreshUrl in metadata. Quote requestId when reporting a fault.
| Code | Reason | When |
|---|---|---|
-32700 | PARSE_ERROR | The body is not JSON (HTTP 400). |
-32600 | INVALID_REQUEST | The envelope is malformed (HTTP 400) or the key is missing or invalid (HTTP 401). |
-32600 | FORBIDDEN | The key lacks a scope (missingScopes lists it) or its agent is not the party the action needs. |
-32601 | METHOD_NOT_FOUND | Unknown method. |
-32602 | INVALID_PARAMS | A missing, unknown or mistyped field, a filter outside the grammar, or an action the record's state does not allow; errorCode gives the specific cause. |
-32001 | TASK_NOT_FOUND | No record with that id that the key can see. |
-32002 | TASK_NOT_CANCELABLE | CancelTask on a record that is past cancelling. |
-32009 | VERSION_NOT_SUPPORTED | An A2A-Version other than 0.3 or 1.0 (HTTP 400). |
-32603 | INTERNAL_ERROR | A server fault or the per-call deadline (30 seconds by default); carries retryable. |
-32000 | varies | Other refusals before dispatch, including the rate limit (HTTP 429). |
Rate limit. /a2a allows 200 operations a minute per key, the same budget as POST /v1/records. A batch costs one operation per entry, so a batch of 100 spends half the minute. Operators raise it with RATE_LIMIT_A2A_OPS.
Idempotency. create_record, transition and submit_completion take an idempotencyKey field in the data part; other actions refuse it. Repeating a key with the same fields returns the original result without doing the work again, and repeating it with different fields is refused with -32602. A duplicate that arrives while the first is still in flight is also -32602, but carries retryable: "true": retry only then. On create_record and transition the key lasts 7 days and is capped at 256 characters; on submit_completion it is scoped to the record, capped at 255 characters, and binds to the evidence, so send a fresh key when the evidence changes.
Related
- A2A specification
- Records and the Chain: records, the chain, and delegation chains
- Define Custom Types: register the Type a delegated task uses
- Authentication: agent keys, and acting on behalf of a person
- Webhooks: hear about state changes instead of polling
GetTask - Agent Work Context: checkpoint long-running work, also over A2A
- API reference: the REST routes every action mirrors