Skip to content

guides

Intended Documentation

Agent Intent API

Submit an agent's proposed action to POST /v1/agent-intent for a governed decision. The response is a decision, a signed token on approval and a receipt; your agent acts and can report the outcome. inputQuality feedback when the action text names no action.

Agent Intent API#

POST /v1/agent-intent is the route the SDK (guard(), evaluate(), authorize()), the MCP server, the MCP gateway and the LLM gateway use. It takes one proposed agent action and returns a governed decision: APPROVED (HTTP 200), ESCALATED (202) or DENIED (403). Every decision is classified, recorded in the tenant's hash-linked audit chain and gets a receipt. An approved decision also carries a signed authority token.

Intended decides; your agent acts#

Intended decides and proves; your agent acts. The response is the decision, a signed authority token when the decision is APPROVED, and a receipt. Intended never runs the action and never calls the target system: not when the decision is made, not when a person later approves an escalation, and not when an operator overrides the decision. It holds none of your third-party credentials.

Once the verdict is APPROVED, your agent performs the action with its own credentials and passes the authority token to the downstream service, which verifies it offline against the published JWKS before acting. See Verify Tokens & Decisions.

Request#

bash
curl -X POST https://api.intended.so/v1/agent-intent \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId": "tenant_acme_prod",
    "actor": { "id": "billing-agent", "type": "agent" },
    "proposedAction": "Send subscription renewal reminder email",
    "targetSystem": "billing/subscriptions",
    "arguments": { "subscriptionId": "sub_123", "daysPastPeriodEnd": 3 }
  }'
FieldRequiredWhat to send
tenantIdyesYour tenant id.
actoryes{ id, type }. type is agent, service, user or system.
proposedActionyesThe action to take, as a verb phrase: "Send renewal reminder email", "Revoke expired API keys", "Deploy billing to staging". Not a status line or an alert.
targetSystemnoThe system the action touches, such as stripe/subscriptions or github/acme/api. Recorded as you send it (trimmed).
argumentsnoThe action's details: ids, counts, recipients, the tool call's arguments. Policies and the classifier read them in memory; they are stored only as a SHA-256 digest.
metadatanoFlat context to record with the decision (stored as sent, up to 25 keys). Do not put secrets or personal data here; use arguments.
riskContextnoRisk hints (touchesProduction, requiresPrivilegedAccess, containsSensitiveData, baseRiskScore, policyCompliant, justification). Hints can only raise scrutiny; see Risk hints can only raise scrutiny.
sourcenoAttribution: { connector, harness, tool }, naming the integration that submitted the intent (for example the MCP gateway or a framework integration).

targetSystem when you leave it out#

Intended does not guess a target from your sentence. If targetSystem is missing:

  1. A tool-call action names the tool: Call MCP tool "run_query" with args: sql is recorded as mcp/run_query, and Call tool "Bash" as tool/bash.
  2. Otherwise the tool in source.tool (or metadata.tool) is used.
  3. Otherwise the decision records unspecified. Per-resource signals, such as "first time this agent touches this resource", are not computed for an unspecified target.

Risk hints can only raise scrutiny#

Intended works out the risk of every action itself, from the action it classified, the action's verb, the target system and the arguments. It reads that text after decoding and normalising it, so encoded or disguised text (percent-encoding, HTML entities, look-alike letters, split SQL keywords, base64) is read as what it means. Your riskContext is then combined with what Intended worked out:

FieldHow your value is used
touchesProduction, requiresPrivilegedAccess, containsSensitiveDatatrue adds scrutiny. false does not remove it: if the target, action or arguments name production, privileged access or sensitive data, the decision treats the action that way.
baseRiskScoreRaises the score. The score is never lower than the floor Intended works out (for example, any write that is not a confirmed read, a high-risk action category or a destructive verb).
policyCompliantfalse denies the action under the starter policy. true is the default and changes nothing.

When Intended raises a field above what you sent, the decision's rationale says which field and why. The value you sent is recorded unchanged. The same applies to POST /intent and every SDK.

Reporting the outcome#

After your agent acts, it can report what happened. Outcome reports never change the decision, the token or the receipt; they append an immutable signal to the decision's record.

bash
curl -X POST https://api.intended.so/intents/$INTENT_ID/outcome \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "success", "idempotencyKey": "<your-idempotency-key>" }'

outcome is one of success, failure, rolled_back, incident or retried. The key needs the intent:outcome:write scope. The TypeScript SDK wraps this as reportOutcome(intentId, outcome, options).

inputQuality#

When proposedAction does not name an action up front, the response carries an inputQuality object. The decision is still made, fail-closed: an action the classifier cannot place is escalated under the default policy. inputQuality tells your agent how to phrase the next request.

json
{
  "authorityDecision": { "decision": "ESCALATED" },
  "inputQuality": {
    "status": "no_action_detected",
    "hint": "proposedAction names no action. It reads like a status or an alert. Send the action to take as a verb phrase, e.g. \"Send renewal reminder email\", and put counts, ids and the observation in arguments.",
    "expectedShape": {
      "proposedAction": "A verb phrase naming the one action to take, e.g. \"Send renewal reminder email\".",
      "targetSystem": "The system acted on, e.g. \"stripe/subscriptions\" or \"github/acme/api\".",
      "arguments": "Details such as ids, counts and recipients, as a JSON object (stored only as a digest)."
    }
  }
}
statusMeaning
no_action_detectedThe text has no action verb, for example "escalation queue depth is 156, queue may be backed up".
action_not_leadingThe action is buried after a description, for example "5 API keys unused for 90 days, send rotation reminders". Lead with the action.

The field is absent when the action text is fine. The SDKs expose it as decision.inputQuality (TypeScript, @intended-inc/sdk 0.6.1 and later) and decision.input_quality (Python, intended 0.2.3 and later); the MCP server's submit_intent result includes it as inputQuality (@intended-inc/mcp-server 0.1.4 and later).

Responses#

HTTPDecisionNotes
200APPROVEDauthorityDecisionToken is set. Your agent may now act, carrying the token.
202ESCALATEDescalationId names the approval request.
403DENIEDNo token.
400noneThe body failed validation.
Agent Intent API | Intended