# Policy reference

This page is the canonical reference for policy fields, JavaScript conditions, workflow objects, and policy-writing rules. For the concept, see [Policies](/products/custody/v1.36/governance/policies). For operations, see [Manage policies](/products/custody/v1.36/governance/policies/manage-policies). For reusable patterns, see [Policy examples](/products/custody/v1.36/governance/policies/examples).

## API reference

| Task | API reference |
|  --- | --- |
| Create, update, lock, or unlock a policy | [Propose an intent](/products/custody/v1.36/reference/api/openapi/intents/createintent) |
| Test a policy intent | [Perform a dry run](/products/custody/v1.36/reference/api/openapi/intents/intentdryrun) |
| Query policies | [List policies](/products/custody/v1.36/reference/api/openapi/policies/getpolicies), [Get policy details](/products/custody/v1.36/reference/api/openapi/policies/getpolicy) |
| Check the full schema | [OpenAPI specification](/products/custody/v1.36/reference/api/openapi) |


## Create policy fields

These fields are used in a `v0_CreatePolicy` payload and in genesis policy entries. Genesis policy entries are embedded in a domain's `policies` array and do not include the `type` field.

| Field | Type | Required | Rules |
|  --- | --- | --- | --- |
| `id` | UUID string | Yes | Unique policy ID. |
| `alias` | string | Yes | 1-75 characters. Use a stable operational name. |
| `rank` | integer | Yes | 0-1000. Higher rank wins among matching policies. |
| `intentTypes` | string array | No | If omitted, the policy can match all intent types. Use only for deliberate fallback or emergency policies. |
| `intentOrigin` | enum | No | `UserSigned` or `SystemSigned`. If omitted, the policy matches user-signed proposals only. |
| `scope` | enum | Yes | `Self`, `Descendants`, or `SelfAndDescendants`. |
| `scriptingEngine` | enum | Yes | `Javascript_v0`. |
| `condition` | object | No | If omitted, the condition behaves as always true. |
| `workflow` | array | No | If omitted or `null`, matching intents are auto-approved. An empty array rejects matching intents. |
| `lock` | enum | Yes | `Unlocked` or `Locked`. |
| `description` | string | No | 0-250 characters. |
| `customProperties` | object | Yes | String key-value metadata. Use `{}` when empty. |
| `type` | string | Yes for intent payloads | `v0_CreatePolicy`. Omit inside genesis policy entries. |


## Update and lock fields

`v0_UpdatePolicy` uses a `reference` instead of `id` so the update targets a specific policy revision.


```json
{
  "payload": {
    "reference": {
      "id": "5b440ba5-d013-11eb-8cd0-dcfb48cfb3cb",
      "revision": 3
    },
    "alias": "policy-governance",
    "rank": 750,
    "intentTypes": ["v0_CreatePolicy", "v0_UpdatePolicy"],
    "intentOrigin": "UserSigned",
    "scope": "Self",
    "scriptingEngine": "Javascript_v0",
    "condition": {
      "expression": "context.references.users[context.request.author.id].roles.includes('policy-operator')",
      "type": "Expression"
    },
    "workflow": [
      {
        "role": "policy-operator",
        "quorum": 2,
        "type": "RoleQuorum"
      }
    ],
    "description": "Controls policy lifecycle operations.",
    "customProperties": {},
    "type": "v0_UpdatePolicy"
  }
}
```

`v0_LockPolicy` and `v0_UnlockPolicy` use only `reference` and `type`.


```json
{
  "payload": {
    "reference": {
      "id": "5b440ba5-d013-11eb-8cd0-dcfb48cfb3cb",
      "revision": 3
    },
    "type": "v0_LockPolicy"
  }
}
```

## Policy selection

When an intent is submitted, policy evaluation:

1. Finds policies in the target domain and any applicable parent or child domains.
2. Filters by `intentTypes`. A policy with omitted `intentTypes` can match any intent type.
3. Filters by `scope` and domain governing strategy.
4. Filters by `intentOrigin`. A policy with omitted `intentOrigin` matches user-signed proposals only.
5. Evaluates `condition`. A condition must return `true` for the policy to remain eligible.
6. Selects the highest-ranked eligible policy.
7. If two eligible policies have the same rank, the older policy wins.
8. Applies the selected policy's workflow.


If no policy matches, the intent cannot execute.

## Scope values

| Value | Applies to |
|  --- | --- |
| `Self` | Intents targeting the policy's own domain. |
| `Descendants` | Intents targeting descendant domains only. |
| `SelfAndDescendants` | Intents targeting the policy's own domain and descendant domains. |


