Skip to main content

Policy language

Signing policies are a typed TransactionContext evaluated by four layers. The same payload is available as Cedar resource attributes, OPA input.transaction / input.tx, built-in tx_conditions, and consensus triggers.

See cookbooks for copy-paste rules. Getting started: Policy Engine.

Four layers

LayerWhoWhen
Agent guardrailsPer-agent knobs (tx_max_value_eth, allowlists, …)Every Intents sign/submit
Built-in tx_conditionsaccess_policies.tx_conditions JSON (all tiers)After calldata is known
Cedar / OPAOrg backend builtin+cedar / builtin+opaShadow or enforce; payload-aware second eval
Consensusconsensus_trigger on an access policyMatch → 202 pending approval

Human X-Auth-Confirm on export/import is AND with activity policies (export / import / raw_sign). Agents cannot export keys.

Evaluation order

sign or submit
→ build TransactionContext (ABI / IDL / request construction)
→ agent guardrails
→ built-in path + tx_conditions
→ Cedar or OPA with tx (action sign / submit / raw_sign)
→ consensus (202) or sign

Key-path Cedar/OPA still runs at key lookup (tx: None). A second evaluation runs once the payload is decoded — evaluating the transaction content, not only the key access.

TransactionContext fields

Serialized as context.tx (alias transaction for OPA). Optional fields require Cedar has guards.

FieldTypeNotes
chainstringethereum, solana, …
chain_familystringevm / solana / bitcoin / xrp / cardano / tron
intent_typestringtransaction, typed_data, personal_sign, eip712_digest, treasury_send
sign_onlybooltrue on /sign
to / fromstring?
value_gweilong?Overflow → value_overflow
function_namestring?ABI name or ERC-20/System/SPL hardcoded
function_selectorstring?0x + 4 bytes
function_signaturestring?alias of selector
function_argsmap of stringsNamed decoded args lifted as arg_amount, arg_to, etc.
arg_amount / arg_to / arg_from / arg_spenderstring?Lifted named args
decode_failedboolCalldata present but ABI decode failed — fail-closed
erc20_*string?Normalized transfer/approve
eip712_primary_typestring?typed_data
program_idsstring[]Solana
btc_outputs{address, value_sat}[]Bitcoin
xrp_tx_typestring?default Payment
tron_contract_typestring?Transfer vs TriggerSmartContract

Outermost calldata by default. Set deep_inspect: true on tx_conditions or consensus_trigger to also evaluate inner calls from multicall, Safe execTransaction, and ERC-4337 handleOps. Treat decode_failed as deny.

Cedar (resource attributes)

entity Transaction carries the fields above. Qualify actions as OneClaw::Action::"sign" so they match the schema. Documented form:

permit(principal, action, resource);
forbid(principal, action == OneClaw::Action::"sign", resource)
when {
resource has function_name && resource.function_name == "transfer" &&
resource has value_gwei && resource.value_gwei > 1000000000
};

Actions: read, write, rotate, sign, submit, export, import, raw_sign.

Nested context.tx is also present (primitives only). Prefer resource.* for grammar that matches the schema.

Org backend scope defaults to ["sign"]. Add export / raw_sign to enforce activity policies.

OPA

input.tx and input.transaction are the same object (Rust serializes tx; both keys are hoisted).

deny[msg] {
input.transaction.function_name == "transfer"
msg := "transfer denied"
}

Built-in tx_conditions (all tiers)

Fields are combined according to match_mode. effect on the policy is allow or deny.

FieldMatch
match_mode"all" (default, AND) or "any" (OR)
function_name_incase-insensitive
function_selector_incase-insensitive
erc20_amount_aboveraw token units
value_abovegwei
to_address_in
chain_in
intent_type_in
decode_failedbool
program_id_inany listed program in program_ids
deep_inspectbool — also evaluate against inner calls

Ignored on secret read (no TransactionContext). Agent guardrails stay the simple knobs; tx_conditions is the free expression layer.

OR logic with match_mode

By default, all present fields must match (AND). Set match_mode: "any" so at least one field match is sufficient (OR). This is useful for deny policies that block a transaction matching any of several criteria:

