{"id":36346621,"url":"https://github.com/wavezync/durable","last_synced_at":"2026-01-11T13:17:41.117Z","repository":{"id":331340458,"uuid":"1086033006","full_name":"wavezync/durable","owner":"wavezync","description":"A durable workflow execution engine for Elixir ","archived":false,"fork":false,"pushed_at":"2026-01-03T16:23:14.000Z","size":451,"stargazers_count":73,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-01-06T15:52:22.175Z","etag":null,"topics":["ai-workflow","automation","elixir","postgresql","queue","workflow"],"latest_commit_sha":null,"homepage":"https://wavezync.com","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wavezync.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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":"2025-10-29T21:12:37.000Z","updated_at":"2026-01-06T10:17:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/wavezync/durable","commit_stats":null,"previous_names":["wavezync/durable"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/wavezync/durable","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavezync%2Fdurable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavezync%2Fdurable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavezync%2Fdurable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavezync%2Fdurable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wavezync","download_url":"https://codeload.github.com/wavezync/durable/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavezync%2Fdurable/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28304266,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-11T11:18:18.743Z","status":"ssl_error","status_checked_at":"2026-01-11T11:07:56.842Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["ai-workflow","automation","elixir","postgresql","queue","workflow"],"created_at":"2026-01-11T13:17:40.568Z","updated_at":"2026-01-11T13:17:41.111Z","avatar_url":"https://github.com/wavezync.png","language":"Elixir","readme":"# Durable\n\n[![Build Status](https://github.com/wavezync/durable/actions/workflows/ci.yml/badge.svg)](https://github.com/wavezync/durable/actions/workflows/ci.yml)\n[![Hex.pm](https://img.shields.io/hexpm/v/durable.svg)](https://hex.pm/packages/durable)\n\nA durable, resumable workflow engine for Elixir. Similar to Temporal/Inngest.\n\n## Features\n\n- **Declarative DSL** - Clean macro-based workflow definitions\n- **Resumability** - Sleep, wait for events, wait for human input\n- **Branching** - Pattern-matched conditional flow control\n- **Parallel** - Run steps concurrently with merge strategies\n- **ForEach** - Process collections with configurable concurrency\n- **Compensations** - Saga pattern with automatic rollback\n- **Cron Scheduling** - Recurring workflows with cron expressions\n- **Reliability** - Automatic retries with exponential/linear/constant backoff\n- **Persistence** - PostgreSQL-backed execution state\n\n## Installation\n\n```elixir\ndef deps do\n  [{:durable, \"~\u003e 0.0.0-alpha\"}]\nend\n```\n\n## Quick Start\n\n### 1. Create Migration\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddDurable do\n  use Ecto.Migration\n  def up, do: Durable.Migration.up()\n  def down, do: Durable.Migration.down()\nend\n```\n\n### 2. Add to Supervision Tree\n\n```elixir\nchildren = [\n  MyApp.Repo,\n  {Durable, repo: MyApp.Repo, queues: %{default: [concurrency: 10]}}\n]\n```\n\n### 3. Define \u0026 Run\n\n```elixir\ndefmodule MyApp.OrderWorkflow do\n  use Durable\n  use Durable.Context\n\n  workflow \"process_order\", timeout: hours(2) do\n    step :validate do\n      order = input().order\n      put_context(:order_id, order.id)\n      put_context(:items, order.items)\n    end\n\n    step :calculate_total do\n      total =\n        get_context(:items)\n        |\u003e Enum.map(\u0026 \u00261.price)\n        |\u003e Enum.sum()\n\n      put_context(:total, total)\n    end\n\n    step :charge_payment, retry: [max_attempts: 3, backoff: :exponential] do\n      {:ok, charge} = PaymentService.charge(get_context(:order_id), get_context(:total))\n      put_context(:charge_id, charge.id)\n    end\n\n    step :send_confirmation do\n      EmailService.send_confirmation(get_context(:order_id))\n    end\n  end\nend\n\n# Start it\n{:ok, id} = Durable.start(MyApp.OrderWorkflow, %{order: order})\n```\n\n## Examples\n\n### Approval Workflow\n\nWait for human approval with timeout fallback.\n\n```elixir\ndefmodule MyApp.ExpenseApproval do\n  use Durable\n  use Durable.Context\n  use Durable.Wait\n\n  workflow \"expense_approval\" do\n    step :request_approval do\n      result = wait_for_approval(\"manager\",\n        prompt: \"Approve $#{input().amount} expense?\",\n        timeout: days(3),\n        timeout_value: :auto_rejected\n      )\n      put_context(:decision, result)\n    end\n\n    branch on: get_context(:decision) do\n      :approved -\u003e step :process, do: Expenses.reimburse(input().employee_id, input().amount)\n      _ -\u003e step :notify_rejection, do: Mailer.send_rejection(input().employee_id)\n    end\n  end\nend\n\n# Approve externally\nDurable.provide_input(workflow_id, \"manager\", :approved)\n```\n\n### Parallel Data Fetch\n\nFetch data concurrently, then combine.\n\n```elixir\ndefmodule MyApp.DashboardBuilder do\n  use Durable\n  use Durable.Context\n\n  workflow \"build_dashboard\" do\n    parallel do\n      step :user, do: put_context(:user, Users.get(input().user_id))\n      step :orders, do: put_context(:orders, Orders.recent(input().user_id))\n      step :notifications, do: put_context(:notifs, Notifications.unread(input().user_id))\n    end\n\n    step :render do\n      Dashboard.build(get_context(:user), get_context(:orders), get_context(:notifs))\n    end\n  end\nend\n```\n\n### Batch Processing\n\nProcess items with controlled concurrency.\n\n```elixir\ndefmodule MyApp.BulkEmailer do\n  use Durable\n  use Durable.Context\n\n  workflow \"send_campaign\" do\n    step :load do\n      put_context(:recipients, Subscribers.active(input().campaign_id))\n    end\n\n    foreach :send_emails, items: :recipients, concurrency: 10, on_error: :continue do\n      step :send do\n        Mailer.send_campaign(current_item(), input().campaign_id)\n      end\n    end\n  end\nend\n```\n\n### Trip Booking (Saga)\n\nBook multiple services with automatic rollback on failure.\n\n```elixir\ndefmodule MyApp.TripBooking do\n  use Durable\n  use Durable.Context\n\n  workflow \"book_trip\" do\n    step :book_flight, compensate: :cancel_flight do\n      put_context(:flight, Flights.book(input().flight))\n    end\n\n    step :book_hotel, compensate: :cancel_hotel do\n      put_context(:hotel, Hotels.book(input().hotel))\n    end\n\n    step :charge do\n      total = get_context(:flight).price + get_context(:hotel).price\n      Payments.charge(input().card, total)\n    end\n\n    compensate :cancel_flight, do: Flights.cancel(get_context(:flight).id)\n    compensate :cancel_hotel, do: Hotels.cancel(get_context(:hotel).id)\n  end\nend\n```\n\n### Scheduled Reports\n\nRun daily at 9am.\n\n```elixir\ndefmodule MyApp.DailyReport do\n  use Durable\n  use Durable.Scheduler.DSL\n  use Durable.Context\n\n  @schedule cron: \"0 9 * * *\", timezone: \"America/New_York\"\n  workflow \"daily_sales_report\" do\n    step :generate do\n      report = Reports.sales_summary(Date.utc_today())\n      put_context(:report, report)\n    end\n\n    step :distribute do\n      Mailer.send_report(get_context(:report), to: \"team@company.com\")\n      Slack.post_summary(get_context(:report), channel: \"#sales\")\n    end\n  end\nend\n\n# Register in supervision tree\n{Durable, repo: MyApp.Repo, scheduled_modules: [MyApp.DailyReport]}\n```\n\n### Delayed \u0026 Scheduled Execution\n\nSleep, schedule for specific times, and wait for events.\n\n```elixir\ndefmodule MyApp.TrialReminder do\n  use Durable\n  use Durable.Context\n  use Durable.Wait\n\n  workflow \"trial_reminder\" do\n    step :welcome do\n      Mailer.send_welcome(input().user_id)\n    end\n\n    step :wait_3_days do\n      sleep(days(3))\n    end\n\n    step :check_in do\n      Mailer.send_tips(input().user_id)\n    end\n\n    step :wait_until_trial_ends do\n      trial_end = DateTime.add(input().trial_started_at, 14, :day)\n      schedule_at(trial_end)\n    end\n\n    step :convert_or_remind do\n      if Subscriptions.active?(input().user_id) do\n        put_context(:converted, true)\n      else\n        Mailer.send_upgrade_reminder(input().user_id)\n      end\n    end\n  end\nend\n```\n\n### Event-Driven Workflow\n\nWait for external webhook events.\n\n```elixir\ndefmodule MyApp.PaymentFlow do\n  use Durable\n  use Durable.Context\n  use Durable.Wait\n\n  workflow \"payment_flow\" do\n    step :create_invoice do\n      invoice = Invoices.create(input().order_id, input().amount)\n      put_context(:invoice_id, invoice.id)\n    end\n\n    step :await_payment do\n      {event, _payload} = wait_for_any([\"payment.success\", \"payment.failed\"],\n        timeout: days(7),\n        timeout_value: {\"payment.expired\", nil}\n      )\n      put_context(:result, event)\n    end\n\n    branch on: get_context(:result) do\n      \"payment.success\" -\u003e step :fulfill, do: Orders.fulfill(input().order_id)\n      _ -\u003e step :cancel, do: Orders.cancel(input().order_id)\n    end\n  end\nend\n\n# Webhook handler sends event\nDurable.send_event(workflow_id, \"payment.success\", %{transaction_id: \"txn_123\"})\n```\n\n## Reference\n\n### Context\n\n```elixir\ninput()                       # Initial workflow input\nget_context(:key)             # Get value\nget_context(:key, default)    # With default\nput_context(:key, value)      # Set value\nappend_context(:list, item)   # Append to list\n```\n\n### Time Helpers\n\n```elixir\nseconds(30)   # 30_000 ms\nminutes(5)    # 300_000 ms\nhours(2)      # 7_200_000 ms\ndays(7)       # 604_800_000 ms\n```\n\n### API\n\n```elixir\nDurable.start(Module, input)\nDurable.start(Module, input, queue: :priority, scheduled_at: datetime)\nDurable.get_execution(id)\nDurable.list_executions(workflow: Module, status: :running)\nDurable.cancel(id, \"reason\")\nDurable.send_event(id, \"event\", payload)\nDurable.provide_input(id, \"input_name\", data)\n```\n\n## Guides\n\n- [Branching](guides/branching.md) - Conditional flow control\n- [Parallel](guides/parallel.md) - Concurrent execution\n- [ForEach](guides/foreach.md) - Collection processing\n- [Compensations](guides/compensations.md) - Saga pattern\n- [Waiting](guides/waiting.md) - Sleep, events, human input\n\n## Coming Soon\n\n- Workflow orchestration (parent/child workflows)\n- Phoenix LiveView dashboard\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavezync%2Fdurable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwavezync%2Fdurable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavezync%2Fdurable/lists"}