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

# Policies — author and apply guardrail policies from the dashboard

> Author guardrail policies as versioned documents in the dashboard, review the exact change set, backtest against real traffic, and apply — through the same plan/apply pipeline and the same refusals as `zespan policy`.

**Policies is where a guardrail policy comes from. [Guardrails](/dashboard/guardrails) is what it does at runtime.** A policy compiles into the guardrail rules a project enforces; Guardrails is the execution history and live configuration of those rules. The split exists so authoring a change and watching it fire are two different, un-confusable views of the same control.

Whether a policy is written as a YAML file in your repository and applied with [`zespan policy apply`](/cli/policy), or authored here and applied from the dashboard, it reaches your project's guardrails through the exact same computation: the same parser, the same `plan` → `apply` pipeline, the same refusals. There is no second, dashboard-only way to write a guardrail rule from a policy — this page is a second **entrypoint** into the pipeline documented in [Policy as code](/policies/as-code), not a second implementation of it.

<Note>
  Policies is available on the **Pro** plan and above, the same gate as Guardrails — see [Plan requirement](/dashboard/guardrails#plan-requirement).
</Note>

## One writer per policy

Every policy has exactly one owner at a time: **Code** (a file in your repository, applied by CI) or **Dashboard** (authored here). The owner decides who may change or remove it — the other side is refused, not silently overwritten. See [Ownership](#ownership-and-handing-a-policy-over) below; this is the same ownership model [Policy as code](/policies/as-code#ownership-what-code-owns-and-what-the-dashboard-owns) already describes for Guardrails, extended to cover policies authored here instead of hand-built guardrail rules.

## The inventory

The main Policies page lists every policy enforcing (or once enforced) in the selected environment — the same environment switcher Traces, Guardrails, and Evaluations use. One row per policy:

| Column  | Meaning                                                                                                   |
| ------- | --------------------------------------------------------------------------------------------------------- |
| Policy  | The policy's name, with its id or repository path underneath                                              |
| Owner   | **Code** or **Dashboard** — see [Ownership](#ownership-and-handing-a-policy-over)                         |
| Posture | `Dry run`, `Warn`, or `Deny` — the enforcement ladder's current rung, or `—` if nothing is live           |
| Rules   | How many compiled rules this policy contributes in this environment                                       |
| Tested  | Whether a backtest vouches for exactly the text that's live — see [Backtest evidence](#backtest-evidence) |
| Status  | Whether what's enforced matches what's authored — see below                                               |

**Status** values:

| Status            | Meaning                                                                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| In sync           | What is enforced matches the applied revision                                                                                                        |
| Unapplied changes | A newer revision exists that hasn't been applied to this environment                                                                                 |
| Not applied       | This policy enforces nothing in this environment yet                                                                                                 |
| Detached          | Ownership was handed to the dashboard (see [Detaching](/dashboard/guardrails#detaching)); applying from code will report a conflict                  |
| Unrecognised      | What's deployed doesn't match any revision Zespan holds — this should never happen through normal use, and is surfaced rather than hidden if it does |

**Tested** values:

| Status     | Meaning                                         |
| ---------- | ----------------------------------------------- |
| Tested     | A backtest exists for exactly this text         |
| Stale test | The policy changed after it was last backtested |
| Untested   | No backtest has been run against this policy    |

A filter box searches by name or id, and an owner toggle narrows to **Code** or **Dashboard**. Click a row to open that policy's [detail page](#reviewing-a-policy).

<Tip>
  A policy live in more than one environment is a separate inventory row per environment. Switch environments with the same selector Guardrails uses to see another.
</Tip>

If a project has no policies yet, the inventory explains what a policy is and offers both starting points side by side: **Author a policy** here, or run `zespan policy apply ./policies --env prod` from CI.

## Authoring

<Steps>
  <Step title="Open New policy">
    From the Policies page, click **New policy**.
  </Step>

  <Step title="Name it">
    The **Name** and **id** fields write straight into the document's `metadata.name` and `metadata.id` as you leave each field — the fields and the YAML never disagree, because one is the source for the other. If the id or name line in the YAML has been hand-edited into something the fields can't unambiguously update (duplicated, removed, or re-indented), authoring refuses to guess and asks you to fix it directly in the editor instead.
  </Step>

  <Step title="Set the enforcement posture">
    `Dry run`, `Warn`, or `Deny`, matching [the enforcement ladder](/policies/as-code#the-enforcement-ladder). Start new policies at dry run.
  </Step>

  <Step title="Write the rules">
    The YAML editor is the source of truth — full syntax highlighting and inline error squiggles, powered by the same server-side validator `zespan policy validate` uses, so an error shown here is the same error the CLI would report. Alongside it, the **rule builder** inserts a generated rule fragment at the end of the document's `rules:` list for the common types (regex, keyword, PII, toxicity, LLM classifier). It only ever inserts — it never reads the document back into itself, so nothing you've hand-written (comments, key order, an unusual structure) is ever silently rewritten. If it can't find a single unambiguous `rules:` list to insert into, it says so and leaves the fragment for you to paste in yourself.
  </Step>

  <Step title="Save">
    **Save draft** commits the document's first revision. Nothing is enforced yet — saving only creates the record. Review and apply it from the policy's own page.
  </Step>
</Steps>

<Note>
  Saving is disabled until the YAML validates and both Name and id are filled in. That's deliberate: nothing invalid or unidentified reaches a revision.
</Note>

## Reviewing a policy

A saved policy's detail page has four tabs:

* **Rules** — how many compiled rules this policy contributes in the current environment, with a link to their runtime view in Guardrails.
* **Source** — the applied revision's YAML, read-only, with a revision picker. A policy authored in git shows its repository path instead of an editor — Zespan holds no document for it to render.
* **Tests** — the current test status in plain language: tested, stale, or untested, and what to do about it.
* **History** — when this policy was last applied in this environment and by whom, with a link to the full [apply history](#apply-history).

When a revision hasn't been applied yet, a **Review changes** button appears in the header.

## Review and apply

Review renders the exact plan `zespan policy plan` would print for the same change — `creates`, `updates`, `adopts`, `deletes`, and any `conflicts` — because it's computed by the same server endpoint. Reviewing needs only `policy:read`, which every role has; **Apply** needs `policy:apply` and is disabled while any conflict is unresolved.

Applying goes through the identical set of refusals `zespan policy apply` has always had. Each one opens a dialog naming what happened and offering the action that resolves it, rather than a generic error:

| Refusal                   | What it means                                                                          | What the dialog offers                                                                                                                                           |
| ------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENVIRONMENT_NOT_ALLOWED` | This policy's `appliesTo.environments` doesn't list the environment you're applying to | No override — apply to a listed environment, or add this one to the policy                                                                                       |
| `STALE_PLAN`              | Something else changed enforcement since this plan was computed                        | **Re-plan and review** — recomputes the plan against current state                                                                                               |
| `CONFLICTS`               | Part of this change is owned by a policy file in your repository                       | No override here — [detach](/dashboard/guardrails#detaching) the conflicting policy first, or leave it to code                                                   |
| `REMOVAL_NOT_ALLOWED`     | This apply would stop enforcing one or more policies                                   | Lists every policy that would be removed; type **REMOVE** to confirm, then **Remove them**                                                                       |
| `ADOPTION_NOT_ALLOWED`    | This apply would take over one or more hand-built guardrails from the Guardrails page  | **Take them over** — explicit, one click, no undo dialog beyond this one                                                                                         |
| `UNTESTED_DENY`           | This would enforce at deny with no backtest behind it                                  | **Run backtest now** (primary) runs one against the last 7 days right there; **Apply without testing** (secondary) overrides and is audited like any other apply |

<Warning>
  There is no `--force` equivalent in the dashboard. Taking a policy away from the other writer is always **Detach** — a deliberate, named, audited action on the Guardrails page — never a checkbox on an apply.
</Warning>

The `UNTESTED_DENY` treatment is the one worth using deliberately: it's a speed bump, not a wall, because someone mid-incident has to be able to ship — but the dialog makes the safe path (see what the backtest would have caught) as fast as the override.

A successful apply lands you on [apply history](#apply-history) at the entry it just created.

## Backtest evidence

The **Tested** badge and the `UNTESTED_DENY` gate read the same fact: whether a `PolicyTest` row exists for this policy's exact current text, keyed by content hash. A test run from the dashboard and one run with `zespan policy test` write to the same table, so whichever surface tested a given revision satisfies the deny gate on both.

The one place the dashboard triggers a backtest today is inside the `UNTESTED_DENY` dialog — **Run backtest now** runs it against the last 7 days of traffic and reports what it would have caught and broken, the same report [Policy testing](/policies/testing) describes for the CLI. To backtest against a different corpus (`issues`, or a specific dataset) before you're at the deny gate, use `zespan policy test --against <corpus>`.

## Apply history

**Activity** (linked from the Policies page header) lists every apply in the selected environment, newest first:

| Column   | Meaning                                                            |
| -------- | ------------------------------------------------------------------ |
| When     | When the apply ran                                                 |
| From     | **Dashboard** or **CI**, with `(forced)` if the CLI used `--force` |
| Policies | Every policy id the apply touched                                  |
| Changes  | Created / updated / removed counts, as `+N ~N -N`                  |

Every dashboard apply records the exact revision it deployed, so an apply made here always names a specific, immutable piece of text — not just "whatever the document held at the time."

## Ownership and handing a policy over

The **Owner** badge is the whole mechanism that lets git and the dashboard share a project without fighting over it:

| Badge     | Meaning                                                                                                                                                          |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Code      | Authored in your repository. Read-only here — applying a dashboard document that claims the same id is refused (`CONFLICTS`) rather than silently overwriting it |
| Dashboard | Authored here. A policy file claiming this id is refused unless the apply explicitly adopts it (`ADOPTION_NOT_ALLOWED`) or the policy has been detached          |

To move a policy from code to the dashboard — during an incident, or because ownership is genuinely changing hands — use **Detach** on its guardrail card or detail page in Guardrails, exactly as [Policy as code](/policies/as-code#detaching-a-policy) describes. The policy becomes dashboard-owned immediately; the next `zespan policy apply` reports it as a conflict and refuses to overwrite it without `--force`.

There's no dashboard-side equivalent for taking a policy the other direction — from dashboard to code — beyond what already exists: point a file at the same id and run `zespan policy apply --adopt`, the same adoption flow [Policy as code](/policies/as-code#adopting-a-project-that-already-has-guardrails) describes for hand-built guardrails.

<Tip>
  Archiving a policy (from its detail page) doesn't remove what it enforces — it retires the document from the inventory while leaving live rows exactly as they are. To stop enforcing it, apply with removal allowed first, then archive.
</Tip>

## Organisation view

Everything above is one project's inventory. The top-level **Policies** entry in the org admin sidebar (`/[org]/admin/policies`) rolls the same facts up across every project in the organisation, for the question a security lead actually asks: "is this policy deployed everywhere it should be?"

Four cards summarise the current scope:

| Card                | Meaning                                                                   |
| ------------------- | ------------------------------------------------------------------------- |
| Policies            | Distinct policy ids in scope                                              |
| Projects covered    | Projects with at least one policy in scope, out of every project in scope |
| Occurrences at deny | Occurrences enforcing at `deny`, not just observing                       |
| Untested or stale   | Occurrences with no backtest matching the live text                       |

Below the cards, one row per **policy id**, not per project — every project carrying that id is grouped underneath it. A row shows how many projects carry the policy, a posture breakdown (dry run / warn / deny), how many occurrences need attention (`out of band`, `detached`, `untested`, or `stale`), and the owner split (**Code** vs **Dashboard**). Expand a row to see every occurrence: which project, which environment, posture, rule count, owner, test status, and sync status, each linking to that project's own policy detail page. If projects use different display names for the same policy id, the row flags it — that disagreement is itself a governance signal, not noise to hide.

<Tip>
  The **Environment** selector defaults to **all environments** — not production. Every count and the "present in N of M" figure describe whatever scope is currently selected. Switching to **Production only**, or to one named environment, narrows every number on the page; read the scope before reading the numbers.
</Tip>

**Present in 9 of 12 projects** is coverage, not compliance: it means 9 of the 12 projects measurable in the current scope carry at least one occurrence of that policy id. Under a narrowed scope (Production only, or one named environment), a project with no matching environment at all was never measurable and is excluded from both the numerator and the denominator, rather than counted as absent for a reason that has nothing to do with policy coverage.

<Note>
  This page reports what is deployed. It does not, and cannot, declare a project **non-compliant** — no standard exists yet in Zespan for a project to be measured against; that's a later capability. "Present in 9 of 12 projects" and "Not present in: \<project>" are facts about what's deployed, never a verdict on the projects that don't carry a policy.
</Note>

A **Recent applies** section below the table lists the most recent applies across the whole organisation, newest first — project, environment, the policy ids touched, the change counts, and which entrypoint ran it (**from CLI** or **from dashboard**). This section itself has no time window or row cap — it is cursor-paginated, newest first, and you can keep paging back through the org's full apply history. The 90-day window and 2,000-row cap belong to `lastAppliedAt` on the rows further up: a policy's row can still show `Not applied` even while this section shows recent activity for the same project, because activity outside that window, or on a different environment, doesn't move `lastAppliedAt` on the row.

## What is not in this release

* **Editing a policy again after it's been saved.** The dashboard supports authoring, reviewing, backtesting, and applying a policy today; changing an already-saved policy's text from here is not yet available.
* **Restoring or diffing a specific past apply from the Activity page.** Both exist as API endpoints today — restore resolves an old apply back to a plan you confirm through the ordinary apply flow — but neither has a dashboard button yet.
* **A cross-environment view on a policy's detail page.** The detail page shows one environment at a time, whichever the switcher has selected; switch environments to see the same policy's posture elsewhere.

## Next steps

<CardGroup cols={2}>
  <Card title="Guardrails" icon="shield" href="/dashboard/guardrails">
    The runtime side: execution history, code-managed badges, and Detach.
  </Card>

  <Card title="Policy as code" icon="file-code" href="/policies/as-code">
    The authoring loop and ownership model from the repository side.
  </Card>

  <Card title="zespan policy" icon="terminal" href="/cli/policy">
    Every verb and flag, and the same six refusals from the CLI's perspective.
  </Card>

  <Card title="Policy file reference" icon="list-check" href="/policies/file-reference">
    Every field a policy document supports, and the supported YAML subset.
  </Card>

  <Card title="Policy testing" icon="flask" href="/policies/testing">
    How a backtest is scored, and what the deny gate is protecting against.
  </Card>

  <Card title="SDK guardrails" icon="code" href="/sdk/guardrails">
    Handle a guardrail verdict in your application code.
  </Card>
</CardGroup>
