Cases and Execution
A case is one triggered run of a plan. It pins the plan version it opened under, walks a phase machine designed to give the subject every chance to come back, and ends either stood down, voided, or executed against a signed receipt.
Opening a case
snug succession case open --plan plan_11eae22fb8114757b5b40c9fbf5f6780 \
--trigger explicit --event account_deleted
snug succession case open --plan plan_11eae22fb8114757b5b40c9fbf5f6780 \
--trigger admin --reason "owner verified deceased"An admin trigger requires a platform admin token and a non-empty
--reason; without one the call is 400 invalid_request ("admin
invocation requires a reason string"), and a non-admin caller is
403 insufficient_permissions. Explicit-event and inactivity triggers
instead need subject authority - the subject itself, or an admin.
Nothing checks the trigger against the plan. A case opens under whatever trigger you send, whether or not the plan declares it; the trigger is recorded on the case as provenance, not enforced as a precondition.
Both deadlines are computed at open time from the plan's safeguards and
frozen onto the case: grace_ends_at is now + grace_period_days, and
contestation_ends_at is grace_ends_at + contestation_window_days.
{
"status": 201,
"msg": "Created",
"data": {
"case_id": "case_02c4eb1c878f45999ca1371a4e435a9c",
"plan_id": "plan_11eae22fb8114757b5b40c9fbf5f6780",
"plan_version": 1,
"subject": { "kind": "guild", "id": "thorn-hall" },
"trigger": { "kind": "inactivity", "source": "liveness", "threshold_days": 30 },
"trigger_reason": null,
"opened_by": "docs-wave-admin",
"phase": "dead_man_switch",
"designated_successor": null, "challenges": [],
"grace_ends_at": "2026-09-04T18:50:53.174028Z",
"contestation_ends_at": "2026-09-18T18:50:53.174028Z",
"execution_receipt": null,
"created_at": "2026-08-28T18:50:53.174654Z", "updated_at": "..."
}
}The phases
A case opens in dead_man_switch if the plan has a dead_man_switch
safeguard, otherwise in grace.
| Phase | Meaning |
|---|---|
dead_man_switch | The confirmation ladder is running; each missed interval increments a counter |
grace | The ladder exhausted (or there was none); still waiting the grace period out |
succession_resolution | Grace is over; the successor is being settled |
contestation | At least one challenge is on file |
partially_executed | Execution started and a leg did not complete |
executed | Every leg reached a terminal success and a receipt was written |
stood_down | The subject answered; false alarm |
voided | A challenge was upheld |
Three things move a case forward on demand: standing down, resolving a
challenge, and executing. Everything time-driven - the dead-man ladder
advancing, grace expiring into resolution, resolution expiring into
contestation - belongs to a background sweeper that ticks every
SUCCESSION_SWEEPER_INTERVAL_SECONDS. On a sweeper-enabled server a case
opened with both windows at zero walked grace to succession_resolution
to contestation on its own within two ticks.
The ladder is the exception: its steps are interval_days multiplied by
SUCCESSION_HEARTBEAT_WINDOW_SECONDS, which defaults to 86400, so the
shortest possible confirmation interval is a real day. The
dead_man_switch to grace escalation is therefore the one transition in
the table above not observed directly.
The sweeper is off by default (SUCCESSION_SWEEPER_ENABLED=false), and
with it off a case sits in the phase it opened in indefinitely - verified
on a default-configured server, where a case whose grace_ends_at had
already passed stayed in grace through several minutes of polling. That
also puts succession_resolution and contestation out of reach, and with
them the whole contestation flow below. If deadlines are meant to mean
anything in your deployment, turn the sweeper on.
Standing down
A live subject cancels its own succession. The endpoint is the "I am still
here" signal, callable by the subject or an admin during
dead_man_switch or grace:
snug succession case respond --id case_02c4eb1c878f45999ca1371a4e435a9c{
"case_id": "case_02c4eb1c878f45999ca1371a4e435a9c",
"phase": "stood_down",
"message": "Case stood down; you are confirmed present"
}Standing down releases the subject's open-case slot, so a later trigger can
open a fresh case against the same plan. Calling it twice, or in any later
phase, is 409 invalid_phase; so is executing a stood-down case.
The ladder itself is invisible from outside. A case stores
confirmations_required, confirmations_missed, and
next_heartbeat_deadline, but none of the three appear in the case
response, so you cannot see how many pings are left.
Contestation
Anyone authenticated may file a challenge against a case in
succession_resolution or contestation, up to contestation_ends_at:
snug succession challenge file --case case_01HXJ \
--claim "Heir 0 faked the inactivity window" \
--evidence timeline:evt_4411Filing moves the case to contestation and returns the challenge_id plus
the route that should adjudicate it, taken from the plan's
safeguards.challenge_resolution and defaulting to hitl. The challenge
then shows up in the case's challenges array, attributed to its filer:
{ "challenge_id": "chal_6a086293e4614ee0a5860d6a17bb9f4b",
"by": { "kind": "auth_user", "id": "docs-wave" },
"claim": "heir 0 fabricated the inactivity window",
"evidence": [ "timeline:evt_4411", "kv:audit-note" ],
"status": "under_review", "route": "hitl",
"filed_at": "2026-08-28T19:09:58.676855Z" }Filing is not gated by subject authority - a plain user filed the challenge
above against a guild they have no control over. Filing in a phase that
does not accept one is 409 invalid_phase ("challenges may only be filed
during resolution or contestation, not Grace"), an empty claim is
400 BAD_REQUEST, and an unknown case is 404 case_not_found.
The deadline is enforced separately from the phase: once
contestation_ends_at has passed, filing is 409 contestation_closed
even though the case is sitting in the contestation phase. A plan with
contestation_window_days: 0 therefore reaches that phase already closed
to challenges.
While any challenge is under_review the case is frozen: execute returns
409 invalid_phase, "execution is frozen while challenges are under
review".
Resolution is admin-only:
snug succession challenge resolve --case case_01HXJ --challenge chal_01HXK --dismiss
snug succession challenge resolve --case case_01HXJ --challenge chal_01HXK \
--uphold --note "claim substantiated"Dismissing returns the case to succession_resolution - unless other
challenges are still under review, in which case it stays in
contestation. Upholding responds
{ "status": "upheld", "phase": "voided" }: terminal, and it frees the
subject's open-case slot, verified by opening a new case against the same
plan immediately afterwards.
A non-admin caller gets 403 insufficient_permissions, an unknown
challenge 404 challenge_not_found, and resolving one twice
409 invalid_phase ("challenge ... is already Dismissed"). The optional
--note has no field of its own - it is appended to the challenge's
claim, which afterwards reads original claim [resolution: your note].
Executing the will
Execution requires a platform admin token; the subject itself cannot execute its own case.
snug succession case execute --id case_ef989ee621bf49b69c80ded536996a1aThe saga walks the plan's dispositions in order. For a treasury balance it
moves the source wallet's entire available balance - there is no
partial amount. transfer sends it to the successor, or to the
disposition's explicit to. split floors each share and credits the
flooring remainder to the first heir, so the source always ends at zero.
A wallet that is already empty is a successful leg with no transfer
(dep_ref: null). An Item disposition moves the whole item into the
heir's to_container and reports the item id as its dep_ref.
{
"case_id": "case_ef989ee621bf49b69c80ded536996a1a",
"phase": "executed",
"receipt": {
"case_id": "case_ef989ee621bf49b69c80ded536996a1a",
"plan_version": 1,
"successor": { "kind": "auth_user", "id": "heir-primary" },
"legs": [
{ "leg_index": 0,
"asset": { "kind": "treasury_balance", "domain": "estate", "currency_id": "gold" },
"rule": { "kind": "split", "shares": [ ... ] },
"status": "succeeded", "detail": null,
"dep_ref": "txn_c18d0311f6eb4c9394afc54698d6f02a" }
],
"receipt_hash": "sha256:1b89c57693c510c7ea6140c78975eb467ea050a29f518d65536825c7be6ab7ca",
"executed_at": "2026-08-28T18:51:16.541105Z"
}
}Each dep_ref is the downstream Treasury transaction or Item transfer the
leg produced, so the receipt is a joinable audit trail. receipt_hash is a
SHA-256 over the case, plan version, successor, and succeeded legs.
Executing an already-executed case is a no-op that returns the original
receipt unchanged - verified by re-running the split above and confirming
the hash, timestamp, and wallet balances did not move.
Partial execution is the failure mode to plan for. The saga stops at
the first leg that does not succeed, and the case lands in
partially_executed with receipt: null. Re-running execute resumes
rather than repeats: a leg that already succeeded is skipped, verified by
refunding a drained source wallet between two runs and watching the money
stay put. But the response tells you only the phase - the per-leg status
and detail explaining what broke are stored on the case and returned by
no endpoint, so diagnosing a stall means checking the downstream service
yourself. Three ways to get there:
- a
nameorroledisposition, which is accepted but not executable - an
escrowdisposition on a currency, which is deferred - a
burndisposition, which needs the wallet named bySUCCESSION_BURN_SINK_WALLETto exist in the same domain
A partially_executed case also keeps holding the subject's open-case
slot, and nothing but a successful re-run clears it, so a subject whose
will contains a leg that can never succeed can never open another case.
Listing cases
snug succession case list --subject guild:thorn-hall
snug succession case list --phase executed --page-size 25Cases come back newest first. --phase takes a phase name exactly as
spelled in the table above; an unrecognized value is not an error, it
just matches nothing and returns an empty page.
Full parameters and schemas are in the Succession API reference.