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

# Delegation and References

A record names two agents: the **principal**, who asks for the work and renders the verdict on
it, and the **performer**, who does it. When the performer needs another agent for part of the
work, it delegates that part as a child record. The records form a delegation chain, and every
link in it is signed. References then tie any of those records to the identifiers your other
systems already use.

The calls below use one key per agent: `$ORCHESTRATOR_KEY`, `$COORDINATOR_KEY` and
`$SOURCING_KEY`, each an `agent-full` key minted for that agent with `POST /v1/admin/api-keys`.
For delegation between agents on different hosts over the A2A protocol, see
[Delegate over A2A](/docs/guides/a2a/).

## Ask another agent to do the work

The principal writes the record, naming the performer:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $ORCHESTRATOR_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "type": "principal-gate-generic-v1",
    "gateMode": "principal",
    "performerAgentId": "'"$COORDINATOR_ID"'",
    "criteria": {
      "summary": "Source 1,800 HX-4471-B seal kits for the Boise plant",
      "poRef": "REQ-2026-08817",
      "quantity": 1800,
      "deliverBy": "2026-11-14"
    }
  }'
```

The record lands at `CREATED`. The principal can take either of two routes from here:

- **Record the performer's agreement.** Propose the record:

  ```bash
  curl -s -X POST "$AGLEDGER_API_URL/v1/records/$RECORD_ID/transition" \
    -H "Authorization: Bearer $ORCHESTRATOR_KEY" -H 'Content-Type: application/json' \
    -H "Idempotency-Key: $(uuidgen)" -d '{"action":"propose"}'
  ```

- **Assign it without asking.** Send `"autoActivate": true` on the create instead. The record
  goes straight to `ACTIVE` with `acceptanceStatus: "BYPASSED"`, and the chain records that
  the performer never opted in.

## Find it, and accept or reject it

The performer finds open proposals in its inbox:

```bash
curl -s "$AGLEDGER_API_URL/v1/records/agent/proposals" -H "Authorization: Bearer $COORDINATOR_KEY"
```

```json
{
  "data": [
    {
      "id": "01a0dc02-c41d-7239-b2cb-2960d1476c65",
      "type": "principal-gate-generic-v1",
      "status": "PROPOSED",
      "principalAgentId": "01a0dc02-c39e-7685-ba2d-1998031b0a41",
      "criteria": { "summary": "Source 1,800 HX-4471-B seal kits for the Boise plant" }
    }
  ]
}
```

`GET /v1/records?actionable=true` returns the same proposal alongside everything else waiting
on this agent. That query is the one to use when an agent restarts or misses a notification.

The terms are fixed at creation. There is no counter-proposal: the performer either accepts
them as written or rejects them. To accept:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records/$RECORD_ID/accept" \
  -H "Authorization: Bearer $COORDINATOR_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" -d '{"message":"Accepted as written."}'
```

The record returns to `CREATED` with `acceptanceStatus: "ACCEPTED"`. The principal then
activates it (`{"action":"activate"}` on `/transition`), and it becomes `ACTIVE`.

To refuse the terms, the performer calls `/reject`, and the record ends at `REJECTED`:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records/$RECORD_ID/reject" \
  -H "Authorization: Bearer $COORDINATOR_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" -d '{"reason":"Part is discontinued; no supplier quotes it."}'
```

The `message` or `reason` is signed into the `RECORD_ACCEPTED` or `RECORD_REJECTED` chain entry,
kept on the record as `lastTransitionReason` until the next transition replaces it, and carried
as `data.reason` on the `record.proposal_accepted` / `record.proposal_rejected` event and webhook.

A principal that wants different terms creates a new record.

## Delegate part of the work onward

Only the record's performer can delegate from it. It creates the child record with
`parentRecordId`, and it becomes the child's principal:

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $COORDINATOR_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "type": "principal-gate-generic-v1",
    "gateMode": "principal",
    "parentRecordId": "'"$RECORD_ID"'",
    "performerAgentId": "'"$SOURCING_ID"'",
    "criteria": { "summary": "Quote 1,800 HX-4471-B, FKM only, ISO 9001, delivered Boise by 2026-11-14" }
  }'
```

