Skip to content

Trigger State Machine — States, Transitions, and Failure Modes


Overview

The trigger engine is the heart of the platform. It manages: - Dead man's switches - Scheduled releases - Event-based triggers (death verification) - Conditional triggers (combinations)

Core Principle: Irreversibility is delayed, not denied. Every state has a challenge path.


Trigger Types

1. Dead Man's Switch (dead_man_switch)

Fires if user fails to check in within configured interval.

Configuration:

{
  type: 'dead_man_switch',
  check_interval_days: 7,      // How often user must check in
  grace_period_days: 3,        // Warning period before execution
  reminder_channels: ['email', 'sms', 'push'],
  secondary_contacts: ['contact_id_1', 'contact_id_2'],
  require_secondary_confirmation: true
}

2. Scheduled (scheduled)

Fires at a specific date/time.

Configuration:

{
  type: 'scheduled',
  execute_at: '2030-01-01T00:00:00Z',
  require_confirmation: true,  // User must confirm before execution
  confirmation_window_days: 7
}

3. Death Verification (death_verification)

Fires when death is reported and verified through process.

Configuration:

{
  type: 'death_verification',
  require_certificate: true,
  require_secondary_confirmation: true,
  mandatory_delay_days: 7,
  challenge_window_days: 14
}

4. Event-Based (event)

Fires when specific event occurs.

Configuration:

{
  type: 'event',
  event_type: 'document_uploaded',
  event_filter: { document_type: 'death_certificate' },
  require_verification: true
}

5. Conditional (conditional) — v2

Fires when combination of conditions are met.

Configuration:

{
  type: 'conditional',
  conditions: {
    operator: 'AND',
    items: [
      { trigger_id: 'trigger_1', state: 'triggered' },
      { trigger_id: 'trigger_2', state: 'triggered' }
    ]
  }
}


State Definitions

Core States

┌─────────────────────────────────────────────────────────────────────┐
│                         TRIGGER STATES                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐  │
│  │  draft   │────▶│  armed   │────▶│ triggered│────▶│ pending_ │  │
│  │          │     │          │     │          │     │ execution│  │
│  └──────────┘     └──────────┘     └──────────┘     └──────────┘  │
│       │                │                 │               │         │
│       │                │                 │               ▼         │
│       │                │                 │         ┌──────────┐   │
│       │                │                 │         │executing │   │
│       │                │                 │         └──────────┘   │
│       │                │                 │               │         │
│       │                │                 │               ▼         │
│       │                │                 │         ┌──────────┐   │
│       │                │                 │         │ released │   │
│       │                │                 │         └──────────┘   │
│       │                │                 │               │         │
│       │                │                 │               ▼         │
│       │                │                 │         ┌──────────┐   │
│       │                │                 │         │finalized │   │
│       │                │                 │         └──────────┘   │
│       │                │                 │                         │
│       ▼                ▼                 ▼                         │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐                   │
│  │ deleted  │     │ disarmed │     │ aborted  │                   │
│  └──────────┘     └──────────┘     └──────────┘                   │
│                                                                     │
│  FAILURE STATES:                                                   │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐                   │
│  │execution_│     │notificat_│     │ system_  │                   │
│  │ failed   │     │ion_failed│     │ failure  │                   │
│  └──────────┘     └──────────┘     └──────────┘                   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

State Descriptions

State Description User Visible Abort Allowed
draft Trigger created but not activated "Not active" N/A (delete)
armed Active, waiting for condition "Active - monitoring" Yes
triggered Condition met, challenge window open "Triggered - awaiting confirmation" Yes
pending_execution Challenge window passed, abort window open "Executing soon - abort available" Yes
executing Actions in progress "Executing..." Limited
released Access granted, data released "Released" Reversal possible
finalized Irreversible completion "Complete" No
disarmed User disabled trigger "Disabled" N/A
aborted Execution stopped "Aborted" N/A
deleted Trigger removed N/A N/A
execution_failed Action failed, retry pending "Error - retrying" Yes
notification_failed Notification failed "Notification error" Yes
system_failure System error, manual intervention "System error - support notified" Manual

Transition Table

Happy Path

From To Preconditions Side Effects
draft armed User activates Audit log, start monitoring
armed triggered Trigger condition met Audit log, send alerts, start challenge timer
triggered pending_execution Challenge window elapsed, no abort Audit log, send final warning
pending_execution executing Abort window elapsed Audit log, begin actions
executing released All actions complete Audit log, notify recipients
released finalized Finalization criteria met OR 7 days elapsed Audit log, lock state

Abort/Disable Path

From To Preconditions Side Effects
armed disarmed User request Audit log
triggered aborted User abort OR secondary contact abort Audit log, notify
pending_execution aborted User abort (with confirmation) Audit log, notify
executing aborted Limited: only incomplete actions aborted Audit log, partial rollback
released aborted Reversal request within 7 days Audit log, access revoked

Failure Path

