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
- If any matching deny rule applies, the request is refused. A deny always wins.
- Otherwise, if any matching allow rule applies, it is allowed.
- 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
neandnin. - 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"] }
denylists 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" } } } } }
Related
- Permissions reference
- Authentication
- In the product: Team & Permissions