```json
{
  "id": "01a0dc02-c502-78fc-b975-573515055db3",
  "status": "CREATED",
  "parentRecordId": "01a0dc02-c41d-7239-b2cb-2960d1476c65",
  "rootRecordId": "01a0dc02-c41d-7239-b2cb-2960d1476c65",
  "chainDepth": 1,
  "principalAgentId": "01a0dc02-c3cb-78a3-b252-aa6f571634b0",
  "performerAgentId": "01a0dc02-c3e7-7e98-b919-a383e4121197"
}
```

The child goes through the same propose, accept and activate steps, or `autoActivate`. A few
rules apply to it:

- **The parent must be `ACTIVE` or `RECORDED`.** A notarize-only parent can anchor children too.
- **Its deadline cannot be later than the parent's.** A child created without a `deadline` takes
  the parent's, which the create response shows, so it expires with the parent. A child of a
  parent with no deadline, or of a notarize-only (`RECORDED`) parent, has none unless it sets one.
- **Depth is capped.** The default is 5 levels (`limits.delegationMaxDepthDefault` on
  `GET /v1/conformance`). A platform key can set an org's own cap, up to
  `limits.delegationMaxDepthCeiling`.
- **Any other agent is refused (403 `CALLER_NOT_PARENT_PERFORMER`).** The `recoveryHint`
  says that an org-admin key can create the child by passing the parent's performer as
  `principalAgentId`. It names that performer only to a caller that can already read the
  parent: an org-admin key, or the parent's principal.

Only `criteria` is carried to the child. Put in it everything the child's performer needs: it
is the performer's record of what was asked.

## Read the tree

A party to a record reads its direct children, or the whole tree from the root:

```bash
curl -s "$AGLEDGER_API_URL/v1/records/$RECORD_ID/sub-records" -H "Authorization: Bearer $ORCHESTRATOR_KEY"
curl -s "$AGLEDGER_API_URL/v1/records/$CHILD_ID/chain" -H "Authorization: Bearer $ORCHESTRATOR_KEY"
```

`/chain` returns the tree from the root, depth-ordered, with each record's `parentRecordId` and
`chainDepth`. It carries the records your key could read one at a time: a party on a record
sees it and the whole subtree under it, so the orchestrator sees every branch and a subagent
sees its own branch but not the root or its siblings. The parent's own body also carries
`hasChildren` and `childRecordIds`. To page through the children of one parent with filters,
use `GET /v1/records/search?parentRecordId=<id>`.

## What is signed at creation

The child's first chain entry is `RECORD_DELEGATED`. Its actor is the delegating agent, and
its signed payload carries the link:

```json
{
  "entryType": "RECORD_DELEGATED",
  "actorDisplayName": "procurement-coordinator",
  "payload": {
    "recordId": "01a0dc02-c502-78fc-b975-573515055db3",
    "parentRecordId": "01a0dc02-c41d-7239-b2cb-2960d1476c65",
    "chainDepth": 1,
    "principalAgentId": "01a0dc02-c3cb-78a3-b252-aa6f571634b0",
    "performerAgentId": "01a0dc02-c3e7-7e98-b919-a383e4121197",
    "criteria": { "summary": "Quote 1,800 HX-4471-B, FKM only, ISO 9001, delivered Boise by 2026-11-14" }
  }
}
```

Get it with `GET /v1/records/{id}/audit-export`. The parent link, both named agents and the
child's criteria are inside the signature, so none of them can be changed afterwards without
the export failing verification.

That entry is the delegating agent's statement. The work itself is attributed to the child's
performer when it submits its completion (`COMPLETION_SUBMITTED`).

## Who renders the verdict on delegated work

The verdict on a child belongs to the child's principal, which is the agent that delegated it,
or to an org-admin key:

- **The performer cannot render it.** A performer that tries to render its own verdict on an
  open record gets a 403 `WRONG_STRUCTURAL_ROLE`.
- **The parent's principal does not inherit it.** Authority over a sub-record stays with the
  sub-record's named principal.

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records/$CHILD_ID/verdict" \
  -H "Authorization: Bearer $COORDINATOR_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" -d '{"completionId":"'"$COMPLETION_ID"'","verdict":"accept"}'
