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

# Observe for a week, then enforce

> Run the real policy in observe mode against real traffic, execute everything, record what enforcement would have blocked.

You have an agent in production and no idea what a policy would cost. Put the policy in
front of it in observe mode: every action executes as before, every receipt says what enforce
mode would have done, and after a week `ctrlrun stats` gives you the refusals and approval
requests you would have fielded. Then change the one line.

## The policy

```yaml runnable theme={null}
schema: ctrlrun.policy/v3
mode: observe

actions:
  crm.update_record:
    effect: "crm:{record_id}:{field}:{revision}"
    decision: allow
  email.send:
    effect: "email:{message_id}"
    rules:
      - when: { to_domain_eq: "example.com" }
        decision: allow
      - decision: approve
  stripe.refund:
    effect: "refund:{payment_id}"
    rules:
      - when: { amount_gte: 0, amount_lte: 50000 }
        decision: allow
      - when: { amount_gte: 0, amount_lte: 500000 }
        decision: approve
      - decision: deny
```

## The code

```python runnable file=main.py theme={null}
from pathlib import Path

import ctrlrun
from ctrlrun import Control, Policy, SQLiteStateStore

HERE = Path(__file__).resolve().parent
STATE = HERE / ".ctrlrun"
STATE.mkdir(exist_ok=True)
for name in ("state.db", "state.db-wal", "state.db-shm"):
    (STATE / name).unlink(missing_ok=True)

executed: list[str] = []
store = SQLiteStateStore(STATE / "state.db")
control = Control(Policy.from_file(HERE / "ctrlrun.yaml"), store)


@ctrlrun.protect("crm.update_record", effect="crm:{record_id}:{field}:{revision}", control=control)
def update(record_id: str, field: str, value: str, revision: int) -> str:
    executed.append("update")
    return "updated"


@ctrlrun.protect("email.send", effect="email:{message_id}", control=control)
def send(message_id: str, to_domain: str) -> str:
    executed.append("send")
    return "sent"


@ctrlrun.protect("stripe.refund", effect="refund:{payment_id}", control=control)
def refund(payment_id: str, amount: int) -> str:
    executed.append("refund")
    return "refunded"


# A week of traffic, compressed.
with ctrlrun.context(agent="support-agent"):
    update(record_id="c_1", field="phone", value="+353 1 555 0100", revision=3)
    send(message_id="m_1", to_domain="example.com")
    send(message_id="m_2", to_domain="gmail.com")  # would have needed a human
    refund(payment_id="txn_1", amount=12000)
    refund(payment_id="txn_2", amount=250000)  # would have needed a human
    refund(payment_id="txn_3", amount=900000)  # would have been denied
    refund(payment_id="txn_1", amount=12000)  # would have been a duplicate

print("actions executed:", len(executed))
if len(executed) != 7:
    raise SystemExit("observe mode blocked something; it must execute everything")

for receipt in store.receipts():
    if receipt.would_have is not None and receipt.would_have.blocked_reason:
        print(f"{receipt.action} would have been blocked: {receipt.would_have.blocked_reason}")
```

## What the agent sees

```text theme={null}
actions executed: 7
email.send would have been blocked: approval_required
stripe.refund would have been blocked: approval_required
stripe.refund would have been blocked: rule[2]
stripe.refund would have been blocked: duplicate
```

Seven actions, seven executions. The mail to `gmail.com` went out; so did the €9,000 refund.
Observe mode asks no human and stops nothing. It only writes down what it would have done.

## The receipt

```bash runnable theme={null}
ctrlrun stats
```

The numbers are counted from `would_have.blocked_reason` on the receipts in the local store,
nothing else. Change `mode: observe` to `mode: enforce` and the same receipts become `BLOCKED`
receipts and approval requests.

## When an AMBIGUOUS appears

Observe mode changes nothing about outcomes: a lost reply is `AMBIGUOUS` in observe mode too,
and the record is written. What differs is that observe mode records that a retry *would* have
been refused and lets it run, so resolve the effect before switching to enforce, or the first
enforced retry is refused.

## Next

* [Roll out observe, then enforce](/guides/observe-to-enforce): the week, the numbers and the switch.
* [Observe mode](/concepts/observe-mode) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [Cookbook](/cookbook/index.md)
- [Roll out observe, then enforce](/guides/observe-to-enforce.md)
- [Observe mode](/concepts/observe-mode.md)
- [ctrlrun verify](/verify.md)
- [Verify in CI](/guides/verify-in-ci.md)
