Every policy carries
id, name, description, priority, enabled, and version. Set enabled: false to keep a policy as living documentation without enforcing it.
transition_guard
Atransition_guard engages when a task moves from a matching from_group to a matching to_group. Use "*" as a wildcard on either side. When engaged, the move is blocked with transition_blocked unless every condition in require passes — conditions are evaluated against the task as it would look after the move.
require is an implicit all_of. A "*" → done guard is the idiomatic definition-of-done gate — it fires no matter which column a task ships from.
agent_responsibility
Anagent_responsibility policy never blocks. When its when conditions match a written task, the policy appears in that write’s policies_fired list with its message. Omit or leave when empty and the policy always matches.
Conditions
A leaf condition takes a field reference, an operator, and a value:custom_data. Use task.<name> for custom fields. Built-in field references are task.group_id, task.title, task.description, and task.parent_id.
Operators
Operators never throw. A type mismatch, bad operand, or invalid regex evaluates to “condition not met” rather than crashing a write. String operators coerce scalars but not arrays — use
has_any or has_all for string_list fields.Compound conditions
Nest conditions withall_of, any_of, or none_of:
A misspelled key is a validation error, not a silently-ignored field. Policy definitions are validated strictly at
create_policy / update_policy time and on load.What a gate actually guarantees
A gate is a confession step, not a control. It records that the claim was made and blocks until it is. An agent that would skip review under pressure can also set the flag under pressure. The exception is ahuman_only field: an agent’s write tools structurally refuse to set it, so a guard requiring it genuinely waits on a person. See Fields and Schema.
Two habits follow from this:
Prove your rails fire
Run
substrate validate and exercise your guards before relying on them. Definitions that parse don’t always do what you expect at runtime.Dry-run instead of guessing
Call
check_transition before attempting a move. It returns allowed, from_group, to_group, and the blocking policy with its message — no side effects, no version bump.Reading the result
A successful write returns anok: true envelope with the updated entity state and any policies_fired:
transition_blocked. See Envelopes and Errors for the full error shape.
See it run
Walk through a complete session — creation, blocked moves, gate satisfaction, human approval, and revocation — with real request and response payloads.