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 01a049b5-e6c8-7db9-a12f-7619a0cbfce3 \
--trigger explicit --event account_deleted
snug succession case open --plan 01a049b5-e6c8-7db9-a12f-7619a0cbfce3 \
--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.
Without a dead-man switch, grace starts when the case opens. Its deadline is
now + grace_period_days, followed by the contestation window. With a dead-man
switch, a fresh grace period starts after the confirmation ladder is exhausted.
{
"status": 201,
"msg": "Created",
"data": {
"case_id": "01a049b5-e6c8-7598-a218-24c44e13cf72",
"plan_id": "01a049b5-e6c8-7db9-a12f-7619a0cbfce3",
"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 advances once per configured interval. That interval is interval_days
multiplied by SUCCESSION_HEARTBEAT_WINDOW_SECONDS, which defaults to 86400.
After the required number of missed confirmations, the case enters grace.
Enable the sweeper for deadline-driven progress. Set
SUCCESSION_SWEEPER_ENABLED=true. It is off by default. A server with the
sweeper disabled still accepts explicit responses, challenges and execution,
but does not advance cases simply because a deadline passed.
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 01a049b5-e6c8-7598-a218-24c44e13cf72{
"case_id": "01a049b5-e6c8-7598-a218-24c44e13cf72",
"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 01a049b5-e6c8-71db-a2d6-edfb4bced96c \
--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": "01a049b5-e6c8-772c-a2d7-92ef2ae9cc17",
"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 01a049b5-e6c8-71db-a2d6-edfb4bced96c --challenge 01a049b5-e6c8-7792-a504-005eac56921f --dismiss
snug succession challenge resolve --case 01a049b5-e6c8-71db-a2d6-edfb4bced96c --challenge 01a049b5-e6c8-7792-a504-005eac56921f \
--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 01a049b5-e6c8-74f6-a8d1-cec9e2c049b7The 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": "01a049b5-e6c8-74f6-a8d1-cec9e2c049b7",
"phase": "executed",
"receipt": {
"case_id": "01a049b5-e6c8-74f6-a8d1-cec9e2c049b7",
"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": "01a049b5-e6c8-77e0-a429-aa589d11b09a" }
],
"receipt_hash": "sha256:7b0069f48b411f6802e7165b6b380b365d99dc4c1493ae90bdc90b5e2d853b18",
"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 returns the original receipt unchanged.
Its hash, timestamp and accepted transfers remain the same.
Each accepted transfer is recorded with its execution entry. A database failure rolls the operation back. A recoverable disposition failure can leave earlier successful legs in place, as described below. Retrying does not apply those completed transfers again.
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 --filter 'subject_kind[eq]=guild' --filter 'subject_id[eq]=thorn-hall'
snug succession case list --filter 'phase[eq]=executed' --limit 25Cases come back newest first. Phase filters use the names in the table above.
Use --filter 'phase[eq]=executed' to select an exact phase. Unknown phase names
are rejected as invalid filters.
Full parameters and schemas are in the Succession API reference.
