---
title: Write a retention/PI policy
path: guides/write-a-policy
status: published
---

# Write a retention/PI policy

A policy is the declarative YAML that decides, for every PI-marked field, what
ScaiLog does with it — and how long entries are kept. Developers mark a field as
PI with `pi(value, pi_type)`; the policy is where you, as an operator, turn that
`pi_type` into a concrete action per service and environment. Each tenant has one
active policy, versioned. Editing it needs the `admin` capability.

## The four actions

For each PI field the policy resolves to exactly one action:

| Action | Effect |
| --- | --- |
| `encrypt` | Store the value encrypted under the subject's per-subject DEK. Decryptable via DSAR; destroyed on erasure. This is what makes cryptographic erasure work. |
| `redact` | Replace the value with `[REDACTED:<pi_type>]` before storage. No plaintext, no ciphertext. |
| `drop` | Discard the field entirely; only its category is recorded. |
| `keep` | Store the value in plaintext. Lawful only for genuinely non-identifying types (`pseudonymous`); the validator warns otherwise. |

## Author the YAML

A policy is a `policy` name, a `version`, an optional `match` block, a list of
`rules`, and two retention settings. This is the built-in default:

```yaml
policy: default
version: 1
rules:
  - pi_type: "*"
    action: default
entry_retention_days: 365
pi_entry_retention_days: 90
```

`entry_retention_days` is how long ordinary entries are kept; entries carrying PI
use the (typically shorter) `pi_entry_retention_days`. A rule targets a
`pi_type` and sets an `action`, optionally narrowed by `service` and
`environment`, and may set a per-rule `retention_days`. A richer example:

```yaml
policy: acme
version: 3
match:
  tenant: acme
rules:
  # Encrypt direct identifiers everywhere.
  - pi_type: direct_identifier
    action: encrypt
  # In dev, drop them instead — don't accumulate real PII in a test tenant.
  - pi_type: direct_identifier
    environment: dev
    action: drop
  # Never keep credentials or special-category data.
  - pi_type: credentials
    action: drop
  - pi_type: special_category
    action: drop
  # A pseudonymous session id is safe to keep in plaintext.
  - pi_type: pseudonymous
    action: keep
entry_retention_days: 365
pi_entry_retention_days: 30
```

## Action `default` and the taxonomy

A rule with `action: default` (and any field with no matching rule) falls through
to the built-in taxonomy default for that `pi_type`. The defaults are:

| `pi_type` | Default action |
| --- | --- |
| `direct_identifier` | `encrypt` |
| `online_identifier` | `encrypt` |
| `location` | `encrypt` |
| `financial` | `encrypt` |
| `freeform_user_content` | `encrypt` |
| `credentials` | `drop` |
| `special_category` | `drop` |
| `pseudonymous` | `keep` |

An unknown custom `pi_type` with no matching rule defaults to `drop` — the safe
choice is to never store PI you have not classified.

## Most-specific resolution

When several rules match a field, ScaiLog picks the **most specific** one. Each
condition adds to a specificity score: a concrete `pi_type` (not `*`) is worth 4,
a `service` match 2, an `environment` match 1; ties break toward the
later-declared rule. So in the example above, a `direct_identifier` field emitted
in `dev` resolves to `drop` (pi_type + environment = 5) while the same field in
`prod` resolves to `encrypt` (pi_type only = 4). Resolution runs across
`(service, environment, pi_type)`; the `match` block scopes the whole policy to a
tenant/environment.

Whichever version resolved a given entry is frozen into that entry, so historical
entries stay explainable even after you change the policy.

## Validate and version it

Validation runs server-side when you store a policy — the server checks the YAML,
rejects bad actions or malformed rules with a `400`, and returns non-fatal
`warnings` (for example, using `keep` on an identifying `pi_type`). Store a new
version with the CLI:

```bash
scailog policy show                 # print the active policy + version
scailog policy edit acme-policy.yaml   # validate + store; prints the new version
scailog policy history              # list versions and their effective dates
```

`scailog policy edit` surfaces any validation warnings after storing. The
underlying API is a GET/PUT pair:

```http
GET /v1/policies?tenant=acme
```

```http
PUT /v1/policies
Authorization: Bearer <admin key>
Content-Type: application/json

{ "tenant": "acme", "yaml": "policy: acme\nversion: 3\n..." }
```

A successful `PUT` stores a new version and returns `{ "version": N, "warnings":
[...] }`. Each store is a new version; the previous ones remain in
`scailog policy history`.
