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

# Roll a policy out safely

> Preview a draft, prove it on a test agent, then turn it on live.

A policy is the thing standing between an agent and your money, so the expensive mistakes are
both directions: a rule that refuses work you meant to allow, and a rule that allows work you
meant to refuse. Three steps separate writing a rule from letting it decide anything real.

## Before you start

<Snippet file="env-agents.mdx" />

```bash Identifiers theme={"dark"}
export KORDIO_TEST_AGENT_ID="7d2a6c15-3f89-4b02-9e64-8a15d7c0b3f2"
export KORDIO_LIVE_AGENT_ID="b4e91f27-08c5-4d63-a71e-5f3b2c8d09a4"
export KORDIO_BUDGET_ID="3f1c8a92-5d41-4c8e-9f2b-7a6e1d0c4b83"
```

`KORDIO_AGENT_KEY` is the test agent's key. A policy belongs to one agent, so the two agent ids
matter: rolling out means writing the rules against the live agent once they behave.

The amounts here assume the agent's starter policy is disabled, as the
[quickstart](/agents/quickstart) does in step 2. Leave it on and it holds anything over \$100 for
a person before your own rules get a say.

## 1. Preview the draft against nothing

`policy_previews` evaluates a policy you have not saved, on a request that never happened:

```bash theme={"dark"}
curl -s -X POST https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/policy_previews \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"agent_id\": \"$KORDIO_TEST_AGENT_ID\",
    \"action_type\": \"payment.create\",
    \"resource\": \"acme-supplies.example\",
    \"cost_cents\": 24000,
    \"metadata\": { \"account_age_days\": 12 },
    \"policy\": {
      \"per_transaction_cap_cents\": 50000,
      \"rules\": [
        {
          \"kind\": \"condition\",
          \"effect\": \"require_approval\",
          \"rule_name\": \"new_buyer_needs_a_person\",
          \"action_types\": [\"payment.create\"],
          \"when\": { \"field\": \"metadata.account_age_days\", \"operator\": \"lt\", \"value\": 30 }
        }
      ]
    }
  }"
```

```json 200 OK theme={"dark"}
{
  "data": {
    "outcome": "requires_approval",
    "rule": "new_buyer_needs_a_person",
    "detail": {
      "rule_id": "0b6d9a41-5e28-4c17-83f9-2d4a7b0e6c53",
      "matched": { "field": "metadata.account_age_days", "operator": "lt", "value": 30 }
    },
    "headroom": { "session_remaining_cents": 100000 }
  }
}
```

No policy, no intent, no reservation, no audit event. The `rule_id` is minted for that one
evaluation and thrown away, so it will not match the id the rule gets when you save it. `imports` resolve against the modules the
workspace really has, so a draft that builds on a saved module previews honestly.
`session_budget_cents` sets the synthetic budget and defaults to `100000`. Previewing needs only
the `read` capability, deliberately, because it changes nothing.

Two things preview will not tell you. It evaluates the draft alone, so it says nothing about how
the draft interacts with the agent's other active policies, and window rules read the agent's
real recent usage rather than a clean slate.

## 2. Let a test agent run into it

Save the draft against the test-mode agent:

```bash theme={"dark"}
curl -s -X POST https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/policies \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"agent_id\": \"$KORDIO_TEST_AGENT_ID\",
    \"per_transaction_cap_cents\": 50000,
    \"rules\": [
      {
        \"kind\": \"condition\",
        \"effect\": \"require_approval\",
        \"rule_name\": \"new_buyer_needs_a_person\",
        \"action_types\": [\"payment.create\"],
        \"when\": { \"field\": \"metadata.account_age_days\", \"operator\": \"lt\", \"value\": 30 }
      }
    ]
  }"
```

```json 201 Created theme={"dark"}
{
  "data": {
    "id": "f28c0b95-4a71-4e30-96d2-7c1b5a8e0f36",
    "agent_id": "7d2a6c15-3f89-4b02-9e64-8a15d7c0b3f2",
    "status": "active",
    "mode": "blocklist",
    "per_transaction_cap_cents": 50000,
    "rules": [
      { "rule_name": "new_buyer_needs_a_person", "id": "4c1e8f60-7a29-4d03-b5e8-9f2a1c740db6" }
    ],
    "created_at": "2026-08-22T14:41:09Z"
  }
}
```

A test agent runs the same engine, the same rules, and the same shapes as a live one. Replay
yesterday's real requests through it, either as real authorizations or through `simulate`, which
records an `action.simulated` audit event and reserves nothing:

```bash theme={"dark"}
curl -s -X POST https://api.kordio.io/control/v1/agent/actions/simulate \
  -H "Authorization: Bearer $KORDIO_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"budget_id\": \"$KORDIO_BUDGET_ID\",
    \"action_type\": \"payment.create\",
    \"cost_cents\": 24000,
    \"metadata\": { \"account_age_days\": 12 }
  }"
```

