Skip to content

Database Schema

Duraflow uses PostgreSQL for persistent storage. This document describes the database schema.

Tables Overview

TablePurpose
agent_tasksWorkflow task records
step_runsStep execution checkpoints
dead_letter_queueFailed compensation records

agent_tasks

Stores workflow task (run) information.

sql
CREATE TABLE agent_tasks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  workflow_name VARCHAR(255) NOT NULL,
  status VARCHAR(50) NOT NULL DEFAULT 'pending',
  input JSONB DEFAULT '{}',
  output JSONB,
  error JSONB,
  priority INTEGER DEFAULT 0,
  scheduled_at TIMESTAMPTZ,
  started_at TIMESTAMPTZ,
  completed_at TIMESTAMPTZ,
  heartbeat_at TIMESTAMPTZ,
  worker_id VARCHAR(255),
  retry_count INTEGER DEFAULT 0,
  max_retries INTEGER DEFAULT 3,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  deleted_at TIMESTAMPTZ
);

Columns

ColumnTypeDescription
idUUIDPrimary key, unique task identifier
workflow_nameVARCHAR(255)Name of the workflow to execute
statusVARCHAR(50)Current task status (pending/running/completed/failed/cancelled/rolled_back/partial_rollback)
inputJSONBWorkflow input data
outputJSONBWorkflow output (set on completion)
errorJSONBError details (set on failure)
priorityINTEGERTask priority (higher = first)
scheduled_atTIMESTAMPTZWhen task should run
started_atTIMESTAMPTZWhen task started processing
completed_atTIMESTAMPTZWhen task completed
heartbeat_atTIMESTAMPTZLast heartbeat update
worker_idVARCHAR(255)ID of worker processing task
retry_countINTEGERNumber of retries after worker death
max_retriesINTEGERMaximum retry attempts
created_atTIMESTAMPTZCreation timestamp
updated_atTIMESTAMPTZLast update timestamp

Indexes

sql
-- Partial index for pending task queries (most common query)
CREATE INDEX idx_agent_tasks_status_scheduled_priority
ON agent_tasks(status, scheduled_at, priority DESC)
WHERE status = 'pending';

-- For heartbeat-based staleness detection
CREATE INDEX idx_agent_tasks_heartbeat
ON agent_tasks(heartbeat_at)
WHERE status = 'running';

step_runs

Stores step execution results for crash recovery and memoization.

sql
CREATE TABLE step_runs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  task_id UUID NOT NULL REFERENCES agent_tasks(id) ON DELETE CASCADE,
  step_key VARCHAR(255) NOT NULL,
  status VARCHAR(50) NOT NULL DEFAULT 'pending',
  input JSONB,
  output JSONB,
  error JSONB,
  compensation_fn TEXT,
  compensated_at TIMESTAMPTZ,
  started_at TIMESTAMPTZ,
  completed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  attempt INTEGER DEFAULT 0,
  UNIQUE(task_id, step_key)
);

Columns

ColumnTypeDescription
idUUIDPrimary key
task_idUUIDForeign key to agent_tasks
step_keyVARCHAR(255)Unique step identifier within the workflow
statusVARCHAR(50)Step status (pending/running/completed/failed)
inputJSONBStep input data
outputJSONBStep output (for memoization)
errorJSONBError details if failed
compensation_fnTEXTCompensation function name (for saga)
compensated_atTIMESTAMPTZWhen compensation executed
started_atTIMESTAMPTZWhen step started
completed_atTIMESTAMPTZWhen step completed
attemptINTEGERCurrent retry attempt number

Indexes

sql
-- For memoization lookup
CREATE UNIQUE INDEX idx_step_runs_task_step
ON step_runs(task_id, step_key);

-- For rollback queries
CREATE INDEX idx_step_runs_compensation
ON step_runs(task_id)
WHERE compensation_fn IS NOT NULL;

dead_letter_queue

Stores failed compensation records for manual intervention.

sql
CREATE TABLE dead_letter_queue (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  task_id UUID NOT NULL,
  step_id UUID NOT NULL,
  error JSONB,
  retry_count INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Columns

ColumnTypeDescription
idUUIDPrimary key
task_idUUIDReference to failed task
step_idUUIDReference to step whose compensation failed
errorJSONBFull error details
retry_countINTEGERNumber of retry attempts
created_atTIMESTAMPTZWhen DLQ entry created

Indexes

sql
-- For task-based DLQ queries
CREATE INDEX idx_dlq_task_id ON dead_letter_queue(task_id);

Task Lifecycle & Data Flow

Creating a Task

typescript
// Via gRPC
await client.submitTask({
  workflowName: "order-processing",
  input: JSON.stringify({ orderId: "123" }),
});

// Inserted into agent_tasks:
// { status: "pending", input: {...}, ... }

Processing a Task

typescript
// 1. Worker claims task (SKIP LOCKED)
UPDATE agent_tasks
SET status = "running", worker_id = "worker-1", heartbeat_at = NOW()
WHERE id = 'task-id' AND status = 'pending';

// 2. For each step:
INSERT INTO step_runs (task_id, step_key, status)
VALUES ('task-id', 'validate-order', 'running');

UPDATE step_runs
SET status = "completed", output = {...}, completed_at = NOW()
WHERE id = 'step-id';

Task Completion

typescript
// Success
UPDATE agent_tasks
SET status = "completed", output = {...}, completed_at = NOW()
WHERE id = 'task-id';

// Failure
UPDATE agent_tasks
SET status = "failed", error = {...}, completed_at = NOW()
WHERE id = 'task-id';

Saga Rollback

typescript
// Find completed steps with compensation
SELECT * FROM step_runs
WHERE task_id = 'task-id'
AND status = 'completed'
AND compensation_fn IS NOT NULL
ORDER BY completed_at DESC;  -- LIFO

// Execute compensations...

// Mark as compensated
UPDATE step_runs
SET compensated_at = NOW()
WHERE id = 'step-id';

// Final status
UPDATE agent_tasks
SET status = "rolled_back"  -- or "partial_rollback" if any failed
WHERE id = 'task-id';

// Failed compensations go to DLQ
INSERT INTO dead_letter_queue (task_id, step_id, error)
VALUES ('task-id', 'step-id', {...});

Query Examples

Find pending tasks

sql
SELECT * FROM agent_tasks
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 10;

Find stale running tasks

sql
SELECT * FROM agent_tasks
WHERE status = 'running'
AND heartbeat_at < NOW() - INTERVAL '30 seconds';

Check step memoization

sql
SELECT * FROM step_runs
WHERE task_id = 'task-id'
AND step_key = 'validate-order'
AND status = 'completed';

Find failed compensations

sql
SELECT dlq.*, at.workflow_name
FROM dead_letter_queue dlq
JOIN agent_tasks at ON dlq.task_id = at.id
ORDER BY dlq.created_at DESC;

Migrations

Run migrations with:

bash
cd apps/engine
npx tsx src/db/migrate.ts

The migration script creates all tables and indexes automatically.

Released under the MIT License.