```

The verdict itself, the Settlement Signal webhook it fires, revisions and disputes are covered in
[Approvals](/docs/guides/approvals/).

## How children affect the parent

A parent is graded on its own completion, like any record. Its children can fail it, and
nothing they do settles it: a child that delivered only says the delegated work passed, not that
the parent's own criteria were met. Once every child has ended, the rollup reads them:

- **No child failed.** The parent stays `ACTIVE` and its performer submits the parent's own
  completion, which the parent's gate grades. The parent refuses that completion while any child
  is still open, and until then reads `awaitingActor: "system"` and sits in neither party's
  `?actionable=true` queue: the rollup moves it once the children settle.
- **A child failed.** The parent fails in the same step as the child's last transition (a
  verdict, a cancel, an expiry). An `auto` parent goes to `FAILED`, which stays recoverable. A
  `principal` parent waits at `PROCESSING` with `awaitingActor: "principal"`, carrying the
  failing rollup in `verdictChecks.auto_rollup` and a synthesized `latestCompletionId`. No agent
  submitted that completion, so the principal's verdict on it takes `reject` only; `accept` is
  refused with a 422 whose `allowedActions` is `["verdict_reject"]`.
- **A parent already past its own deadline** when the rollup reads the failure is expired
  instead, in either mode: `EXPIRED` with `terminalReason: TIMED_OUT` and a `record.expired`
  event, and no `cascading.gate.complete`. An expired parent counts as a failed child of its own
  parent, the same as a failed one.

```json
{
  "status": "PROCESSING",
  "awaitingActor": "principal",
  "verdictChecks": { "auto_rollup": { "pass": false } }
}
```

In either mode, the parent reaches `FULFILLED` with a `SETTLE` signal only on a completion its
own performer submits. After a failure, the parent's principal requests a revision
(`POST /v1/records/{id}/revision`; in `principal` mode, after rejecting the synthesized
completion). The performer then submits the parent's own completion to
`POST /v1/records/{id}/completions`, which the parent's gate grades in `auto` mode, and which
waits for the principal's verdict in `principal` mode, where `accept` settles it. The one other
way to `FULFILLED` is a dispute of the failure that the principal resolves `OVERTURNED`, which
signals `RELEASE`.

What a child counts as in the rollup:

| Child status | Rollup |
|---|---|
| `FULFILLED`, or `REMEDIATED` after an overturned dispute | pass |
| `RECORDED`, or `CANCELLED` with `terminalReason: CANCEL_PRE_WORK` | neutral |
| `REJECTED`, `EXPIRED` (`terminalReason: TIMED_OUT`), `CANCELLED` with `terminalReason: CANCEL_IN_PROGRESS`, `FAILED` that can no longer be revised | fail |

Work starts at activation. A child cancelled while `CREATED` or `PROPOSED` is neutral; one
cancelled once `ACTIVE` fails the parent, whether or not anything was submitted. A child created
with `autoActivate` is `ACTIVE` from its create response, so it has no neutral cancel window:
leave `autoActivate` off on a child you may withdraw, and activate it when the work begins. The
cancel step in a child's `nextSteps` says which of the two a cancel would be.

A child at `FAILED` has not ended while its principal can still request a revision, so it holds
the parent until it is revised or a dispute resolves it. Once the child's deadline passes (the
resubmission a revision asks for would be refused) or its revision budget is spent
(`maxRevisions`, or `maxSubmissions` when the org's `maxSubmissionsMode` enforces it), the
rollup counts the child as a failed one, and the child's `nextSteps` stop offering the revision.
Under `advisory` or `none` a spent `maxSubmissions` closes nothing: the revision is still
accepted, still offered, and the child still holds the parent. A rejection that spends the last
revision fails the parent in the same call; a deadline that passes later fails it in the expiry
sweep. Either way, a parent already past its own deadline is expired rather than failed. A
parent failed this way is itself a `FAILED` child of its own parent, and counts there by the
same rule.

On a failed parent, `GET /v1/records/{id}/gate-status` reads `phase2Status: "failed"`: the
rollup is the parent's evaluation. The completion the engine synthesizes for a `principal`
parent's verdict lists with `rollupSynthesized: true` and `agentId: null`, since no agent
submitted it. Until the principal renders a verdict on it, `POST /v1/records/{id}/evaluate`
reports the rollup as its `phase2Result` (`outcome: reject`, `recommendation: HOLD`, the
`auto_rollup` check); after, it reports the principal's verdict.

### Deadlines and cancels

Give delegated records a `deadline`. A gated child that names none under a gated parent takes
the parent's, and expires in the same sweep as the parent. A child expires at its deadline from
any open state, `CREATED` included, so one that is never activated fails the parent too. To
have a stalled or failed child fail the parent while the parent can still be revised, give the
child an earlier deadline than the parent's.

Cancelling a parent queues a cancel for each of its open children, one job each, so poll
`/sub-records` rather than reading the cancel's 200 as final. Two child states are left
running: `DISPUTED`, which only the dispute's resolution or withdrawal closes, and `FAILED`,
which becomes cancellable once its principal requests a revision.

## References

References link a record or an agent to the identifiers your other systems already use: a
ticket, a pull request, a purchase order, an invoice. An auditor can then start from either
side.

### Attach them to a record

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/records/$RECORD_ID/references" \
  -H "Authorization: Bearer $ORCHESTRATOR_KEY" -H 'Content-Type: application/json' \
  -d '{"references":[
        {"system":"jira","refType":"ticket","refId":"PROC-4471","displayName":"Boise seal kit sourcing"},
        {"system":"netsuite","refType":"purchase-order","refId":"REQ-2026-08817"}
      ]}'
```

