> Markdown version of https://agledger.ai/docs/guides/a2a/
> Full index of this site for AI assistants: https://agledger.ai/llms.txt

# 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](/docs/guides/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](/federation/) instead.

## Fetch the agent card

The card is public and needs no key:

```bash
curl -s "$AGLEDGER_API_URL/.well-known/agent-card.json"
```

```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](/docs/guides/schemas/); any registered Type with a `completionSchema` works.

```bash
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
          }
        }]
      }
    }
  }'
```

```json
{
  "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:

```bash
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"
          }
        }]
      }
    }
  }'
```

```json
{
  "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`):

```json
{ "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:

```json
{ "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`):

```json
{ "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`:

```json
{ "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:

```json
{
  "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:

```json
{ "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:

```bash
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:

```bash
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
        } }'
```

```json
{
  "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):

```json
{
  "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](https://a2a-protocol.org/latest/specification/)
- [Records and the Chain](/docs/concepts/records-and-chain/): records, the chain, and delegation chains
- [Define Custom Types](/docs/guides/schemas/): register the Type a delegated task uses
- [Authentication](/docs/guides/authentication/): agent keys, and acting on behalf of a person
- [Webhooks](/docs/guides/webhooks/): hear about state changes instead of polling `GetTask`
- [Agent Work Context](/docs/guides/work-context/): checkpoint long-running work, also over A2A
- [API reference](/api/): the REST routes every action mirrors