From To Preconditions Side Effects
executing execution_failed Action throws error Audit log, alert ops, schedule retry
executing notification_failed Notification fails Audit log, try alternate channel
Any system_failure System error Audit log, alert ops, hold state

Recovery Path

From To Preconditions Side Effects
execution_failed executing Retry successful Audit log
notification_failed executing Alternate channel successful Audit log
system_failure Previous state Manual intervention Audit log, explain recovery

Timing Fields

Every trigger tracks:

CREATE TABLE vault_core.triggers (
  id UUID PRIMARY KEY,

  -- Configuration
  trigger_type TEXT NOT NULL,
  config JSONB NOT NULL,
  actions JSONB NOT NULL,

  -- Current state
  status TEXT NOT NULL DEFAULT 'draft',

  -- Timing (all nullable until relevant)
  created_at TIMESTAMPTZ DEFAULT NOW(),
  armed_at TIMESTAMPTZ,
  last_check_in TIMESTAMPTZ,
  next_check_required TIMESTAMPTZ,

  -- Trigger timing
  condition_met_at TIMESTAMPTZ,      -- When trigger condition was met
  triggered_at TIMESTAMPTZ,          -- When entered 'triggered' state
  challenge_window_ends_at TIMESTAMPTZ,

  -- Execution timing
  eligible_at TIMESTAMPTZ,           -- When execution becomes allowed
  abort_window_ends_at TIMESTAMPTZ,
  execution_started_at TIMESTAMPTZ,
  execution_completed_at TIMESTAMPTZ,

  -- Release timing
  released_at TIMESTAMPTZ,
  reversal_window_ends_at TIMESTAMPTZ,
  finalized_at TIMESTAMPTZ,

  -- Failure tracking
  last_error TEXT,
  retry_count INT DEFAULT 0,
  next_retry_at TIMESTAMPTZ
);

Why these fields matter: - eligible_at proves we waited the required time - abort_window_ends_at proves abort was possible - finalized_at proves when irreversibility occurred - All fields are immutable once set (append-only)


Dead Man's Switch Logic

Check-In Flow

User checks in
Update last_check_in = NOW()
Calculate next_check_required = NOW() + check_interval_days
Reset any warning states

Monitoring Flow (runs every 5 minutes)

For each armed dead_man_switch trigger:
    ├─ If NOW() < next_check_required
    │      └─ No action
    ├─ If NOW() >= next_check_required AND NOW() < (next_check_required + grace_period)
    │      └─ Send reminders (if not already sent)
    │      └─ Contact secondary contacts (if configured)
    │      └─ Remain in 'armed' state
    └─ If NOW() >= (next_check_required + grace_period)
           ├─ If secondary_confirmation_required AND NOT secondary_confirmed
           │      └─ Escalate to secondaries, extend grace
           └─ If all confirmations received OR not required
                  └─ Transition to 'triggered'
                  └─ Start challenge window

Multi-Signal Verification

A trigger NEVER fires on a single signal. Minimum verification:

async function shouldTriggerFire(trigger: Trigger): Promise<boolean> {
  const signals = [];

  // Signal 1: Check-in missed
  if (isCheckInOverdue(trigger)) {
    signals.push('check_in_missed');
  }

  // Signal 2: Email reminder not acknowledged
  if (reminderSent(trigger) && !reminderAcknowledged(trigger)) {
    signals.push('reminder_ignored');
  }

  // Signal 3: SMS verification failed
  if (smsSent(trigger) && !smsConfirmed(trigger)) {
    signals.push('sms_unconfirmed');
  }

  // Signal 4: Secondary contact confirms concern
  if (secondaryContactedAndConfirmed(trigger)) {
    signals.push('secondary_confirmed');
  }

  // Require minimum 2 signals
  return signals.length >= 2;
}

Death Verification Flow

Process Recording (Not Truth Assertion)

Executor initiates death claim
Record: "Executor {id} initiated death claim at {timestamp}"
Executor uploads certificate
Record: "Document {id} uploaded by {executor_id} at {timestamp}"
Record: "Document type: death_certificate"
(NO assertion that document is valid or person is dead)
If secondary confirmation required:
    ├─ Notify secondary contact
    ├─ Record: "Secondary confirmation requested from {contact_id} at {timestamp}"
    └─ Wait for confirmation
           ├─ Record: "Secondary {contact_id} confirmed at {timestamp}"
           └─ OR timeout (extend delay, escalate)
Mandatory delay begins (e.g., 7 days)
Record: "Mandatory delay started at {timestamp}, ends at {end_timestamp}"
If user authenticates during delay:
    ├─ Abort trigger
    └─ Record: "User {id} authenticated at {timestamp}, death claim aborted"
Delay elapsed, enter 'pending_execution'
Challenge window begins (e.g., 14 days)
Challenge window elapsed without challenge
Execute release actions
Record: "Release executed at {timestamp} per process: death_verification"

Abort Mechanics

User Abort