Domain governing strategy changes how parent and child policies interact:

| Governing strategy | Policy effect |
|  --- | --- |
| `ConsiderDescendants` | Parent and child policies can both be considered. Highest-rank eligible policy wins. |
| `CoerceDescendants` | Matching parent policies override matching child policies. |


## Intent origin values

`intentOrigin` controls whether a policy matches a normal user-signed proposal or an API-only system-signed proposal.

| Value | Applies to |
|  --- | --- |
| Omitted | User-signed proposals. |
| `UserSigned` | User-signed proposals. |
| `SystemSigned` | System-signed proposals submitted through the API. |


Existing policies that omit `intentOrigin` do not match system-signed proposals. To allow a service caller to submit an intent without a client-side payload signature, create or update a policy with `intentOrigin: "SystemSigned"`.

## Rank guidance

| Range | Typical use |
|  --- | --- |
| 900-1000 | Explicit emergency or breakglass controls with narrow intent types and high quorum. |
| 500-800 | High-risk governance such as policy, domain, user, and root administration. |
| 100-500 | Standard operational policies. |
| 1-99 | Catch-all or fallback policies that should lose to specific policies. |
| 0 | Last-resort behavior or intentionally lowest priority. |


## Condition object

A condition is either an `Expression`, an `And`, or an `Or`.


```json
{
  "condition": {
    "expression": "context.references.users[context.request.author.id].roles.includes('transaction-operator')",
    "type": "Expression"
  }
}
```

`And` and `Or` use a binary tree. Use `left` and `right`, not arrays.


```json
{
  "condition": {
    "left": {
      "expression": "context.request.payload.rank >= 700",
      "type": "Expression"
    },
    "right": {
      "expression": "context.references.users[context.request.author.id].roles.includes('policy-operator')",
      "type": "Expression"
    },
    "type": "And"
  }
}
```

You can also use JavaScript logical operators inside one expression when that is clearer.


```json
{
  "condition": {
    "expression": "context.request.payload.rank >= 700 && context.references.users[context.request.author.id].roles.includes('policy-operator')",
    "type": "Expression"
  }
}
```

## Condition context

Condition expressions can use the `context` object. System-signed policy conditions can also use `submitter` and `intentOrigin`.

| Object | Contains |
|  --- | --- |
| `context.request` | The intent being evaluated. |
| `context.references` | Entities explicitly referenced by the intent. You cannot query arbitrary entities from a policy condition. |
| `submitter` | Identity of the user or service caller that submitted the proposal. |
| `intentOrigin` | Proposal origin, either `UserSigned` or `SystemSigned`. |


Common `context.request` paths:

| Path | Contains |
|  --- | --- |
| `context.request.author.id` | User ID that created the intent. |
| `context.request.author.domainId` | Author's domain ID. |
| `context.request.targetDomainId` | Domain targeted by the intent. |
| `context.request.payload` | Intent-specific payload. |
| `context.request.payload.type` | Intent type, such as `v0_CreatePolicy`. |
| `context.request.payload.customProperties` | Custom metadata supplied in the payload, when present. |


For system-signed proposals, `context.request.author` is not present. Use `submitter` to check the service caller instead.

Submitter paths:

| Path | Contains |
|  --- | --- |
| `submitter.type` | `User` or `Service`. |
| `submitter.reference.id` | User ID for user-signed proposals. |
| `submitter.reference.domainId` | User domain ID for user-signed proposals. |
| `submitter.subject` | Service subject for system-signed proposals. |
| `submitter.custodyRoles` | Roles from the service caller token for system-signed proposals. If the configured roles claim is absent, this is an empty array. |


System-signed condition example:


```json
{
  "condition": {
    "expression": "submitter.type === 'Service' && submitter.custodyRoles.includes('gas-station')",
    "type": "Expression"
  }
}
```

Useful payload paths by intent type:

| Intent type | Useful paths |
|  --- | --- |
| `v0_CreateUser` | `context.request.payload.roles`, `context.request.payload.alias`, `context.request.payload.loginIds` |
| `v0_UpdateUser` | `context.request.payload.reference.id`, `context.request.payload.roles`, `context.request.payload.loginIds` |
| `v0_CreatePolicy` | `context.request.payload.rank`, `context.request.payload.intentTypes`, `context.request.payload.scope`, `context.request.payload.workflow` |
| `v0_UpdatePolicy` | `context.request.payload.reference.id`, `context.request.payload.rank`, `context.request.payload.intentTypes`, `context.request.payload.scope`, `context.request.payload.workflow` |
| `v0_CreateDomain` | `context.request.payload.alias`, `context.request.payload.governingStrategy`, `context.request.payload.permissions`, `context.request.payload.childrenDomainIds` |
| `v0_UpdateDomainPermissions` | `context.request.payload.reference.id`, `context.request.payload.permissions.readAccess` |
| `v0_CreateTransactionOrder` | `context.request.payload.accountId`, `context.request.payload.ledgerId`, `context.request.payload.parameters` |
| `v0_ReleaseQuarantinedTransfers` | Payload fields for the quarantine release request. Use existence checks before accessing optional fields. |


