Skip to content

Ritual

Try the interactive example ↓

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 role otherwise), and they are create-only: no update or delete endpoints, version is always 1.
  • Parts are identity-bound. Each part names an assigned_to principal; 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_ms is satisfied when any quorum-sized set of performances spans at most window_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, and of must equal the part count. An optional ordering must list every part exactly once and forces that sequence.
  • An attempt is one live run of a definition, created by arm: armed, then pending as parts land, ending completed or timed_out when the arm TTL expires first (fired is 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: true in 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 -created_at

list takes the shared list parameters: q, filter[mode], filter[created_at], filter[updated_at], and sort by name, version or created_at (e.g. filter[mode][eq]=inverse). 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 zsfDCyRSNrAdvtEVcmLZ

arm 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

  • 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 is 400 ordering_violated.
  • Concluded - a completed or timed-out attempt is 409 not_actionable.
  • Wrong mode - perform on an inverse ritual and checkin on a forward one are both 400 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, newest first (--filter 'status[eq]=completed' or rehearsal[eq]=true narrows them), 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 --filter 'status[eq]=timed_out'
snug ritual analytics --def-id KBWergseuNgABVcZUDjr --sample-size 200

Completion 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.

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.

Reference

Interactive example

Coordinate several participants within one time window. Complete an action only when the required people have contributed in time.

An illustrative example that runs in your browser. No account or API calls required.