---
title: The policy engine
path: concepts/policies
status: published
---

# The policy engine

A ScaiLog policy is declarative YAML that decides, for every PI-marked field,
exactly one of four things to do with it: encrypt, redact, drop, or keep. This
page explains how a policy is written, how the engine resolves which rule wins,
and how policies are versioned so old entries stay explainable forever.

## What a policy decides

Each PI field carries a `pi_type` from the taxonomy. When an entry is processed,
the engine resolves an **action** for every PI field from the tuple
`(service, environment, pi_type)`, yielding exactly one of:

| Action | Effect on the field |
|--------|---------------------|
| `encrypt` | Encrypt the value under the subject's DEK; store as ciphertext in `pi_fields`. |
| `redact` | Replace the value with `[REDACTED:<pi_type>]`; store no PI. |
| `drop` | Discard the value entirely; record only that a field of this type was dropped. |
| `keep` | Store the value in plaintext (only appropriate for `pseudonymous` or DPO-cleared types). |

The action is recorded per field in the entry's `pi_meta`, so you can always
explain why a given value was encrypted, redacted, dropped, or kept.

## A policy is YAML

A policy is a named, versioned document with a list of rules. Each rule matches
some subset of `(service, environment, pi_type)` and names an action; a rule can
also set `retention_days`. The built-in default policy simply falls every type
through to its taxonomy default:

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

`action: default` means "use the taxonomy default for this `pi_type`" — so with
the policy above, `direct_identifier` encrypts, `credentials` drops,
`pseudonymous` keeps, and so on, exactly as the taxonomy prescribes. A more
specific policy narrows behaviour, for example:

```yaml
policy: default
version: 2
rules:
  - pi_type: "*"
    action: default
  - pi_type: online_identifier
    service: checkout
    environment: prod
    action: encrypt
    retention_days: 30
  - pi_type: freeform_user_content
    environment: dev
    action: redact
```

## Most-specific rule wins

Resolution is deterministic. The engine collects every rule that matches the
field's `(service, environment, pi_type)`, then picks the **most specific** one.
Specificity is scored by which dimensions a rule pins down:

- matching an exact `pi_type` (not `*`) is worth the most,
- then a specific `service`,
- then a specific `environment`.

If two rules tie on specificity, the later rule (higher index) wins. If the
chosen rule says `action: default`, or if no rule matches at all, the engine
falls through to the taxonomy default for that `pi_type`. A field with a custom
`pi_type` that has neither a matching rule nor a taxonomy default is dropped —
the safe default is never to store an unrecognized PI value.

## Versioning and validation

Policies are never edited in place. Storing a new policy creates a new version
(the next version number, or a higher one the document names explicitly), and
old versions are kept forever. The resolved `policy_version` is frozen into every
entry, so you can always reconstruct exactly which policy governed a historical
entry and why each field was handled as it was.

Before a policy is accepted it is validated. Hard errors — such as an unknown
action — raise immediately. Softer issues produce warnings: notably, a `keep`
action on a PI type other than `pseudonymous` warns that it stores plaintext PI
and is only lawful for pseudonymous or DPO-cleared custom types.

The two retention fields (`entry_retention_days` and `pi_entry_retention_days`)
let PI-bearing entries expire sooner than ordinary entries.
