Skip to content

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.

Machine idapprovals
Maturitystable
Commands[0, itemId, payload, required, deadlineMillis] PROPOSE; [1, itemId] APPROVE; [2, itemId] REJECT
Settingsoptional on-approved effect under machines.approvals.on-approved-effect.*
Statei/<itemId> → [status, proposer, payloadHash, required, deadline, approvers, rejecter]; with the effect, ae/p/<itemId> and ae/s/<itemId>
Statuses0 PENDING, 1 APPROVED, 2 REJECTED, 3 EXPIRED
Proof subjectbasic-approval-outcome-v1: coordinate proposal-id; claims status, quorum-reached, payload-digest
Result codesnone: a command that breaks a rule is a final no-op
Eventsapprovals.item-proposed.v1, approvals.item-approved.v1, approvals.item-rejected.v1, in composites only
Life of an approvalIllustrative data

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.

  1. Propose Member A proposes release-7 with 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
    KeyValue
    i/release-7PENDING · 0 of 2 · approvers: none
    ae/p/release-7staged payload
  2. 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
    KeyValue
    i/release-7PENDING · 1 of 2 · approvers: B
    ae/p/release-7staged payload
  3. 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)
    KeyValue
    i/release-7PENDING · 1 of 2 · approvers: B
    ae/p/release-7staged payload
  4. 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
    KeyValue
    i/release-7APPROVED · 2 of 2 · approvers: B, C
    ae/s/release-7effect PENDING
  5. 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)
    KeyValue
    i/release-7APPROVED · 2 of 2 · approvers: B, C
    ae/s/release-7effect PENDING
  6. 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
    KeyValue
    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.

  1. Propose Member A proposes release-7 with 2 required approvals and a deadline. The item starts PENDING.
    • passes: No item with this id exists yet
    approvals state
    KeyValue
    i/release-7PENDING · 0 of 2 · approvers: none
    ae/p/release-7staged payload
  2. 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
    KeyValue
    i/release-7EXPIRED · 0 of 2

What if: A member rejects

One rejection from any member ends a pending item.

  1. Propose Member A proposes release-7 with 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
    KeyValue
    i/release-7PENDING · 0 of 2 · approvers: none
    ae/p/release-7staged payload
  2. 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
    KeyValue
    i/release-7PENDING · 1 of 2 · approvers: B
    ae/p/release-7staged payload
  3. 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
    KeyValue
    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.

  1. Propose Member A proposes release-7 with 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
    KeyValue
    i/release-7PENDING · 0 of 2 · approvers: none
    ae/p/release-7staged payload
  2. Propose again Member B proposes release-7 with 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)
    KeyValue
    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:

  1. 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. required must be positive.
  2. APPROVE or REJECT of an unknown id, or of an item that is not PENDING, is a no-op.
  3. If the item has a deadline (deadlineMillis greater than 0) and the block time is after it, the command marks the item EXPIRED instead. This applies to REJECT too.
  4. APPROVE from a member that already approved is a no-op. Otherwise the member is added; when the count reaches required the item is APPROVED.
  5. 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.

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.

The stock local cluster hosts effects-chain, which runs approvals with a demonstration effect. From the top-level directory of the extracted release:

Terminal window
./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.

Encode the commands with the tutorial helper:

Terminal window
TOOL=docs/appchain/tutorials/tools/stdlib_command.py
ITEM=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:

Terminal window
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 .
done

202 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:

Terminal window
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"}')
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.

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.

Terminal window
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.

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: 1

Without the effects and machines.approvals blocks, the machine needs no settings. A new chain needs its own genesis id.

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 effect
ae/p/<itemId> the staged payload written at PROPOSE, deleted when used
ae/s/<itemId> PENDING -> CONFIRMED | FAILED the delivery record
effect 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.

  • 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 required count.

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 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: EXPIRED is written by the first vote after it, so a stored PENDING item may already be past it.
  • Post-state facts, after an approved command: approverCountAfter, required, approvedNow and proposerIsSender; absent when no item exists.