guides
Intended Documentation
What your agent gets back
ALLOW, DENY and ESCALATE, and how an escalated action reaches its final answer. The SDKs, the MCP server, the gateways and the Claude Code hook wait for the approver for you; the raw API exposes the same wait as GET /v1/decisions/{intentId}/wait.
What your agent gets back#
Intended decides whether an agent may take an action. It never takes the action itself. Your agent asks, gets an answer, and acts only on ALLOW.
There are three first answers:
| First answer | Meaning | What your agent does |
|---|---|---|
ALLOW | Policy approved the action | Acts, using the signed single-use authority token that comes with the answer |
DENY | Policy blocked the action | Does not act. The answer includes the reason |
ESCALATE | A person has to decide | Waits for the final answer below. Does not act yet |
An escalated action ends in one of these final answers:
| Final answer | Meaning | What your agent does |
|---|---|---|
ALLOW | An approver approved it | Acts, using the token minted at approval |
DENY | An approver rejected it (the answer carries their reason), or the escalation was canceled | Does not act |
EXPIRED | No approver decided before the escalation expired | Does not act. Treat it exactly like DENY. Submit the action again to ask again |
PENDING | Still waiting for an approver | Keeps waiting, or stops and checks again later |
You do not need to build polling or webhooks. Every Intended client below waits for the approver for you. If another system has to react to an escalation, subscribe it to the decision.escalated, decision.approved, decision.rejected, decision.canceled and decision.expired webhooks.
Expiry#
An escalation waits for an approver for 24 hours by default. An admin or owner can change the window per workspace with PUT /v1/authority-config and escalationTtlSeconds (60 seconds to 30 days). A new window applies to escalations raised after the change. Once an escalation expires, the agent gets EXPIRED and the escalation can no longer be approved or rejected. Intended records the expiry within about a minute of the deadline, whether or not an agent is waiting: the escalation leaves the review queue and the audit trail gets one ESCALATION_EXPIRED event.
TypeScript SDK#
guard(), evaluate() and authorize() wait for the approver by default.
- Approved: the callback runs with the token minted at approval, exactly as for a direct
ALLOW. - Rejected or expired:
status: "denied"with the reason.expiredistruefor an expiry. The callback does not run. - Still waiting when
waitTimeoutMs(default 10 minutes) passes:status: "pending_approval"with theintentId. The callback does not run. Callintended.waitForDecision(intentId)to keep waiting. { onEscalate: "return" }returnspending_approvalat once instead of waiting.
waitForDecision(intentId, { timeoutMs }) returns { status, reason, decidedBy, decidedAt, token, tokenExpiresAt, receiptUrl, expiresAt, timedOut }.
Python SDK#
authorize() waits for the approver by default, on both IntendedSdk and AsyncIntendedClient.
on_escalate="wait"(default) or"return", andwait_timeout_seconds(default 600).- Approved:
allowedisTrueandtokenis the token minted at approval. Rejected:deniedwith the reason. Expired:deniedwithexpired=True. Still waiting at the timeout: still escalated, with the intent id. wait_for_decision(intent_id, timeout_seconds=600)returns aDecisionOutcomewith the same fields as the API.- The LangChain, OpenAI Agents, PydanticAI and MCP helpers inherit this: an escalated tool call waits, then runs or is blocked.
MCP server#
submit_intent returns ESCALATE with an instruction for the assistant: do not perform the action yet, and call check_decision with the intent id. check_decision takes intentId and waitSeconds (default 25, max 50) and returns the final answer, or PENDING with an instruction to call it again. The assistant performs the action only when check_decision returns ALLOW. It needs the intent:read scope.
MCP gateway#
On ESCALATE the gateway holds the tool call while an approver decides, for up to INTENDED_GATEWAY_ESCALATION_WAIT_SECONDS (default 50; most MCP clients give up on a request after about 60 seconds).
- Approved in time: the call is forwarded to the upstream server with the token minted at approval.
- Rejected or expired: the reason is returned as an error and the upstream server is not called.
- Still waiting: a pending result with the intent id. When the assistant makes the same call with the same arguments after approval, the gateway forwards it without asking again. The token is single-use, so a third identical call is a new request.
LLM gateway#
When a model asks to call a tool and the answer is ESCALATE, the gateway holds the model response for up to INTENDED_LLM_GATEWAY_ESCALATION_WAIT_SECONDS (default 30). Approved: the tool call passes through. Rejected or expired: the tool call is replaced with the reason. Still waiting: the tool call is replaced with a pending note carrying the intent id, and the same tool call made again after approval passes through.
Claude Code hook#
On ESCALATE the hook waits up to INTENDED_HOOK_ESCALATION_WAIT_SECONDS (default 45). Approved: the tool call runs. Rejected or expired: it is denied with the reason. Still waiting: it is denied with a message naming the intent id; after an approver approves it in the console, retry the same tool call and it runs. Claude Code stops a hook after 60 seconds by default, so raise the hook's timeout if you set a longer wait. INTENDED_HOOK_ON_PENDING=ask hands a still-pending call to Claude Code's own permission prompt instead.
The hook needs INTENDED_API_KEY (scopes intent:write and intent:read) and INTENDED_TENANT_ID. If either is missing, it denies every tool call it governs and says which setting is missing. To try the hook before configuring it, set INTENDED_HOOK_UNCONFIGURED=allow: tool calls then run without an authority decision, and Claude Code shows a warning on every call.
Raw API#
POST /v1/agent-intent answers an escalation with HTTP 202, authorityDecision.decision: "ESCALATED", an intentId and an escalationId. Then:
The request returns at once if the decision is final, otherwise it is held until an approver decides or timeout seconds pass (0 to 50, default 25). Call it again while the answer is PENDING.
statusisALLOW,DENY,EXPIREDorPENDING.decidedBy.typeispolicy,human,autopilotorsystem(expiry). For a human it carries the console role, never a name or email.tokenis present only onALLOW, and only on the firstALLOWanswer for an approved escalation (see below).receiptUrlpoints at the offline-verifiable receipt once the decision is final.- On
PENDING, waitretryAfterMsbefore calling again (0 means call again now). 404for an unknown intent or another workspace's intent,403for a key withoutintent:read,429when you call too often (up to 120 calls a minute per key).
The token is single-use#
The token for an approved escalation is minted once, at approval, and handed to the agent once: the first wait call that answers ALLOW carries it, and Intended erases its stored copy at that moment. Later wait calls for the same intent still answer ALLOW, with token: null and a reason saying the token was already handed over. For an action approved by policy straight away, the token comes back in the POST /intent response and the wait endpoint never returns it. The token is never minted again.
Using the token is single-use too, if the service that acts on it checks. Each token carries a single-use nonce: POST /verify/token with "consume": true accepts it once and refuses every later attempt with TOKEN_ALREADY_CONSUMED, and offline verifiers keep their own nonce cache. Intended never sees the action, so a service that only checks the signature (or calls /verify/token without consume) would accept the same token twice within its 5 minutes. Verify with consume: true, or record the nonce yourself.
Retrying POST /intent with the same Idempotency-Key within 24 hours replays the original response, including its token, which may already have been used. Treat a replayed token like any other: if the verifier says TOKEN_ALREADY_CONSUMED, the action already ran once on that decision. The token is valid for 5 minutes from approval. The clients above pick it up within seconds; if you pick up an approval later than that, or the response carrying the token was lost, submit the action again.