An SQL-based Durable Workflow System for PostgreSQL and SQL Server
C#
1
114 commits
updated Oct 11, 2026
SqlFlow is a simple durable execution workflow system for PostgreSQL and SQL Server. It handles scheduling, state checkpointing, and retries without needing heavy external workflow engines like Temporal or Cadence.
The SQL Scripts for creating the SqlFlow Database Schema for Postgres:
The SQL Scripts for creating the SqlFlow Database Schema for SQL Server:
sql/ssf-sqlserver-minimal.sqlsql/ssf-sqlserver-signaling.sqlWhile SqlFlow took a large deal of inspiration from Absurd, it expands significantly on the core concepts to deliver a production-ready, multi-language ecosystem. It features:
SqlFlow also comes with a Management API and a Control Panel to understand your system's health, monitor task processing, debug slow tasks, inspect event blockades, and search for specific executions.
Please note, that there's also NATS support for signaling!
Before diving into how the system operates, it helps to understand the vocabulary SqlFlow uses to manage durable execution.
Step, SqlFlow saves the result to the database. If the server crashes and the Task restarts, SqlFlow skips executing the Step again and simply loads the saved result from the database.while loop, workflows AwaitEvent. This permanently saves the state to the database, unloads the workflow from memory, and frees up the worker thread until the external system fires the expected event.At its core, SqlFlow solves the problem of long-running, brittle application logic. If a server crashes while waiting for an API call, local memory is lost. SqlFlow fixes this by separating the Storage (the source of truth) from the Signaling (the control plane).
Here is how the architecture fits together from start to finish:
When your application triggers a new workflow (spawns a Task), two things happen instantly:
"ping" over a signaling channel.This guarantees that even if the signal gets lost due to network issues, the actual work is safely stored in the database.
Historically, database-backed queues rely on infinite WHILE loops (polling) to check for new tasks, which crushes database CPU. SqlFlow avoids this entirely by suspending idle workers in memory until they receive that wake-up signal.
SqlFlow adapts this signaling layer natively to your chosen infrastructure:
LISTEN / NOTIFY mechanism. A single connection per application instance listens for NOTIFY events and instantly wakes up the local worker threads.Because workers are woken up by a generic ping, they don't blindly execute everything at once. When a worker thread wakes up, it executes a transaction to claim a batch of tasks from the database.
This creates a natural, highly efficient rate limit:
Concurrency = 5, it will claim a maximum of 5 tasks from the DB.SqlFlow handles time natively in the database. If you schedule a task for tomorrow, or if a task fails and enters an exponential backoff retry state, its next_execution_time is pushed to the future.
Workers simply ignore these rows until the time arrives. Because workers utilize a smart-polling fallback, if they are asleep, they will naturally wake up at intervals to check if any scheduled tasks have become due, ensuring reliable execution without constant database hammering.
Instead of writing complex state machines or infinite while loops, you write standard, sequential code. You simply wrap fragile operations in a Step and replace blocking waits with AwaitEvent.
Here is a conceptual example of a durable workflow:
public async Task<Result> ExecuteAsync(TaskContext ctx, Order order)
{
// 1. Durably execute a fragile API call
var payment = await ctx.Step("charge-card", async () =>
await _stripe.ChargeAsync(order.Amount));
if (!payment.Success) return new Result { Failed = true };
// 2. Safely suspend the workflow to the DB until a human approves.
// The worker thread is completely freed during this time!
var approval = await ctx.AwaitEvent<HumanApproval>("manager-approval", "wait-for-human");
if (approval.Approved)
{
// 3. Resume and execute the final step
await ctx.Step("ship-product", async () =>
await _fulfillment.ShipAsync(order.Id));
}
return new Result { Success = true };
}
Why this matters:
_stripe.ChargeAsync(), the workflow automatically restarts.AwaitEvent is called, the state is serialized to the database and the application uses zero CPU/RAM until the "manager-approval" event is fired.Choose your preferred language to see how to install the SDK and build a real-world durable AI Agent that handles long-running LLM calls and asynchronous human approvals:
A tutorial on how to send HTTP requests for the examples is available at:
An SQL-based Durable Workflow System for PostgreSQL and SQL Server
C#
1
114 commits
updated Oct 11, 2026
SqlFlow is a simple durable execution workflow system for PostgreSQL and SQL Server. It handles scheduling, state checkpointing, and retries without needing heavy external workflow engines like Temporal or Cadence.
The SQL Scripts for creating the SqlFlow Database Schema for Postgres:
The SQL Scripts for creating the SqlFlow Database Schema for SQL Server:
sql/ssf-sqlserver-minimal.sqlsql/ssf-sqlserver-signaling.sqlWhile SqlFlow took a large deal of inspiration from Absurd, it expands significantly on the core concepts to deliver a production-ready, multi-language ecosystem. It features:
SqlFlow also comes with a Management API and a Control Panel to understand your system's health, monitor task processing, debug slow tasks, inspect event blockades, and search for specific executions.
Please note, that there's also NATS support for signaling!
Before diving into how the system operates, it helps to understand the vocabulary SqlFlow uses to manage durable execution.
Step, SqlFlow saves the result to the database. If the server crashes and the Task restarts, SqlFlow skips executing the Step again and simply loads the saved result from the database.while loop, workflows AwaitEvent. This permanently saves the state to the database, unloads the workflow from memory, and frees up the worker thread until the external system fires the expected event.At its core, SqlFlow solves the problem of long-running, brittle application logic. If a server crashes while waiting for an API call, local memory is lost. SqlFlow fixes this by separating the Storage (the source of truth) from the Signaling (the control plane).
Here is how the architecture fits together from start to finish:
When your application triggers a new workflow (spawns a Task), two things happen instantly:
"ping" over a signaling channel.This guarantees that even if the signal gets lost due to network issues, the actual work is safely stored in the database.
Historically, database-backed queues rely on infinite WHILE loops (polling) to check for new tasks, which crushes database CPU. SqlFlow avoids this entirely by suspending idle workers in memory until they receive that wake-up signal.
SqlFlow adapts this signaling layer natively to your chosen infrastructure:
LISTEN / NOTIFY mechanism. A single connection per application instance listens for NOTIFY events and instantly wakes up the local worker threads.Because workers are woken up by a generic ping, they don't blindly execute everything at once. When a worker thread wakes up, it executes a transaction to claim a batch of tasks from the database.
This creates a natural, highly efficient rate limit:
Concurrency = 5, it will claim a maximum of 5 tasks from the DB.SqlFlow handles time natively in the database. If you schedule a task for tomorrow, or if a task fails and enters an exponential backoff retry state, its next_execution_time is pushed to the future.
Workers simply ignore these rows until the time arrives. Because workers utilize a smart-polling fallback, if they are asleep, they will naturally wake up at intervals to check if any scheduled tasks have become due, ensuring reliable execution without constant database hammering.
Instead of writing complex state machines or infinite while loops, you write standard, sequential code. You simply wrap fragile operations in a Step and replace blocking waits with AwaitEvent.
Here is a conceptual example of a durable workflow:
public async Task<Result> ExecuteAsync(TaskContext ctx, Order order)
{
// 1. Durably execute a fragile API call
var payment = await ctx.Step("charge-card", async () =>
await _stripe.ChargeAsync(order.Amount));
if (!payment.Success) return new Result { Failed = true };
// 2. Safely suspend the workflow to the DB until a human approves.
// The worker thread is completely freed during this time!
var approval = await ctx.AwaitEvent<HumanApproval>("manager-approval", "wait-for-human");
if (approval.Approved)
{
// 3. Resume and execute the final step
await ctx.Step("ship-product", async () =>
await _fulfillment.ShipAsync(order.Id));
}
return new Result { Success = true };
}
Why this matters:
_stripe.ChargeAsync(), the workflow automatically restarts.AwaitEvent is called, the state is serialized to the database and the application uses zero CPU/RAM until the "manager-approval" event is fired.Choose your preferred language to see how to install the SDK and build a real-world durable AI Agent that handles long-running LLM calls and asynchronous human approvals:
A tutorial on how to send HTTP requests for the examples is available at: