> ## Documentation Index
> Fetch the complete documentation index at: https://docs.humanos.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Actions

> Let an AI agent act under a scoped, user-approved policy: publish an action, request a mandate, issue a verifiable presentation, and verify it at execution time.

When an autonomous agent wants to do something on a user's behalf, Humanos lets you prove, cryptographically and at the moment of action, that the user approved *this* kind of action within *these* limits. The flow has four moving parts:

1. **Action**: a reusable policy template you publish. It defines the parameters and rules.
2. **Mandate**: a signed credential the user issues against an action, pinning the values they approve.
3. **Verifiable Presentation (VP)**: a short-lived proof the agent derives from the mandate for a single verification.
4. **Verify**: Humanos checks the VP and evaluates the agent's requested execution against the user-approved rules, returning **allow** or **deny**.

<div className="hm-tip">
  The user approves the mandate *once*. The agent then verifies *every* action against it, with no repeated user prompts. Revoke the mandate and every future action is denied.
</div>

## 1. Define and publish an action

Actions are authored and published in the [dashboard](/dashboard/actions); they're read-only over the API. An action's content has these parts:

| Part              | What it is                                                                                                                |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `userParams`      | Values the **user** approves at mandate time, e.g. a spending cap. Each declares a type and a human-readable description. |
| `executionParams` | Values the **agent** supplies at verify time, e.g. the amount it wants to spend.                                          |
| `rules`           | Named **CEL** expressions that must all evaluate `true`, comparing `executionParams` against `userParams`.                |
| `target`          | *Optional.* The `{ method, domain, path }` the action pertains to.                                                        |
| `mapper`          | *Optional.* Maps params to outbound wire-field names.                                                                     |

A rule looks like `executionParams.amount <= userParams.maxAmount`. Publishing computes an integrity digest, the `digestSRI`, and freezes that version. Mandates always reference the exact version they were issued against, so republishing never changes an existing mandate.

Copy the published action's id, `urn:via:action:<uuid>`. You'll reference it when requesting mandates.

## 2. Request a mandate

Ask a user to grant a mandate with `POST /request`, using a credential of `type: POLICY` that references the action and the `userParams` the user is approving.

```javascript theme={"dark"}
const res = await signedFetch("/request", {
  method: "POST",
  body: {
    securityLevel: "CONTACT",
    users: [{ contact: "user@example.com" }],
    authorizedDIDs: ["did:web:your-agent.example.com"],
    credentials: [
      {
        type: "POLICY",
        name: "Purchasing mandate", // shown to the user
        action: {
          id: "urn:via:action:<uuid>",
          userParams: {
            maxAmount: 10000,
            allowedCategories: ["BOOKS", "OFFICE"],
          },
        },
      },
    ],
  },
});
```

<ParamField body="credentials[].type" type="string" required>
  Must be `POLICY` for a mandate. A `POLICY` credential carries an `action` and no `data`.
</ParamField>

<ParamField body="credentials[].action" type="object" required>
  <Expandable title="action">
    <ParamField body="id" type="string" required>
      The published action URN, `urn:via:action:<uuid>`.
    </ParamField>

    <ParamField body="userParams" type="object" required>
      The values the user is approving. Validated against the action's declared `userParams` shape and stored on the mandate.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="authorizedDIDs" type="string[]">
  DIDs allowed to derive VPs from, and verify, the resulting mandate, typically your agent's DID. The requester's own DID is always included.
</ParamField>

The user approval flow is identical to [Collecting Approvals](/essentials/guides/collecting-approvals), using link/OTP with optional KYC. On accept, Humanos issues the mandate and sends a `credential` webhook event. **Persist the mandate URN**, `urn:via:credential:<uuid>`. It's the durable handle the agent uses from here on.

## 3. Agent issues a VP

When the agent is about to act, derive a fresh, short-lived Verifiable Presentation from the mandate. Issue a new one per verification.

```javascript theme={"dark"}
const mandateUrn = "urn:via:credential:<uuid>"; // from the credential webhook

const res = await signedFetch(
  `/credential/vp/${encodeURIComponent(mandateUrn)}`,
  {
    method: "POST",
    body: {
      // targetVerifier: "verifier.example.com", // optional, binds the VP to this audience
    },
  },
);

const { data } = await res.json();
// data.presentationEncoded, pass this to verify
```

