{"id":50791782,"url":"https://github.com/thoven87/strand","last_synced_at":"2026-06-12T11:32:06.955Z","repository":{"id":355729786,"uuid":"1226473467","full_name":"thoven87/strand","owner":"thoven87","description":"Postgres-native durable workflow engine for Swift 6.3.**","archived":false,"fork":false,"pushed_at":"2026-06-07T03:21:57.000Z","size":2319,"stargazers_count":16,"open_issues_count":11,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-07T05:09:20.439Z","etag":null,"topics":["background-jobs","durable-execution","durable-workflows","orchestration","postgres","scheduler","swift-on-server","workflow-engine","workflow-orchestration","workflows"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thoven87.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-01T12:52:42.000Z","updated_at":"2026-05-30T16:45:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thoven87/strand","commit_stats":null,"previous_names":["thoven87/strand"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/thoven87/strand","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoven87%2Fstrand","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoven87%2Fstrand/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoven87%2Fstrand/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoven87%2Fstrand/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thoven87","download_url":"https://codeload.github.com/thoven87/strand/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoven87%2Fstrand/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34243051,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["background-jobs","durable-execution","durable-workflows","orchestration","postgres","scheduler","swift-on-server","workflow-engine","workflow-orchestration","workflows"],"created_at":"2026-06-12T11:32:06.853Z","updated_at":"2026-06-12T11:32:06.945Z","avatar_url":"https://github.com/thoven87.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"logo.svg\" width=\"128\" alt=\"Strand\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eStrand\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\u003cstrong\u003ePostgres-native durable workflow engine for Swift 6.3.\u003c/strong\u003e\u003c/p\u003e\n\nNo separate coordination service. No Redis. No Cassandra. Just Swift workers and Postgres.\n\n```swift\n@Workflow\nstruct OrderWorkflow {\n    struct Input:  Codable, Sendable { let amount: Int; let orderID: String }\n    struct Output: Codable, Sendable { let trackingNumber: String }\n\n    mutating func run(\n        context: WorkflowContext\u003cSelf\u003e,\n        input: Input\n    ) async throws -\u003e Output {\n        let charge = try await context.runActivity(\n            ChargeCardActivity.self,\n            input: .init(amount: input.amount)\n        )\n        return try await context.runActivity(\n            ShipOrderActivity.self,\n            input: .init(paymentID: charge.paymentID)\n        )\n    }\n}\n```\n\nIf 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.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"loom/screenshot.png\" alt=\"Strand dashboard\" width=\"900\"\u003e\n\u003c/p\u003e\n\n## Documentation\n\n- **[Getting started](Sources/Strand/Strand.docc/GettingStarted.md)** — installation, first workflow, first worker\n- **[Core concepts](Sources/Strand/Strand.docc/Concepts.md)** — execution model, checkpointing, determinism\n- **[Activities](Sources/Strand/Strand.docc/Activities.md)** — I/O, retries, heartbeats, timeouts\n- **[Workflows](Sources/Strand/Strand.docc/Workflows.md)** — orchestration, fan-out, sleep, signals\n- **[Scheduling](Sources/Strand/Strand.docc/Scheduling.md)** — cron, intervals, catch-up\n- **[Signals and events](Sources/Strand/Strand.docc/SignalsAndEvents.md)** — external wakeup\n- **[Retry strategies](Sources/Strand/Strand.docc/RetryStrategies.md)** — backoff, non-retryable errors, deadlines\n- **[Testing](Sources/Strand/Strand.docc/TestingWorkflows.md)** — integration test helpers\n- **[Data pipelines](Sources/Strand/Strand.docc/BuildingDataPipelines.md)** — fan-out, multi-queue, crash recovery\n\n## Quick start\n\n### 1. Start Postgres\n\n```yaml\n# docker-compose.yml\nservices:\n  db:\n    image: postgres:18-alpine\n    environment:\n      POSTGRES_USER: strand\n      POSTGRES_PASSWORD: strand\n      POSTGRES_DB: strand_dev\n    ports:\n      - \"5499:5432\"\n```\n\n```bash\ndocker compose up -d\npsql \"postgresql://strand:strand@localhost:5499/strand_dev\" -f strand.sql\n```\n\n### 2. Add to `Package.swift`\n\n```swift\n.package(url: \"https://github.com/thoven87/strand\", from: \"0.1.0\"),\n```\n\n### 3. Run\n\n```swift\nimport Logging\nimport PostgresNIO\nimport ServiceLifecycle\nimport Strand\n\nvar logger = Logger(label: \"my-app\")\n\nlet postgres = PostgresClient(\n    configuration: .init(\n        host: \"localhost\", port: 5499,\n        username: \"strand\", password: \"strand\",\n        database: \"strand_dev\", tls: .disable\n    ),\n    backgroundLogger: logger\n)\n\nvar strand = StrandService(\n    postgres: postgres,\n    options: .init(\n        queues: [\n            .init(\n                name: \"default\",\n                workflows: [OrderWorkflow.self],\n                activities: [ChargeCardActivity(), ShipOrderActivity()]\n            )\n        ]\n    )\n)\n\n// Trigger a workflow from anywhere — returns a handle you can await:\nlet client = strand.client(queue: \"default\")\nTask {\n    let handle = try await client.startWorkflow(\n        OrderWorkflow.self,\n        input: OrderWorkflow.OrderInput(amount: 99_00, orderID: \"ord-1\")\n    )\n    let result = try await handle.result()\n    print(result)\n}\n\nlet group = ServiceGroup(\n    services: [postgres, strand],\n    gracefulShutdownSignals: [.sigterm, .sigint],\n    logger: logger\n)\ntry await group.run()\n```\n\n## Scheduling\n\nDeclare recurring schedules on `StrandService` — they are upserted to the database\nwhen the service starts:\n\n```swift\nvar strand = StrandService(\n    postgres: postgres,\n    options: .init(\n        queues: [\n            .init(\n                name: \"default\",\n                workflows: [DailyReportWorkflow.self, MarketOpenWorkflow.self]\n            )\n        ],\n        scheduler: .init()\n    )\n)\n\nstrand.addSchedule(\n    .workflow(\n        \"daily-report\",\n        pattern: .daily(offset: \"PT9H\"),          // 09:00 UTC every day\n        workflowType: DailyReportWorkflow.self,\n        input: ReportInput()\n    )\n)\nstrand.addSchedule(\n    .workflow(\n        \"market-open\",\n        pattern: .cron(\"30 8 * * 1-5\",\n                       timezone: TimeZone(identifier: \"America/New_York\")!),\n        workflowType: MarketOpenWorkflow.self,\n        input: StrandVoid()\n    )\n)\n\n// Custom timetable — fire on any calendar logic you can express in Swift\nstruct UKBankHolidayFreeSchedule: StrandTimeTable {\n    let holidays: Set\u003cDateComponents\u003e   // loaded at init time\n    var description: String { \"UK working days, 09:00 London\" }\n\n    func nextRunTime(after _: Date?, earliest: Date) -\u003e Date? {\n        var greg = Calendar(identifier: .gregorian)\n        greg.timeZone = TimeZone(identifier: \"Europe/London\")!\n        var candidate = greg.startOfDay(for: earliest)\n        while !isWorkingDay(candidate, calendar: greg) {\n            candidate = greg.date(byAdding: .day, value: 1, to: candidate)!\n        }\n        var comps = greg.dateComponents(in: greg.timeZone, from: candidate)\n        comps.hour = 9; comps.minute = 0; comps.second = 0\n        return greg.date(from: comps)\n    }\n    // ...\n}\n\nstrand.addSchedule(\n    .workflow(\n        \"daily-settlement\",\n        timetable: UKBankHolidayFreeSchedule(holidays: holidays),\n        workflowType: SettlementWorkflow.self,\n        input: StrandVoid()\n    )\n)\n\nlet group = ServiceGroup(\n    services: [postgres, strand],\n    gracefulShutdownSignals: [.sigterm, .sigint],\n    logger: logger\n)\ntry await group.run()\n```\n\nFor schedules created at runtime (e.g. from an HTTP API), call\n`client.schedule(name:pattern:workflowType:input:)` directly — it is always a\nlive database write.\n\nSee **[Scheduling](Sources/Strand/Strand.docc/Scheduling.md)** for patterns,\ncatch-up behaviour, time zones, and runtime management.\n\n## Examples\n\nSee [`Examples/`](Examples/) for complete runnable examples:\n\n| Example | What it shows |\n|---|---|\n| [`HackerNewsSummary`](Examples/Sources/HackerNewsSummary) | Multi-child fan-out with Ollama summarisation; `@ActivityContainer` |\n| [`GroundwaterPipeline`](Examples/Sources/GroundwaterPipeline) | 6.2 M-row data pipeline, parallel download, Ollama trend analysis |\n| [`CIPipeline`](Examples/Sources/CIPipeline) | DAG-style CI workflow with human-in-the-loop approval signal |\n| [`SmartBuilding`](Examples/Sources/SmartBuilding) | IoT sensor aggregation, multi-tenant, scheduled reports |\n| [`DevServer`](Examples/Sources/DevServer) | Local dev server with seeded workflows and the Loom dashboard |\n\n## Requirements\n\n- Swift 6.3+\n- PostgreSQL 17+\n- Runs on Linux and macOS — any platform supported by [SwiftNIO](https://github.com/apple/swift-nio) and [PostgresNIO](https://github.com/vapor/postgres-nio)\n\n## License\n\nApache 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoven87%2Fstrand","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthoven87%2Fstrand","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoven87%2Fstrand/lists"}