Ritual
N-party synchronized action - the thing locks cannot do. A ritual definition names a set of parts, each bound to a specific actor, plus a simultaneity window: participants arm an attempt and perform their parts, and the server atomically detects whether enough parts landed close enough together, using only server-side clocks. Inverted, the same primitive is a dead-man switch that fires when someone fails to check in.
When to reach for it: two-person production deploy confirmations, co-op game mechanics (four players on four pressure plates), N-of-M founder kill switches, synchronized start signals, dead-man switches with escalation.
When not to: "at most one actor" is a lock - use Distributed State; weighing votes or proposals over rounds belongs to Consensus; a single human approving a request on their own schedule belongs to Human-in-the-Loop.
Concepts
- Definitions are reusable templates - name, parts, window, policies.
Creating one requires a platform admin token (
403 Ritual definitions require admin roleotherwise), and they are create-only: no update or delete endpoints,versionis always 1. - Parts are identity-bound. Each part names an
assigned_toprincipal; a perform or check-in only counts when the caller's JWT subject equals it (admins bypass). A ritual is an authenticated ceremony, not a convention. - The window slides.
window_msis satisfied when any quorum-sized set of performances spans at mostwindow_ms- an early stray perform does not doom the attempt if enough later ones cluster. - Quorum is N-of-M -
{"required": 3, "of": 5}; omitted, it defaults to all parts, andofmust equal the part count. An optionalorderingmust list every part exactly once and forces that sequence. - An attempt is one live run of a definition, created by
arm:armed, thenpendingas parts land, endingcompletedortimed_outwhen the arm TTL expires first (firedis the inverse-mode terminal state). All timestamps are server-assigned - clients never supply clocks. - Rehearsal mode runs the full detection pipeline but suppresses
completion side effects, and is tagged
rehearsal: truein history.
Define a ceremony
define takes the request body from a JSON file. This one is a two-person
vault door - two keys, five seconds, all parts required:
{
"name": "Two-Person Vault Door",
"description": "Both keyholders must turn their keys within 5 seconds.",
"parts": [
{ "part_id": "key_a", "assigned_to": "docs-wave" },
{ "part_id": "key_b", "assigned_to": "docs4-other" }
],
"window_ms": 5000,
"arm_ttl_seconds": 120
}snug ritual define --file ./vault-door.json # requires a platform admin token
snug ritual get --def-id BEYQVwcUuXMnNpTNemuX
snug ritual list --query vault --filter mode:eq:forward --sort-by created_atlist accepts the shared search grammar
(q, filter, sort_by); filter mode:eq:inverse was verified live.
Fetching a definition or attempt, arming, and viewing history are
restricted to assigned participants, the definition owner, or an admin -
anyone else gets 403 insufficient_permissions. Listing is the exception:
it is not participant-scoped and returns full definition bodies to any
authenticated caller (verified with a second user), so keep secrets out of
names and descriptions.
Run the ceremony
snug ritual arm --def-id BEYQVwcUuXMnNpTNemuX # opens an attempt
snug ritual countdown --attempt-id zsfDCyRSNrAdvtEVcmLZ --seconds 3
snug ritual perform --attempt-id zsfDCyRSNrAdvtEVcmLZ --part key_a
snug ritual status --attempt-id zsfDCyRSNrAdvtEVcmLZarm returns the attempt_id plus server-assigned armed_at_ms and
deadline_ms (armed time plus the definition's arm_ttl_seconds, default
300). countdown returns an authoritative window_open_ms a few seconds
out - share it with participants as the go-signal so everyone performs
against the same clock.
Over HTTP, performing a part is a POST with the enveloped response:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" -d '{"part_id": "key_a"}' \
http://localhost:4000/api/v1/rituals/attempts/zsfDCyRSNrAdvtEVcmLZ/perform{
"status": 200,
"msg": "OK",
"data": {
"attempt_id": "zsfDCyRSNrAdvtEVcmLZ",
"part_id": "key_a",
"status": "pending",
"performed": 1,
"required": 2,
"report": null
}
}When the second keyholder performed key_b 44ms later, that response came
back "status": "completed" with a report:
"ritual completed with 2 of 2 parts within 44ms spread".
What perform rejects
Each of these was reproduced live:
- Not your part - someone else's part is
403 insufficient_permissions, and the performance is not recorded. - Duplicate - a part already performed here is
409 part_already_performed. - Out of order - against a declared
ordering, an early part is400 ordering_violated. - Concluded - a completed or timed-out attempt is
409 not_actionable. - Wrong mode -
performon an inverse ritual andcheckinon a forward one are both400 mode_mismatch.
Near-miss reports
When the arm TTL expires before quorum, the background scheduler (a few
seconds' sweep granularity) finalizes the attempt as timed_out and writes
a report saying exactly who was missing:
{
"performed": 1,
"required": 2,
"missing_parts": ["plate_b"],
"spread_ms": 0,
"verdict": "1 of 2 parts performed; plate_a was last at 1787926607161ms (spread 0ms)"
}history pages through past attempts (--status completed filters), and
analytics aggregates recent terminal attempts into a success rate, median
spread, and the parts most often responsible for near-misses:
snug ritual history --def-id KBWergseuNgABVcZUDjr --status timed_out
snug ritual analytics --def-id KBWergseuNgABVcZUDjr --sample-size 200Completion hooks
A definition's on_complete block fans completion out as events:
pubsub_channel publishes {"attempt_id": "...", "event": "completed", "ritual_def_id": "..."} to that channel, and webhook_ids and
timeline_stream dispatch to those services. Rehearsal attempts complete
without publishing anything - both behaviors verified live.
Dead-man switches
Inverse mode - rituals that fire when someone does not act, with check-ins and escalation ladders - has its own page: Dead-man switches.
Limits and configuration
Up to 64 parts per ritual, windows up to 24 hours, arm TTLs up to 24 hours,
and 16 escalation steps by default - all tunable, along with the scheduler
sweep interval, via the RITUAL_* variables in the
Ritual CONFIG reference.
Reference
- Ritual API - every endpoint, callable
- Related: Distributed State for locks and CAS, Consensus for weighted voting, Human-in-the-Loop for single-approver decisions, PubSub and Webhooks for receiving completion and escalation events