Postgres-native durable workflow engine for Swift 6.3.
No separate coordination service. No Redis. No Cassandra. Just Swift workers and Postgres.
@Workflow
struct OrderWorkflow {
struct Input: Codable, Sendable { let amount: Int; let orderID: String }
struct Output: Codable, Sendable { let trackingNumber: String }
mutating func run(
context: WorkflowContext<Self>,
input: Input
) async throws -> Output {
let charge = try await context.runActivity(
ChargeCardActivity.self,
input: .init(amount: input.amount)
)
return try await context.runActivity(
ShipOrderActivity.self,
input: .init(paymentID: charge.paymentID)
)
}
}If a worker crashes mid-workflow the next worker that picks it up resumes from the last checkpoint. No work is duplicated, no state is lost.
- Getting started — installation, first workflow, first worker
- Core concepts — execution model, checkpointing, determinism
- Activities — I/O, retries, heartbeats, timeouts
- Workflows — orchestration, fan-out, sleep, signals
- Scheduling — cron, intervals, catch-up
- Signals and events — external wakeup
- Retry strategies — backoff, non-retryable errors, deadlines
- Testing — integration test helpers
- Data pipelines — fan-out, multi-queue, crash recovery
# docker-compose.yml
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: strand
POSTGRES_PASSWORD: strand
POSTGRES_DB: strand_dev
ports:
- "5499:5432"docker compose up -d
psql "postgresql://strand:strand@localhost:5499/strand_dev" -f strand.sql.package(url: "https://github.com/thoven87/strand", from: "0.1.0"),import Logging
import PostgresNIO
import ServiceLifecycle
import Strand
var logger = Logger(label: "my-app")
let postgres = PostgresClient(
configuration: .init(
host: "localhost", port: 5499,
username: "strand", password: "strand",
database: "strand_dev", tls: .disable
),
backgroundLogger: logger
)
var strand = StrandService(
postgres: postgres,
options: .init(
queues: [
.init(
name: "default",
workflows: [OrderWorkflow.self],
activities: [ChargeCardActivity(), ShipOrderActivity()]
)
]
)
)
// Trigger a workflow from anywhere — returns a handle you can await:
let client = strand.client(queue: "default")
Task {
let handle = try await client.startWorkflow(
OrderWorkflow.self,
input: OrderWorkflow.OrderInput(amount: 99_00, orderID: "ord-1")
)
let result = try await handle.result()
print(result)
}
let group = ServiceGroup(
services: [postgres, strand],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: logger
)
try await group.run()Declare recurring schedules on StrandService — they are upserted to the database
when the service starts:
var strand = StrandService(
postgres: postgres,
options: .init(
queues: [
.init(
name: "default",
workflows: [DailyReportWorkflow.self, MarketOpenWorkflow.self]
)
],
scheduler: .init()
)
)
strand.addSchedule(
.workflow(
"daily-report",
pattern: .daily(offset: "PT9H"), // 09:00 UTC every day
workflowType: DailyReportWorkflow.self,
input: ReportInput()
)
)
strand.addSchedule(
.workflow(
"market-open",
pattern: .cron("30 8 * * 1-5",
timezone: TimeZone(identifier: "America/New_York")!),
workflowType: MarketOpenWorkflow.self,
input: StrandVoid()
)
)
// Custom timetable — fire on any calendar logic you can express in Swift
struct UKBankHolidayFreeSchedule: StrandTimeTable {
let holidays: Set<DateComponents> // loaded at init time
var description: String { "UK working days, 09:00 London" }
func nextRunTime(after _: Date?, earliest: Date) -> Date? {
var greg = Calendar(identifier: .gregorian)
greg.timeZone = TimeZone(identifier: "Europe/London")!
var candidate = greg.startOfDay(for: earliest)
while !isWorkingDay(candidate, calendar: greg) {
candidate = greg.date(byAdding: .day, value: 1, to: candidate)!
}
var comps = greg.dateComponents(in: greg.timeZone, from: candidate)
comps.hour = 9; comps.minute = 0; comps.second = 0
return greg.date(from: comps)
}
// ...
}
strand.addSchedule(
.workflow(
"daily-settlement",
timetable: UKBankHolidayFreeSchedule(holidays: holidays),
workflowType: SettlementWorkflow.self,
input: StrandVoid()
)
)
let group = ServiceGroup(
services: [postgres, strand],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: logger
)
try await group.run()For schedules created at runtime (e.g. from an HTTP API), call
client.schedule(name:pattern:workflowType:input:) directly — it is always a
live database write.
See Scheduling for patterns, catch-up behaviour, time zones, and runtime management.
See Examples/ for complete runnable examples:
| Example | What it shows |
|---|---|
HackerNewsSummary |
Multi-child fan-out with Ollama summarisation; @ActivityContainer |
GroundwaterPipeline |
6.2 M-row data pipeline, parallel download, Ollama trend analysis |
CIPipeline |
DAG-style CI workflow with human-in-the-loop approval signal |
SmartBuilding |
IoT sensor aggregation, multi-tenant, scheduled reports |
DevServer |
Local dev server with seeded workflows and the Loom dashboard |
- Swift 6.3+
- PostgreSQL 17+
- Runs on Linux and macOS — any platform supported by SwiftNIO and PostgresNIO
Apache 2.0