```json 200 OK theme={"dark"}
{
  "data": {
    "outcome": "requires_approval",
    "rule": "new_buyer_needs_a_person",
    "detail": {
      "rule_id": "4c1e8f60-7a29-4d03-b5e8-9f2a1c740db6",
      "matched": { "field": "metadata.account_age_days", "operator": "lt", "value": 30 }
    },
    "headroom": { "session_remaining_cents": 50000 }
  }
}
```

Unlike preview, this runs every active policy on the agent, which is the number you actually
care about before promoting anything.

## 3. Read what it held, test agents only

Held intents can be narrowed to one agent mode, so a live queue is not polluted by a rehearsal:

```bash theme={"dark"}
curl -s "https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/action_intents?agent_mode=test&state=requires_approval" \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN"
```

```json 200 OK theme={"dark"}
{
  "data": [
    {
      "id": "9b2ef0c1-6a3d-4e75-8c19-2f4b7d5a0e63",
      "agent_id": "7d2a6c15-3f89-4b02-9e64-8a15d7c0b3f2",
      "action_type": "payment.create",
      "cost_cents": 24000,
      "state": "requires_approval",
      "metadata": { "account_age_days": 12 }
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

The console does the same thing on the Approvals page, where the queue filters to all agents,
live agents, or test agents. `agent_mode` accepts `live` or `test`, and anything else is a `400`
rather than a silent full listing, because answering a narrowed question about held money with
every intent in the workspace is the wrong way to be wrong. The same filter works on
`payment_intents`.

## 4. Promote it to the live agent

Write the proven rules against the live agent. Create it turned off, check it reads the way you
expect, then flip it:

```bash theme={"dark"}
POLICY=$(curl -s -X POST https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/policies \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"agent_id\": \"$KORDIO_LIVE_AGENT_ID\",
    \"status\": \"disabled\",
    \"per_transaction_cap_cents\": 50000,
    \"rules\": [
      {
        \"kind\": \"condition\",
        \"effect\": \"require_approval\",
        \"rule_name\": \"new_buyer_needs_a_person\",
        \"action_types\": [\"payment.create\"],
        \"when\": { \"field\": \"metadata.account_age_days\", \"operator\": \"lt\", \"value\": 30 }
      }
    ]
  }")

export KORDIO_POLICY_ID=$(echo "$POLICY" | jq -r '.data.id')
```

```json 201 Created theme={"dark"}
{
  "data": {
    "id": "31d70e6a-8c45-4b29-9f07-2e6a4d13b805",
    "agent_id": "b4e91f27-08c5-4d63-a71e-5f3b2c8d09a4",
    "status": "disabled",
    "mode": "blocklist",
    "per_transaction_cap_cents": 50000,
    "rules": [
      { "rule_name": "new_buyer_needs_a_person", "id": "9e3c5b18-6f04-4a72-b81d-0c7e2a94f56b" }
    ],
    "created_at": "2026-08-22T15:06:12Z"
  }
}
```

A `disabled` policy is still validated when you save it, and is left out of evaluation entirely.
Turn it on when the diff reads right:

```bash theme={"dark"}
curl -s -X PATCH https://api.kordio.io/control/v1/workspaces/$KORDIO_WORKSPACE/policies/$KORDIO_POLICY_ID \
  -H "Authorization: Bearer $KORDIO_DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'
```

```json 200 OK theme={"dark"}
{
  "data": {
    "id": "31d70e6a-8c45-4b29-9f07-2e6a4d13b805",
    "agent_id": "b4e91f27-08c5-4d63-a71e-5f3b2c8d09a4",
    "status": "active",
    "per_transaction_cap_cents": 50000,
    "updated_at": "2026-08-22T15:07:33Z"
  }
}
```

<Warning>
  Disabling is not a safe rollback for an agent's only policy. An agent with no active policy
  denies everything with the rule `no_policy`, so turning the last one off stops the agent
  rather than opening it up. Roll back by editing the rule, or by activating the policy you are
  replacing before disabling this one.
</Warning>

Both agents now carry the same rules twice. A [policy module](/agents/policies#reusable-modules)
removes the duplication, at a price worth knowing: editing a module changes every policy that
imports it, so a module shared by the test and live agents reaches live the moment you save it.
Share the module once the rule is settled, not while you are still tuning it.

## Next steps

<CardGroup cols={2}>
  <Card title="Give an agent a budget for one run" icon="wallet" href="/agents/guides/session-budget">
    The other half of a safe first run.
  </Card>

  <Card title="Require a person above a threshold" icon="users" href="/agents/guides/approval-threshold">
    Resolve the holds this rollout produced.
  </Card>

  <Card title="Writing a policy" icon="scale" href="/agents/policies">
    The rule kinds, modules, and the preview endpoint in full.
  </Card>

  <Card title="How a decision is reached" icon="shield" href="/agents/concepts">
    Evaluation order, and why the first denial wins.
  </Card>
</CardGroup>