<ParamField body="targetVerifier" type="string">
  The verifier's domain. When set, the VP is bound to that audience via
  `proof.domain` and a single-use challenge nonce, valid for about 5 minutes. Omit for
  an unbound VP, which instead relies on the verifier's DID being in the
  mandate's `authorizedDIDs`.
</ParamField>

The response includes `presentation`, `presentationEncoded` as base64, and a signed receipt. The caller's DID must be authorized on the mandate, or issuance is denied.

## 4. Verify

Send the VP plus the agent's `executionParams` to the verify endpoint. Humanos runs the full pipeline: signature, status, validity window, audience binding, `executionParams` validation, then the action's CEL rules.

```javascript theme={"dark"}
const res = await signedFetch("/credential/verify", {
  method: "POST",
  body: {
    presentationEncoded: data.presentationEncoded,
    executionParams: { amount: 5000, category: "BOOKS" },
  },
});

const result = await res.json();
if (result.decision === "allow") {
  // agent may act
} else {
  console.error("Denied:", result.reason, result.evaluations);
}
```

<ParamField body="presentationEncoded" type="string">
  The base64 VP from step 3. Takes precedence over `presentation` if both are sent; at least one is required.
</ParamField>

<ParamField body="executionParams" type="object" default="{}">
  What the agent wants to do. Field names must match the action's declared `executionParams` and are referenced in rules as `executionParams.<field>`.
</ParamField>

### `201` Allow

<ResponseField name="decision" type="string" required>Always `allow`.</ResponseField>
<ResponseField name="credentialId" type="string" required>URN of the mandate that was verified.</ResponseField>
<ResponseField name="evaluations" type="object[]" required>Per-rule results, each `{ rule, result: "pass" | "fail" | "error", reason? }`.</ResponseField>
<ResponseField name="receipt" type="object" required>Signed `VERIFICATION_APPROVED` receipt.</ResponseField>

### `403` Deny

<ResponseField name="decision" type="string" required>Always `deny`.</ResponseField>
<ResponseField name="reason" type="string" required>Machine-readable denial reason; see the table below.</ResponseField>
<ResponseField name="statusCode" type="number" required>`403`.</ResponseField>
<ResponseField name="message" type="string" required>`Verification denied: <reason>`.</ResponseField>
<ResponseField name="credentialId" type="string" required>URN of the mandate that was evaluated.</ResponseField>
<ResponseField name="evaluations" type="object[]" required>Per-rule results, populated when a rule failed.</ResponseField>
<ResponseField name="receipt" type="object" required>Signed `VERIFICATION_DENIED` receipt.</ResponseField>

| `reason`                                       | Meaning                                                                                       |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `vp_signature_invalid`                         | The VP's signature failed verification                                                        |
| `vp_signer_unknown`                            | The VP was not signed by Humanos                                                              |
| `credential_revoked`                           | The mandate was revoked; other `credential_<status>` values appear for canceled/rejected/etc. |
| `expired` / `not_yet_valid`                    | The mandate is outside its validity window                                                    |
| `verifier_not_authorized`                      | For an unbound VP, the verifier's DID is not on the mandate's `authorizedDIDs`                |
| `vp_domain_mismatch`                           | The VP's bound `targetVerifier` domain doesn't match the verifier                             |
| `vp_challenge_expired` / `vp_challenge_replay` | The challenge on a bound VP is stale or already used                                          |
| `action_not_found` / `action_invalid`          | The referenced action version is missing or malformed                                         |
| `execution_params_invalid`                     | `executionParams` don't match the action's declared shape                                     |
| `rule_failed`                                  | A CEL rule evaluated to `false`; check `evaluations`                                          |

<div className="hm-tip">
  A `403` with `rule_failed` is a legitimate policy outcome, not an error: the agent asked to do something the user didn't authorize. Inspect `evaluations` to see which rule blocked it.
</div>

## 5. Revoke a mandate

To retire a mandate, whether the user withdraws consent or the underlying rule changes, revoke it. Afterwards, VP issuance and verification both fail with `credential_revoked`, so any VP an agent still holds stops working.

```javascript theme={"dark"}
await signedFetch(`/credential/revoke/${encodeURIComponent(mandateUrn)}`, {
  method: "DELETE",
  body: { reason: "user_initiated" },
});
```

<ParamField body="reason" type="string">
  Optional label, max 200 chars, recorded on the credential and the revocation
  receipt.
</ParamField>
