WayToClawEarn
Intermediate30 min readMay 29, 2026

PostgreSQL persistence workflow practice: build a task orchestration system in 30 minutes

Use PostgreSQL to replace Temporal/Airflow and build a lightweight persistence workflow engine in 30 minutes

WayToClawEarn EditorialPublished May 29, 2026Updated Aug 8, 2026

Editorial review of public sources · AI-assisted drafting. How we work

Tutorial Objectives

In 30 minutes, build a lightweight persistence workflow engine using PostgreSQL. There is no need to deploy additional heavy-duty orchestration systems such as Temporal and Airflow - PostgreSQL itself is the orchestrator.

What will you learn?

  • Implement workflow state persistence using PostgreSQL tables
  • Use SELECT ... FOR UPDATE SKIP LOCKED to implement multi-Worker task distribution
  • Implement breakpoint continuation and automatic recovery of Worker crashes
  • Use SQL query to directly obtain the workflow running status, without the need for a dedicated Dashboard

Preparation list

  • PostgreSQL 14+ (local installation, or use Neon / Supabase cloud service, free version is sufficient)
  • Python 3.9+ (the example uses psycopg2, replace it with Node.js/Go for the same reason)
  • A task scenario that requires reliable asynchronous execution

Overall architecture

Traditional workflow engines (Temporal, Airflow, AWS Step Functions) rely on the two-layer architecture of "central orchestrator + worker pool". The orchestrator is responsible for receiving tasks, allocating workers, recording status, and handling failover. This architecture is powerful, but has high operation and maintenance costs—it requires additional deployment and maintenance of orchestrator clusters.

The PostgreSQL solution "sinks" the orchestration logic to the database layer: the application server no longer communicates through an intermediary, but directly reads and writes Postgres tables to complete task retrieval, execution and status updates.

ComponentsTraditional ScenarioPostgreSQL Scenario
Task queueRedis/RabbitMQPostgres table + SKIP LOCKED
State storageOrchestrator internal KV storagePostgres row-level data
Worker discoveryOrchestrator proactive allocationWorker polling + row lock contention
Failure recoveryOrchestrator heartbeat detection → reallocationWorker read checkpoint → skip completed steps
ObservabilityDedicated Web DashboardSQL queries to integrate with existing monitoring tools

Database-driven workflow engine architecture comparison

Step 1: Create workflow status table (5 minutes)

Design two core tables: workflows records the global status of the workflow instance, and workflow_steps records the execution history of each step - this table is your checkpoint storage.

