---
title: How it works
description: The twelve stages a protected action passes through, what each one can refuse, and what gets recorded.
---

Every protected action runs the same pipeline, in the same order, every time.
There is no fast path and no cache of a previous decision. The order is not
arbitrary: each stage is cheaper than the one after it and narrows what the next
stage has to consider, and anything that can refuse does so before a credential
is ever brokered.

The stage list is a published enum — `ACTION_STAGES` — and the stage results
come back on every decision in `stages[]`, so what you read here is what you can
inspect at runtime.

:::diagram pipeline | The twelve stages, in order. Everything above the dashed line can refuse with no effect outside Veefore.
Stages one to nine each check one thing and each can refuse, and none of them
touches anything outside Veefore. Stage ten brokers a credential, which is the
first step with an external side effect. That ordering is what makes a blocked
action mean the secret never left — see
[the table below](#why-this-order).
:::

The diagram is generated from `ACTION_STAGES`, so adding a stage to the system
adds it here. The build fails if a new stage arrives without a description.

## The pipeline

| # | Stage | Decides | Refuses with |
| --- | --- | --- | --- |
| 1 | `REQUEST` | The request is well-formed and in scope | `VALIDATION_FAILED`, `TIMESTAMP_OUT_OF_WINDOW`, `NONCE_REPLAYED` |
| 2 | `AUTHENTICATION` | The signature matches a live agent key | `AGENT_AUTHENTICATION_FAILED`, `AGENT_TOKEN_EXPIRED` |
| 3 | `IDENTITY` | The identity is current, not superseded or revoked | `AGENT_IDENTITY_REVOKED`, `IDENTITY_GENERATION_STALE`, `PASSPORT_INVALID` |
| 4 | `LIFECYCLE` | Agent and environment are operational | `AGENT_NOT_OPERATIONAL`, `AGENT_ISOLATED`, `ENVIRONMENT_PAUSED` |
| 5 | `CAPABILITY` | The agent holds the capability, and the tool is enabled and authorized | `CAPABILITY_NOT_GRANTED`, `TOOL_NOT_AUTHORIZED`, `TOOL_DISABLED`, `DESTINATION_BLOCKED` |
| 6 | `DELEGATION` | The delegation chain resolves and covers this request | `DELEGATION_INVALID`, `DELEGATION_EXPIRED`, `AUTHORITY_EXCEEDED` |
| 7 | `POLICY` | The active policy's decision | `ACTION_BLOCKED` |
| 8 | `RISK` | Risk score and signals, which policy can test | `VELOCITY_LIMIT_EXCEEDED` |
| 9 | `APPROVAL` | Whether a human must decide, or has | `APPROVAL_REQUIRED`, `APPROVAL_NOT_GRANTED`, `APPROVAL_EXPIRED` |
| 10 | `CREDENTIAL` | A scoped, short-lived credential is issued to the runtime | `INVALID_CREDENTIAL_REFERENCE`, `CREDENTIAL_SERVICE_UNAVAILABLE` |
| 11 | `EXECUTION` | The tool actually runs | `EXECUTION_FAILED`, `EXECUTION_TIMEOUT` |
| 12 | `RESULT` | Limits and masking are applied, the record is sealed | — |

## Why this order

**Identity before authority.** There is no point resolving what an agent may do
until you know which agent it is. Signature verification is stage 2 and nothing
downstream runs without it.

**Lifecycle before capability.** A paused agent is refused by state, not by
policy. This matters operationally: pausing an agent must work instantly, and it
must not depend on anyone having written a policy that happens to cover it.

**Capability before delegation, delegation before policy.** Capability is a
static grant, delegation is a bounded runtime grant, policy is a conditional
rule. Each is strictly narrower and strictly more expensive to evaluate than the
one before.

**Risk before approval.** A policy rule can test `risk.score` and
`risk.signals`, so the signals have to be computed before the rule that reads
them. This is why an unusual request can require approval while the identical
request an hour earlier did not.

**Credential after every refusal.** Stage 10 is the first stage with a
side effect outside Veefore. Everything that can say no has already said it. A
blocked action never causes a credential to be issued, which is what makes
"blocked" mean blocked rather than "blocked, but the secret went out anyway".

:::security Fail closed, at every stage
If a dependency a stage needs is unavailable, the action is refused, not
permitted. Identity verification down means `IDENTITY_SERVICE_UNAVAILABLE` and
no protected action proceeds. There is no mode in which an error becomes an
allow. See [Fail closed](/secure/fail-closed).
:::

## What the mode changes

The pipeline is identical in all three modes. The mode only changes what happens
to a refusal and whether execution is real.

:::diagram modes | The same twelve stages in every mode. Only the consequence of a refusal changes.
`OBSERVE` runs every stage, records what enforcement would have decided, and
lets the action execute anyway — so it protects nothing. `SIMULATE` refuses for
real and executes nothing. `ENFORCE` refuses for real, and brokers a credential
and executes only when the action is allowed.
:::

| | `OBSERVE` | `SIMULATE` | `ENFORCE` |
| --- | --- | --- | --- |
| Stages 1–9 run | yes | yes | yes |
| A refusal stops the action | **no** | yes | yes |
| Credential brokered | yes | no | yes, if allowed |
| Tool actually executes | **yes** | no | yes, if allowed |
| `shadow.decision` populated | yes | yes | — |
| Decision recorded | yes | yes | yes |

:::warning OBSERVE executes everything
In `OBSERVE` the pipeline computes a refusal, records it in `shadow`, and then
lets the action run anyway. That is the whole point of the mode — you learn what
would break before it breaks — but it means OBSERVE provides **no protection**.
Treat it as instrumentation, not as a safety setting.
:::

`SIMULATE` is the honest middle ground: the full pipeline, no execution, no
credential, no side effect anywhere. It is what `veefore deploy` replays your
traffic through during preflight.

## What a decision looks like

```json
{
  "actionId": "act_01JAV7YQ4T2K9M3E8H6R5N1XZ0",
  "requestId": "req_01JAV7YQ4T4ZC1P8W2M6B9K3QD",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "mode": "ENFORCE",
  "decision": "REQUIRE_APPROVAL",
  "outcome": "APPROVAL_REQUIRED",
  "allowed": false,
  "reason": "Refunds above ₹1,000 require a human approver.",
  "explanation": {
    "decision": "REQUIRE_APPROVAL",
    "policy": { "id": "pol_01JAV…", "key": "refund-limits", "name": "Refund limits", "version": 4 },
    "matched_rule": { "id": "rule-refund-ceiling", "name": "Approval above 1000" },
    "authority": {
      "valid": true,
      "max_amount": 500000,
      "requested_amount": 120000,
      "capabilities": ["refund.create"],
      "depth": 1,
      "violations": []
    },
    "delegation": { "valid": true, "chain": ["del_01JAV…"], "reason": null },
    "risk": { "level": "MEDIUM", "score": 46, "top_signals": ["Amount is 4.2× this agent's trailing median."] },
    "approval": { "required": true, "reason": "Refunds above ₹1,000 require a human approver." },
    "reason": "Refunds above ₹1,000 require a human approver."
  },
  "approval": {
    "required": true,
    "approvalId": "apr_01JAV…",
    "status": "PENDING",
    "expiresAt": "2026-10-03T12:40:00.000Z",
    "pollAfterMs": 2000
  },
  "risk": { "level": "MEDIUM", "score": 46 },
  "shadow": null,
  "limits": { "appliedAmount": null, "maskedFields": [] },
  "latencyMs": 38,
  "stages": [
    { "stage": "AUTHENTICATION", "outcome": "PASS", "message": "Signature verified.", "latency_ms": 3 },
    { "stage": "POLICY", "outcome": "PASS", "message": "Rule matched: Approval above 1000.", "latency_ms": 2 },
    { "stage": "APPROVAL", "outcome": "PENDING", "message": "Awaiting a human decision.", "latency_ms": 1 }
  ]
}
```

Three fields do the work:

- `decision` is the policy outcome. One of `ALLOW`, `BLOCK`, `REQUIRE_APPROVAL`, `STEP_UP_AUTH`, `LIMIT`, `MASK`.
- `allowed` is the single boolean to branch on. It already accounts for mode.
- `explanation` names the policy, the version, the deciding rule, the resolved authority, the delegation chain, the top risk signals and any authority violations. This is what makes a refusal answerable rather than mysterious.

`explanation.matched_rules` carries *every* rule that matched, each with a
per-condition trace of expected value, actual value and result. That is the
field to read when a decision is surprising: it shows you not just which rule
won, but which conditions in which rules were true.

`LIMIT` and `MASK` are the two decisions that let an action through in modified
form: `limits.appliedAmount` is the ceiling that was imposed, and
`limits.maskedFields` lists the response fields that were redacted before the
agent saw them.

## The action record

Every run writes an action record, and the record is the full lineage: the
request as received, the resolved authority, every rule consulted, every risk
signal that fired, the approval and who decided it, the credential lease (by
reference, never by value), the downstream response status, and the final
outcome.

```bash
veefore logs --decision BLOCK --limit 20
```

or `GET /v1/actions/{actionId}` for the whole thing. This is the difference
between "the agent was blocked" and "the agent was blocked by rule
`rule-refund-ceiling` of policy version 4 because `amount` was 120000 and the
ceiling is 100000, at 12:38:04, and here is the delegation it was acting under".

## Where to go deeper

:::cards

:::card Request path | /architecture/request-path
The same pipeline from the process and network point of view.
:::

:::card Policy | /concepts/policy
Rules, operators, priority, versioning and the default decision.
:::

:::card Enforcement modes | /concepts/enforcement-modes
How to move through the modes without breaking production.
:::

:::card Error codes | /reference/errors
Every refusal, what it means and what to do about it.
:::

:::
