Sub-agent delegation
A sub-agent a request creates is an agent of its own, with its own credential and its own key, that works for a parent agent. Eniyan holds it to what a person approved and to what its parent may do at that moment, and it can never do more than its parent. Sub-agents added the older way, under Inherit, follow the older rule (see Adding one directly). This page is the whole contract: the account’s setting, how a parent asks, how a person decides, what the sub-agent may do, and how each refusal reads on the wire.
The sub-agent setting
The account’s Sub-agent setting decides how sub-agents come to exist and whether they may act. It has three choices, named in the API as shown:
- Request (request). A person approves every sub-agent. Its parent asks with its own credential, and the sub-agent can use only what was approved, never more than its parent can right now. New accounts start here.
- Off (off). No sub-agent can be added or act. Requests already waiting can still be declined.
- Inherit (inherit). The older rule, which accounts created before this setting keep until an admin changes it. An agent allowed to have sub-agents can add them directly with your API key. Sub-agents created by an approved request stay held to it.
Every account created since the setting existed, PACH 1 and EACH 1 alike, starts on Request. Accounts created before it kept Inherit, so nothing they were doing changed, and they stay on it until an admin changes it.
You change it on the Sub-agent setting card on the Account tab. The same card is on the Agent Directory page (/dashboard/settings/agent-directory#subagent-setting). The same card holds two numbers. A temporary approval lasts at most the account’s cap: 240 minutes unless an admin sets it, from 1 to 1,440 minutes. A sub-agent created by a request that has had nothing waiting and nothing approved for the account’s idle window is retired: 7 days unless an admin sets it, from 1 to 90 days. Changing the setting, and what each change does, is under Changing the setting.
How a parent asks
Under Request a parent agent asks for each sub-agent with its own credential. (Under Inherit it may ask too, and a sub-agent created that way is held to its delegation exactly as under Request.) One call names the sub-agent and what it may use; when the sub-agent is new, the same call creates it, and it can do nothing until a person approves. The call is authenticated like the gates’ other machine routes: your API key in the header and the parent’s credential token in the body, never in a URL.
POST /v1/agents/credentials/subagent-delegations, with a JSON body:
- credential_token: the parent’s own credential.
- child: external_agent_id (the sub-agent’s id in your account, 1 to 255 characters), and optionally model_name, framework and purpose. An id the account has never seen creates the sub-agent; the id of one of this parent’s sub-agents asks again for that one.
- scopes: 1 to 50 permissions, each one the parent may use right now.
- web_sites: optional, up to 20 hostnames. Each must be one the parent reaches through its own standing websites, and they need a web: permission in scopes. Eniyan stores the parent’s matching website entry, never the host as sent.
- label: optional, at most 80 characters with no control characters. A short name for the task, shown to the person deciding.
- instruction_digest: optional, the lowercase sha256 hex digest of the instruction the parent gives the sub-agent. Eniyan never receives the instruction.
- minutes: optional, 1 to 1,440. How long a temporary approval should last; the person deciding can change it, and the account’s cap still applies.
- run_id or task_id: optional. Ties the delegation to one of the parent’s own running runs, or to its active task, and the delegation ends when that does. A parent inside an open just-in-time (JIT) task window is tied to the window whatever it sends, and can hand down only what the window allows.
- harness (a tool label, at most 50 characters) and channel (sdk, mcp or api, the default): how the request was made, shown beside it.
A new request answers 201. The same request again, while the first is still waiting, answers 200 with the waiting one and counts the repeat; nothing new is created. The body carries delegation_ref (sdl_…), status, child_agent_id, child_external_agent_id, reused (false when this call created the sub-agent), envelope (scopes and web_patterns as asked), decision_expires_at, poll_interval_seconds, and approval_hint while it waits. When this call created the sub-agent it also carries credential_token and child_api_key, in this answer only: hand both to the sub-agent (see The sub-agent’s credential and key).
A parent that cannot act itself is refused like any gate call, and the billing checks that enrolling an agent has apply here too. The request’s own refusals:
| Code | Status | When |
|---|---|---|
| SUBAGENTS_OFF | 403 | The account's sub-agent setting is Off. |
| SUBAGENT_NESTING_NOT_ALLOWED | 403 | The parent is itself a sub-agent. |
| PARENT_UNDECLARED | 422 | The parent has no declared permissions, so there is nothing to hold a sub-agent within. |
| SCOPE_NOT_DELEGABLE | 422 | A permission that can never be handed down, listed in scopes. See What a sub-agent may do. |
| SCOPE_EXCEEDS_PARENT | 422 | More than the parent may use right now, or than its open task window allows, listed in excess_scopes. |
| WEB_SITES_NEED_WEB_SCOPE | 422 | Websites were asked for with no web permission. |
| WEB_ACCESS_NOT_CONFIGURED | 409 | The parent has no website access to share. |
| WEB_SITE_EXCEEDS_PARENT | 422 | A website the parent cannot reach through its standing websites, listed in excess_sites. |
| RUN_NOT_LIVE | 422 | The run named is not one of the parent's own running runs. |
| TASK_NOT_ACTIVE | 422 | The task named is not the parent's own active task. |
| CHILD_NOT_YOURS | 409 | The id belongs to an agent in your account that is not this parent's sub-agent. |
| CHILD_RETIRED | 409 | That sub-agent was retired. Use a new id. |
| CHILD_NOT_GOVERNED | 409 | Under Inherit, the id is a sub-agent added the older way, to which a delegation would not apply. Use a new id. |
| DELEGATION_ALREADY_PENDING | 409 | A different request for this sub-agent is waiting; the detail names its delegation_ref. |
| DELEGATION_LIMIT | 422 | The parent has too many requests waiting. |
| SUBAGENT_LIMIT | 422 | The parent has too many sub-agents. |
| INVALID_LABEL | 422 | The label is too long or holds control characters. |
| SUBAGENT_BIRTH_FAILED | 503 | The sub-agent could not be created. Try again. |
Following and releasing a request
The parent follows its own requests with the same authentication, its credential token in the body ({ "credential_token": … }):
- POST /v1/agents/credentials/subagent-delegations/{ref}/status answers status, kind, live, lapse_reason, child_agent_id, envelope (what was approved, else what was asked), decision_expires_at, expires_at, and deny_reason once it is declined. Poll it no faster than poll_interval_seconds.
- POST /v1/agents/credentials/subagent-delegations/{ref}/release says the work is done: the delegation ends at once. Releasing a request that is still waiting withdraws it, and a sub-agent it created, which never acted, is retired. Releasing one that has already ended changes nothing.
A parent reads and releases only its own requests; any other reference answers 404 DELEGATION_NOT_FOUND. Asking, waiting and releasing from inside a tool through the eniyan package ships in the next release of the eniyan package, which is not published yet; until then a parent uses these routes.
Deciding
On a PACH 1 account you decide: the owner is the account's admin. Deciding needs a dashboard session and a confirmed email; the routes refuse an API key, so no agent can approve its own sub-agent.
The decision is made on the parent agent’s page, on its Sub-agents card (/dashboard/agents/{agent_id}?section=agents#subagents). The request is also listed in the Inbox, under Waiting on me, and you are emailed. Both link to that card; neither decides anything.
- Approve temporary, the default: the approval ends after the minutes chosen, and never later than the account’s cap. The card shows it as “temporary, ends at the time shown”.
- Approve standing: no end time, shown as “standing, until revoked”. One tied to the parent’s run or task reads “standing, until revoked or when the parent's run or task ends”, and a temporary one tied to a task ends at whichever comes first.
- Approve less than was asked by unticking permissions or websites. A decision can only narrow the request, never add to it.
- Decline needs a reason. Declining a request that created its sub-agent retires that sub-agent, which never acted.
- Revoke ends an approved delegation at once; a reason is optional.
- A request nobody decides by its decision_expires_at lapses, and that counts as declined.
- Approving a newer request for a sub-agent replaces its current approval. The first approval of a sub-agent that a request created admits it.
The routes, all with a dashboard session:
- GET /v1/me/subagent-delegations, with optional status, agent_id (the parent or the sub-agent) and limit, answers { "delegations": [...] }. An admin sees every row; anyone else sees the rows for agents they operate. Each row carries delegation_ref, status, kind, parent, child, child_born_here, label, requested_scopes, requested_web_patterns, approved_scopes, approved_web_patterns, requested_minutes, task_bound, its times, end_reason, deny_reason, ask_count, can_decide, can_revoke and held_by. An approved row also carries live and not_live_reason: an approval can confer nothing for a while, for example while its parent cannot act, and confer again afterwards. not_live_reason is one of the delegation values under On the wire, or not_governed: an approval for a sub-agent added the older way while the setting is Inherit, which then acts on its own permissions (this value is never sent to a gate). An approval that has already ended (past its time, its run or task over, or one of its agents retired) keeps status approved until the hourly sweep closes it, with can_revoke false: there is nothing left to revoke.
- POST /v1/me/subagent-delegations/{ref}/approve, body kind (temporary, the default, or standing), minutes (temporary only, capped by the account), and optionally scopes and web_sites. Leave the two lists out to approve what was asked.
- POST /v1/me/subagent-delegations/{ref}/decline, body reason (required, at most 500 characters), and POST /v1/me/subagent-delegations/{ref}/revoke, body reason (optional).
| Code | Status | When |
|---|---|---|
| DELEGATION_NOT_FOUND | 404 | No such request in your account. |
| DELEGATION_DECIDER_FORBIDDEN | 403 | You are not one of the parent's operators or an admin, or your email is not confirmed. |
| DELEGATION_ALREADY_DECIDED | 409 | Someone has decided it already, or it has ended (status says how). |
| DELEGATION_EXPIRED | 409 | It lapsed before anyone decided, and the hourly sweep has not closed it yet. |
| SUBAGENTS_OFF | 409 | Approving while the setting is Off. Declining still works. |
| CHILD_RETIRED | 409 | The sub-agent was retired. |
| DELEGATION_ANCHOR_ENDED | 409 | The run or task it was for has already ended. |
| PARENT_AGENT_NOT_ACTIVE | 409 | The parent agent cannot act. |
| ACCOUNT_KILL_SWITCH_ENGAGED | 409 | The account switch is on. Release it first. |
| DECISION_WIDENS_REQUEST | 422 | The decision adds something that was not asked for. |
| DECISION_EMPTY | 422 | Nothing is left to approve. Decline instead. |
| SCOPE_EXCEEDS_PARENT / WEB_SITE_EXCEEDS_PARENT | 422 | The parent no longer has a permission or website the decision keeps. |
| DECISION_REASON_REQUIRED | 422 | Declining without a reason. |
| DELEGATION_NOT_ACTIVE | 409 | Revoking something that is not approved. |
| DELEGATION_BUSY | 409 | Something else changed the agent at the same moment. Nothing changed; try again. |
What a sub-agent may do
Under Request and Off, and for a sub-agent a request created whatever the setting, a sub-agent may use only what a person approved, and only while its parent may do it right now, minus anything switched off on the parent:
own ∩ (approved ∩ the parent’s permitted set now − the parent’s switched-off permissions)
It is read live at every check (validate, verify, the token mint and introspection, and the Authority tab), and nothing is copied onto the sub-agent’s credential. Narrowing, stopping or switching off the parent reaches the sub-agent at its next call, and giving the parent its access back gives it back to the sub-agent. The delegation confers anything only while the setting is not Off, the approval stands and has not run out, the parent can act (its current credential live, not between just-in-time windows), the sub-agent is not retired, and any run or task it is tied to is still going.
A website is reached only inside the approved sites, only where the parent’s own website rules allow that host and ability right now, and only within the sub-agent’s own website list when it has one. A sub-agent cannot ask for a website itself: its parent asks, in a delegation request.
Whatever the setting, for every sub-agent:
- One level only: a sub-agent cannot have sub-agents (SUBAGENT_NESTING_NOT_ALLOWED).
- Its trust level is capped at its parent’s.
- These can never be delegated (SCOPE_NOT_DELEGABLE): web:purchase, web:pay, web:temp_grant, subagent:delegate, consumer:*. A sub-agent held to a delegation cannot make purchases, and a consumer delegation token is refused for it at the mint.
Under Inherit, a sub-agent added the older way follows the older rule instead (see Adding one directly).
Changing the setting
GET /v1/agent-roles/policy answers subagent_access_mode, subagent_delegation_max_minutes, subagent_idle_retire_days, subagent_count, legacy_subagent_count (sub-agents added the older way), resumable_envelope_count (approvals that can still apply: not past their time, and neither agent retired) and can_edit (whether you may change it). An admin changes it with PUT /v1/agent-roles/policy: subagent_access_mode (off, request or inherit), subagent_delegation_max_minutes (1 to 1,440), subagent_idle_retire_days (1 to 90) and acknowledge_widening. Both take a dashboard session.
A change that lets sub-agents act that cannot act now (Off to Request, Off to Inherit, Request to Inherit) needs a confirmation. Without "acknowledge_widening": true the PUT answers 422 WIDENING_ACKNOWLEDGEMENT_REQUIRED, with the counts above in its detail, and changes nothing; the card asks before it saves. Once such a change is made, every admin of the account is emailed, the person who made it included.
- Inherit to Request: sub-agents added the older way stop acting at their next call, until their parent asks for a delegation for that same sub-agent (its own id) and someone approves. legacy_subagent_count says how many there are. Nothing is revoked.
- To Off: no sub-agent acts, whichever way it was added, and waiting requests can be declined but not approved. Nothing is revoked, and an approval still in force applies again when the setting moves back. While it is Off, temporary approvals still run out, approvals tied to a run or task still end with it, and a sub-agent a request created with nothing waiting and nothing approved is still retired after the idle window.
- Off to Request: approvals still in force apply again.
- To Inherit: sub-agents added the older way act under the older rule again. Sub-agents created by a request stay held to their delegation.
The sub-agent’s credential and key
- A sub-agent a request created gets its own credential and its own API key, returned once in the answer that created it. The key is bound to that one agent. It is never your account’s API key, and your account’s key never needs to reach the sub-agent.
- The sub-agent uses its key with its own credential: for the gates’ checks and settings, for POST /v1/credentials/verify on its own credential, for the runs routes, and for the gates’ reports. Any other route, or a call naming another agent, answers it as an invalid key.
- In GET /v1/api-keys such a key carries bound_agent, naming its sub-agent. It cannot be rotated there (409 BOUND_KEY_ROTATES_WITH_AGENT), only revoked.
- If the key or the credential is lost, reissue the sub-agent’s credential (POST /v1/verifications/agents/{agent_id}/reissue). The answer carries a new credential_token and a new child_api_key, once; the old credential and the old key stop working at once.
- Retiring the sub-agent revokes its key and ends its delegations.
When it ends
- A temporary approval ends at its time; a standing one when someone revokes it. Either one tied to a run or task also ends when that does. The parent can release it at any time.
- An hourly sweep closes what has ended, but no check waits for it: an approval past its end confers nothing at once.
- A sub-agent held to a delegation cannot act while its parent cannot, for example while the parent is suspended or stopped, and resumes when the parent can. A retired parent never can (next item).
- Retiring the sub-agent or its parent ends every request and approval between them at once, waiting or approved. An approval whose parent was retired never comes back, and nothing can be asked for that sub-agent again.
- A sub-agent a request created, with nothing waiting and nothing approved for the account’s idle window (7 days unless an admin sets it), is retired: its credential and its key are revoked. The window counts from its last delegation’s end, or from its creation. Sub-agents added the older way are never retired this way.
Billing
A sub-agent a request created is admitted, and counted, at its first approval. It counts as an agent on your plan while it is active. Retiring it, by hand or after the idle window, ends that.
On the wire
There are no new gate codes. Every refusal rides a code the gates published so far already refuse hard, and the cause rides beside it in an additive delegation field, present only on a refusal it explains.
| Case | Validate | Verify |
|---|---|---|
| A sub-agent a request created, before its first approval | 403 AGENT_NOT_ACTIVE, on the gates’ settings routes too, with delegation: delegation_pending | valid: false, reason: agent_pending_admission, with delegation: delegation_pending |
| Any other sub-agent with no live delegation | 403 AGENT_NOT_ACTIVE, on the gates’ settings routes too | valid: false, reason: agent_suspended |
| Outside what was approved | The permission’s own hard code: web:* WEB_DOMAIN_NOT_ALLOWED; mail:* MAIL_ACCESS_NOT_CONFIGURED; a2a:* A2A_NOT_ALLOWED; fs:* AGENT_NOT_ACTIVE; any other SCOPE_NOT_ENTITLED | valid: false, reason: scope_not_authorized, scope_enforcement: block, before any policy runs |
Every gate published so far refuses all of these, but words a lapsed delegation as a dead credential, as if the agent's token had stopped working. Reissuing does not help there: the parent asks again, or a person approves. Gates that name the delegation ship in the next release of the eniyan package.
The delegation field’s values. When no delegation is live:
| Value | Meaning |
|---|---|
| subagents_off | Sub-agents are switched off for this account. |
| no_delegation | This sub-agent has no approved delegation. Its parent has to ask for one. |
| delegation_pending | This sub-agent's delegation is waiting for a person to approve it. |
| delegation_expired | This sub-agent's delegation has ended. Its parent has to ask again. |
| delegation_revoked | This sub-agent's delegation was revoked. |
| delegation_released | Its parent agent released this sub-agent's delegation. |
| delegation_superseded | This sub-agent's delegation was replaced by a newer one. |
| delegation_declined | This sub-agent's delegation was declined. |
| parent_not_live | Its parent agent can't act right now, so this sub-agent can't either. |
| run_finished | The parent's run this sub-agent was working for has finished. |
| task_closed | The parent's task this sub-agent was working for has ended. |
| child_retired | This sub-agent was retired, so it can't act again. |
| parent_retired | This sub-agent's parent was retired, so it can't act again. |
When the delegation is live but this call is outside it:
| Value | Meaning |
|---|---|
| outside_envelope | The permission was not approved for this sub-agent, or its parent cannot use it right now. |
| site_outside_envelope | The website is outside the approved sites, or the parent's own website rules refuse it right now. |
| child_cannot_request | A sub-agent cannot ask for a website itself: its parent asks for websites in its delegation request. |
| domain_not_delegable | A sub-agent cannot make purchases. |
- A token mint for a sub-agent with no live delegation answers invalid_grant with subagent_delegation_not_live: <reason>. A consumer delegation mint answers invalid_grant with subagent_domain_not_delegable: <domain>.
- A token minted for a sub-agent carries a delegation_ref claim. Introspection answers active: false with revocation_reason subagent_delegation_not_live (no live delegation now), delegation_changed (the token was minted under another delegation) or delegation_narrowed (none of its scopes is still allowed). When only some are, the answer’s scope leaves the rest out.
Webhooks
Every payload carries delegation_ref, parent_agent_id, child_agent_id, child_born_here, status and kind. They carry counts and ids, never the permissions, the label or the digest.
- agent.subagent_delegation_requested: a new request (a repeat of one that is waiting fires nothing). Adds scope_count, site_count and decision_expires_at.
- agent.subagent_delegation_decided: approved or declined. Adds decided_by_user_id, scope_count, site_count and expires_at.
- agent.subagent_delegation_ended: revoked, released, run out, its run or task over, its sub-agent or its parent retired (a waiting request too), or a request nobody decided. Adds end_reason.
The first approval of a sub-agent a request created also fires agent.admitted.
What is stored
A sub-agent delegation stores the permissions and websites asked for and approved, a short label and an optional digest of the parent’s instruction, never the instruction itself. It also keeps the tool label and channel the request came through, and the reason a person types when declining or revoking it. The audit trail records the permissions, websites, tool label and channel, and only whether a task label, digest or reason was given, never their text.
Subagents inside a tool
Subagents inside a tool like Claude Code or Codex are not Eniyan sub-agents: they share their agent's connection and act as that agent. Each gate process holds one agent’s credential, and Eniyan finds the agent from the credential, so a helper that works through its tool’s session is that agent to Eniyan: it acts under that agent’s permissions, switches and policy, and nothing on this page applies to it. To govern a helper on its own, run it as a sub-agent with its own credential, its own key and gates of its own. See What one agent covers.
Adding one directly (Inherit)
Under Inherit a sub-agent can also be enrolled directly under its parent, with your API key or from the dashboard (Add a sub-agent on the parent’s Sub-agents card), as accounts did before the setting existed. Under Request that is refused with 403 SUBAGENT_DELEGATION_REQUIRED, and under Off with 403 SUBAGENTS_OFF.
- The parent must be allowed to have sub-agents: its Can have sub-agents switch (can_spawn_subagents), which an admin or role admin sets in a dashboard session (GET or PUT /v1/verifications/agents/{agent_id}/sub-agents/policy), never with an API key, because it lets the key enroll children. Turning it off stops new sub-agents only.
- Such a sub-agent is never born broader than its parent. With no permissions of its own it takes the parent’s permitted set; a declared set must fit inside it (422 SCOPE_EXCEEDS_PARENT); a parent whose permissions are currently empty refuses it (422 PARENT_HAS_NO_AUTHORITY).
- Narrowing the parent later reaches it only while the account’s sub-agent ceiling is on (the default for new PACH 1 accounts). See Sub-agent ceiling.
- It counts on your plan like any agent you enroll, and is never retired for being idle.
- From code, see Harness integration: Sub-agents.