sql
--
CREATE TABLE workflows (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    workflow_type VARCHAR(100) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    input_data JSONB,
    current_step INTEGER DEFAULT 0,
    max_retries INTEGER DEFAULT 3,
    error_message TEXT,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- ()
CREATE TABLE workflow_steps (
    id SERIAL PRIMARY KEY,
    workflow_id UUID REFERENCES workflows(id),
    step_number INTEGER NOT NULL,
    step_name VARCHAR(200),
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    output_data JSONB,
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    error_message TEXT,
    UNIQUE(workflow_id, step_number)
);

-- Worker
CREATE INDEX idx_workflows_status ON workflows(status, created_at);
CREATE INDEX idx_workflow_steps_lookup ON workflow_steps(workflow_id, step_number);

****UNIQUE(workflow_id, step_number) 。 Worker ,PostgreSQL (),。

2 Worker (10 )

SQLSELECT ... FOR UPDATE SKIP LOCKED

  1. pending
  2. Worker (SKIP LOCKED
  3. Worker
python
import psycopg2
import json

def dequeue_workflow(conn):
 """ Postgres """
    with conn.cursor() as cur:
        cur.execute("""
            SELECT id, workflow_type, input_data, current_step
            FROM workflows
            WHERE status = 'pending'
            ORDER BY created_at
            LIMIT 1
            FOR UPDATE SKIP LOCKED
        """)
        row = cur.fetchone()
        if row:
            cur.execute(
                "UPDATE workflows SET status = 'running', updated_at = now() WHERE id = %s",
                (row[0],)
            )
            conn.commit()
            return {
                "id": row[0],
                "type": row[1],
                "input": row[2],
                "step": row[3]
            }
    conn.rollback()
    return None

PostgreSQL row lock and SKIP LOCKED mechanism description

SKIP LOCKED PostgreSQL 9.5 ,—— Redis、 RabbitMQ、。「PostgreSQL 」。

3 (10 )

workflow_steps 。 Worker , Worker ,,。

python
def execute_step(conn, workflow_id, step_number, step_fn, step_name=""):
 ""","""

# ()
    with conn.cursor() as cur:
        cur.execute(
            """SELECT status, output_data FROM workflow_steps
               WHERE workflow_id = %s AND step_number = %s""",
            (workflow_id, step_number)
        )
        existing = cur.fetchone()
        if existing and existing[0] == 'completed':
 return existing[1] # ,

# (ON CONFLICT DO NOTHING )
    with conn.cursor() as cur:
        cur.execute(
            """INSERT INTO workflow_steps (workflow_id, step_number, step_name, status, started_at)
               VALUES (%s, %s, %s, 'running', now())
               ON CONFLICT (workflow_id, step_number) DO NOTHING""",
            (workflow_id, step_number, step_name)
        )
        conn.commit()

    try:
        result = step_fn()
        with conn.cursor() as cur:
            cur.execute(
                """UPDATE workflow_steps
                   SET status = 'completed', output_data = %s, completed_at = now()
                   WHERE workflow_id = %s AND step_number = %s""",
                (json.dumps(result), workflow_id, step_number)
            )
            cur.execute(
                "UPDATE workflows SET current_step = %s, updated_at = now() WHERE id = %s",
                (step_number, workflow_id)
            )
        conn.commit()
        return result
    except Exception as e:
        with conn.cursor() as cur:
            cur.execute(
                """UPDATE workflow_steps
                   SET status = 'failed', error_message = %s, completed_at = now()
                   WHERE workflow_id = %s AND step_number = %s""",
                (str(e), workflow_id, step_number)
            )
        conn.commit()
        raise
PostgreSQL
WorkerWorker →
WorkerSKIP LOCKED + UNIQUE
Python timeout, failed
WorkerWorker , Postgres

4 SQL (5 )

Postgres , SQL Dashboard—— Prometheus、 Grafana。

sql
-- 24
SELECT
    workflow_type,
    COUNT(*) AS total,
    COUNT(*) FILTER (WHERE status = 'completed') AS completed,
    ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'completed') / COUNT(*), 1) AS success_pct
FROM workflows
WHERE created_at > now() - INTERVAL '24 hours'
GROUP BY workflow_type;

-- 5 ( Worker )
SELECT id, workflow_type, current_step, updated_at
FROM workflows
WHERE status = 'running'
  AND updated_at < now() - INTERVAL '5 minutes';

-- ,
SELECT
    w.workflow_type,
    ws.step_name,
    ROUND(AVG(EXTRACT(EPOCH FROM (ws.completed_at - ws.started_at)))::numeric, 2) AS avg_seconds
FROM workflow_steps ws
JOIN workflows w ON w.id = ws.workflow_id
WHERE ws.status = 'completed'
GROUP BY w.workflow_type, ws.step_name
ORDER BY avg_seconds DESC;

SQL ( SELECT ... WHERE status = 'running' AND updated_at < now() - INTERVAL '5 minutes',)。

(FAQ)

Q1 n8n / Temporal , PostgreSQL?

n8n API ,Temporal 。PostgreSQL 「 Postgres,」。 SaaS , Postgres,。

Q2 Postgres ?

8 vCPU PostgreSQL 。, CockroachDB( PostgreSQL ), Citus ——。

Q3 Redis ,?

。PostgreSQL 「」, Redis 。 fire-and-forget(),Redis 。 5 、、—— PostgreSQL 。

PostgreSQL()、Neon(Serverless Postgres,)、Supabase( Postgres + API )、n8n()、DBOS (an enterprise-level framework based on the ideas of this article, providing SDK packaging).

Internal link guidance

Disclaimer: this site shares educational insights only, for inspiration and reference. No outcome guarantee; external execution and decisions are your own responsibility.

Related tutorials