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.
Ask another agent to do the work
The principal writes the record, naming the performer:
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:
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": trueon the create instead. The record goes straight toACTIVEwithacceptanceStatus: "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:
curl -s "$AGLEDGER_API_URL/v1/records/agent/proposals" -H "Authorization: Bearer $COORDINATOR_KEY"
{
"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:
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:
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:
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" }
}'
{
"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
ACTIVEorRECORDED. A notarize-only parent can anchor children too. - Its deadline cannot be later than the parent's. A child created without a
deadlinetakes 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.delegationMaxDepthDefaultonGET /v1/conformance). A platform key can set an org's own cap, up tolimits.delegationMaxDepthCeiling. - Any other agent is refused (403
CALLER_NOT_PARENT_PERFORMER). TherecoveryHintsays that an org-admin key can create the child by passing the parent's performer asprincipalAgentId. 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:
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:
{
"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.
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.
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
ACTIVEand 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 readsawaitingActor: "system"and sits in neither party's?actionable=truequeue: 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
autoparent goes toFAILED, which stays recoverable. Aprincipalparent waits atPROCESSINGwithawaitingActor: "principal", carrying the failing rollup inverdictChecks.auto_rollupand a synthesizedlatestCompletionId. No agent submitted that completion, so the principal's verdict on it takesrejectonly;acceptis refused with a 422 whoseallowedActionsis["verdict_reject"]. - A parent already past its own deadline when the rollup reads the failure is expired
instead, in either mode:
EXPIREDwithterminalReason: TIMED_OUTand arecord.expiredevent, and nocascading.gate.complete. An expired parent counts as a failed child of its own parent, the same as a failed one.
{
"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
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:
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.
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
curl -s "$AGLEDGER_API_URL/v1/references?system=jira&refType=ticket&refId=PROC-4471" \
-H "Authorization: Bearer $ADMIN_KEY"
{
"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:
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.
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.