Common `context.references` collections:

| Collection | Access pattern | Use when |
|  --- | --- | --- |
| Users | `context.references.users[userId]` | Check the author's current roles or target user state. |
| Accounts | `context.references.accounts[accountId]` | Check ledger, account metadata, or account custom properties. |
| Endpoints | `context.references.endpoints[endpointId]` | Check endpoint trust score or endpoint metadata. |
| Vaults | `context.references.vaults[vaultId]` | Check vault properties for account or vault operations. |
| Tickers | `context.references.tickers[tickerId]` | Check ticker or asset metadata when referenced. |
| Domains | `context.references.domains[domainId]` | Check domain metadata when referenced. |


The following diagram shows how a condition can navigate from an intent payload to a referenced account's custom properties:

![Context references](/assets/context-references.69067cf7957fdbb8bc88302907817d2c6d7411eb1edade8a45e5ee83b87f36cc.b893f081.png)

## JavaScript rules for conditions

Conditions are JavaScript expressions. Write one expression that evaluates to `true` or `false`.

| Rule | Guidance |
|  --- | --- |
| Use `Javascript_v0` | This is the supported scripting engine. |
| Return a boolean | The expression should evaluate to `true` or `false` for every matching intent. |
| Prefer strict paths | Check the actual payload shape for each intent type in the API reference before writing paths. |
| Check optional fields | Use `hasOwnProperty`, null checks, or array length checks before accessing optional nested fields. |
| Use role names exactly | Role names are case-sensitive and must match user roles. |
| Keep expressions deterministic | Avoid relying on external state. Policies cannot call APIs or query arbitrary entities. |


Supported JavaScript patterns:

| Pattern | Example |
|  --- | --- |
| Comparison | `context.request.payload.rank >= 700` |
| Logical operators | `a && b`, `a |
| Array includes | `roles.includes('compliance')` |
| Array some/every/filter | `outputs.some(output => BigInt(output.amount) > 100000000n)` |
| Array reduce | `outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n)` |
| String methods | `alias.startsWith('prod-')`, `alias.includes('treasury')` |
| Object checks | `payload.hasOwnProperty('parameters')` |
| JSON methods | `JSON.parse(rawTx)`, `JSON.stringify(roles)` |
| Date methods | `new Date().getHours()`, `new Date().getDay()` |


Amount handling:

- Use BigInt for amount comparisons.
- Use an `n` suffix for integer literals, such as `50000000000n`.
- When the payload value is a string, wrap it with `BigInt(...)`.
- For multi-output payloads, sum amounts with `reduce`.



```json
{
  "condition": {
    "expression": "context.request.payload.parameters.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n) >= 500000000n",
    "type": "Expression"
  }
}
```

Null-safe nested access:


```json
{
  "condition": {
    "expression": "context.request.payload.hasOwnProperty('parameters') && context.request.payload.parameters != null && context.request.payload.parameters.hasOwnProperty('destination') && context.request.payload.parameters.destination.type == 'Endpoint'",
    "type": "Expression"
  }
}
```

## Condition examples

### High-risk Bitcoin transaction

This condition matches when the author is an `operator`, the transaction parameters are Bitcoin, and either the amount is at least 1 BTC or the source account has a custom property named `escalatePolicy` set to `true`.


```json
{
  "expression": "context.references.users[context.request.author.id].roles.includes('operator') && context.request.payload.hasOwnProperty('parameters') && context.request.payload.parameters != null && context.request.payload.parameters.type == 'Bitcoin' && (context.request.payload.parameters.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n) >= 100000000n || context.references.accounts[context.request.payload.accountId].metadata.customProperties.escalatePolicy == 'true')",
  "type": "Expression"
}
```

### Medium-risk Bitcoin transaction

This condition matches operator-created Bitcoin transactions below 1 BTC where the account is not escalated and the destination is either an unknown address or an endpoint with a trust score of 50 or lower.


```json
{
  "left": {
    "expression": "context.references.users[context.request.author.id].roles.includes('operator') && context.request.payload.hasOwnProperty('parameters') && context.request.payload.parameters != null && context.request.payload.parameters.type == 'Bitcoin' && context.request.payload.parameters.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n) <= 100000000n && context.references.accounts[context.request.payload.accountId].metadata.customProperties.escalatePolicy != 'true' && context.request.payload.parameters.outputs[0].destination.type == 'Address'",
    "type": "Expression"
  },
  "right": {
    "expression": "context.references.users[context.request.author.id].roles.includes('operator') && context.request.payload.hasOwnProperty('parameters') && context.request.payload.parameters != null && context.request.payload.parameters.type == 'Bitcoin' && context.request.payload.parameters.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n) <= 100000000n && context.references.accounts[context.request.payload.accountId].metadata.customProperties.escalatePolicy != 'true' && context.request.payload.parameters.outputs[0].destination.type == 'Endpoint' && context.references.endpoints[context.request.payload.parameters.outputs[0].destination.endpointId].trustScore <= 50",
    "type": "Expression"
  },
  "type": "Or"
}
```

### Low-risk Bitcoin transaction

This condition matches operator-created Bitcoin transactions below 1 BTC where the account is not escalated and the destination is either an internal account or an endpoint with a trust score of 50 or higher.


```json
{
  "left": {
    "expression": "context.references.users[context.request.author.id].roles.includes('operator') && context.request.payload.hasOwnProperty('parameters') && context.request.payload.parameters != null && context.request.payload.parameters.type == 'Bitcoin' && context.request.payload.parameters.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n) <= 100000000n && context.references.accounts[context.request.payload.accountId].metadata.customProperties.escalatePolicy != 'true' && context.request.payload.parameters.outputs[0].destination.type == 'Account'",
    "type": "Expression"
  },
  "right": {
    "expression": "context.references.users[context.request.author.id].roles.includes('operator') && context.request.payload.hasOwnProperty('parameters') && context.request.payload.parameters != null && context.request.payload.parameters.type == 'Bitcoin' && context.request.payload.parameters.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n) <= 100000000n && context.references.accounts[context.request.payload.accountId].metadata.customProperties.escalatePolicy != 'true' && context.request.payload.parameters.outputs[0].destination.type == 'Endpoint' && context.references.endpoints[context.request.payload.parameters.outputs[0].destination.endpointId].trustScore >= 50",
    "type": "Expression"
  },
  "type": "Or"
}
```

### Create or update privileged users

This condition matches user creation or update intents when the target user has the `admin` or `compliance` role.


```json
{
  "expression": "context.request.payload.roles.includes('admin') || context.request.payload.roles.includes('compliance')",
  "type": "Expression"
}
```

This condition matches user creation or update intents when the target user does not have either privileged role.


```json
{
  "expression": "!context.request.payload.roles.includes('admin') && !context.request.payload.roles.includes('compliance')",
  "type": "Expression"
}
```

Avoid these patterns:

| Avoid | Reason |
|  --- | --- |
| `fetch`, imports, network calls, or database calls | Conditions cannot call external systems. |
| DOM, browser APIs, timers, or process APIs | Policy evaluation is server-side. |
| Statements that require a block | The field expects an expression. Prefer array methods and logical operators. |
| Missing optional-field checks | Undefined nested paths can make condition evaluation fail. |
| Comparing large amounts as normal numbers | JavaScript numbers can lose precision. |


## Workflow objects

A workflow is an ordered array of approval steps. Each step must be satisfied before the next step is active.

| Type | Shape | Use when |
|  --- | --- | --- |
| `RoleQuorum` | `{ "role": "operator", "quorum": 2, "type": "RoleQuorum" }` | A number of users with one role must approve. |
| `And` | `{ "left": {...}, "right": {...}, "type": "And" }` | Multiple approval groups are required in the same step. |
| `Or` | `{ "left": {...}, "right": {...}, "type": "Or" }` | Either approval group can satisfy the step. |


Example workflow:


```json
{
  "workflow": [
    {
      "role": "transaction-operator",
      "quorum": 1,
      "type": "RoleQuorum"
    },
    {
      "left": {
        "role": "compliance",
        "quorum": 1,
        "type": "RoleQuorum"
      },
      "right": {
        "role": "risk",
        "quorum": 1,
        "type": "RoleQuorum"
      },
      "type": "And"
    }
  ]
}
```

Workflow rules:

- For user-signed proposals, the intent submitter counts as the first approval.
- For user-signed proposals, the submitter must have a role compatible with the first workflow step.
- For system-signed proposals, the service submitter does not count as a user approval. If a system-signed policy includes a workflow, user-signed approvers must satisfy it.
- If quorum is not reached, the step remains open and users can change their decision. Once a step is executed, it cannot be changed.
- A user can count only once in the complete approval workflow.
- Duplicate approvals are checked at public-key level.
- If a user has roles on both sides of an `And` or `Or`, the engine uses one approval on the quickest valid path.
- `RoleQuorum.quorum` must be at least `1`.
- `And` and `Or` use `left` and `right`, not arrays.
- An omitted or `null` workflow auto-approves a matching intent.
- An empty workflow array rejects a matching intent.


## Workflow examples

### Operator followed by supervisor or supervisor bot

Step 1 is satisfied by the submitter with the `operator` role. Step 2 is satisfied by one `supervisor` or one `supervisor-bot`.


```json
[
  {
    "role": "operator",
    "quorum": 1,
    "type": "RoleQuorum"
  },
  {
    "left": {
      "role": "supervisor",
      "quorum": 1,
      "type": "RoleQuorum"
    },
    "right": {
      "role": "supervisor-bot",
      "quorum": 1,
      "type": "RoleQuorum"
    },
    "type": "Or"
  }
]
```

### Operator, compliance or risk alternatives, then CISO

Step 1 is satisfied by the submitter with the `operator` role. Step 2 requires compliance or compliance-bot approval and risk or risk-bot approval. Step 3 requires one `ciso` approval.


```json
[
  {
    "role": "operator",
    "quorum": 1,
    "type": "RoleQuorum"
  },
  {
    "left": {
      "left": {
        "role": "compliance",
        "quorum": 1,
        "type": "RoleQuorum"
      },
      "right": {
        "role": "compliance-bot",
        "quorum": 1,
        "type": "RoleQuorum"
      },
      "type": "Or"
    },
    "right": {
      "left": {
        "role": "risk",
        "quorum": 1,
        "type": "RoleQuorum"
      },
      "right": {
        "role": "risk-bot",
        "quorum": 1,
        "type": "RoleQuorum"
      },
      "type": "Or"
    },
    "type": "And"
  },
  {
    "role": "ciso",
    "quorum": 1,
    "type": "RoleQuorum"
  }
]
```

## Role rules

Role names used in users, read access, conditions, and workflows must match exactly.

| Valid | Avoid |
|  --- | --- |
| `policy-operator` | `PolicyOperator` |
| `transaction-operator` | `transaction_operator` |
| `compliance` | `Compliance Team` |
| `platform-admin` | `platform admin` |


The API schema accepts role names that match `[a-z0-9\-]+`.

## Minimal create-policy example


```json
{
  "id": "c35fa005-1640-42ee-a8a5-c43aaae6eb87",
  "alias": "operator-approval",
  "rank": 100,
  "scope": "Self",
  "intentTypes": ["v0_CreateTransactionOrder"],
  "scriptingEngine": "Javascript_v0",
  "condition": {
    "expression": "context.references.users[context.request.author.id].roles.includes('transaction-operator')",
    "type": "Expression"
  },
  "workflow": [
    {
      "role": "transaction-operator",
      "quorum": 2,
      "type": "RoleQuorum"
    }
  ],
  "lock": "Unlocked",
  "description": "Requires transaction operator quorum.",
  "customProperties": {},
  "type": "v0_CreatePolicy"
}
```

## Common mistakes

| Mistake | Result | Fix |
|  --- | --- | --- |
| Empty `workflow: []` | Matching intents are rejected. | Omit workflow or use `null` for auto-approval, or define approval steps. |
| Omitted `intentTypes` on a high-rank policy | Policy can match unexpectedly broad workflows. | Specify intent types unless the policy is a deliberate fallback. |
| `customProperties: null` | Payload validation can fail. | Use `{}` when empty. |
| Role names with uppercase, underscores, spaces, or padding | Workflows cannot be satisfied by expected roles. | Use lowercase hyphenated role names, such as `policy-operator`. |
| `And` or `Or` written as arrays | Payload validation fails. | Use `left`, `right`, and `type`. |
| Amount compared as a JavaScript number | Precision loss or mismatched comparisons. | Use `BigInt(...)` and `n` literals. |
| Missing optional-field checks | Condition can fail for intent types with different payload shapes. | Check existence before accessing nested fields. |
| Required quorum has too few users | Intent cannot be approved. | Create enough users before deploying the policy. |
| First workflow step excludes maker role for a user-signed policy | Intent fails at submission or cannot progress. | Include the maker role in the first workflow step. For system-signed policies, make sure user approvers can satisfy the workflow. |


For implementation patterns, see [Policy examples](/products/custody/v1.36/governance/policies/examples).