approvals
approvals is a k-of-n decision among member nodes. One member proposes
an item, distinct members approve it, and the item ends APPROVED, REJECTED or
EXPIRED. Every item can be proved against the state root, and an approved item
can trigger one effect.
The machine id is exactly approvals. Item ids and topics are chosen by the
application; they do not create new machine types.
At a glance
Section titled “At a glance”| Machine id | approvals |
| Maturity | stable |
| Commands | [0, itemId, payload, required, deadlineMillis] PROPOSE; [1, itemId] APPROVE; [2, itemId] REJECT |
| Settings | optional on-approved effect under machines.approvals.on-approved-effect.* |
| State | i/<itemId> → [status, proposer, payloadHash, required, deadline, approvers, rejecter]; with the effect, ae/p/<itemId> and ae/s/<itemId> |
| Statuses | 0 PENDING, 1 APPROVED, 2 REJECTED, 3 EXPIRED |
| Proof subject | basic-approval-outcome-v1: coordinate proposal-id; claims status, quorum-reached, payload-digest |
| Result codes | none: a command that breaks a rule is a final no-op |
| Events | approvals.item-proposed.v1, approvals.item-approved.v1, approvals.item-rejected.v1, in composites only |
How it works
Section titled “How it works”What you're seeing: three members and one item that needs 2 approvals, on a chain with the on-approved effect enabled. The checks run in the order the machine evaluates them; a failed check means a final no-op.
- Propose Member A proposes
release-7with 2 required approvals and no deadline. The item starts PENDING with no approvers: proposing is not approving. With the on-approved effect active, the payload is also staged.- passes: No item with this id exists yet
approvals state Key Value i/release-7PENDING · 0 of 2 · approvers: none ae/p/release-7staged payload - Approve Member B approves. B is a new approver, so the count becomes 1 of 2.
- passes: The item exists
- passes: The item is PENDING
- passes: No deadline (0), or the block time is not after it
- passes: The sender has not approved this item yet
approvals state Key Value i/release-7PENDING · 1 of 2 · approvers: B ae/p/release-7staged payload - Approve again Member B approves a second time. A member counts once, so this final message changes nothing.
- passes: The item exists
- passes: The item is PENDING
- passes: No deadline (0), or the block time is not after it
- fails: The sender has not approved this item yet
approvals state (unchanged) Key Value i/release-7PENDING · 1 of 2 · approvers: B ae/p/release-7staged payload - Reach the threshold Member C approves: 2 of 2. The item becomes APPROVED, a terminal status. The staged payload becomes one effect, its record starts PENDING, and the staged copy is deleted.
- passes: The item exists
- passes: The item is PENDING
- passes: No deadline (0), or the block time is not after it
- passes: The sender has not approved this item yet
approvals state Key Value i/release-7APPROVED · 2 of 2 · approvers: B, C ae/s/release-7effect PENDING - Reject too late Member A now rejects. The item is no longer PENDING, so the rejection is a no-op. A terminal item never changes again.
- passes: The item exists
- fails: The item is PENDING
- not reached: No deadline (0), or the block time is not after it
- not reached: The sender has not approved this item yet
approvals state (unchanged) Key Value i/release-7APPROVED · 2 of 2 · approvers: B, C ae/s/release-7effect PENDING - Record the result The executor reports success, and the result returns as an ordered message. The effect record becomes CONFIRMED. The decision stays APPROVED whatever the effect’s outcome; a failure would read FAILED.
approvals state Key Value i/release-7APPROVED · 2 of 2 · approvers: B, C ae/s/release-7effect CONFIRMED
What if: The deadline passes
A deadline is Unix milliseconds compared with the block time. Time alone writes nothing.
- Propose Member A proposes
release-7with 2 required approvals and a deadline. The item starts PENDING.- passes: No item with this id exists yet
approvals state Key Value i/release-7PENDING · 0 of 2 · approvers: none ae/p/release-7staged payload - Approve after the deadline Member B approves in a block whose time is after T. The deadline check runs before the approval counts, so the item becomes EXPIRED instead, and the staged payload is deleted.
- passes: The item exists
- passes: The item is PENDING
- fails: No deadline (0), or the block time is not after it
- not reached: The sender has not approved this item yet
approvals state Key Value i/release-7EXPIRED · 0 of 2
What if: A member rejects
One rejection from any member ends a pending item.
- Propose Member A proposes
release-7with 2 required approvals and no deadline. The item starts PENDING with no approvers: proposing is not approving. With the on-approved effect active, the payload is also staged.- passes: No item with this id exists yet
approvals state Key Value i/release-7PENDING · 0 of 2 · approvers: none ae/p/release-7staged payload - Approve Member B approves. B is a new approver, so the count becomes 1 of 2.
- passes: The item exists
- passes: The item is PENDING
- passes: No deadline (0), or the block time is not after it
- passes: The sender has not approved this item yet
approvals state Key Value i/release-7PENDING · 1 of 2 · approvers: B ae/p/release-7staged payload - Reject Member C rejects. The item is PENDING and inside its deadline, so it becomes REJECTED, a terminal status, and the staged payload is deleted. No effect is emitted.
- passes: The item exists
- passes: The item is PENDING
- passes: No deadline (0), or the block time is not after it
approvals state Key Value i/release-7REJECTED · rejecter: C
What if: Propose the same id again
The first proposal for an id wins. A later one cannot replace its payload.
- Propose Member A proposes
release-7with 2 required approvals and no deadline. The item starts PENDING with no approvers: proposing is not approving. With the on-approved effect active, the payload is also staged.- passes: No item with this id exists yet
approvals state Key Value i/release-7PENDING · 0 of 2 · approvers: none ae/p/release-7staged payload - Propose again Member B proposes
release-7with a different payload. An item with this id exists, so the proposal is a no-op and the original payload hash stays.- fails: No item with this id exists yet
approvals state (unchanged) Key Value i/release-7PENDING · 0 of 2 · approvers: none ae/p/release-7staged payload
- member node
- deterministic state machine
- executor, outside consensus
- terminal and approved
- no-op or terminal failure
The rules, in the order the machine applies them:
- PROPOSE creates a PENDING item if the id is new. A later PROPOSE for the same
id is a no-op, even with another payload.
requiredmust be positive. - APPROVE or REJECT of an unknown id, or of an item that is not PENDING, is a no-op.
- If the item has a deadline (
deadlineMillisgreater than 0) and the block time is after it, the command marks the item EXPIRED instead. This applies to REJECT too. - APPROVE from a member that already approved is a no-op. Otherwise the member
is added; when the count reaches
requiredthe item is APPROVED. - REJECT from any member marks the item REJECTED.
Deadlines are Unix milliseconds compared with the finalized block timestamp. Time passing writes nothing: an item stays PENDING in state until a command touches it after its deadline.
The sender of the envelope is the approver. Through the local cluster, ports 7070, 7071 and 7072 are three different members.
When to use it
Section titled “When to use it”Use approvals when member keys really are the people, services or
organizations that decide:
- release and deployment gates;
- consortium or treasury authorization;
- cross-organization sign-off, or review before an external action.
Use role-approvals when approvers are business actors
with roles and organizations, separate from the member nodes. A REST API key
controls HTTP access; it is not an approval identity.
Try it
Section titled “Try it”The stock local cluster hosts effects-chain, which runs approvals with a
demonstration effect. From the top-level directory of the extracted release:
./yano.sh appchain cluster start 3./yano.sh appchain cluster effect demo "release 2026-07 approved"The demo proposes and approves a one-approval item, emits a demo.webhook
effect, plays the external worker, reports success, and checks the effect
proof. It calls no real webhook.
Submit through REST
Section titled “Submit through REST”Encode the commands with the tutorial helper:
TOOL=docs/appchain/tutorials/tools/stdlib_command.pyITEM=release-2026-07
PROPOSE_HEX=$(python3 "$TOOL" approvals propose "$ITEM" --required 2 \ --payload-text '{"artifact":"inventory-service:2.4.0"}')APPROVE_HEX=$(python3 "$TOOL" approvals approve "$ITEM")REJECT_HEX=$(python3 "$TOOL" approvals reject "$ITEM")Propose through node 0, then approve through two other members:
curl -sS -X POST http://127.0.0.1:7070/api/v1/app-chain/chains/effects-chain/messages \ -H 'Content-Type: application/json' \ -d "{\"topic\":\"approvals.command.v1\",\"bodyHex\":\"$PROPOSE_HEX\"}" | jq .
for port in 7071 7072; do curl -sS -X POST "http://127.0.0.1:$port/api/v1/app-chain/chains/effects-chain/messages" \ -H 'Content-Type: application/json' \ -d "{\"topic\":\"approvals.command.v1\",\"bodyHex\":\"$APPROVE_HEX\"}" | jq .done202 means the receiving member queued the envelope. It does not mean the
command is final or changed the item. The topic is a label; the machine reads
only the body. To reject a pending item instead, submit REJECT_HEX through
any member. Whichever terminal transition is final first wins.
To add a deadline, pass absolute epoch milliseconds:
DEADLINE=$(python3 -c 'import time; print(int((time.time() + 300) * 1000))')PROPOSE_HEX=$(python3 "$TOOL" approvals propose expiring-review-001 --required 2 \ --deadline-millis "$DEADLINE" --payload-text '{"document":"policy-v3"}')Submit from Java
Section titled “Submit from Java”implementation "org.yanoproject.x:yano-x-client:${yanoXVersion}"import org.yanoproject.x.client.AppChainClient;import org.yanoproject.x.client.StdlibAppChainClient;
import java.nio.charset.StandardCharsets;import java.time.Instant;
var proposer = new StdlibAppChainClient(AppChainClient.builder("http://127.0.0.1:7070/api/v1") .chainId("effects-chain").build());var approver1 = new StdlibAppChainClient(AppChainClient.builder("http://127.0.0.1:7071/api/v1") .chainId("effects-chain").build());var approver2 = new StdlibAppChainClient(AppChainClient.builder("http://127.0.0.1:7072/api/v1") .chainId("effects-chain").build());
String itemId = "release-2026-07";byte[] payload = "{\"artifact\":\"inventory-service:2.4.0\"}".getBytes(StandardCharsets.UTF_8);long deadline = Instant.now().plusSeconds(300).toEpochMilli();
proposer.propose(itemId, payload, 2, deadline);approver1.approve(itemId);approver2.approve(itemId);Add .apiKey("...") to each builder when API authentication is enabled. Two
client objects against the same node are one approver: the envelope sender is
that node’s member key.
Read and prove an item
Section titled “Read and prove an item”The item lives under the UTF-8 key i/<itemId>. It stores the Blake2b-256 hash
of the payload, not the payload itself; the original bytes stay in block
history while the node retains it.
ITEM_KEY_HEX=$(python3 -c 'print("i/release-2026-07".encode().hex())')
curl -sS \ "http://127.0.0.1:7070/api/v1/app-chain/chains/effects-chain/state/proof/$ITEM_KEY_HEX" \ | jq '{committedHeight, stateRoot, presence, valueHex}'valueHex is the CBOR item. In Java:
var item = proposer.approval(itemId).orElseThrow().value();System.out.println("status=" + item.status() + " approvals=" + item.approvers().size());For an audit, verify the proof against a state root from pinned finality or a Cardano anchor, not the root the serving node reports.
Configure
Section titled “Configure”effects-chain in the stock cluster file is a complete example, including the
three state-identity settings and the
on-approved effect:
yano: app-chain: chains[2]: chain-id: "effects-chain" state-machine: approvals state: commitment-profile: mpf-blake2b256-v1 format-fingerprint: 91ee14091200f1e24659112d640e877e9177779dcc81dd06117f013e9190082b genesis-id: 63281a4424bed827004e10185a663bce10de4228953d3959e2ed20aa3a4a9f0b membership: mode: governed block: interval-ms: 1000 effects: enabled: true default-gate: app-final external: enabled: true executor: enabled: true types: demo.webhook tick-ms: 250 metrics: types: demo.webhook machines: approvals: on-approved-effect: enabled: true type: demo.webhook gate: app-final expiry-blocks: 100 activations: on-approved-effect: 1Without the effects and machines.approvals blocks, the machine needs no
settings. A new chain needs its own genesis id.
The on-approved effect
Section titled “The on-approved effect”When an item first becomes APPROVED, the machine can emit one effect. The
proposal payload is the effect’s opaque payload, and type tells an executor
which contract to run; the machine itself never interprets it.
i/<itemId> PENDING -> APPROVED the decision; never changed by the effectae/p/<itemId> the staged payload written at PROPOSE, deleted when usedae/s/<itemId> PENDING -> CONFIRMED | FAILED the delivery recordeffect scope approvals/on-approved/<itemId>- The payload is staged at
ae/p/<itemId>only for proposals finalized once the effect is active, from its activation height. Pick that height before the first proposal that should emit an effect. - While the effect is active, a PROPOSE whose payload is larger than
effects.max-payload-bytes(16,384 by default) is refused at admission. - REJECTED and EXPIRED items delete their staged payload and emit nothing.
- Execution is at least once. Executors must use the effect’s identity to stay idempotent.
For a real HTTP delivery, configure the webhook.post executor as in the
webhook effects tutorial.
Operating notes
Section titled “Operating notes”- Member signing keys are approval authority. Protect and rotate them as consensus identities, not API credentials.
- Payloads are replicated to every member and kept in block history. Encrypt secrets before you propose them.
- Adding members does not change an existing item’s
requiredcount.
Advanced
Section titled “Advanced”Composites
Section titled “Composites”In a declarative composite, approvals emits typed events instead of the
standalone effect, and the payload is always staged. An item that reaches
APPROVED without a staged payload is rejected with
APPROVAL_PAYLOAD_UNAVAILABLE. A chain with the legacy on-approved effect
enabled exposes no composable kernel; use an effect binding instead of both.
Admission-rule views and facts
Section titled “Admission-rule views and facts”Admission rules can read an item (ADR-031.4, see admission rules):
- Value view (namespace
"", key: the item id):status(PENDING,APPROVED,REJECTED,EXPIRED),required,approverCount,proposer,payloadHash. The deadline is not exposed:EXPIREDis written by the first vote after it, so a storedPENDINGitem may already be past it. - Post-state facts, after an approved command:
approverCountAfter,required,approvedNowandproposerIsSender; absent when no item exists.