About Status Policies
What is a status policy?
Status policies are used to define which custom statuses a record can have as well as the required quorum to change to these statuses. They have schema status which defines a specific structure of policy rules contained in values of the policy.
POST /v2/policies
{
...,
data: {
handle: '...',
schema: 'status',
record: '<record>',
filter: { ... },
values: [ ... ]
}
}At root level, the status policy can have fields which limit the policy to specific records:
record- defines that policy is only for specific record type, optional, if not defined policy applies to all record typesfilter- record must match the filter in order for policy to be applied to it, optional, if not defined policy applies to all records
POST /v2/policies
{
...,
"data": {
"handle": "fintech-wallet-status",
"schema": "status",
"record": "wallet",
"filter": { "data.schema": "fintech" }
"values": [ ... ]
}]
}
}The principle open unless whitelisted which we follow in all kinds of ledger rules is valid here also. So if for a record whose status is to be changed there is not any matching status policy (by record and filter) then the operation will be accepted. Otherwise the operation needs to be explicitly granted by one of applicable policy rules.
Property filter represents standard ledger filtering concept where language used in filter expression is mongo-compatible. This concept is already used in: access rules and effects.
Statuses policies are exposed on the same endpoint as access policies with distinction that the schema property is equal to status while for access policies schema is access. Schema of the policy record defines the structure of values array in policy.
Constraining quorum for statuses
POST /v2/policies
{
...,
"data": {
"handle": "wallet-status",
"schema": "status",
"record": "wallet",
"values": [{
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}In the above case, in order for status to be changed there must be a proof with custom.status made by a signer with public key WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE= as specified in the quorum field of a policy. Quorum is array of signer references which follows the same structure as the signer references in access rules and policies. For example record owner can also be referenced in policy by using "quorum": [{ "$record": "owner" }] .
If based on policies, the signer is not in quorum required to set a status, or if additional proofs are required by quorum list, there will be no error. Only side effect will be that proof with status will be stored in record proofs but record status will not be changed.
Except quorum, the rule in status policy has optional field status which makes a rule applicable only to specific status. The policy below will requires a proof from signer(public) AweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE= to change wallet status to "active". Setting other statuses to wallet will not be allowed unless there is another policy/rule which grants that explicitly.
POST /v2/policies
{
...,
"data": {
"handle": "wallet-active",
"schema": "status",
"record": "wallet",
"values": [{
"status": "active",
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}By default the status in policy rule is string and in this form it represents equality. It can also be a valid mongo compatible sub-expression for matching single object field as shown below by using $in.
POST /v2/policies
{
...,
"data": {
"handle": "wallet-active-inactive",
"schema": "status",
"record": "wallet",
"values": [{
"status": { $in: ["active", "inactive"] },
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}To define a quorum for removing a label from a record, there is special value null. The same value is used in proofs for requesting the status removal.
POST /v2/policies
{
...,
"data": {
"handle": "wallet-remove-status",
"schema": "status",
"record": "wallet",
"values": [{
"status": null,
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}A rule can also allow combination of removing and setting specific statuses, for example: "status": { $in: ["active", "inactive", null] }
Controlling proof selection
The property quorum.proofSelection in the config object can be used to define how proofs are selected when evaluating quorum for status changes. This setting is specified at the policy level, not within individual rules.
Latest Chain of Proofs (default)
When quorum.proofSelection is set to latest-chain or omitted, only the proofs from the latest set of proofs with the target status are considered for quorum calculation.
A "chain" means a sequence of proofs with same status.
POST /v2/policies
{
...,
"data": {
"handle": "wallet-activate-status",
"schema": "status",
"record": "wallet",
"config": {
"quorum.proofSelection": "latest-chain"
},
"values": [{
"status": {"$in": ["activated", "deactivated"]},
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}This is often required so that proofs used in the past to set some status cannot be reused to set this status again in the future. The latest chain of proofs approach ensures that only the most recent consecutive proofs with the same status are considered. This rule is easy to demonstrate and remember with this example:
[
activated, ❌ (not in latest chain)
activated, ❌ (not in latest chain)
deactivated, ❌ (different status)
deactivated, ❌ (different status)
activated ✅ (latest chain with target status)
activated ✅ (latest chain with target status)
]Entire Set
If quorum.proofSelection is set to entire-set, all proofs for the target status are considered for quorum calculation, regardless of whether they form a continuous chain.
POST /v2/policies
{
...,
"data": {
"handle": "wallet-activate-status",
"schema": "status",
"record": "wallet",
"config": {
"quorum.proofSelection": "entire-set"
},
"values": [{
"status": {"$in": ["activated", "deactivated"]},
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}Using the same example, if the user wants to activate a wallet with entire-set selection, all proofs with the activated status would be considered:
[
activated, ✅ (included in entire set)
activated, ✅ (included in entire set)
deactivated, ❌ (different status)
deactivated, ❌ (different status)
activated ✅ (included in entire set)
]Filtering by status transition
Status policies support a filter field at two levels:
- Policy root — when the filter does not match, the entire policy is excluded from evaluation.
- Individual value — when the filter does not match, just that value is excluded; the rest of the policy's values are still evaluated.
Both levels are evaluated against the same context: the static record data (e.g. data.schema, handle) plus the transition context, which exposes the entity's state at the moment of evaluation.
The transition context provides the following top-level keys inside the filter expression:
| Key | Description |
|---|---|
old | Record DTO before the transition. Exposes old.meta.status, old.meta.labels, old.meta.created, old.meta.updated, old.data.handle, old.data.schema, and any other old.data.* field. |
new | Record DTO after the incoming proof is appended. Same shape as old. |
ctx.req | HTTP request context (method, headers, etc.). |
Using ctx.req in a filter couples the policy evaluation to an HTTP request. Some status transitions are calculated internally — for example, during settlement, reconciliation, or bridge processing — and carry no request context. When ctx.req is absent, any filter that references it will not match, causing the policy or value to be excluded. Avoid ctx.req filters unless you intend to restrict a transition exclusively to direct API calls.
For backward compatibility, all fields from old.data are also spread at the root level of the filter context without any prefix. This means schema and old.data.schema refer to the same value, so existing filters that reference data fields directly (e.g. { "schema": "fintech" }) continue to work without changes.
This lets you create transition-aware policies — policies that only activate for specific "from" or "to" statuses and grant or restrict the transition accordingly.
Example: root-level filter — allow a custom status only from a specific state
The policy below activates only when the wallet's current status is active (root-level filter). All of its values are evaluated only when that condition holds. If the wallet is in any other status, the entire policy is excluded; and if no other matching policy grants post-active, the transition is rejected.
POST /v2/policies
{
...,
"data": {
"handle": "wallet-post-active",
"schema": "status",
"record": "wallet",
"filter": { "old.meta.status": "active" },
"values": [{
"status": "post-active",
"quorum": [{
"public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE="
}]
}]
}
}Example: value-level filter — restrict one value within a shared policy
The policy below covers multiple statuses in a single policy. The third value uses a value-level filter to restrict rejected — it applies only when the current status is prepared. The other values (without a filter) are always evaluated.
POST /v2/policies
{
...,
"data": {
"handle": "intent-status",
"schema": "status",
"record": "intent",
"values": [
{
"status": { "$in": ["pending", "prepared", "committed", "completed"] },
"quorum": [{ "handle": "system" }]
},
{
"filter": { "old.meta.status": "prepared" },
"status": "rejected",
"quorum": [{ "public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=" }]
}
]
}
}In this policy, rejected can only be set when the intent is currently in prepared status, using the specified key. Attempts to set rejected from any other status fail because the filtered value is excluded and no other value covers rejected.
When using transition filters, keep in mind the open unless whitelisted principle. If the filter does not match, the policy or value is excluded. If no remaining policy has a value for the target status, the transition is rejected with an error — not silently allowed. This is different from the case where there are no policies at all (which allows the transition freely).
Example: block a status transition from a terminal state
The combination of a base policy (covering all normal statuses, without the target status in its values) and a transition-filtered policy creates a strict gate: the transition is only open when the filter matches.
// Base policy — covers all standard statuses, no 'post-completed' value
POST /v2/policies
{
...,
"data": {
"handle": "intent-standard-statuses",
"schema": "status",
"record": "intent",
"values": [{
"status": { "$in": ["pending", "prepared", "committed", "completed", "rejected"] },
"quorum": [{ "handle": "system" }]
}]
}
}
// Transition-filtered policy — only active when intent is already 'completed'
POST /v2/policies
{
...,
"data": {
"handle": "intent-post-completed",
"schema": "status",
"record": "intent",
"filter": { "old.meta.status": "completed" },
"values": [{
"status": "post-completed",
"quorum": [{ "public": "WAweF9PHlboQoW0z8NqhZXFmzUTaV74NRFAd/aILprE=" }]
}]
}
}When an intent in completed status receives a proof for post-completed, both policies are evaluated. The base policy has no value for post-completed, but the filtered policy matches (old.meta.status === 'completed') and grants the transition. When the intent is in rejected status, the filtered policy is excluded and the base policy has no matching value — the transition is rejected.
Status policies evaluation
When evaluating status policies for setting a status to record there can be multiple matching policies with multiple values and multiple signers in quorum. To easier understand how they are combined the following rules are applied top-down:
-
If there is at least one status policy which matches the record by
recordandfilterthen, in order to allow setting the status of record, at least one policy must be found which allows setting this specific status based on policy rules. If there are no matching policies, then setting of status is allowed to anyone. -
Policy can have multiple rules in
valuesfield and it is considered that the policy grants setting status to record if at least one rule is found which grants this for this specific status. Emptyvaluesarray grants nothing. -
Single rule can have
statusfield which when exists will make rule applicable only if the desired status matches. Ifstatusfield is omitted then the rule allows all statuses - according to quorum.If there is any policy which matches
record/filterfor a record but none of them explicitly allows setting specific status then the proof which is to be added is not valid and will be rejected. Proof will not be stored and error response will be returned to caller. Restrictive nature of fieldstatusin policy rule is opposite than fieldswallet, filterin policy. While setting specificwalletand/orfilterin the policy will narrow status restrictions only to records which matches them, setting the specificstatusin policy rule will narrow the set of acceptable statuses this rule can apply to. So interpretation that rule withstatus: 'active'implies thatquorumis required only for statusactiveis wrong. Correct interpretation is that this rule with itsquorumcan be used only to grant setting statusactive. -
Rule also has
quorumarray of signer references and it is used to determine when the status from the proof can be persisted to a record. Quorum step will be evaluated only after all previous steps are executed successfully and the specific status from proof is allowed. Status will be persisted to a record only when all signers referenced in quorum had signed the record with a proof which has the desired status incustom.status. Emptyquorumarray means that quorum is not required and will cause that status is persisted to record immediately.If a proof with
statuspasses steps 1.-4. but the key used in this proof is not in the quorum or if another key is required to fulfill the quorum then the proof will be stored successfully, error will not be returned to caller, but status will not be persisted to record until the quorum of the rule which allows this status is achieved.
When a record is modified, it may fall under a policy that it does not satisfy. For example, there could be a wallet status policy that requires a signature from a specific key, but only if the wallet record has a schema like fintech. In this scenario, someone could first create a wallet without a schema or with a schema such as bank, then set the status to active and afterward change the schema to fintech. As a result, the wallet status is now subject to the policy, but the status was set without enforcing this policy.