async function userAbort(triggerId: string, userId: string, reason: string) {
  const trigger = await getTrigger(triggerId);

  // Check if abort is allowed in current state
  if (!ABORTABLE_STATES.includes(trigger.status)) {
    throw new Error(`Cannot abort trigger in state: ${trigger.status}`);
  }

  // For late-stage aborts, require typed confirmation
  if (trigger.status === 'pending_execution') {
    // This is handled in UI - typed confirmation required
  }

  await auditLog.record({
    action: 'trigger_aborted',
    triggerId,
    userId,
    previousState: trigger.status,
    reason,
    timestamp: new Date()
  });

  await updateTriggerState(triggerId, 'aborted', {
    aborted_at: new Date(),
    aborted_by: userId,
    abort_reason: reason
  });

  // Notify relevant parties
  await notifyAbort(trigger);
}

Secondary Contact Abort

Secondary contacts can abort if they believe the user is alive:

async function secondaryAbort(triggerId: string, contactId: string, evidence: string) {
  const trigger = await getTrigger(triggerId);

  // Verify contact is authorized for this trigger
  if (!trigger.config.secondary_contacts.includes(contactId)) {
    throw new Error('Contact not authorized for this trigger');
  }

  await auditLog.record({
    action: 'secondary_abort_requested',
    triggerId,
    contactId,
    evidence,
    timestamp: new Date()
  });

  // Secondary abort enters review state, not immediate abort
  await updateTriggerState(triggerId, 'abort_review', {
    abort_requested_by: contactId,
    abort_evidence: evidence,
    review_deadline: addDays(new Date(), 3)
  });

  // Notify user (if reachable) and other contacts
  await notifyAbortReview(trigger);
}

Failure Handling

Execution Failure

async function handleExecutionFailure(triggerId: string, error: Error) {
  const trigger = await getTrigger(triggerId);

  await auditLog.record({
    action: 'execution_failed',
    triggerId,
    error: error.message,
    stack: error.stack,
    retryCount: trigger.retry_count
  });

  if (trigger.retry_count < MAX_RETRIES) {
    // Schedule retry with exponential backoff
    const backoff = Math.pow(2, trigger.retry_count) * 60 * 1000; // minutes

    await updateTriggerState(triggerId, 'execution_failed', {
      last_error: error.message,
      retry_count: trigger.retry_count + 1,
      next_retry_at: new Date(Date.now() + backoff)
    });
  } else {
    // Max retries exceeded, escalate
    await updateTriggerState(triggerId, 'system_failure', {
      last_error: error.message,
      requires_manual_intervention: true
    });

    await alertOps({
      severity: 'critical',
      triggerId,
      message: 'Trigger execution failed after max retries'
    });

    // Notify user/executor of issue
    await notifyExecutionIssue(trigger);
  }
}

Recovery

async function recoverFromFailure(triggerId: string, operatorId: string, action: string) {
  const trigger = await getTrigger(triggerId);

  if (trigger.status !== 'system_failure') {
    throw new Error('Trigger not in failure state');
  }

  await auditLog.record({
    action: 'manual_recovery',
    triggerId,
    operatorId,
    recoveryAction: action,
    previousState: trigger.status
  });

  switch (action) {
    case 'retry':
      await updateTriggerState(triggerId, 'executing');
      await executeActions(triggerId);
      break;

    case 'skip_failed_action':
      await markActionSkipped(triggerId, trigger.failed_action_id);
      await continueExecution(triggerId);
      break;

    case 'abort':
      await updateTriggerState(triggerId, 'aborted', {
        aborted_by: operatorId,
        abort_reason: 'Manual abort after system failure'
      });
      break;
  }
}

Invariants

These must ALWAYS be true:

  1. No transition without audit: Every state change has a corresponding audit entry committed atomically.

  2. No execution without eligibility: executing state requires eligible_at < NOW().

  3. No finalization without release: finalized state requires released_at to be set.

  4. Abort window always exists: pending_execution state always has abort_window_ends_at set.

  5. Multi-signal for irreversibility: Transition to pending_execution requires >= 2 confirmation signals.

  6. Timing fields are immutable: Once set, timing fields cannot be changed (only new fields added).

  7. User always informed: Every state has a corresponding user-visible message.


Testing Requirements

Unit Tests

  • [ ] Each state transition has a test
  • [ ] Invalid transitions throw errors
  • [ ] Audit log is written for every transition
  • [ ] Timing fields are set correctly
  • [ ] Multi-signal verification works
  • [ ] Abort works from each abortable state

Integration Tests

  • [ ] Full dead man's switch cycle (arm → miss check-in → trigger → execute → release)
  • [ ] Full death verification cycle
  • [ ] Abort at each stage
  • [ ] Failure and recovery
  • [ ] Concurrent operations don't corrupt state

Chaos Tests

  • [ ] Worker crash during execution (state preserved, resumable)
  • [ ] Database failure during transition (transaction rolled back)
  • [ ] Notification failure (alternate channel used)
  • [ ] Clock skew (timing based on server time, not client)

This document defines the trigger state machine. Implementation must match this specification exactly. All transitions must be auditable.