Skip to content

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.

PhaseMeaning
dead_man_switchThe confirmation ladder is running; each missed interval increments a counter
graceThe ladder exhausted (or there was none); still waiting the grace period out
succession_resolutionGrace is over; the successor is being settled
contestationAt least one challenge is on file
partially_executedExecution started and a leg did not complete
executedEvery leg reached a terminal success and a receipt was written
stood_downThe subject answered; false alarm
voidedA 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_4411

Filing 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-cec9e2c049b7

The 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 name or role disposition, which is accepted but not executable
  • an escrow disposition on a currency, which is deferred
  • a burn disposition, which needs the wallet named by SUCCESSION_BURN_SINK_WALLET to 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 25

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