{
"tx_conditions": {
"match_mode": "any",
"function_name_in": ["approve", "setApprovalForAll"],
"to_address_in": ["0xSuspiciousContract"]
}
}

The above policy fires when the function is approve OR setApprovalForAll OR the to address is the listed contract. With "all" (default), all three would need to match simultaneously.

Deep calldata inspection

When deep_inspect: true, conditions are also evaluated against inner calls extracted from wrapper transactions:

  • Multicall (multicall(bytes[])) — each encoded sub-call
  • Safe execTransaction — the inner call target + data
  • ERC-4337 handleOps — each UserOperation's callData

A match on any inner call counts as an overall match. This catches transfers hidden inside batched operations:

{
"tx_conditions": {
"function_name_in": ["transfer"],
"to_address_in": ["0xBlockedRecipient"],
"deep_inspect": true
}
}

Without deep_inspect, the outer function name is multicall or execTransaction and would not match transfer.

Consensus JSON

approvers.count() >= N maps to consensus_trigger.approval.min_approvals. Conditions AND together (value_above, chain_in, to_address_in, function_selector_in, erc20_amount_above, intent_type_in, always). Match → 202.

Consensus composability (skip_when / require_when)

Two optional arrays on consensus_trigger give you fine-grained control over when consensus fires:

  • require_when — consensus is ONLY required when at least one entry matches. If set and none match, the transaction signs immediately (no 202). Use this to limit consensus to high-value or high-risk transactions.
  • skip_when — when ALL conditions in ANY entry match, consensus is bypassed even though conditions matched. Use this to exempt known-safe addresses or low-value transfers.

Each entry is a FlatConditionSet with the same vocabulary as ConsensusCondition: value_above, chain_in, to_address_in, function_selector_in, erc20_amount_above, intent_type_in, always. Fields within one entry are AND-combined; entries within the array are OR-combined.

Evaluation order: require_when is checked first, then skip_when.

{
"consensus_trigger": {
"conditions": [{ "type": "always" }],
"approval": { "min_approvals": 2 },
"expiry_secs": 3600,
"require_when": [
{ "value_above": "1000000000" }
],
"skip_when": [
{ "to_address_in": ["0xTreasuryMultisig"], "chain_in": ["ethereum"] }
]
}
}

In this example, consensus only triggers for transactions above 1 ETH (via require_when), but even those are exempted when sending to the known treasury multisig on Ethereum (via skip_when).

Consensus deep inspection

Set deep_inspect: true on the consensus trigger to also evaluate conditions against inner calls from multicall, Safe, and ERC-4337 wrappers. A match on any inner call triggers the consensus requirement:

{
"consensus_trigger": {
"conditions": [{ "type": "value_above", "threshold_gwei": 5000000000 }],
"approval": { "min_approvals": 2 },
"deep_inspect": true
}
}

Time window

conditions.time_window restricts when a policy is active.

FieldTypeNotes
start_hourint (0–23)Inclusive start
end_hourint (0–24)Exclusive end
days_of_weekint[]0=Sun, 6=Sat
timezonestringIANA timezone (e.g. "America/New_York"). Defaults to UTC.
cron_exprstring6-field cron (with seconds). When set, policy is only active at times matching the cron schedule in the given timezone.

Timezone-aware scheduling

By default, start_hour and end_hour are evaluated in UTC. Set timezone to evaluate in a local timezone instead:

{
"conditions": {
"time_window": {
"start_hour": 9,
"end_hour": 17,
"days_of_week": [1, 2, 3, 4, 5],
"timezone": "America/New_York"
}
}
}

This restricts the policy to US Eastern business hours (9 AM – 5 PM, Monday–Friday).

Cron expressions

For more complex schedules, use cron_expr (6-field: sec min hour day month weekday):

{
"conditions": {
"time_window": {
"cron_expr": "0 0 9-17 * * 1-5",
"timezone": "Europe/London"
}
}
}

This restricts the policy to weekdays 9 AM–5 PM London time. The cron expression is validated on policy creation — invalid expressions are rejected with a 400 error.

Solana interfaces

POST /v1/org/contract-abis with interface_kind: "solana_idl" (address = program id). System/SPL transfer / transferChecked names work without an IDL.