---
title: "Author Policy Data"
url: "https://docs.caracal.run/guides/author-policy/"
markdown_url: "https://docs.caracal.run/markdown/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/guides/author-policy/
Markdown URL: https://docs.caracal.run/markdown/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

---

The platform decision contract owns every authorization decision inside the STS. You do not write `result` — you author **policy data documents** that tell the contract which application owns a resource, which roles hold which scopes, and how to confine or restrict authority. This guide writes those documents and validates them before activation.

## Prerequisites

- A zone, application, and resource.
- Access to the Console or Admin API.
- The resource identifier and scopes you want to grant.

## 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`.

```text
# 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 edge narrows to it. You declare the grant; the contract enforces the
narrowing.

## 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](/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](/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 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.

## 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 and letting
you supply only data.

Every policy is a data document marked with `# caracal:data-document` on its first
line. The document carries only `data` and is **forbidden from defining `result`**,
so it can never decide an authorization:

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

import rego.v1

restrict := {}
```

A `restrict` entry can only subtract authority, and `confinement` can only narrow it.
Neither can widen what the contract already allows, so a careless data change fails
closed.

### 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](/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:

```ts
const trace = await admin.audit.explain(zoneId, requestId);
const input = trace.denied[0]?.policy_input;
```

Feed that `input` straight into [simulation](/guides/activate-policy-set/) against
a candidate policy-set version to confirm your fix before activating it. The
[Iterate Policy Safely](/examples/policy-iterate/) automates this denial-to-simulation loop.
Actor and subject claims are never written to audit, so add any claim-dependent
fields to the input before simulating.

## Keep policies reviewable

- Default to deny.
- Return diagnostics for denies and step-up triggers.
- Keep resource identifiers stable and scopes action-oriented.
- Keep decision logic in policy rather than duplicating allow tables across rules.
- Split policies by ownership only when separate review or activation is useful.

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