Policy format

The rule format behind a role's Advanced rules: effects, actions, resources, conditions, hidden fields and the roles API.

A role's permissions are stored as a list of rules. The role editor in Team & Permissions › Roles builds this list for you from simple levels. Under Advanced rules you can add exceptions, switch between a guided builder and a JSON view, and write the rules yourself. This page describes that format, and the API that reads and changes it.

A policy

A policy is a JSON array of rules (statements). A role holds one policy; the permissions a person or access key has are the combination of all the policies of the roles they hold.

[
  {
    "effect": "allow",
    "actions": ["records:read", "records:create", "records:update"],
    "resources": ["invoices", "payments"]
  },
  {
    "effect": "deny",
    "actions": ["records:update"],
    "resources": ["invoices"],
    "condition": { "resource.status": { "eq": "paid" } }
  },
  {
    "effect": "allow",
    "actions": ["records:read"],
    "resources": ["customers"],
    "fields": { "deny": ["credit_limit"] }
  }
]

This role can read, create and update invoices and payments, but not update an invoice once it is paid, and can read customers except their credit_limit.

A rule

Key Required Meaning
effect Yes allow or deny
actions Yes One or more permission names from the Permissions reference, or a wildcard: records:* for every records permission, * for everything
resources No The items the rule covers: a list of keys such as record types, queues or sites, or *. Left out or empty means every item. Keys must match exactly
condition No Extra tests on the item or on the caller. See below
fields No Which fields of a record are visible. See below

Unknown permission names are refused when you save. A role can hold at most 50 rules.

How a request is decided

  1. If any matching deny rule applies, the request is refused. A deny always wins.
  2. Otherwise, if any matching allow rule applies, it is allowed.
  3. Otherwise it is refused. Having no role at all means having no permissions.

Order does not matter. A rule matches when its actions, resource and condition all match.

Conditions

A condition is an object. Each key names a value, and its object says how to test it. Every test must pass for the rule to match.

"condition": {
  "resource.amount": { "lt": 50000 },
  "resource.owner":  { "eq": "principal.id" }
}

What you can test

Key Value
resource.<field key> The value of that field on the record
resource.owner The id of the person who created the record
resource.created_at When the record was created (RFC 3339)
resource.object_type The record type key
resource.id The record's id
principal.id The id of the person or service account making the request
principal.type user, service or anonymous

Tests

Test Meaning
eq, ne Equal, not equal
gt, gte, lt, lte Greater, at least, less, at most. Numbers, dates and amounts compare by value
in, nin The value is in (or is not in) a list: {"in": ["open", "draft"]}
contains The value contains the text (ignoring upper and lower case)

An operand written as "principal.id" means "the caller's own id", so {"resource.owner": {"eq": "principal.id"}} means "records I created".

Details that matter:

  • A missing value fails every test except ne and nin.
  • Ordering tests compare like with like: numbers with numbers, dates with dates, a currency field by its amount. If the two sides cannot be compared, an allow rule simply does not apply, while a deny rule still applies. This keeps a mistake from opening something up.
  • Conditions on records are tested against the record as it is stored. When you create a record, they are tested against the values you send.
  • A record that fails a read condition is treated as not existing: it is left out of lists and answers 404.

Hiding fields

fields limits what a matching read rule reveals.

"fields": { "deny": ["cost_price", "margin"] }
"fields": { "allow": ["name", "email"] }
  • deny lists field keys that are hidden. A denied field is left out of record responses, leaves exports, cannot be used to filter or sort, and comes back empty (NULL) in analytics queries.
  • allow, when present and not empty, turns the rule into a whitelist: only those fields are readable.
  • If any rule denies a field, it is hidden, whatever other rules allow.

Examples

Everything except deleting records

[
  { "effect": "allow", "actions": ["*"] },
  { "effect": "deny",  "actions": ["records:delete"] }
]

One work queue, scan-only

[
  { "effect": "allow", "actions": ["queues:read", "queues:enqueue"], "resources": ["inbound-boxes"] }
]

Open one form and one website section to visitors who are not signed in (put this in a role marked for visitors who are not signed in)

[
  { "effect": "allow", "actions": ["forms:read", "forms:submit"], "resources": ["contact-us"] },
  { "effect": "allow", "actions": ["content:deliver"], "resources": ["website", "assets"] }
]

People see only their own records

[
  {
    "effect": "allow",
    "actions": ["records:read", "records:update"],
    "resources": ["expenses"],
    "condition": { "resource.owner": { "eq": "principal.id" } }
  }
]

API

All of these need a role that allows them and answer the standard errors in Errors. A rule list you send is checked and refused with 400 VALIDATION_ERROR and a message that names the rule (counting from 0) and the problem, such as statement 0: unknown action "record:read".

Method Path Permission What it does
GET /orgs/{orgId}/iam/me any member or key Your own combined policy
GET /orgs/{orgId}/iam/groups iam:manage_groups List roles
POST /orgs/{orgId}/iam/groups iam:manage_groups Create a role
GET · PUT · DELETE /orgs/{orgId}/iam/groups/{groupId} iam:manage_groups Read, replace or delete a role
POST · DELETE /orgs/{orgId}/iam/groups/{groupId}/members[/{userId}] iam:manage_members Add or remove a person or service account
POST /orgs/{orgId}/iam/simulate iam:simulate Check what a person is allowed

Roles

{
  "id": "0190fd00-…",
  "name": "Accounts clerk",
  "description": "Posts invoices and payments",
  "policy": [ { "effect": "allow", "actions": ["records:*"], "resources": ["invoices", "payments"] } ],
  "is_anonymous": false,
  "created_at": "2026-10-01T08:00:00Z",
  "updated_at": "2026-10-01T08:00:00Z"
}

POST and PUT take name, description, policy and is_anonymous. is_anonymous: true marks the role for visitors who are not signed in. Only the workspace owner can create such a role or switch an existing one over. Adding someone to a role is POST …/members with { "user_id": "…" }. Service accounts use the same call with their id.

You can only create or change a role so that it grants no more than you hold yourself, and only assign people to such roles.

POST /orgs/{orgId}/iam/simulate

The Access check page uses this.

{ "user_id": "0190f0a0-…", "action": "records:update", "resource": "invoices", "attrs": { "status": "paid" } }

resource defaults to *. attrs supplies the record values for conditions. The response says whether it is allowed and which rule decided:

{ "data": { "allowed": false, "effect": "deny", "matched": { "effect": "deny", "actions": ["records:update"], "resources": ["invoices"], "condition": { "resource.status": { "eq": "paid" } } } } }