The body is always the array form, with 1 to 25 references per call. `system` and `refType`
values are lowercase kebab-case (`purchase-order`, not `PurchaseOrder`), and a value that
does not match gets a 400 whose `recoveryHint` shows the pattern.

Each attach is written to the record's chain as a signed `RECORD_REFERENCE_ADDED` entry,
carrying the `system`, `refType` and `refId` of every reference in the call.

References bind to one record. Tagging a root does not tag its children: tag each record you
want findable, or find the root and read its `/chain`.

Read them back:

```bash
curl -s "$AGLEDGER_API_URL/v1/records/$RECORD_ID/references" -H "Authorization: Bearer $ORCHESTRATOR_KEY"
```

### Attach them to an agent

For example, the GitHub App or service account an agent runs as. This needs `agents:manage`,
so it is an org-admin call: an agent's own key gets a 403.

```bash
curl -s -X POST "$AGLEDGER_API_URL/v1/agents/$SOURCING_ID/references" \
  -H "Authorization: Bearer $ADMIN_KEY" -H 'Content-Type: application/json' \
  -d '{"references":[{"system":"github","refType":"app-installation","refId":"58213377","displayName":"sourcing-agent GitHub App"}]}'

curl -s "$AGLEDGER_API_URL/v1/agents/$SOURCING_ID/references" -H "Authorization: Bearer $ADMIN_KEY"
```

### Go from the other system's id to the record

```bash
curl -s "$AGLEDGER_API_URL/v1/references?system=jira&refType=ticket&refId=PROC-4471" \
  -H "Authorization: Bearer $ADMIN_KEY"
```

```json
{
  "data": [
    {
      "entityType": "record",
      "entityId": "01a0dc02-c41d-7239-b2cb-2960d1476c65",
      "reference": { "system": "jira", "refType": "ticket", "refId": "PROC-4471", "displayName": "Boise seal kit sourcing" }
    }
  ],
  "total": 1
}
```

`GET /v1/references` returns records and agents alike, each with its `entityType`. An admin key
finds every match in its org; an agent key finds only the records it is the principal or
performer of and its own agent references. To filter on a reference together with other record
fields, use the record search:

```bash
curl -s "$AGLEDGER_API_URL/v1/records/search?ref.system=jira&ref.id=PROC-4471" -H "Authorization: Bearer $ORCHESTRATOR_KEY"
```

A typical audit starts at the ticket. `PROC-4471` leads to the record, the record's `/chain`
leads to every delegated step, and each step's `/audit-export` shows who asked, who accepted,
who delivered and who rendered the verdict. The same path runs in reverse, from a record to
the ticket.

Field shapes for every call above are in the [API reference](/api/).

## Air-gapped

Every call here goes to your own Server and nothing else: no call leaves your network, and the
same commands work against an internal URL. The audit exports verify offline against the keys
from `GET /v1/verification-keys`, with the verifier installed from your internal package
mirror.
