---
title: "Author Policy Data"
url: "https://docs.caracal.run/v1.0/guides/author-policy/"
markdown_url: "https://docs.caracal.run/markdown/v1.0/guides/author-policy.md"
description: "Author the grant, binding, and confinement data the platform decision contract reads, and validate it before activation."
page_type: "page"
concepts: []
requires: []
---

# Author Policy Data

Canonical URL: https://docs.caracal.run/v1.0/guides/author-policy/
Markdown URL: https://docs.caracal.run/markdown/v1.0/guides/author-policy.md
Description: Author the grant, binding, and confinement data the platform decision contract reads, and validate it before activation.
Page type: page
Concepts: none
Requires: none

---

Use this guide after resources and applications exist and before activating production access.

## Why data, not decisions

Authorization logic - delegation narrowing, role and grant checks, label confinement, bootstrap isolation - is identical for every adopter and is the part most dangerous to get wrong. A single typo in a hand-written rule (`if { false }` → `if { true }`) silently turns a deny into an allow-all. Caracal removes that footgun by owning the logic in a signed, versioned [platform decision contract](/v1.0/concepts/policy/#decision-contract) and letting you supply only **data**.

Every policy you author is a data document marked with `# caracal:data-document` on its first line. It carries only data tables and is **forbidden from defining `result`**, so it can never decide an authorization on its own. `restrict` entries can only subtract authority and `confinement` can only narrow it - a careless data change fails closed.

## Prerequisites

* A zone, application, and resource.
* Access to the web console or Admin API.
* The resource identifier and scopes you want to grant.
* The real application ID and the Session labels the application emits.
* Representative allow and deny inputs for simulation.

## Start from a grant

The core document is `grants`: it names the application that owns a resource view and the scopes each role may hold. Pair it with `app_ids`, which binds the application key you use in `grants` to the control-plane id the STS sees as `input.principal.id`.

```rego
# caracal:data-document
package caracal.authz

import rego.v1

app_ids := {
  "pipernet": "app-pipernet",
}

grants := {
  "resource://pipernet": {
    "application": "pipernet",
    "roles": {"reader": ["pipernet:read", "pipernet:write"]},
  },
}
```

This is the canonical "application A may call resource B with scopes C" pattern,
expressed as data. The platform contract allows a mint only when the acting
application owns the view, the agent's role label grants the scope, and the
Delegation narrows to it. You declare the grant; the contract enforces the
narrowing. Several `app_ids` keys may bind to one application id - the contract
treats them as one identity - but one binding key per application keeps grant
review straightforward, and the Admin SDK's grant helpers author exactly that.

## Gate scopes on human approval

Two further documents add an optional human gate on top of a grant. `risk` names a
tier for each sensitive scope, and `approval_tiers` declares which tiers hold the
mint until a person decides it:

```rego
# caracal:data-document
package caracal.authz

import rego.v1

risk := [
  {"scope": "pipernet:refund", "tier": "high"},
]

approval_tiers := [
  {"tier": "high", "approver": "operator", "ttl_seconds": 1800, "privacy": "identified"},
]
```

Like `restrict` and `confinement`, an approval declaration can only add a gate,
never widen authority, and a malformed declaration fails the gated mint closed.
[Human Approval](/v1.0/guides/human-approval/) covers the tier fields, both decision planes,
and the agent-side wait-and-retry flow.

## Start from a template

You do not have to write each document by hand. Caracal ships a built-in catalog of
data-document starters, served at `/v1/policy-templates` and through the Admin SDK:

```ts
import { AdminClient } from '@caracalai/admin'

const admin = new AdminClient({
  apiUrl: process.env.CARACAL_API_URL!,
  adminToken: process.env.CARACAL_ADMIN_TOKEN!,
})

const templates = await admin.policyTemplates.list()
const starter = await admin.policyTemplates.get('resource-grants')
```

| Template               | Use it for                                                                     |
| ---------------------- | ------------------------------------------------------------------------------ |
| `application-bindings` | Map each application key used in `grants` to its control-plane application id. |
| `resource-grants`      | Declare the owning application and per-role scope sets for a resource view.    |
| `label-confinement`    | Cap every session carrying a label prefix to a fixed scope set.                |
| `zone-restriction`     | A deny overlay that freezes the zone while an entry is present.                |

For assisted authoring, describe the outcome to the [Caracal Operator](/v1.0/concepts/operator/#authoring-policy). Its policy author models the use case as grant, binding, and confinement data, validates and previews each document against the platform contract, and proposes a governed create you review and approve - so the policy that lands is already contract-valid.

## How your data maps to the request

The decision contract evaluates a fixed input contract and resolves it against your
data. The acting application is the principal - there is no `input.application` or
`input.grant` object. See the full [Policy Input Contract](/v1.0/concepts/policy/#policy-input-contract)
for every field.

| Input the contract reads         | Resolved against                                           |
| -------------------------------- | ---------------------------------------------------------- |
| `input.principal.id`             | `app_ids` - to find the application key used in `grants`.  |
| `input.principal.labels`         | `grants[...].roles` and `confinement` label prefixes.      |
| `input.resource.identifier`      | the top-level key in `grants`.                             |
| `input.context.requested_scopes` | the role's scope set and any matching `confinement` rule.  |
| `input.delegation_edge.scopes`   | the narrowing floor every requested scope must sit inside. |

## Validate before versioning

Use the web console policy workflow to paste the document and run validation. For automation, validate through the Admin API or `@caracalai/admin`:

```ts
import { AdminClient } from '@caracalai/admin'

const admin = new AdminClient({
  apiUrl: process.env.CARACAL_API_URL!,
  adminToken: process.env.CARACAL_ADMIN_TOKEN!,
})

const validation = await admin.policies.validate(policySource)
if (!validation.valid) {
  throw new Error('policy failed validation')
}
```

Validation enforces the data-document contract: the package must be `caracal.authz`,
the first line must carry the `# caracal:data-document` directive, the document must
define at least one data rule, and it must **not** define `result`. Validation also
checks the schema version, balanced syntax, and forbidden built-ins. Because the
platform contract owns the decision, a data document can never authorize on its own.

### Preview how the document parses

A successful validation returns a `preview` describing exactly what the engine
parsed, so you can confirm the backend reads your data the way you intend before
activating it:

```ts
const { preview } = await admin.policies.validate(policySource)
// preview = {
//   package: "caracal.authz",
//   rules: ["app_ids", "grants"],   // the data documents you defined
//   default_result: false,           // data documents never define result
//   decisions: [],                   // the platform contract owns every decision
//   inputs_referenced: [],
//   data_referenced: [],
// }
```

Use `rules` to confirm the document defines the data tables you intended. The
preview is a static read of the source; for an end-to-end decision run a
[simulation](/v1.0/guides/activate-policy-set/) with representative input against the
platform decision contract.

## Iterate from a denied request

When a real request is denied, you do not have to guess the input: the audit explain endpoint reconstructs a redaction-safe policy input for every denied decision, and you replay it against a candidate policy-set version before activating the fix. The workflow, snippet, and caveats live in [Iterate from real denials](/v1.0/guides/activate-policy-set/#iterate-from-real-denials); [Iterate Policy Safely](/v1.0/examples/policy-iterate/) automates the whole loop.

## Keep policies reviewable

* Default to deny.
* Keep resource identifiers stable and scopes action-oriented.
* Keep grant data normalized: one `grants` entry per resource view, rather than duplicating the same scope sets across documents.
* Split policies by ownership only when separate review or activation is useful.

Next, [activate the policy in a policy set](/v1.0/guides/activate-policy-set/).

## Validate the authored data

Validation must return `valid: true`; preview must list only intended data rules; simulation must allow the intended role and deny a missing role, extra scope, wrong resource, and confinement escape. Syntax validity alone is not production readiness.

:::caution[Failure point: identity fields]
The acting application is `input.principal.id`; Session labels are `input.principal.labels`. Do not invent `input.user`, `input.application`, or an authoritative subject field. A Federated user is a separate explicit identity flow, not default policy authority.
:::

## Next Step

[Activate a Policy Set](/v1.0/guides/activate-policy-set/) with the allow and deny simulations used here.
