Task Lifecycle & Status Transitions

Valid states: open → in_progress → completed / blocked / failed. Who can drive each transition, what writes to the audit log, and how to query historical state changes.

The Full Task Lifecycle

What you'll learn

Trace an issue from creation to resolution through every state transition, understand what records are created at each step, and build a timeline view from the database.

Complete State Machine

CREATED
  │  INSERT INTO issues (status='open')
  ▼
OPEN
  │  Heartbeat claims it: UPDATE status='claimed'
  ▼
CLAIMED
  │  Adapter invoke starts: UPDATE status='in_progress'
  ▼
IN_PROGRESS
  │
  ├─ Adapter succeeds ──────────► RESOLVED
  │    UPDATE status='resolved', result=..., resolved_at=now()
  │    INSERT INTO cost_events (...)
  │
  ├─ Adapter fails, retry ──────► OPEN (attempt_count++)
  │    UPDATE status='open'
  │
  ├─ Max attempts exceeded ─────► ESCALATED
  │    UPDATE status='escalated'
  │    INSERT INTO approvals (trigger='task_failed', ...)
  │
  └─ Manual cancel ─────────────► CANCELLED
       UPDATE status='cancelled'

What Each Transition Creates

Transition Database records created
open → claimed issues.claimed_at set
claimed → in_progress heartbeat_runs row updated with this issue
in_progress → resolved issues.result set; cost_events row inserted
in_progress → escalated approvals row inserted

Building a Timeline View

SELECT
  'Created'                        AS event,
  i.created_at                     AS occurred_at,
  NULL                             AS details
FROM issues i WHERE i.id = 'issue-id'

UNION ALL

SELECT
  'Claimed by heartbeat',
  i.claimed_at,
  'Heartbeat run: ' || i.heartbeat_run_id::text
FROM issues i WHERE i.id = 'issue-id' AND i.claimed_at IS NOT NULL

UNION ALL

SELECT
  'Resolved',
  i.resolved_at,
  'Cost: $' || ce.cost_usd::text
FROM issues i
JOIN cost_events ce ON ce.issue_id = i.id
WHERE i.id = 'issue-id' AND i.resolved_at IS NOT NULL

ORDER BY occurred_at;

Retry Behaviour in Detail

When an adapter call fails (exception thrown or non-zero exit), the heartbeat:

  1. Sets issues.status = 'open' and increments attempt_count
  2. Logs the error in heartbeat_runs.error_detail (JSON array)
  3. Does NOT immediately retry — the issue re-enters the open queue
  4. Next heartbeat tick will pick it up again (if attempt_count < max_attempts)

This means a failed issue is retried on the next heartbeat tick, not immediately. For a 5-minute heartbeat with max_attempts = 3, a repeatedly-failing issue will be retried at T+5m and T+10m before escalating.

Adjusting Retry Limits Per Issue

-- Give a complex issue more retries
UPDATE issues
SET max_attempts = 5
WHERE id = 'issue-id';

-- Reset a stuck issue
UPDATE issues
SET status = 'open', attempt_count = 0
WHERE id = 'issue-id';

Checkpoint ✓

Sign in for progress tracking & Q&A →