| name | pg-durable-sql |
| description | Generate correct pg_durable SQL code for durable function workflows. USE WHEN: writing pg_durable DSL, creating durable functions, using df.start(), composing ~> |=> & | ?> !> @> operators, building ETL pipelines, loops, parallel joins, race conditions, conditional branching, HTTP requests, signals, cron scheduling, or variable substitution in pg_durable. DO NOT USE FOR: general PostgreSQL queries unrelated to pg_durable, Rust extension development, or duroxide internals. |
pg_durable SQL Generation
Generate correct, idiomatic pg_durable durable function SQL using the df.* schema functions and operators.
Critical Rules
- All DSL expressions are TEXT. Operators and functions return JSON-encoded TEXT strings representing a function graph. Only
df.start() actually executes anything.
- SQL strings are auto-wrapped. Plain SQL strings like
'SELECT 1' are automatically converted to SQL nodes — you do NOT need df.sql().
- Single-quote escaping. Each DSL node is itself a single-quoted SQL string, so any single quotes inside it must be doubled. To filter
status = 'pending', write the whole node as 'SELECT * FROM orders WHERE status = ''pending''' (note the doubled quotes around pending and the closing ''').
- Operators are SQL-level custom operators. They work on
TEXT operands. Parentheses control grouping.
df.setvar() must be called BEFORE df.start(). Variables are captured at start time and are immutable during execution.
- Two variable syntaxes:
{varname} for durable function variables (from df.setvar), $name for result captures (from |=>). Do NOT mix them up.
Operators — Complete Reference
| Operator | Name | What It Does | Example |
|---|
~> | Sequence | Run left, then right | 'SELECT 1' ~> 'SELECT 2' |
|=> | Name/Capture | Capture result as named variable | 'SELECT id FROM t' |=> 'row_id' |
& | Join | Run both in parallel, wait for ALL | 'SELECT 1' & 'SELECT 2' |
| | Race | Run both in parallel, FIRST wins | 'fast' | df.sleep(30) |
?> | If-Then | Conditional then branch | 'SELECT true' ?> 'then SQL' |
!> | Else | Conditional else branch | 'cond' ?> 'then' !> 'else' |
@> | Loop | Infinite loop (prefix operator) | @> ('body' ~> df.sleep(60)) |
Operator Precedence and Grouping
~> chains left to right: 'A' ~> 'B' ~> 'C' means A then B then C
& groups parallel branches: 'A' & 'B' & 'C' runs all three concurrently
?> and !> combine for if/then/else: condition ?> then_branch !> else_branch
@> is a PREFIX operator — it goes BEFORE the loop body: @> (body)
- Use parentheses to nest:
('A' & 'B') ~> 'C' means run A and B in parallel, then C
Functions — Complete Reference
Node Creation
df.sql(query TEXT) → TEXT
df.sleep(seconds INT) → TEXT
df.wait_for_schedule(cron_expr TEXT) → TEXT
df.http(
url TEXT,
method TEXT DEFAULT 'POST',
body TEXT DEFAULT NULL,
headers JSONB DEFAULT NULL,
timeout_seconds INT DEFAULT 30
) → TEXT
df.wait_for_signal(
name TEXT,
timeout_seconds INT DEFAULT NULL
) → TEXT
Control Flow
df.seq(a TEXT, b TEXT) → TEXT
df.as(fut TEXT, name TEXT) → TEXT
df.join(a TEXT, b TEXT) → TEXT
df.join3(a TEXT, b TEXT, c TEXT) → TEXT
df.race(a TEXT, b TEXT) → TEXT
df.if(condition TEXT, then_branch TEXT, else_branch TEXT) → TEXT
df.if_rows(result_name TEXT, then_branch TEXT, else_branch TEXT) → TEXT
df.loop(body TEXT) → TEXT
df.loop(body TEXT, condition TEXT) → TEXT
df.break() → TEXT
df.break(value TEXT) → TEXT
Execution & Management
df.start(
fut TEXT,
label TEXT DEFAULT NULL,
database TEXT DEFAULT NULL,
transaction_mode TEXT DEFAULT 'caller'
) → TEXT
df.start('INSERT INTO audit ...', 'audit', transaction_mode => 'new');
df.cancel(instance_id TEXT, reason TEXT DEFAULT 'Cancelled by user') → TEXT
df.signal(
instance_id TEXT,
signal_name TEXT,
signal_data TEXT
) → TEXT
Use a JSON object workflow expects structured fields; use plain text simple opaque values.
df.status(instance_id TEXT) → TEXT
df.result(instance_id TEXT) → TEXT
df.explain(input TEXT) → TEXT
Durable Function Variables
df.setvar(name TEXT, value TEXT) → TEXT
df.getvar(name TEXT) → TEXT
df.unsetvar(name TEXT) → TEXT
df.clearvars() → TEXT
Monitoring
df.list_instances(status_filter TEXT DEFAULT NULL, limit_count INT DEFAULT 100)
df.instance_info(instance_id TEXT)
df.instance_nodes(instance_id TEXT)
df.instance_executions(instance_id TEXT, limit_count INT DEFAULT 5)
df.metrics()
Variable Substitution
There are TWO separate variable systems. Do not confuse them.
1. Result Variables: $name (from |=>)
Capture a step's result and use it later in the same workflow:
SELECT df.start(
'SELECT id FROM users WHERE active LIMIT 1' |=> 'user_id'
~> 'UPDATE users SET last_seen = now() WHERE id = $user_id'
);
- Set by:
|=> operator or df.as() function
- Syntax in SQL:
$name
- Scope: Within the running durable function instance
- Values: Auto-quoted strings, JSON objects accessible with
$var::jsonb
2. Durable Function Variables: {name} (from df.setvar())
Pre-configured values captured when df.start() is called:
SELECT df.setvar('api_url', 'https://api.example.com');
SELECT df.setvar('api_key', 'secret123');
SELECT df.start(
df.http('{api_url}/data', 'GET', NULL, '{"Authorization": "Bearer {api_key}"}'::jsonb)
);
- Set by:
df.setvar() BEFORE df.start()
- Syntax in SQL:
{name}
- Captured: Snapshot taken at
df.start() time
- Immutable: Cannot be changed during execution
3. System Variables: {sys_*}
Automatically available during execution:
{sys_instance_id} — Current instance ID (8-char hex)
{sys_label} — Instance label (if provided to df.start())
Condition Evaluation (Truthiness)
Used by: ?>, !>, df.if(), df.loop(body, condition)
The first column of the first row is evaluated:
| Type | Truthy | Falsy |
|---|
| Boolean | true, t | false, f |
| Number | Any non-zero | 0, 0.0 |
| String | 'true', 't', 'yes', non-zero numeric strings, and any other non-empty string (e.g. 'hello') | 'false', 'f', 'no', '0', '' (empty/whitespace) |
| Array | Non-empty [1,2] | Empty [] |
| Object | Non-empty {"a":1} | Empty {} |
| NULL | — | Always falsy |
Best practice: Use explicit boolean expressions:
'SELECT COUNT(*) > 0 FROM pending_tasks'
'SELECT EXISTS(SELECT 1 FROM orders WHERE status = ''pending'')'
'SELECT COUNT(*) FROM pending_tasks'
Common Patterns
Sequential ETL Pipeline
SELECT df.start(
'DELETE FROM target WHERE loaded_at < now() - interval ''7 days'''
~> 'UPDATE staging SET processed_at = now() WHERE processed_at IS NULL'
~> 'INSERT INTO target (data) SELECT data FROM staging WHERE processed_at IS NOT NULL',
'etl-pipeline'
);
Variable Capture and Reuse
SELECT df.start(
'SELECT id FROM orders WHERE status = ''pending'' LIMIT 1' |=> 'order_id'
~> 'UPDATE orders SET status = ''processing'' WHERE id = $order_id'
~> df.sleep(2)
~> 'UPDATE orders SET status = ''completed'' WHERE id = $order_id',
'process-order'
);
Parallel Fan-Out / Fan-In
SELECT df.start(
('SELECT COUNT(*) FROM users' & 'SELECT COUNT(*) FROM orders')
~> 'INSERT INTO logs (msg) VALUES (''Counts collected'')',
'parallel-counts'
);
SELECT df.start(
df.join3(
'SELECT COUNT(*) FROM users',
'SELECT COUNT(*) FROM orders',
'SELECT COUNT(*) FROM products'
),
'three-way-count'
);
Race with Timeout
SELECT df.start(
df.race(
'SELECT slow_query()',
df.sleep(30) ~> 'SELECT ''timeout'' AS result'
),
'query-with-timeout'
);
Conditional Branching
SELECT df.start(
'SELECT COUNT(*) > 10 FROM task_queue WHERE status = ''pending'''
?> 'INSERT INTO logs (msg) VALUES (''High load!'')'
!> 'INSERT INTO logs (msg) VALUES (''Normal load'')',
'load-check'
);
SELECT df.start(
df.if(
'SELECT EXISTS(SELECT 1 FROM orders WHERE status = ''pending'')',
'UPDATE orders SET status = ''processing'' WHERE status = ''pending''',
'INSERT INTO logs (msg) VALUES (''Nothing to process'')'
),
'conditional-processing'
);
Infinite Loop with Sleep
SELECT df.start(
@> (
'INSERT INTO heartbeats (ts) VALUES (now())'
~> df.sleep(30)
),
'heartbeat'
);
While-Loop with Break
SELECT df.start(
df.loop(
'UPDATE counter SET val = val + 1'
~> df.if(
'SELECT val >= 10 FROM counter',
df.break('{"done": true}'),
'SELECT ''continuing'''
)
),
'counted-loop'
);
Cron Scheduled Job
SELECT df.start(
@> (
'DELETE FROM logs WHERE created_at < now() - interval ''30 days'''
~> df.wait_for_schedule('0 0 * * *')
),
'daily-cleanup'
);
HTTP Request with Variable Substitution
SELECT df.setvar('webhook_url', 'https://hooks.example.com/notify');
SELECT df.start(
'SELECT id, status FROM orders WHERE id = 1' |=> 'order'
~> df.http(
'{webhook_url}',
'POST',
'{"order": $order}'
),
'order-webhook'
);
Signal-Based Approval Workflow
SELECT df.start(
'INSERT INTO logs (msg) VALUES (''Requesting approval'')'
~> df.wait_for_signal('approval', 3600) |=> 'decision'
~> df.if(
'SELECT ($decision::jsonb->>''approved'')::boolean',
'UPDATE orders SET status = ''approved''',
'UPDATE orders SET status = ''rejected'''
),
'approval-flow'
);
Multi-Database Execution
SELECT df.start(
'INSERT INTO reports (date, total) SELECT now(), count(*) FROM events',
'analytics-report',
'analytics'
);
SELECT df.start('SELECT 1', database => 'other_db');
Common Mistakes to Avoid
-
Forgetting to double single quotes inside SQL strings:
'SELECT ''pending'''
'SELECT 'pending''
-
Using {var} when you mean $var (or vice versa):
-
Calling df.setvar() inside a running workflow:
SELECT df.start('SELECT 1' ~> df.sql('SELECT df.setvar(''x'', ''y'')'));
SELECT df.setvar('x', 'y');
SELECT df.start('SELECT {x}');
-
Forgetting @> is a PREFIX operator:
'body' @> df.sleep(60)
@> ('body' ~> df.sleep(60))
-
Not wrapping parallel branches in parentheses before sequencing:
'A' & 'B' ~
( )