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

# A data-deletion agent under a retention rule

> An agent handles deletion requests: records past retention are purged on their own, records inside retention wait for a human.

An agent processes deletion requests against customer data. A record past its retention
period can go; a record still inside retention is a person's decision; a record under legal
hold is not deleted by an agent at all. The policy sees only the arguments, so the executor
passes the facts it decided on: how many days the record has been held, and whether a hold
applies.

## The policy

```yaml runnable theme={null}
schema: ctrlrun.policy/v2

actions:
  records.purge:
    effect: "purge:{record_id}"
    resource: "record:{record_id}"
    rules:
      - when: { legal_hold_eq: true }
        decision: deny
      - when: { age_days_gte: 730 }
        decision: allow
      - decision: approve
```

## 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)

purged: list[str] = []


def purge_at_warehouse(record_id: str, lose_reply: bool = False) -> str:
    purged.append(record_id)
    if lose_reply:
        raise TimeoutError("warehouse did not answer in 30s")
    return "purged"


store = SQLiteStateStore(STATE / "state.db")
control = Control(Policy.from_file(HERE / "ctrlrun.yaml"), store)


@ctrlrun.protect(
    "records.purge", effect="purge:{record_id}", resource="record:{record_id}", control=control
)
def purge(record_id: str, age_days: int, legal_hold: bool, lose_reply: bool = False) -> str:
    return purge_at_warehouse(record_id, lose_reply)


with ctrlrun.context(agent="deletion-agent"):
    print("r_100, 900 days old:", purge(record_id="r_100", age_days=900, legal_hold=False))

    try:
        purge(record_id="r_101", age_days=400, legal_hold=False)
    except ctrlrun.ApprovalRequired as pending:
        print("r_101, 400 days old: a human decides:", pending.request_id)
    else:
        raise SystemExit("a record inside retention was purged without a human")

    try:
        purge(record_id="r_102", age_days=900, legal_hold=True)
    except ctrlrun.ActionDenied:
        print("r_102, under legal hold: refused")
    else:
        raise SystemExit("a record under legal hold was purged")

    try:
        purge(record_id="r_103", age_days=900, legal_hold=False, lose_reply=True)
    except TimeoutError:
        print("r_103: reply lost")
    try:
        purge(record_id="r_103", age_days=900, legal_hold=False)
    except ctrlrun.AmbiguousEffect:
        print("r_103 again: refused until someone checks the warehouse")
    else:
        raise SystemExit("a purge of unknown outcome was repeated")

print("purge calls at the warehouse:", purged)
```

## What the agent sees

```text theme={null}
r_100, 900 days old: purged
r_101, 400 days old: a human decides: apr_…
r_102, under legal hold: refused
r_103: reply lost
r_103 again: refused until someone checks the warehouse
purge calls at the warehouse: ['r_100', 'r_103']
```

The rule order matters: the legal-hold check is first, so a held record is refused whatever
its age. Rules are first-match, and the receipt names which one decided.

## The receipt

```bash runnable theme={null}
ctrlrun receipts --last 4
```

## When an AMBIGUOUS appears

A purge is the one action here you cannot repeat to be safe, because a second purge of a
record that is gone may error in a way that looks like a failure, and a second purge of one
that is not gone is what you wanted. Query the warehouse for `r_103`, then
`ctrlrun resolve purge:r_103 --committed` or `--failed`, and let the agent's next attempt
follow from that.

## Next

* [A CRM-update agent](/cookbook/crm-update-agent).
* [Decisions](/concepts/decisions) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [A CRM-update agent](/cookbook/crm-update-agent.md)
- [Cookbook](/cookbook/index.md)
- [Running on Postgres](/postgres.md)
- [Frequently asked questions](/faq.md)
- [The Agent Control Standard](/ACS.md)
