Jaal

Jaal vs enterprise orchestrators

Durable execution engines, workflow schedulers, process automation and integration platforms. Each product checked against its own documentation on 26 October 2026, sources below. Where we could not find a feature in a product's docs we write not documented: that means we did not find it, not that it is missing. For Jaal we say what is built and what is planned. Corrections are welcome.

From plain code to a durable, parallel system
JaalTemporalRestateInngestGolemResonateDBOSHatchetTrigger.devConductorCadenceDapr WorkflowStep FunctionsLambda durableAzure DurableCloudflare WorkflowsVercel WorkflowPrefectAirflowLangGraphRay
Score, out of 2514½13108½1289½96½108½8½10½1110½878½8½98½
Plain code: no workflow constructs to write (no activities, steps, decorators, DAGs, BPMN or JSON)✓1✗2✗3✗4✗5✗6✗7✗8✗9✗10✗11✗12✗13✗14✗15✗16✗17✗18✗19✗20✗21
Independent calls run in parallel without futures, Promise.all or parallel nodes✓✗22✗23✗24✗25✗26✗27✗28✗29✗30✗31✗32✗33✗34✗35✗36✗37✗38✗39✗40
The execution graph is derived from ordinary code (calls, if, for), not from edges or a definition you write✓✗41✗42✗43✗44✓45✗46✓47✗48✗49✗50✗51✗52✗53✓54✓55✗56✓57✓58✓59✓60
Editing the graph rewrites the code✓61✗62✗63✗64✗65✗66✗67✗68✗69✓70✗71✗72✓73✗74✗75✗76✗77✗78✗79✗80✗81
A version's identity is derived from its content, and running work stays on it✓✗82✓83✗84✓85✗86✓✓87✗88✗89✓90✓91✗92✓93✓94✗95
After a crash, it resumes from the last state: no replay of the code from the start✓96✗97✗98✗99✓100✗101✗102✗103✗104✓✗105✗106✓107✗108✗109✗110✗111✓112
Compensation built in, not a saga you hand-write✓✗113✗114✓✓✗115✗116✗117✓✗118✓
A deploy that would break the contract is refused by the platform, by name✓✓119✗120✗121
Compensation declared by the called service; the caller writes no undo code✓✗122✓123✗124✓125
A call to another service reads as a plain method call: no stubs, context clients or step wrappers✓✓126✓127✗128✓129✗130✓131✗132✗133✗134✓135✗136✗137✗138✓139✗140✓141✓142✗143✓144✗145
A streamed response cut by a crash continues where it left off: no tokens regeneratedWIP146✓147✓148✓149✓150✗151✓152✓153✓154
No data races in the parallel version, proven by a compiler before the run (not lint rules or run-time checks)WIP155✓156✗157✗158✗159✗160✗161✗162✗163✗164✓165✓166✓167✗168✗169✗170✗171✗172✗173
Non-deterministic calls (clock, random, any library) recorded automatically: no determinism rules✓✓174✗175✗176✓✗177✗178✗179✗180✗181✗182✗183✗184✓185✗186
A conversation already running finishes on the new code, with no version markers in code✓✓187✗188✓✓189✗190✗191✗192✓193✓194✗195✓196✓197✗198✓199✓200
Runtime out of the box: engine, process management and scaling ship with it; no server, worker fleet and database to assemble✗201✓202✓203✓204✓✓205✓206✓207✓208✓209✗210✗211✓✓✓✓✓212✓213✗214✓215✓216
New code swapped into running work in place, no side-by-side deployment✓✓✓217✓✓✗218✗219✗220✓✓✗221✓222✓✗223✓✓
Running work stays on the version it started on when new code is deployed✓✓✓✗224✓✓✓✓✗225✗226✓✓227✓228✓✓229✗230
Execution history is an append-only log✓✓✓✓✗231✓✓✓✓✓✓✓232
Languages you write the workflow in
Write the workflow itself in Java✓✓✓✓233✗234✓235✓✗236✗237✓238✓✓✓239✓✓✗240✗241✗242✓243✓244✓245
Write the workflow itself in Python✗246✓✓✓✗247✓✓✓✓248✓249✓✓✓250✓✓✓251✓252✓✓✓✓
Write the workflow itself in TypeScript✗253✓✓✓✓✓✓✓✓✓254✗255✓✓256✓✓✓✓✗257✗258✓✗259
Write the workflow itself in Go✗260✓✓✓✓261✓262✓✓✗263✓264✓✓✓265✓✗266✗267✗268✗269✓270✓271✗272
Write the workflow itself in Rust✗273✓✓✗274✓✓275✗276✗277✗278✓279✗280✗281✓282✗283✗284✗285✗286✗287✗288✗289✗290
Write the workflow itself in C/C++✗291✗292✗293✗294✗295✗296✗297✗298✗299✗300✗301✗302✓303✗304✗305✗306✗307✗308✗309✗310✓311
Write the workflow itself in C#/.NET✗312✓✗313✗314✗315✗316✗317✗318✗319✓320✗321✓✓322✓✓✗323✗324✗325✗326✗327✗328

✓ documented (1 point)   ✓ partly, see its note (½)   blank: not found in its docs (½)   ✗ its own docs show it is not there, see its note (0). Checked against each product's documentation on 26 October 2026. Every Jaal tick is built today; WIP and the red Jaal marks are not built yet (0).

The notes behind each grey and red mark (328)
  1. Jaal: A service extends Service, and a parallel loop takes one annotation (@Parallel) on its index.
  2. Temporal: You write deterministic Workflows plus Activities with its SDK. source
  3. Restate: You write handlers, virtual objects and workflows, wrapping work in ctx.run. source
  4. Inngest: You write steps with step.run and the other step functions. source
  5. Golem: Durability needs no wrappers, but parallel work is written with worker clients, create_promise and await_promise. source
  6. Resonate: You write generator functions that call context.run. source
  7. DBOS: You annotate workflows and steps. source
  8. Hatchet: You define tasks, DAG workflows and durable tasks. source
  9. Trigger.dev: You define tasks with task() and call triggerAndWait. source
  10. Conductor: Workflows are JSON definitions. source
  11. Cadence: You write deterministic workflows plus activities with its SDK. source
  12. Dapr Workflow: You write workflows plus activities with its SDK. source
  13. Step Functions: Workflows are JSON state machines (Amazon States Language). source
  14. Lambda durable: You write DurableContext steps, waits and invokes. source
  15. Azure Durable: You write orchestrator, activity and entity functions. source
  16. Cloudflare Workflows: You write step.do and step.sleep calls. source
  17. Vercel Workflow: You mark code with the "use workflow" and "use step" directives. source
  18. Prefect: You decorate code with @flow and @task. source
  19. Airflow: You define Python DAGs. source
  20. LangGraph: You build a graph of nodes and edges, or use @entrypoint and @task. source
  21. Ray: You write @ray.remote tasks and actors. source
  22. Temporal: Parallel work is written with futures, Promise.all or asyncio.gather. source
  23. Restate: Parallel work is written with DurableFuture.all or Select. source
  24. Inngest: Parallel steps are written with Promise.all. source
  25. Golem: Parallel work fans out to child agents explicitly. source
  26. Resonate: Parallel work is written with beginRun / beginRpc futures. source
  27. DBOS: Parallel work goes through durable queues. source
  28. Hatchet: Parallel work is declared as DAG dependencies or child tasks. source
  29. Trigger.dev: Parallel work is written with batchTriggerAndWait. source
  30. Conductor: Parallel work is a FORK_JOIN declared in the definition. source
  31. Dapr Workflow: Parallel work is written with when_all / Task.WhenAll. source
  32. Step Functions: Parallel work is a Parallel or Map state. source
  33. Lambda durable: Parallel work is written with context.parallel or context.map. source
  34. Azure Durable: Parallel work is an explicit fan-out / fan-in (Task.WhenAll). source
  35. Cloudflare Workflows: Parallel steps are written with Promise.all. source
  36. Vercel Workflow: Parallel steps are written with Promise.all. source
  37. Prefect: Parallel work is written with .submit() and task runners. source
  38. Airflow: Parallel work is declared as DAG dependencies. source
  39. LangGraph: Parallel work is declared with fan-out edges and Send. source
  40. Ray: Parallel work is written with .remote() futures. source
  41. Temporal: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  42. Restate: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  43. Inngest: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  44. Golem: Inspection is by CLI and the operation log; no graph view is documented. source
  45. Resonate: A CLI draws the call graph of a run as it grows, not of the code. source
  46. DBOS: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  47. Hatchet: Draws the edges you declare with parents=[...]; durable tasks have no graph. source
  48. Trigger.dev: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  49. Conductor: The workflow is a JSON definition; the UI draws that definition. source
  50. Cadence: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  51. Dapr Workflow: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  52. Step Functions: The workflow is an ASL definition; the console draws that definition. source
  53. Lambda durable: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  54. Azure Durable: The dashboard plots the flow of one run, not of the code. source
  55. Cloudflare Workflows: The dashboard draws the parsed workflow code (steps, Promise.all, if, loops); in beta, TypeScript only. source
  56. Vercel Workflow: Its UI shows a run's history (timeline, events or traces), not a graph of the code. source
  57. Prefect: The UI graph is built from a run; a pre-run diagram is a local dry run of one path. source
  58. Airflow: Draws the dependencies you declare between tasks (a >> b). source
  59. LangGraph: Draws the nodes and edges you declare with add_node and add_edge. source
  60. Ray: A local image of a compiled DAG you built with bind(); the dashboard has no graph. source
  61. Jaal: One graph edit is built: pick steps and make them a sequence, with undo; the code is rewritten to match.
  62. Temporal: Its UI acts on runs (signal, reset, terminate); it does not edit code. source
  63. Restate: No graph editor is documented. source
  64. Inngest: No graph editor is documented. source
  65. Golem: No graph editor is documented. source
  66. Resonate: No graph editor is documented. source
  67. DBOS: From the steps view you can only fork a run from a step. source
  68. Hatchet: No graph editor is documented. source
  69. Trigger.dev: No graph editor is documented. source
  70. Conductor: The paid Orkes builder edits the JSON definition, not code; the open-source UI only views. source
  71. Cadence: No graph editor is documented. source
  72. Dapr Workflow: No graph editor is documented. source
  73. Step Functions: Workflow Studio edits the ASL definition, not application code. source
  74. Lambda durable: The console has a code editor but no graph editor. source
  75. Azure Durable: The dashboard manages runs; it does not edit code. source
  76. Cloudflare Workflows: The diagram is read-only. source
  77. Vercel Workflow: No graph editor is documented. source
  78. Prefect: No graph editor is documented. source
  79. Airflow: The Graph view only views; DAGs are edited in code. source
  80. LangGraph: Studio runs and debugs a graph; no feature writes graph edits back into code. source
  81. Ray: No graph editor is documented. source
  82. Temporal: A version is a Build ID supplied in the worker's deployment options. source
  83. Restate: Running requests stay on their version automatically, but a version is the endpoint you deploy and register, not derived from the code. source
  84. Inngest: There are no versions: a resumed run uses the current code. source
  85. Golem: Agents stay on their revision, but the docs do not say a revision is derived from content. source
  86. Resonate: A version is a developer option, 1 by default. source
  87. Trigger.dev: Each deploy makes a version and a run never changes version, but the identity is a date and counter. source
  88. Cadence: Changes are guarded with GetVersion markers the developer numbers. source
  89. Dapr Workflow: Versioning is by patching and developer-named workflow versions. source
  90. Step Functions: Executions stay on a published version, but publishing is explicit and the number is a counter. source
  91. Lambda durable: Executions can be pinned to a published version, but you publish and invoke it yourself. source
  92. Azure Durable: Orchestration versioning uses a version string the developer sets. source
  93. Vercel Workflow: Runs are pinned to the deployment that starts them, identified by deployment ID, not by content. source
  94. Airflow: With a Git bundle a run keeps its commit; the default local bundle has no versioning. source
  95. LangGraph: There are no versions: the latest graph applies to every thread. source
  96. Jaal: A run resumes from its last recorded step; a conversation is rebuilt by re-running its settled turns, each call answered from the record.
  97. Temporal: Recovery replays the workflow code against the event history. source
  98. Restate: Recovery is replay of the handler, skipping journaled steps. source
  99. Inngest: A run resumes by replaying the handler with saved step results. source
  100. Golem: Recovery replays the oplog; opt-in snapshots shorten the replay. source
  101. Resonate: After a crash, execution starts from the top of the function. source
  102. DBOS: An interrupted workflow is restarted by calling it again with its inputs. source
  103. Hatchet: The durable event log is replayed to rebuild state. source
  104. Trigger.dev: A retried task starts from the beginning, with cached subtask results. source
  105. Cadence: Workflow code is re-executed to rebuild state. source
  106. Dapr Workflow: On replay the workflow runs again from the beginning. source
  107. Step Functions: A failed run can be redriven from the failed step, by hand. source
  108. Lambda durable: On replay the code runs from the beginning, skipping completed checkpoints. source
  109. Azure Durable: The orchestrator re-executes the whole function from the start. source
  110. Cloudflare Workflows: Code outside steps runs again after hibernation. source
  111. Vercel Workflow: Workflow code is re-run, using the event log to reach its spot. source
  112. LangGraph: Graph API nodes re-run from the start of the interrupted node; the Functional API replays from the top. source
  113. Temporal: Compensation is a documented saga pattern written in workflow code. source
  114. Restate: Sagas are implemented in user code. source
  115. Dapr Workflow: Compensation is a documented pattern written in the workflow. source
  116. Step Functions: Compensation is a pattern built from Catch fields. source
  117. Azure Durable: Compensation is written in a catch block. source
  118. Vercel Workflow: Compensation is a saga recipe written in the workflow. source
  119. Restate: Registration refuses removed or renamed handlers, but checks no input or output types, and a flag overrides it. source
  120. Dapr Workflow: Changing a workflow's name, input or output deploys and can fail at runtime. source
  121. Azure Durable: An activity signature change deploys and can break orchestrators that use it. source
  122. Restate: Sagas are written in the calling handler, adding compensations step by step. source
  123. Golem: An operation carries its own undo, but the caller must open the transaction. source
  124. Conductor: The workflow author configures a failure workflow. source
  125. Prefect: A task declares its rollback hook, but the flow must open a transaction. source
  126. Temporal: The call reads like a function, but on a handle built first with proxyActivities in the workflow. source
  127. Restate: Calls go through a typed client taken from the context: ctx.serviceClient(...).handler(...). source
  128. Inngest: Another function is called through step.invoke with an ID and options. source
  129. Golem: Methods are called directly, on a client obtained first with get(...). source
  130. Resonate: Every durable call goes through ctx.run or ctx.rpc. source
  131. DBOS: A step is called as a plain function, but it is a step in the same program, not another service. source
  132. Hatchet: A child is spawned with a generic run() on a task object. source
  133. Trigger.dev: A child is called with triggerAndWait and returns a result wrapper to unwrap. source
  134. Conductor: Tasks are referenced by registered names in a JSON definition. source
  135. Cadence: Java calls a method on a stub made with newActivityStub; Go uses ExecuteActivity(ctx, fn). source
  136. Dapr Workflow: Activities are called through the context: CallActivityAsync and its kin. source
  137. Step Functions: Task states call services through a Resource ARN in JSON. source
  138. Lambda durable: Service calls are wrapped in steps on the DurableContext. source
  139. Azure Durable: The default is CallActivityAsync(nameof(...)); typed calls come from a source generator in preview. source
  140. Cloudflare Workflows: Work is written as step.do(name, fn). source
  141. Vercel Workflow: A step is called with a plain await, but it is a step in the same program, not another service. source
  142. Prefect: A task is called like a normal function, but it is a task in the same flow, not another service. source
  143. Airflow: TaskFlow calls look like functions but build DAG dependencies behind the scenes. source
  144. LangGraph: A @task call returns a future read with .result(); the Graph API is nodes and edges. source
  145. Ray: A remote function is called with .remote() and read with ray.get. source
  146. Jaal: Work in progress: the recorded stream and its resume point are designed, not built.
  147. Temporal: Workflow Streams (preview) resume a subscriber from its offset; a retried activity streams its events again. source
  148. Inngest: The browser can reattach, but a step that fails and retries streams fresh data. source
  149. Golem: Recorded chunks replay, but an unfinished stream is resumed by asking the model to continue, so the rest is regenerated. source
  150. DBOS: Writes survive a restart, but a retried step can write its output again. source
  151. Hatchet: Events published before a consumer starts are dropped; no resume is described. source
  152. Trigger.dev: A lost connection resumes from the last chunk; a task crash or retry is not covered. source
  153. Vercel Workflow: Clients reconnect from a start index; a step crashing mid-stream is not described. source
  154. LangGraph: A dropped connection resumes with Last-Event-ID; a server crash is not covered. source
  155. Jaal: Rust's compiler checks the parallel versions Jaal derives for its test suite (38 of 39 accepted); checking every program before it runs is next.
  156. Temporal: An optional Go-only checker, workflowcheck, can help; other SDKs check at run time. source
  157. Restate: Its docs warn that calls between exclusive handlers may deadlock; avoiding it is the developer's job. source
  158. Inngest: The parallel-steps guide lists interleaving and race caveats for the developer; no tool check. source
  159. Golem: The rule is never to have two agents await each other; no check is described. source
  160. Resonate: Determinism is left to the author, with no static check. source
  161. DBOS: The only check is a run-time non-determinism error. source
  162. Trigger.dev: The docs tell developers which concurrent calls to avoid. source
  163. Cadence: Workflow code must use Cadence channels and selectors; no official checker. source
  164. Dapr Workflow: Determinism is a rule for authors, with no analyzer. source
  165. Step Functions: Parallel branches get copies of their input, so workflow state cannot race; the work in tasks is unchecked. source
  166. Lambda durable: A Go-only linter, durablelint, flags issues and can be silenced. source
  167. Azure Durable: A C#-only Roslyn analyzer warns at build time; its warnings can be suppressed. source
  168. Cloudflare Workflows: Concurrency over steps is written guidance only. source
  169. Vercel Workflow: Race losers keep running and need idempotency keys; correctness is left to testing. source
  170. Prefect: Tasks run in threads, so any global state must be thread-safe; the developer's job. source
  171. Airflow: Best practices set rules for the developer (idempotent tasks, no local files); no check. source
  172. LangGraph: Conflicting parallel writes are refused only while the graph runs. source
  173. Ray: Don't share state through global variables; wrap it in an actor. A rule, not a check. source
  174. Temporal: Only in the TypeScript sandbox are Math.random and Date made deterministic; other effects must be Activities. source
  175. Restate: Non-deterministic work must be wrapped in run(). source
  176. Inngest: Non-deterministic logic must go inside step.run(). source
  177. Resonate: Non-deterministic calls go in a leaf function run via ctx.run. source
  178. DBOS: A workflow function must be deterministic. source
  179. Hatchet: Durable-task code must be deterministic; avoid clock and random reads. source
  180. Cadence: Workflow code must use Cadence APIs for time and must be deterministic. source
  181. Dapr Workflow: Workflow code must be deterministic. source
  182. Lambda durable: Every non-deterministic call must be wrapped in a step. source
  183. Azure Durable: Orchestrator code must not use time APIs or random values directly. source
  184. Cloudflare Workflows: Non-deterministic logic must go inside step.do. source
  185. Vercel Workflow: Math.random and Date are fixed automatically; any other effect must be a step. source
  186. LangGraph: Random numbers and clock reads must be kept inside tasks. source
  187. Temporal: Running workflows continue on new code only for replay-compatible changes; otherwise patching is needed. source
  188. Restate: Requests start and end on the same version. source
  189. Golem: An update replays the agent on the new revision, but must be triggered and falls back if replay diverges. source
  190. DBOS: Only workflows whose version matches the current application are recovered. source
  191. Trigger.dev: A run never changes version. source
  192. Conductor: Running executions keep the definition snapshot taken at start. source
  193. Cadence: Continues on new code only for compatible changes; otherwise GetVersion is needed. source
  194. Dapr Workflow: Continues on new code only while it stays deterministic; otherwise patching. source
  195. Step Functions: A redriven execution uses the same definition; an update needs a new execution. source
  196. Lambda durable: Unpinned executions continue only if steps are not changed in ways that break replay. source
  197. Azure Durable: Running orchestrations replay on new code; breaking changes need version branching. source
  198. Vercel Workflow: Runs are pinned to the deployment that starts them. source
  199. Airflow: Only with the unversioned local bundle do remaining tasks use the latest code. source
  200. LangGraph: Old threads resume on the new graph, but renames break them and changes need a version stamp. source
  201. Jaal: Not yet: the host agent (process manager) and deploy are built, but scaling is ruled (10-24) and not built, and you install Postgres.
  202. Temporal: You host and scale your own workers, even on Temporal Cloud; self-hosting adds a server cluster and a database. source
  203. Restate: The server is one binary with no database, but you deploy and run your own services. source
  204. Inngest: The engine is one binary or a cloud; your function code runs in your own app or host. source
  205. Resonate: One command with SQLite for development; Postgres and your own processes in production. source
  206. DBOS: A library in your app plus Postgres; you host and scale the app, or use DBOS Cloud. source
  207. Hatchet: An engine image plus Postgres, or Hatchet Cloud; you always run the workers. source
  208. Trigger.dev: Fully managed in its own cloud; self-hosting needs Postgres, Redis and runners. source
  209. Conductor: A server plus a database and a lock service, or Orkes Cloud; your task workers are yours. source
  210. Cadence: You run frontend, history and matching services plus Cassandra, MySQL or Postgres. source
  211. Dapr Workflow: You configure an actor state store and host the app (and a control plane on Kubernetes). source
  212. Vercel Workflow: Automatic on Vercel; elsewhere you supply a storage and queue backend such as Postgres. source
  213. Prefect: A server, a database and workers you run; Prefect Cloud's managed pools come close. source
  214. Airflow: You assemble a metadata database, scheduler, API server, DAG processor and executor. source
  215. LangGraph: Fully managed on LangSmith Cloud; self-hosting needs Postgres, Redis and a license. source
  216. Ray: Ships head, workers and an autoscaler, but you provision the cluster. source
  217. Restate: Forcing an existing registration is for local development; production versions are new deployments. source
  218. DBOS: Only workflows of the current version are recovered. source
  219. Trigger.dev: A run never changes version, even after new deploys. source
  220. Conductor: Running executions keep their start-time snapshot. source
  221. Step Functions: An updated definition needs a new execution. source
  222. Lambda durable: Unpinned executions may resume on changed code, but the docs recommend pinning. source
  223. Vercel Workflow: Workflows keep running on the deployment they were created on. source
  224. Inngest: A resumed run uses the current code. source
  225. Cadence: Running workflows replay on the updated code. source
  226. Dapr Workflow: Code updates apply to running instances. source
  227. Lambda durable: Executions are pinned when you invoke a version or alias. source
  228. Azure Durable: Version settings can match work to versions; by default code changes affect running work. source
  229. Airflow: With a versioned bundle a run keeps its code; the default local bundle does not. source
  230. LangGraph: The latest graph applies to every thread. source
  231. Resonate: Its blog describes un-sequenced promises rather than a sequenced log. source
  232. LangGraph: Checkpoints accumulate per thread, but are snapshots, not described as a log. source
  233. Inngest: Only a Kotlin SDK in its GitHub org, not on the SDK list; its Java example says coming soon. source
  234. Golem: Scala is listed; Java is not. source
  235. Resonate: Labelled pre-release. source
  236. Hatchet: Not on its language list. source
  237. Trigger.dev: Not on its language list. source
  238. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  239. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  240. Cloudflare Workflows: Not on its language list. source
  241. Vercel Workflow: Not on its language list. source
  242. Prefect: Not on its language list. source
  243. Airflow: An experimental task SDK: task logic only; DAGs are always Python. source
  244. LangGraph: A community port only, not in the vendor's docs. source
  245. Ray: The Java APIs are experimental and community-supported. source
  246. Jaal: Not built yet. A Python orchestrator turns green once it compiles and runs end to end.
  247. Golem: Not on its language list. source
  248. Trigger.dev: A TypeScript task can run Python scripts; the task itself stays TypeScript. source
  249. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  250. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  251. Cloudflare Workflows: Python Workflows are in open beta. source
  252. Vercel Workflow: Python is labelled beta. source
  253. Jaal: Not built yet.
  254. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  255. Cadence: Not on its language list. source
  256. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  257. Prefect: Not on its language list. source
  258. Airflow: Not on its language list. source
  259. Ray: Not on its language list. source
  260. Jaal: Not built yet.
  261. Golem: Go is listed only in v1.6, still a release candidate. source
  262. Resonate: Labelled early release. source
  263. Trigger.dev: Not on its language list. source
  264. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  265. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  266. Azure Durable: Not on its language list. source
  267. Cloudflare Workflows: Not on its language list. source
  268. Vercel Workflow: Not on its language list. source
  269. Prefect: Not on its language list. source
  270. Airflow: An experimental task SDK: task logic only; DAGs are always Python. source
  271. LangGraph: A community port only, not in the vendor's docs. source
  272. Ray: Not on its language list. source
  273. Jaal: Not built yet.
  274. Inngest: Not on its language list. source
  275. Resonate: Labelled early development. source
  276. DBOS: Not on its language list. source
  277. Hatchet: Not on its language list. source
  278. Trigger.dev: Not on its language list. source
  279. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  280. Cadence: Not on its language list. source
  281. Dapr Workflow: Not on its language list. source
  282. Step Functions: Only as Lambda tasks; the workflow is Amazon States Language. source
  283. Lambda durable: Not on its language list. source
  284. Azure Durable: Not on its language list. source
  285. Cloudflare Workflows: Not on its language list. source
  286. Vercel Workflow: Not on its language list. source
  287. Prefect: Not on its language list. source
  288. Airflow: Not on its language list. source
  289. LangGraph: Not on its language list. source
  290. Ray: Not on its language list. source
  291. Jaal: Not built yet.
  292. Temporal: Not on its language list. source
  293. Restate: Not on its language list. source
  294. Inngest: Not on its language list. source
  295. Golem: Not on its language list. source
  296. Resonate: Not on its language list. source
  297. DBOS: Not on its language list. source
  298. Hatchet: Not on its language list. source
  299. Trigger.dev: Not on its language list. source
  300. Conductor: Not on its language list. source
  301. Cadence: Not on its language list. source
  302. Dapr Workflow: Not on its language list. source
  303. Step Functions: Only as Lambda tasks; the workflow is Amazon States Language. source
  304. Lambda durable: Not on its language list. source
  305. Azure Durable: Not on its language list. source
  306. Cloudflare Workflows: Not on its language list. source
  307. Vercel Workflow: Not on its language list. source
  308. Prefect: Not on its language list. source
  309. Airflow: Not on its language list. source
  310. LangGraph: Not on its language list. source
  311. Ray: The C++ APIs are experimental and community-supported. source
  312. Jaal: Not built yet.
  313. Restate: Not on its language list. source
  314. Inngest: Not on its language list. source
  315. Golem: Not on its language list. source
  316. Resonate: Not on its language list. source
  317. DBOS: Not on its language list. source
  318. Hatchet: Not on its language list. source
  319. Trigger.dev: Not on its language list. source
  320. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  321. Cadence: Not on its language list. source
  322. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  323. Cloudflare Workflows: Not on its language list. source
  324. Vercel Workflow: Not on its language list. source
  325. Prefect: Not on its language list. source
  326. Airflow: Not on its language list. source
  327. LangGraph: Not on its language list. source
  328. Ray: Not on its language list. source
The notes behind each grey and red mark (275)
  1. Temporal: You write deterministic Workflows plus Activities with its SDK. source
  2. Restate: You write handlers, virtual objects and workflows, wrapping work in ctx.run. source
  3. Inngest: You write steps with step.run and the other step functions. source
  4. Golem: Durability needs no wrappers, but parallel work is written with worker clients, create_promise and await_promise. source
  5. Resonate: You write generator functions that call context.run. source
  6. DBOS: You annotate workflows and steps. source
  7. Hatchet: You define tasks, DAG workflows and durable tasks. source
  8. Trigger.dev: You define tasks with task() and call triggerAndWait. source
  9. Conductor: Workflows are JSON definitions. source
  10. Cadence: You write deterministic workflows plus activities with its SDK. source
  11. Dapr Workflow: You write workflows plus activities with its SDK. source
  12. Step Functions: Workflows are JSON state machines (Amazon States Language). source
  13. Lambda durable: You write DurableContext steps, waits and invokes. source
  14. Azure Durable: You write orchestrator, activity and entity functions. source
  15. Cloudflare Workflows: You write step.do and step.sleep calls. source
  16. Vercel Workflow: You mark code with the "use workflow" and "use step" directives. source
  17. Prefect: You decorate code with @flow and @task. source
  18. Airflow: You define Python DAGs. source
  19. LangGraph: You build a graph of nodes and edges, or use @entrypoint and @task. source
  20. Ray: You write @ray.remote tasks and actors. source
  21. Temporal: Parallel work is written with futures, Promise.all or asyncio.gather. source
  22. Restate: Parallel work is written with DurableFuture.all or Select. source
  23. Inngest: Parallel steps are written with Promise.all. source
  24. Golem: Parallel work fans out to child agents explicitly. source
  25. Resonate: Parallel work is written with beginRun / beginRpc futures. source
  26. DBOS: Parallel work goes through durable queues. source
  27. Hatchet: Parallel work is declared as DAG dependencies or child tasks. source
  28. Trigger.dev: Parallel work is written with batchTriggerAndWait. source
  29. Conductor: Parallel work is a FORK_JOIN declared in the definition. source
  30. Dapr Workflow: Parallel work is written with when_all / Task.WhenAll. source
  31. Step Functions: Parallel work is a Parallel or Map state. source
  32. Lambda durable: Parallel work is written with context.parallel or context.map. source
  33. Azure Durable: Parallel work is an explicit fan-out / fan-in (Task.WhenAll). source
  34. Cloudflare Workflows: Parallel steps are written with Promise.all. source
  35. Vercel Workflow: Parallel steps are written with Promise.all. source
  36. Prefect: Parallel work is written with .submit() and task runners. source
  37. Airflow: Parallel work is declared as DAG dependencies. source
  38. LangGraph: Parallel work is declared with fan-out edges and Send. source
  39. Ray: Parallel work is written with .remote() futures. source
  40. Temporal: A version is a Build ID supplied in the worker's deployment options. source
  41. Restate: Running requests stay on their version automatically, but a version is the endpoint you deploy and register, not derived from the code. source
  42. Inngest: There are no versions: a resumed run uses the current code. source
  43. Golem: Agents stay on their revision, but the docs do not say a revision is derived from content. source
  44. Resonate: A version is a developer option, 1 by default. source
  45. Trigger.dev: Each deploy makes a version and a run never changes version, but the identity is a date and counter. source
  46. Cadence: Changes are guarded with GetVersion markers the developer numbers. source
  47. Dapr Workflow: Versioning is by patching and developer-named workflow versions. source
  48. Step Functions: Executions stay on a published version, but publishing is explicit and the number is a counter. source
  49. Lambda durable: Executions can be pinned to a published version, but you publish and invoke it yourself. source
  50. Azure Durable: Orchestration versioning uses a version string the developer sets. source
  51. Vercel Workflow: Runs are pinned to the deployment that starts them, identified by deployment ID, not by content. source
  52. Airflow: With a Git bundle a run keeps its commit; the default local bundle has no versioning. source
  53. LangGraph: There are no versions: the latest graph applies to every thread. source
  54. Temporal: Recovery replays the workflow code against the event history. source
  55. Restate: Recovery is replay of the handler, skipping journaled steps. source
  56. Inngest: A run resumes by replaying the handler with saved step results. source
  57. Golem: Recovery replays the oplog; opt-in snapshots shorten the replay. source
  58. Resonate: After a crash, execution starts from the top of the function. source
  59. DBOS: An interrupted workflow is restarted by calling it again with its inputs. source
  60. Hatchet: The durable event log is replayed to rebuild state. source
  61. Trigger.dev: A retried task starts from the beginning, with cached subtask results. source
  62. Cadence: Workflow code is re-executed to rebuild state. source
  63. Dapr Workflow: On replay the workflow runs again from the beginning. source
  64. Step Functions: A failed run can be redriven from the failed step, by hand. source
  65. Lambda durable: On replay the code runs from the beginning, skipping completed checkpoints. source
  66. Azure Durable: The orchestrator re-executes the whole function from the start. source
  67. Cloudflare Workflows: Code outside steps runs again after hibernation. source
  68. Vercel Workflow: Workflow code is re-run, using the event log to reach its spot. source
  69. LangGraph: Graph API nodes re-run from the start of the interrupted node; the Functional API replays from the top. source
  70. Temporal: Compensation is a documented saga pattern written in workflow code. source
  71. Restate: Sagas are implemented in user code. source
  72. Dapr Workflow: Compensation is a documented pattern written in the workflow. source
  73. Step Functions: Compensation is a pattern built from Catch fields. source
  74. Azure Durable: Compensation is written in a catch block. source
  75. Vercel Workflow: Compensation is a saga recipe written in the workflow. source
  76. Restate: Registration refuses removed or renamed handlers, but checks no input or output types, and a flag overrides it. source
  77. Dapr Workflow: Changing a workflow's name, input or output deploys and can fail at runtime. source
  78. Azure Durable: An activity signature change deploys and can break orchestrators that use it. source
  79. Restate: Sagas are written in the calling handler, adding compensations step by step. source
  80. Golem: An operation carries its own undo, but the caller must open the transaction. source
  81. Conductor: The workflow author configures a failure workflow. source
  82. Prefect: A task declares its rollback hook, but the flow must open a transaction. source
  83. Temporal: The call reads like a function, but on a handle built first with proxyActivities in the workflow. source
  84. Restate: Calls go through a typed client taken from the context: ctx.serviceClient(...).handler(...). source
  85. Inngest: Another function is called through step.invoke with an ID and options. source
  86. Golem: Methods are called directly, on a client obtained first with get(...). source
  87. Resonate: Every durable call goes through ctx.run or ctx.rpc. source
  88. DBOS: A step is called as a plain function, but it is a step in the same program, not another service. source
  89. Hatchet: A child is spawned with a generic run() on a task object. source
  90. Trigger.dev: A child is called with triggerAndWait and returns a result wrapper to unwrap. source
  91. Conductor: Tasks are referenced by registered names in a JSON definition. source
  92. Cadence: Java calls a method on a stub made with newActivityStub; Go uses ExecuteActivity(ctx, fn). source
  93. Dapr Workflow: Activities are called through the context: CallActivityAsync and its kin. source
  94. Step Functions: Task states call services through a Resource ARN in JSON. source
  95. Lambda durable: Service calls are wrapped in steps on the DurableContext. source
  96. Azure Durable: The default is CallActivityAsync(nameof(...)); typed calls come from a source generator in preview. source
  97. Cloudflare Workflows: Work is written as step.do(name, fn). source
  98. Vercel Workflow: A step is called with a plain await, but it is a step in the same program, not another service. source
  99. Prefect: A task is called like a normal function, but it is a task in the same flow, not another service. source
  100. Airflow: TaskFlow calls look like functions but build DAG dependencies behind the scenes. source
  101. LangGraph: A @task call returns a future read with .result(); the Graph API is nodes and edges. source
  102. Ray: A remote function is called with .remote() and read with ray.get. source
  103. Jaal: Work in progress: the recorded stream and its resume point are designed, not built.
  104. Temporal: Workflow Streams (preview) resume a subscriber from its offset; a retried activity streams its events again. source
  105. Inngest: The browser can reattach, but a step that fails and retries streams fresh data. source
  106. DBOS: Writes survive a restart, but a retried step can write its output again. source
  107. Hatchet: Events published before a consumer starts are dropped; no resume is described. source
  108. Trigger.dev: A lost connection resumes from the last chunk; a task crash or retry is not covered. source
  109. Vercel Workflow: Clients reconnect from a start index; a step crashing mid-stream is not described. source
  110. LangGraph: A dropped connection resumes with Last-Event-ID; a server crash is not covered. source
  111. Temporal: An optional Go-only checker, workflowcheck, can help; other SDKs check at run time. source
  112. Resonate: Determinism is left to the author, with no static check. source
  113. DBOS: The only check is a run-time non-determinism error. source
  114. Trigger.dev: The docs tell developers which concurrent calls to avoid. source
  115. Cadence: Workflow code must use Cadence channels and selectors; no official checker. source
  116. Dapr Workflow: Determinism is a rule for authors, with no analyzer. source
  117. Lambda durable: A Go-only linter, durablelint, flags issues and can be silenced. source
  118. Azure Durable: A C#-only Roslyn analyzer warns at build time; its warnings can be suppressed. source
  119. Cloudflare Workflows: Concurrency over steps is written guidance only. source
  120. LangGraph: Conflicting parallel writes are refused only while the graph runs. source
  121. Temporal: Only in the TypeScript sandbox are Math.random and Date made deterministic; other effects must be Activities. source
  122. Restate: Non-deterministic work must be wrapped in run(). source
  123. Inngest: Non-deterministic logic must go inside step.run(). source
  124. Resonate: Non-deterministic calls go in a leaf function run via ctx.run. source
  125. DBOS: A workflow function must be deterministic. source
  126. Hatchet: Durable-task code must be deterministic; avoid clock and random reads. source
  127. Cadence: Workflow code must use Cadence APIs for time and must be deterministic. source
  128. Dapr Workflow: Workflow code must be deterministic. source
  129. Lambda durable: Every non-deterministic call must be wrapped in a step. source
  130. Azure Durable: Orchestrator code must not use time APIs or random values directly. source
  131. Cloudflare Workflows: Non-deterministic logic must go inside step.do. source
  132. Vercel Workflow: Math.random and Date are fixed automatically; any other effect must be a step. source
  133. LangGraph: Random numbers and clock reads must be kept inside tasks. source
  134. Temporal: Running workflows continue on new code only for replay-compatible changes; otherwise patching is needed. source
  135. Restate: Requests start and end on the same version. source
  136. Golem: An update replays the agent on the new revision, but must be triggered and falls back if replay diverges. source
  137. DBOS: Only workflows whose version matches the current application are recovered. source
  138. Trigger.dev: A run never changes version. source
  139. Conductor: Running executions keep the definition snapshot taken at start. source
  140. Cadence: Continues on new code only for compatible changes; otherwise GetVersion is needed. source
  141. Dapr Workflow: Continues on new code only while it stays deterministic; otherwise patching. source
  142. Step Functions: A redriven execution uses the same definition; an update needs a new execution. source
  143. Lambda durable: Unpinned executions continue only if steps are not changed in ways that break replay. source
  144. Azure Durable: Running orchestrations replay on new code; breaking changes need version branching. source
  145. Vercel Workflow: Runs are pinned to the deployment that starts them. source
  146. Airflow: Only with the unversioned local bundle do remaining tasks use the latest code. source
  147. LangGraph: Old threads resume on the new graph, but renames break them and changes need a version stamp. source
  148. Jaal: The host agent (process manager) and deploy are built; scaling by instances is ruled (10-24) but not built; you install Postgres.
  149. Temporal: You host and scale your own workers, even on Temporal Cloud; self-hosting adds a server cluster and a database. source
  150. Restate: The server is one binary with no database, but you deploy and run your own services. source
  151. Inngest: The engine is one binary or a cloud; your function code runs in your own app or host. source
  152. Resonate: One command with SQLite for development; Postgres and your own processes in production. source
  153. DBOS: A library in your app plus Postgres; you host and scale the app, or use DBOS Cloud. source
  154. Hatchet: An engine image plus Postgres, or Hatchet Cloud; you always run the workers. source
  155. Trigger.dev: Fully managed in its own cloud; self-hosting needs Postgres, Redis and runners. source
  156. Conductor: A server plus a database and a lock service, or Orkes Cloud; your task workers are yours. source
  157. Cadence: You run frontend, history and matching services plus Cassandra, MySQL or Postgres. source
  158. Dapr Workflow: You configure an actor state store and host the app (and a control plane on Kubernetes). source
  159. Vercel Workflow: Automatic on Vercel; elsewhere you supply a storage and queue backend such as Postgres. source
  160. Prefect: A server, a database and workers you run; Prefect Cloud's managed pools come close. source
  161. Airflow: You assemble a metadata database, scheduler, API server, DAG processor and executor. source
  162. LangGraph: Fully managed on LangSmith Cloud; self-hosting needs Postgres, Redis and a license. source
  163. Ray: Ships head, workers and an autoscaler, but you provision the cluster. source
  164. Restate: Forcing an existing registration is for local development; production versions are new deployments. source
  165. DBOS: Only workflows of the current version are recovered. source
  166. Trigger.dev: A run never changes version, even after new deploys. source
  167. Conductor: Running executions keep their start-time snapshot. source
  168. Step Functions: An updated definition needs a new execution. source
  169. Lambda durable: Unpinned executions may resume on changed code, but the docs recommend pinning. source
  170. Vercel Workflow: Workflows keep running on the deployment they were created on. source
  171. Inngest: A resumed run uses the current code. source
  172. Cadence: Running workflows replay on the updated code. source
  173. Dapr Workflow: Code updates apply to running instances. source
  174. Lambda durable: Executions are pinned when you invoke a version or alias. source
  175. Azure Durable: Version settings can match work to versions; by default code changes affect running work. source
  176. Airflow: With a versioned bundle a run keeps its code; the default local bundle does not. source
  177. LangGraph: The latest graph applies to every thread. source
  178. Resonate: Its blog describes un-sequenced promises rather than a sequenced log. source
  179. LangGraph: Checkpoints accumulate per thread, but are snapshots, not described as a log. source
  180. Inngest: Only a Kotlin SDK in its GitHub org, not on the SDK list; its Java example says coming soon. source
  181. Golem: Scala is listed; Java is not. source
  182. Resonate: Labelled pre-release. source
  183. Hatchet: Not on its language list. source
  184. Trigger.dev: Not on its language list. source
  185. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  186. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  187. Cloudflare Workflows: Not on its language list. source
  188. Vercel Workflow: Not on its language list. source
  189. Prefect: Not on its language list. source
  190. Airflow: An experimental task SDK: task logic only; DAGs are always Python. source
  191. LangGraph: A community port only, not in the vendor's docs. source
  192. Ray: The Java APIs are experimental and community-supported. source
  193. Jaal: Not built yet. A Python orchestrator turns green once it compiles and runs end to end.
  194. Golem: Not on its language list. source
  195. Trigger.dev: A TypeScript task can run Python scripts; the task itself stays TypeScript. source
  196. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  197. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  198. Cloudflare Workflows: Python Workflows are in open beta. source
  199. Vercel Workflow: Python is labelled beta. source
  200. Jaal: Not built yet.
  201. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  202. Cadence: Not on its language list. source
  203. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  204. Prefect: Not on its language list. source
  205. Airflow: Not on its language list. source
  206. Ray: Not on its language list. source
  207. Jaal: Not built yet.
  208. Golem: Go is listed only in v1.6, still a release candidate. source
  209. Resonate: Labelled early release. source
  210. Trigger.dev: Not on its language list. source
  211. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  212. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  213. Azure Durable: Not on its language list. source
  214. Cloudflare Workflows: Not on its language list. source
  215. Vercel Workflow: Not on its language list. source
  216. Prefect: Not on its language list. source
  217. Airflow: An experimental task SDK: task logic only; DAGs are always Python. source
  218. LangGraph: A community port only, not in the vendor's docs. source
  219. Ray: Not on its language list. source
  220. Jaal: Not built yet.
  221. Inngest: Not on its language list. source
  222. Resonate: Labelled early development. source
  223. DBOS: Not on its language list. source
  224. Hatchet: Not on its language list. source
  225. Trigger.dev: Not on its language list. source
  226. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  227. Cadence: Not on its language list. source
  228. Dapr Workflow: Not on its language list. source
  229. Step Functions: Only as Lambda tasks; the workflow is Amazon States Language. source
  230. Lambda durable: Not on its language list. source
  231. Azure Durable: Not on its language list. source
  232. Cloudflare Workflows: Not on its language list. source
  233. Vercel Workflow: Not on its language list. source
  234. Prefect: Not on its language list. source
  235. Airflow: Not on its language list. source
  236. LangGraph: Not on its language list. source
  237. Ray: Not on its language list. source
  238. Jaal: Not built yet.
  239. Temporal: Not on its language list. source
  240. Restate: Not on its language list. source
  241. Inngest: Not on its language list. source
  242. Golem: Not on its language list. source
  243. Resonate: Not on its language list. source
  244. DBOS: Not on its language list. source
  245. Hatchet: Not on its language list. source
  246. Trigger.dev: Not on its language list. source
  247. Conductor: Not on its language list. source
  248. Cadence: Not on its language list. source
  249. Dapr Workflow: Not on its language list. source
  250. Step Functions: Only as Lambda tasks; the workflow is Amazon States Language. source
  251. Lambda durable: Not on its language list. source
  252. Azure Durable: Not on its language list. source
  253. Cloudflare Workflows: Not on its language list. source
  254. Vercel Workflow: Not on its language list. source
  255. Prefect: Not on its language list. source
  256. Airflow: Not on its language list. source
  257. LangGraph: Not on its language list. source
  258. Ray: The C++ APIs are experimental and community-supported. source
  259. Jaal: Not built yet.
  260. Restate: Not on its language list. source
  261. Inngest: Not on its language list. source
  262. Golem: Not on its language list. source
  263. Resonate: Not on its language list. source
  264. DBOS: Not on its language list. source
  265. Hatchet: Not on its language list. source
  266. Trigger.dev: Not on its language list. source
  267. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  268. Cadence: Not on its language list. source
  269. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  270. Cloudflare Workflows: Not on its language list. source
  271. Vercel Workflow: Not on its language list. source
  272. Prefect: Not on its language list. source
  273. Airflow: Not on its language list. source
  274. LangGraph: Not on its language list. source
  275. Ray: Not on its language list. source
The notes behind each grey and red mark (275)
  1. Temporal: You write deterministic Workflows plus Activities with its SDK. source
  2. Restate: You write handlers, virtual objects and workflows, wrapping work in ctx.run. source
  3. Inngest: You write steps with step.run and the other step functions. source
  4. Golem: Durability needs no wrappers, but parallel work is written with worker clients, create_promise and await_promise. source
  5. Resonate: You write generator functions that call context.run. source
  6. DBOS: You annotate workflows and steps. source
  7. Hatchet: You define tasks, DAG workflows and durable tasks. source
  8. Trigger.dev: You define tasks with task() and call triggerAndWait. source
  9. Conductor: Workflows are JSON definitions. source
  10. Cadence: You write deterministic workflows plus activities with its SDK. source
  11. Dapr Workflow: You write workflows plus activities with its SDK. source
  12. Step Functions: Workflows are JSON state machines (Amazon States Language). source
  13. Lambda durable: You write DurableContext steps, waits and invokes. source
  14. Azure Durable: You write orchestrator, activity and entity functions. source
  15. Cloudflare Workflows: You write step.do and step.sleep calls. source
  16. Vercel Workflow: You mark code with the "use workflow" and "use step" directives. source
  17. Prefect: You decorate code with @flow and @task. source
  18. Airflow: You define Python DAGs. source
  19. LangGraph: You build a graph of nodes and edges, or use @entrypoint and @task. source
  20. Ray: You write @ray.remote tasks and actors. source
  21. Temporal: Parallel work is written with futures, Promise.all or asyncio.gather. source
  22. Restate: Parallel work is written with DurableFuture.all or Select. source
  23. Inngest: Parallel steps are written with Promise.all. source
  24. Golem: Parallel work fans out to child agents explicitly. source
  25. Resonate: Parallel work is written with beginRun / beginRpc futures. source
  26. DBOS: Parallel work goes through durable queues. source
  27. Hatchet: Parallel work is declared as DAG dependencies or child tasks. source
  28. Trigger.dev: Parallel work is written with batchTriggerAndWait. source
  29. Conductor: Parallel work is a FORK_JOIN declared in the definition. source
  30. Dapr Workflow: Parallel work is written with when_all / Task.WhenAll. source
  31. Step Functions: Parallel work is a Parallel or Map state. source
  32. Lambda durable: Parallel work is written with context.parallel or context.map. source
  33. Azure Durable: Parallel work is an explicit fan-out / fan-in (Task.WhenAll). source
  34. Cloudflare Workflows: Parallel steps are written with Promise.all. source
  35. Vercel Workflow: Parallel steps are written with Promise.all. source
  36. Prefect: Parallel work is written with .submit() and task runners. source
  37. Airflow: Parallel work is declared as DAG dependencies. source
  38. LangGraph: Parallel work is declared with fan-out edges and Send. source
  39. Ray: Parallel work is written with .remote() futures. source
  40. Temporal: A version is a Build ID supplied in the worker's deployment options. source
  41. Restate: Running requests stay on their version automatically, but a version is the endpoint you deploy and register, not derived from the code. source
  42. Inngest: There are no versions: a resumed run uses the current code. source
  43. Golem: Agents stay on their revision, but the docs do not say a revision is derived from content. source
  44. Resonate: A version is a developer option, 1 by default. source
  45. Trigger.dev: Each deploy makes a version and a run never changes version, but the identity is a date and counter. source
  46. Cadence: Changes are guarded with GetVersion markers the developer numbers. source
  47. Dapr Workflow: Versioning is by patching and developer-named workflow versions. source
  48. Step Functions: Executions stay on a published version, but publishing is explicit and the number is a counter. source
  49. Lambda durable: Executions can be pinned to a published version, but you publish and invoke it yourself. source
  50. Azure Durable: Orchestration versioning uses a version string the developer sets. source
  51. Vercel Workflow: Runs are pinned to the deployment that starts them, identified by deployment ID, not by content. source
  52. Airflow: With a Git bundle a run keeps its commit; the default local bundle has no versioning. source
  53. LangGraph: There are no versions: the latest graph applies to every thread. source
  54. Temporal: Recovery replays the workflow code against the event history. source
  55. Restate: Recovery is replay of the handler, skipping journaled steps. source
  56. Inngest: A run resumes by replaying the handler with saved step results. source
  57. Golem: Recovery replays the oplog; opt-in snapshots shorten the replay. source
  58. Resonate: After a crash, execution starts from the top of the function. source
  59. DBOS: An interrupted workflow is restarted by calling it again with its inputs. source
  60. Hatchet: The durable event log is replayed to rebuild state. source
  61. Trigger.dev: A retried task starts from the beginning, with cached subtask results. source
  62. Cadence: Workflow code is re-executed to rebuild state. source
  63. Dapr Workflow: On replay the workflow runs again from the beginning. source
  64. Step Functions: A failed run can be redriven from the failed step, by hand. source
  65. Lambda durable: On replay the code runs from the beginning, skipping completed checkpoints. source
  66. Azure Durable: The orchestrator re-executes the whole function from the start. source
  67. Cloudflare Workflows: Code outside steps runs again after hibernation. source
  68. Vercel Workflow: Workflow code is re-run, using the event log to reach its spot. source
  69. LangGraph: Graph API nodes re-run from the start of the interrupted node; the Functional API replays from the top. source
  70. Temporal: Compensation is a documented saga pattern written in workflow code. source
  71. Restate: Sagas are implemented in user code. source
  72. Dapr Workflow: Compensation is a documented pattern written in the workflow. source
  73. Step Functions: Compensation is a pattern built from Catch fields. source
  74. Azure Durable: Compensation is written in a catch block. source
  75. Vercel Workflow: Compensation is a saga recipe written in the workflow. source
  76. Restate: Registration refuses removed or renamed handlers, but checks no input or output types, and a flag overrides it. source
  77. Dapr Workflow: Changing a workflow's name, input or output deploys and can fail at runtime. source
  78. Azure Durable: An activity signature change deploys and can break orchestrators that use it. source
  79. Restate: Sagas are written in the calling handler, adding compensations step by step. source
  80. Golem: An operation carries its own undo, but the caller must open the transaction. source
  81. Conductor: The workflow author configures a failure workflow. source
  82. Prefect: A task declares its rollback hook, but the flow must open a transaction. source
  83. Temporal: The call reads like a function, but on a handle built first with proxyActivities in the workflow. source
  84. Restate: Calls go through a typed client taken from the context: ctx.serviceClient(...).handler(...). source
  85. Inngest: Another function is called through step.invoke with an ID and options. source
  86. Golem: Methods are called directly, on a client obtained first with get(...). source
  87. Resonate: Every durable call goes through ctx.run or ctx.rpc. source
  88. DBOS: A step is called as a plain function, but it is a step in the same program, not another service. source
  89. Hatchet: A child is spawned with a generic run() on a task object. source
  90. Trigger.dev: A child is called with triggerAndWait and returns a result wrapper to unwrap. source
  91. Conductor: Tasks are referenced by registered names in a JSON definition. source
  92. Cadence: Java calls a method on a stub made with newActivityStub; Go uses ExecuteActivity(ctx, fn). source
  93. Dapr Workflow: Activities are called through the context: CallActivityAsync and its kin. source
  94. Step Functions: Task states call services through a Resource ARN in JSON. source
  95. Lambda durable: Service calls are wrapped in steps on the DurableContext. source
  96. Azure Durable: The default is CallActivityAsync(nameof(...)); typed calls come from a source generator in preview. source
  97. Cloudflare Workflows: Work is written as step.do(name, fn). source
  98. Vercel Workflow: A step is called with a plain await, but it is a step in the same program, not another service. source
  99. Prefect: A task is called like a normal function, but it is a task in the same flow, not another service. source
  100. Airflow: TaskFlow calls look like functions but build DAG dependencies behind the scenes. source
  101. LangGraph: A @task call returns a future read with .result(); the Graph API is nodes and edges. source
  102. Ray: A remote function is called with .remote() and read with ray.get. source
  103. Jaal: Work in progress: the recorded stream and its resume point are designed, not built.
  104. Temporal: Workflow Streams (preview) resume a subscriber from its offset; a retried activity streams its events again. source
  105. Inngest: The browser can reattach, but a step that fails and retries streams fresh data. source
  106. DBOS: Writes survive a restart, but a retried step can write its output again. source
  107. Hatchet: Events published before a consumer starts are dropped; no resume is described. source
  108. Trigger.dev: A lost connection resumes from the last chunk; a task crash or retry is not covered. source
  109. Vercel Workflow: Clients reconnect from a start index; a step crashing mid-stream is not described. source
  110. LangGraph: A dropped connection resumes with Last-Event-ID; a server crash is not covered. source
  111. Temporal: An optional Go-only checker, workflowcheck, can help; other SDKs check at run time. source
  112. Resonate: Determinism is left to the author, with no static check. source
  113. DBOS: The only check is a run-time non-determinism error. source
  114. Trigger.dev: The docs tell developers which concurrent calls to avoid. source
  115. Cadence: Workflow code must use Cadence channels and selectors; no official checker. source
  116. Dapr Workflow: Determinism is a rule for authors, with no analyzer. source
  117. Lambda durable: A Go-only linter, durablelint, flags issues and can be silenced. source
  118. Azure Durable: A C#-only Roslyn analyzer warns at build time; its warnings can be suppressed. source
  119. Cloudflare Workflows: Concurrency over steps is written guidance only. source
  120. LangGraph: Conflicting parallel writes are refused only while the graph runs. source
  121. Temporal: Only in the TypeScript sandbox are Math.random and Date made deterministic; other effects must be Activities. source
  122. Restate: Non-deterministic work must be wrapped in run(). source
  123. Inngest: Non-deterministic logic must go inside step.run(). source
  124. Resonate: Non-deterministic calls go in a leaf function run via ctx.run. source
  125. DBOS: A workflow function must be deterministic. source
  126. Hatchet: Durable-task code must be deterministic; avoid clock and random reads. source
  127. Cadence: Workflow code must use Cadence APIs for time and must be deterministic. source
  128. Dapr Workflow: Workflow code must be deterministic. source
  129. Lambda durable: Every non-deterministic call must be wrapped in a step. source
  130. Azure Durable: Orchestrator code must not use time APIs or random values directly. source
  131. Cloudflare Workflows: Non-deterministic logic must go inside step.do. source
  132. Vercel Workflow: Math.random and Date are fixed automatically; any other effect must be a step. source
  133. LangGraph: Random numbers and clock reads must be kept inside tasks. source
  134. Temporal: Running workflows continue on new code only for replay-compatible changes; otherwise patching is needed. source
  135. Restate: Requests start and end on the same version. source
  136. Golem: An update replays the agent on the new revision, but must be triggered and falls back if replay diverges. source
  137. DBOS: Only workflows whose version matches the current application are recovered. source
  138. Trigger.dev: A run never changes version. source
  139. Conductor: Running executions keep the definition snapshot taken at start. source
  140. Cadence: Continues on new code only for compatible changes; otherwise GetVersion is needed. source
  141. Dapr Workflow: Continues on new code only while it stays deterministic; otherwise patching. source
  142. Step Functions: A redriven execution uses the same definition; an update needs a new execution. source
  143. Lambda durable: Unpinned executions continue only if steps are not changed in ways that break replay. source
  144. Azure Durable: Running orchestrations replay on new code; breaking changes need version branching. source
  145. Vercel Workflow: Runs are pinned to the deployment that starts them. source
  146. Airflow: Only with the unversioned local bundle do remaining tasks use the latest code. source
  147. LangGraph: Old threads resume on the new graph, but renames break them and changes need a version stamp. source
  148. Jaal: The host agent (process manager) and deploy are built; scaling by instances is ruled (10-24) but not built; you install Postgres.
  149. Temporal: You host and scale your own workers, even on Temporal Cloud; self-hosting adds a server cluster and a database. source
  150. Restate: The server is one binary with no database, but you deploy and run your own services. source
  151. Inngest: The engine is one binary or a cloud; your function code runs in your own app or host. source
  152. Resonate: One command with SQLite for development; Postgres and your own processes in production. source
  153. DBOS: A library in your app plus Postgres; you host and scale the app, or use DBOS Cloud. source
  154. Hatchet: An engine image plus Postgres, or Hatchet Cloud; you always run the workers. source
  155. Trigger.dev: Fully managed in its own cloud; self-hosting needs Postgres, Redis and runners. source
  156. Conductor: A server plus a database and a lock service, or Orkes Cloud; your task workers are yours. source
  157. Cadence: You run frontend, history and matching services plus Cassandra, MySQL or Postgres. source
  158. Dapr Workflow: You configure an actor state store and host the app (and a control plane on Kubernetes). source
  159. Vercel Workflow: Automatic on Vercel; elsewhere you supply a storage and queue backend such as Postgres. source
  160. Prefect: A server, a database and workers you run; Prefect Cloud's managed pools come close. source
  161. Airflow: You assemble a metadata database, scheduler, API server, DAG processor and executor. source
  162. LangGraph: Fully managed on LangSmith Cloud; self-hosting needs Postgres, Redis and a license. source
  163. Ray: Ships head, workers and an autoscaler, but you provision the cluster. source
  164. Restate: Forcing an existing registration is for local development; production versions are new deployments. source
  165. DBOS: Only workflows of the current version are recovered. source
  166. Trigger.dev: A run never changes version, even after new deploys. source
  167. Conductor: Running executions keep their start-time snapshot. source
  168. Step Functions: An updated definition needs a new execution. source
  169. Lambda durable: Unpinned executions may resume on changed code, but the docs recommend pinning. source
  170. Vercel Workflow: Workflows keep running on the deployment they were created on. source
  171. Inngest: A resumed run uses the current code. source
  172. Cadence: Running workflows replay on the updated code. source
  173. Dapr Workflow: Code updates apply to running instances. source
  174. Lambda durable: Executions are pinned when you invoke a version or alias. source
  175. Azure Durable: Version settings can match work to versions; by default code changes affect running work. source
  176. Airflow: With a versioned bundle a run keeps its code; the default local bundle does not. source
  177. LangGraph: The latest graph applies to every thread. source
  178. Resonate: Its blog describes un-sequenced promises rather than a sequenced log. source
  179. LangGraph: Checkpoints accumulate per thread, but are snapshots, not described as a log. source
  180. Inngest: Only a Kotlin SDK in its GitHub org, not on the SDK list; its Java example says coming soon. source
  181. Golem: Scala is listed; Java is not. source
  182. Resonate: Labelled pre-release. source
  183. Hatchet: Not on its language list. source
  184. Trigger.dev: Not on its language list. source
  185. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  186. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  187. Cloudflare Workflows: Not on its language list. source
  188. Vercel Workflow: Not on its language list. source
  189. Prefect: Not on its language list. source
  190. Airflow: An experimental task SDK: task logic only; DAGs are always Python. source
  191. LangGraph: A community port only, not in the vendor's docs. source
  192. Ray: The Java APIs are experimental and community-supported. source
  193. Jaal: Not built yet. A Python orchestrator turns green once it compiles and runs end to end.
  194. Golem: Not on its language list. source
  195. Trigger.dev: A TypeScript task can run Python scripts; the task itself stays TypeScript. source
  196. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  197. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  198. Cloudflare Workflows: Python Workflows are in open beta. source
  199. Vercel Workflow: Python is labelled beta. source
  200. Jaal: Not built yet.
  201. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  202. Cadence: Not on its language list. source
  203. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  204. Prefect: Not on its language list. source
  205. Airflow: Not on its language list. source
  206. Ray: Not on its language list. source
  207. Jaal: Not built yet.
  208. Golem: Go is listed only in v1.6, still a release candidate. source
  209. Resonate: Labelled early release. source
  210. Trigger.dev: Not on its language list. source
  211. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  212. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  213. Azure Durable: Not on its language list. source
  214. Cloudflare Workflows: Not on its language list. source
  215. Vercel Workflow: Not on its language list. source
  216. Prefect: Not on its language list. source
  217. Airflow: An experimental task SDK: task logic only; DAGs are always Python. source
  218. LangGraph: A community port only, not in the vendor's docs. source
  219. Ray: Not on its language list. source
  220. Jaal: Not built yet.
  221. Inngest: Not on its language list. source
  222. Resonate: Labelled early development. source
  223. DBOS: Not on its language list. source
  224. Hatchet: Not on its language list. source
  225. Trigger.dev: Not on its language list. source
  226. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  227. Cadence: Not on its language list. source
  228. Dapr Workflow: Not on its language list. source
  229. Step Functions: Only as Lambda tasks; the workflow is Amazon States Language. source
  230. Lambda durable: Not on its language list. source
  231. Azure Durable: Not on its language list. source
  232. Cloudflare Workflows: Not on its language list. source
  233. Vercel Workflow: Not on its language list. source
  234. Prefect: Not on its language list. source
  235. Airflow: Not on its language list. source
  236. LangGraph: Not on its language list. source
  237. Ray: Not on its language list. source
  238. Jaal: Not built yet.
  239. Temporal: Not on its language list. source
  240. Restate: Not on its language list. source
  241. Inngest: Not on its language list. source
  242. Golem: Not on its language list. source
  243. Resonate: Not on its language list. source
  244. DBOS: Not on its language list. source
  245. Hatchet: Not on its language list. source
  246. Trigger.dev: Not on its language list. source
  247. Conductor: Not on its language list. source
  248. Cadence: Not on its language list. source
  249. Dapr Workflow: Not on its language list. source
  250. Step Functions: Only as Lambda tasks; the workflow is Amazon States Language. source
  251. Lambda durable: Not on its language list. source
  252. Azure Durable: Not on its language list. source
  253. Cloudflare Workflows: Not on its language list. source
  254. Vercel Workflow: Not on its language list. source
  255. Prefect: Not on its language list. source
  256. Airflow: Not on its language list. source
  257. LangGraph: Not on its language list. source
  258. Ray: The C++ APIs are experimental and community-supported. source
  259. Jaal: Not built yet.
  260. Restate: Not on its language list. source
  261. Inngest: Not on its language list. source
  262. Golem: Not on its language list. source
  263. Resonate: Not on its language list. source
  264. DBOS: Not on its language list. source
  265. Hatchet: Not on its language list. source
  266. Trigger.dev: Not on its language list. source
  267. Conductor: The SDK builds a JSON definition the server interprets; branches and loops are DSL tasks, not if and for. source
  268. Cadence: Not on its language list. source
  269. Step Functions: CDK in this language only generates the JSON state machine; the workflow is Amazon States Language. source
  270. Cloudflare Workflows: Not on its language list. source
  271. Vercel Workflow: Not on its language list. source
  272. Prefect: Not on its language list. source
  273. Airflow: Not on its language list. source
  274. LangGraph: Not on its language list. source
  275. Ray: Not on its language list. source
The notes behind each grey and red mark (163)
  1. Temporal: You write deterministic Workflows plus Activities with its SDK. source
  2. Restate: You write handlers, virtual objects and workflows, wrapping work in ctx.run. source
  3. Inngest: You write steps with step.run and the other step functions. source
  4. Golem: Durability needs no wrappers, but parallel work is written with worker clients, create_promise and await_promise. source
  5. Resonate: You write generator functions that call context.run. source
  6. DBOS: You annotate workflows and steps. source
  7. Hatchet: You define tasks, DAG workflows and durable tasks. source
  8. Trigger.dev: You define tasks with task() and call triggerAndWait. source
  9. Conductor: Workflows are JSON definitions. source
  10. Cadence: You write deterministic workflows plus activities with its SDK. source
  11. Dapr Workflow: You write workflows plus activities with its SDK. source
  12. Step Functions: Workflows are JSON state machines (Amazon States Language). source
  13. Lambda durable: You write DurableContext steps, waits and invokes. source
  14. Azure Durable: You write orchestrator, activity and entity functions. source
  15. Cloudflare Workflows: You write step.do and step.sleep calls. source
  16. Vercel Workflow: You mark code with the "use workflow" and "use step" directives. source
  17. Prefect: You decorate code with @flow and @task. source
  18. Airflow: You define Python DAGs. source
  19. LangGraph: You build a graph of nodes and edges, or use @entrypoint and @task. source
  20. Ray: You write @ray.remote tasks and actors. source
  21. Temporal: Parallel work is written with futures, Promise.all or asyncio.gather. source
  22. Restate: Parallel work is written with DurableFuture.all or Select. source
  23. Inngest: Parallel steps are written with Promise.all. source
  24. Golem: Parallel work fans out to child agents explicitly. source
  25. Resonate: Parallel work is written with beginRun / beginRpc futures. source
  26. DBOS: Parallel work goes through durable queues. source
  27. Hatchet: Parallel work is declared as DAG dependencies or child tasks. source
  28. Trigger.dev: Parallel work is written with batchTriggerAndWait. source
  29. Conductor: Parallel work is a FORK_JOIN declared in the definition. source
  30. Dapr Workflow: Parallel work is written with when_all / Task.WhenAll. source
  31. Step Functions: Parallel work is a Parallel or Map state. source
  32. Lambda durable: Parallel work is written with context.parallel or context.map. source
  33. Azure Durable: Parallel work is an explicit fan-out / fan-in (Task.WhenAll). source
  34. Cloudflare Workflows: Parallel steps are written with Promise.all. source
  35. Vercel Workflow: Parallel steps are written with Promise.all. source
  36. Prefect: Parallel work is written with .submit() and task runners. source
  37. Airflow: Parallel work is declared as DAG dependencies. source
  38. LangGraph: Parallel work is declared with fan-out edges and Send. source
  39. Ray: Parallel work is written with .remote() futures. source
  40. Temporal: A version is a Build ID supplied in the worker's deployment options. source
  41. Restate: Running requests stay on their version automatically, but a version is the endpoint you deploy and register, not derived from the code. source
  42. Inngest: There are no versions: a resumed run uses the current code. source
  43. Golem: Agents stay on their revision, but the docs do not say a revision is derived from content. source
  44. Resonate: A version is a developer option, 1 by default. source
  45. Trigger.dev: Each deploy makes a version and a run never changes version, but the identity is a date and counter. source
  46. Cadence: Changes are guarded with GetVersion markers the developer numbers. source
  47. Dapr Workflow: Versioning is by patching and developer-named workflow versions. source
  48. Step Functions: Executions stay on a published version, but publishing is explicit and the number is a counter. source
  49. Lambda durable: Executions can be pinned to a published version, but you publish and invoke it yourself. source
  50. Azure Durable: Orchestration versioning uses a version string the developer sets. source
  51. Vercel Workflow: Runs are pinned to the deployment that starts them, identified by deployment ID, not by content. source
  52. Airflow: With a Git bundle a run keeps its commit; the default local bundle has no versioning. source
  53. LangGraph: There are no versions: the latest graph applies to every thread. source
  54. Temporal: Recovery replays the workflow code against the event history. source
  55. Restate: Recovery is replay of the handler, skipping journaled steps. source
  56. Inngest: A run resumes by replaying the handler with saved step results. source
  57. Golem: Recovery replays the oplog; opt-in snapshots shorten the replay. source
  58. Resonate: After a crash, execution starts from the top of the function. source
  59. DBOS: An interrupted workflow is restarted by calling it again with its inputs. source
  60. Hatchet: The durable event log is replayed to rebuild state. source
  61. Trigger.dev: A retried task starts from the beginning, with cached subtask results. source
  62. Cadence: Workflow code is re-executed to rebuild state. source
  63. Dapr Workflow: On replay the workflow runs again from the beginning. source
  64. Step Functions: A failed run can be redriven from the failed step, by hand. source
  65. Lambda durable: On replay the code runs from the beginning, skipping completed checkpoints. source
  66. Azure Durable: The orchestrator re-executes the whole function from the start. source
  67. Cloudflare Workflows: Code outside steps runs again after hibernation. source
  68. Vercel Workflow: Workflow code is re-run, using the event log to reach its spot. source
  69. LangGraph: Graph API nodes re-run from the start of the interrupted node; the Functional API replays from the top. source
  70. Temporal: Compensation is a documented saga pattern written in workflow code. source
  71. Restate: Sagas are implemented in user code. source
  72. Dapr Workflow: Compensation is a documented pattern written in the workflow. source
  73. Step Functions: Compensation is a pattern built from Catch fields. source
  74. Azure Durable: Compensation is written in a catch block. source
  75. Vercel Workflow: Compensation is a saga recipe written in the workflow. source
  76. Restate: Registration refuses removed or renamed handlers, but checks no input or output types, and a flag overrides it. source
  77. Dapr Workflow: Changing a workflow's name, input or output deploys and can fail at runtime. source
  78. Azure Durable: An activity signature change deploys and can break orchestrators that use it. source
  79. Restate: Sagas are written in the calling handler, adding compensations step by step. source
  80. Golem: An operation carries its own undo, but the caller must open the transaction. source
  81. Conductor: The workflow author configures a failure workflow. source
  82. Prefect: A task declares its rollback hook, but the flow must open a transaction. source
  83. Temporal: The call reads like a function, but on a handle built first with proxyActivities in the workflow. source
  84. Restate: Calls go through a typed client taken from the context: ctx.serviceClient(...).handler(...). source
  85. Inngest: Another function is called through step.invoke with an ID and options. source
  86. Golem: Methods are called directly, on a client obtained first with get(...). source
  87. Resonate: Every durable call goes through ctx.run or ctx.rpc. source
  88. DBOS: A step is called as a plain function, but it is a step in the same program, not another service. source
  89. Hatchet: A child is spawned with a generic run() on a task object. source
  90. Trigger.dev: A child is called with triggerAndWait and returns a result wrapper to unwrap. source
  91. Conductor: Tasks are referenced by registered names in a JSON definition. source
  92. Cadence: Java calls a method on a stub made with newActivityStub; Go uses ExecuteActivity(ctx, fn). source
  93. Dapr Workflow: Activities are called through the context: CallActivityAsync and its kin. source
  94. Step Functions: Task states call services through a Resource ARN in JSON. source
  95. Lambda durable: Service calls are wrapped in steps on the DurableContext. source
  96. Azure Durable: The default is CallActivityAsync(nameof(...)); typed calls come from a source generator in preview. source
  97. Cloudflare Workflows: Work is written as step.do(name, fn). source
  98. Vercel Workflow: A step is called with a plain await, but it is a step in the same program, not another service. source
  99. Prefect: A task is called like a normal function, but it is a task in the same flow, not another service. source
  100. Airflow: TaskFlow calls look like functions but build DAG dependencies behind the scenes. source
  101. LangGraph: A @task call returns a future read with .result(); the Graph API is nodes and edges. source
  102. Ray: A remote function is called with .remote() and read with ray.get. source
  103. Jaal: Work in progress: the recorded stream and its resume point are designed, not built.
  104. Temporal: Workflow Streams (preview) resume a subscriber from its offset; a retried activity streams its events again. source
  105. Inngest: The browser can reattach, but a step that fails and retries streams fresh data. source
  106. DBOS: Writes survive a restart, but a retried step can write its output again. source
  107. Hatchet: Events published before a consumer starts are dropped; no resume is described. source
  108. Trigger.dev: A lost connection resumes from the last chunk; a task crash or retry is not covered. source
  109. Vercel Workflow: Clients reconnect from a start index; a step crashing mid-stream is not described. source
  110. LangGraph: A dropped connection resumes with Last-Event-ID; a server crash is not covered. source
  111. Temporal: An optional Go-only checker, workflowcheck, can help; other SDKs check at run time. source
  112. Resonate: Determinism is left to the author, with no static check. source
  113. DBOS: The only check is a run-time non-determinism error. source
  114. Trigger.dev: The docs tell developers which concurrent calls to avoid. source
  115. Cadence: Workflow code must use Cadence channels and selectors; no official checker. source
  116. Dapr Workflow: Determinism is a rule for authors, with no analyzer. source
  117. Lambda durable: A Go-only linter, durablelint, flags issues and can be silenced. source
  118. Azure Durable: A C#-only Roslyn analyzer warns at build time; its warnings can be suppressed. source
  119. Cloudflare Workflows: Concurrency over steps is written guidance only. source
  120. LangGraph: Conflicting parallel writes are refused only while the graph runs. source
  121. Temporal: Only in the TypeScript sandbox are Math.random and Date made deterministic; other effects must be Activities. source
  122. Restate: Non-deterministic work must be wrapped in run(). source
  123. Inngest: Non-deterministic logic must go inside step.run(). source
  124. Resonate: Non-deterministic calls go in a leaf function run via ctx.run. source
  125. DBOS: A workflow function must be deterministic. source
  126. Hatchet: Durable-task code must be deterministic; avoid clock and random reads. source
  127. Cadence: Workflow code must use Cadence APIs for time and must be deterministic. source
  128. Dapr Workflow: Workflow code must be deterministic. source
  129. Lambda durable: Every non-deterministic call must be wrapped in a step. source
  130. Azure Durable: Orchestrator code must not use time APIs or random values directly. source
  131. Cloudflare Workflows: Non-deterministic logic must go inside step.do. source
  132. Vercel Workflow: Math.random and Date are fixed automatically; any other effect must be a step. source
  133. LangGraph: Random numbers and clock reads must be kept inside tasks. source
  134. Temporal: Running workflows continue on new code only for replay-compatible changes; otherwise patching is needed. source
  135. Restate: Requests start and end on the same version. source
  136. Golem: An update replays the agent on the new revision, but must be triggered and falls back if replay diverges. source
  137. DBOS: Only workflows whose version matches the current application are recovered. source
  138. Trigger.dev: A run never changes version. source
  139. Conductor: Running executions keep the definition snapshot taken at start. source
  140. Cadence: Continues on new code only for compatible changes; otherwise GetVersion is needed. source
  141. Dapr Workflow: Continues on new code only while it stays deterministic; otherwise patching. source
  142. Step Functions: A redriven execution uses the same definition; an update needs a new execution. source
  143. Lambda durable: Unpinned executions continue only if steps are not changed in ways that break replay. source
  144. Azure Durable: Running orchestrations replay on new code; breaking changes need version branching. source
  145. Vercel Workflow: Runs are pinned to the deployment that starts them. source
  146. Airflow: Only with the unversioned local bundle do remaining tasks use the latest code. source
  147. LangGraph: Old threads resume on the new graph, but renames break them and changes need a version stamp. source
  148. Restate: Forcing an existing registration is for local development; production versions are new deployments. source
  149. DBOS: Only workflows of the current version are recovered. source
  150. Trigger.dev: A run never changes version, even after new deploys. source
  151. Conductor: Running executions keep their start-time snapshot. source
  152. Step Functions: An updated definition needs a new execution. source
  153. Lambda durable: Unpinned executions may resume on changed code, but the docs recommend pinning. source
  154. Vercel Workflow: Workflows keep running on the deployment they were created on. source
  155. Inngest: A resumed run uses the current code. source
  156. Cadence: Running workflows replay on the updated code. source
  157. Dapr Workflow: Code updates apply to running instances. source
  158. Lambda durable: Executions are pinned when you invoke a version or alias. source
  159. Azure Durable: Version settings can match work to versions; by default code changes affect running work. source
  160. Airflow: With a versioned bundle a run keeps its code; the default local bundle does not. source
  161. LangGraph: The latest graph applies to every thread. source
  162. Resonate: Its blog describes un-sequenced promises rather than a sequenced log. source
  163. LangGraph: Checkpoints accumulate per thread, but are snapshots, not described as a log. source

Write a parallel loop with an async call

The task: call a service once for every element of input, all calls at once, results in output in order. Each snippet is the pattern the product’s own docs show for parallel work, with the names changed to match. Why we think hand-written async is the wrong default: the loop you meant to write.

ProductThe code you writeWhat you add to a plain loop
Jaaljava
int n = input.length;
Out[] output = new Out[n];
for (@Parallel int i = 0; i < n; i++) {
    output[i] = svc.invoke(input[i]);
}
One annotation, @Parallel, on the index, and the count declared before the loop. The compiler checks that each iteration writes only its own cell and refuses the mark by name if not. No await, no futures, no Promise.all.

built today

Temporaltypescript
const { invoke } = proxyActivities<typeof activities>({ startToCloseTimeout: '30s' });

export async function wf(input: In[]): Promise<Out[]> {
  const output = await Promise.all(input.map((x) => invoke(x)));
  return output;
}
Promise.all and a map; invoke comes from proxyActivities with a timeout
How this was adaptedDocs example is processInParallel(items) with items.map(process) + Promise.all; renamed only.

docs

Restatetypescript
const promises = [];
for (const x of input) {
  promises.push(ctx.serviceClient(invokeService).invoke(x));
}
const output = await RestatePromise.all(promises);
collect promises in a loop and await RestatePromise.all; invoke is a handler on a service reached via ctx.serviceClient
How this was adaptedDocs use a for...of loop pushing ctx.serviceClient(fanOutWorker).runSubtask(subtask); must use RestatePromise.all, not Promise.all.

docs

Inngesttypescript
export const wf = inngest.createFunction(
  { id: "wf", triggers: { event: "app/wf.run" } },
  async ({ event, step }) => {
    const input: In[] = event.data.input;
    const output = await Promise.all(
      input.map((x, i) => step.run(`invoke-${i}`, () => invoke(x)))
    );
    return output;
  }
);
Promise.all and a map; each call wrapped in step.run with a unique, stable step id
How this was adaptedDocs example maps chunks to step.run(`summarize-chunk-${index}`, ...) inside Promise.all; trigger syntax per the v4 SDK shown on that page.

docs

Golemtypescript
// in the coordinator agent's method; Worker is a separate agent type with an invoke method
async fanOut({ input }) {
  const promises = input.map((x, i) => Worker.client.get({ id: i }).invoke({ data: x }));
  return await Promise.all(promises);
}
a second agent type (defineAgent + implement) for the worker, one agent instance per element via Worker.client.get({id}), then Promise.all
How this was adaptedDocs say a single agent handles invocations sequentially, so parallelism requires one worker agent instance per item. Docs method is process({ data }); renamed to invoke. URL is versioned (v1.6).

docs

Resonatetypescript
export function* wf(ctx: Context, input: In[]) {
  const futures = [];
  for (const x of input) futures.push(yield* ctx.beginRun(invoke, x));
  const output = [];
  for (const f of futures) output.push(yield* f);
  return output;
}
a generator function; ctx.beginRun per element to start it, then yield* each future to collect
How this was adaptedDocs example spawns four fixed calls (const emailFuture = yield* ctx.beginRun(sendEmail, event); ...) and fans in with yield* on each; the loop form here is an adaptation of that pattern, not shown verbatim.

docs

DBOStypescript
const invokeStep = DBOS.registerStep(invoke);

const wf = DBOS.registerWorkflow(async (input: In[]) => {
  const settled = await Promise.allSettled(input.map((x) => invokeStep(x)));
  return settled.map((r) => { if (r.status === "rejected") throw r.reason; return r.value; });
});
register invoke as a step and the body as a workflow; Promise.allSettled over a map, then unwrap the results
How this was adaptedDocs show Promise.allSettled([step1(..), step2(..), ...]) and say steps must start in a deterministic order; they advise allSettled over Promise.all. registerStep is from the step tutorial. The unwrap line is mine.

docs

Hatchettypescript
export const wf = hatchet.task({
  name: 'wf',
  fn: async (input: { input: In[] }, ctx) => {
    const promises = [];
    for (const x of input.input) promises.push(invoke.run(x));
    const output = await Promise.all(promises);
    return { output };
  },
});
invoke must itself be a hatchet.task (a child task); push child.run() promises and await Promise.all
How this was adaptedDocs parent loops for (let i = 0; i < n; i++) promises.push(child.run({ N: i })) then Promise.all; ctx.bulkRunChildren is the documented bulk alternative. Fetched via docs.hatchet.run/llms/v1/child-spawning.md.

docs

Trigger.devtypescript
export const wf = task({
  id: "wf",
  run: async (input: In[]) => {
    const results = await invokeTask.batchTriggerAndWait(input.map((x) => ({ payload: x })));
    return results.runs.map((r) => { if (!r.ok) throw r.error; return r.output; });
  },
});
invoke must be its own task(); one batchTriggerAndWait with an array of {payload}, then check run.ok and read run.output per run
How this was adaptedDocs forbid wrapping triggerAndWait in Promise.all and say to use batchTriggerAndWait. Docs examples pass a literal array; the .map to build it is mine. Docs do not state explicitly that results.runs is in input order.

docs

Conductorjson
{
  "name": "invoke_fork", "taskReferenceName": "invoke_fork_ref",
  "type": "FORK_JOIN_DYNAMIC",
  "inputParameters": {
    "forkTaskName": "invoke",
    "forkTaskType": "SIMPLE",
    "forkTaskInputs": "${workflow.input.input}"
  }
},
{ "name": "invoke_join", "taskReferenceName": "invoke_join_ref", "type": "JOIN" }
a FORK_JOIN_DYNAMIC task naming the worker task plus a mandatory JOIN; invoke is a separately registered worker (SIMPLE) task
How this was adaptedDocs and Orkes examples set forkTaskInputs to a literal array; the ${workflow.input.input} expression form is an adaptation. JOIN output is a map keyed by each fork's taskReferenceName, not an ordered array; Conductor adds __index to each fork input for re-ordering. forkTaskType appears in the Orkes reference (https://orkes.io/content/reference-docs/operators/dynamic-fork).

docs

Cadencego
func Wf(ctx workflow.Context, input []In) ([]Out, error) {
	futures := make([]workflow.Future, len(input))
	for i, x := range input {
		futures[i] = workflow.ExecuteActivity(ctx, Invoke, x)
	}
	output := make([]Out, len(input))
	for i, f := range futures {
		if err := f.Get(ctx, &output[i]); err != nil { return nil, err }
	}
	return output, nil
}
ctx carrying ActivityOptions (WithActivityOptions); two loops, one to start futures, one to Get them
How this was adaptedDocs state this pattern in prose (repeatedly call workflow.ExecuteActivity, store the futures, call Get later) and show a single-call code sample; the loops are an adaptation. Bounded alternative: workflow.NewBatchFuture (https://cadenceworkflow.io/docs/go-client/batch-future). Server limit of 1024 pending activities per workflow by default.

docs

Dapr Workflowtypescript
const wf: TWorkflow = async function* (ctx: WorkflowContext, input: In[]): any {
  const tasks: Task<any>[] = [];
  for (const x of input) {
    tasks.push(ctx.callActivity(invoke, x));
  }
  const output: Out[] = yield ctx.whenAll(tasks);
  return output;
};
a generator function; push ctx.callActivity tasks in a loop, then yield ctx.whenAll; invoke registered as an activity on the WorkflowRuntime
How this was adaptedDocs JS tab first fetches workItems with an activity; here input is the workflow input. The JS SDK's workflow support is newer than the .NET/Python/Java/Go SDKs; check its stability label.

docs

AWS Step Functionsjson
"Invoke All": {
  "Type": "Map",
  "ItemsPath": "$.input",
  "ResultPath": "$.output",
  "ItemProcessor": {
    "ProcessorConfig": { "Mode": "INLINE" },
    "StartAt": "Invoke",
    "States": { "Invoke": {
      "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": { "FunctionName": "<invoke-arn>", "Payload.$": "$" },
      "OutputPath": "$.Payload", "End": true
    } }
  },
  "End": true
}
a Map state with ItemsPath/ResultPath and an ItemProcessor sub-state-machine whose Task calls the invoke Lambda
How this was adaptedInline mode runs at most 40 iterations concurrently (MaxConcurrency 0 = as many as possible); larger fan-outs need Distributed mode. Map output is a JSON array with one item per iteration. "Payload.$": "$" is my addition to pass the item explicitly (docs example omits Payload).

docs

AWS Lambda durable functionstypescript
export const handler = withDurableExecution(
  async (event: { input: In[] }, context: DurableContext): Promise<Out[]> => {
    const result: BatchResult<Out> = await context.map(
      "invoke-all",
      event.input,
      async (ctx, x, i) => ctx.step(`invoke-${i}`, async () => invoke(x)),
    );
    return result.getResults();
  },
);
wrap the handler in withDurableExecution; context.map with a name and a per-item callback that runs ctx.step
How this was adaptedgetResults() returns succeeded items' results in input order (failed items are dropped from it; inspect the BatchResult). maxConcurrency default is unlimited.

docs

Azure Durable Functionstypescript
const wf: OrchestrationHandler = function* (context: OrchestrationContext) {
  const input: In[] = context.df.getInput();
  const tasks = input.map((x) => context.df.callActivity("invoke", x));
  const output: Out[] = yield context.df.Task.all(tasks);
  return output;
};
df.app.orchestration("wf", wf);
a generator orchestrator; map to context.df.callActivity tasks, then yield context.df.Task.all; invoke registered with df.app.activity
How this was adaptedDocs example maps over a hard-coded cities array; getInput() line is my adaptation for the input.

docs

Cloudflare Workflowstypescript
export class Wf extends WorkflowEntrypoint<Env, { input: In[] }> {
  async run(event: WorkflowEvent<{ input: In[] }>, step: WorkflowStep) {
    const output = await Promise.all(
      event.payload.input.map((x, i) => step.do(`invoke ${i}`, async () => invoke(x)))
    );
    return output;
  }
}
Promise.all and a map; each call wrapped in step.do with a deterministic, unique name
How this was adaptedDocs show Promise.all over two literal step.do calls and separately allow dynamically named steps (one per list entry) when deterministic; the map form combines the two. Results must be JSON-serializable.

docs

Vercel Workflowtypescript
async function invoke(x: In): Promise<Out> {
  "use step";
  // ...
}

export async function wf(input: In[]) {
  "use workflow";
  const output = await Promise.all(input.map((x) => invoke(x)));
  return output;
}
"use workflow" / "use step" directives; Promise.all and a map
How this was adaptedDocs show Promise.all(friends.map((friend, i) => sendRSVPEmail(friend, webhooks[i]))) in a "use workflow" function; product name used here as listed.

docs

Prefectpython
@task
def invoke(x): ...

@flow
def wf(input: list):
    futures = invoke.map(input)
    output = futures.result()
    return output
@task/@flow decorators; task.map returns futures, .result() collects them
How this was adaptedPrefect has no TypeScript SDK. Docs example asserts [6, 7, 8] for inputs [1, 2, 3], implying input order; order is not stated in prose. Real parallelism depends on the flow's task runner (ThreadPoolTaskRunner default).

docs

Apache Airflowpython
@task
def invoke(x): ...

@task
def collect(output):
    return list(output)

collect(invoke.expand(x=input))
@task decorators inside a DAG; .expand(x=...) creates one mapped task instance per element, and a downstream task receives the lazy sequence
How this was adaptedinput must be a literal list or an upstream task's output (XCom), not a runtime Python variable. Docs give no explicit ordering statement for the collected lazy sequence (it follows map_index in practice). Mapped instances run in parallel subject to executor/pool limits.

docs

LangGraphtypescript
const invoke = task("invoke", async (x: In) => {
  // ...
});

const wf = entrypoint({ checkpointer, name: "wf" }, async (input: In[]) => {
  return await Promise.all(input.map(invoke));
});
wrap invoke in task() and the body in entrypoint() with a checkpointer; Promise.all and a map
How this was adaptedThis is the Functional API. In the Graph API the fan-out is addConditionalEdges returning state.items.map(x => new Send("invoke", { x })) with a reducer on state; that form does not preserve order (https://docs.langchain.com/oss/javascript/langgraph/graph-api).

docs

Raypython
@ray.remote
def invoke(x): ...

futures = [invoke.remote(x) for x in input]
output = ray.get(futures)
@ray.remote decorator; .remote() in a list comprehension, then ray.get on the list
How this was adaptedDocs example: futures = [square.remote(i) for i in range(4)]; print(ray.get(futures)) -> [0, 1, 4, 9] (input order). Ray tasks are not durable workflows; no replay on driver failure.

docs

Two calls in parallel, a third that needs both

A payment: score the order for fraud and check the customer’s credit at the same time, then authorize with both answers. Not a loop and not a straight line: two branches that join. Each snippet follows the pattern the product’s own docs show, with the names changed to match. Where the docs document a restriction that makes this shape awkward, it is marked.

ProductThe code you writeWhat you add, and what limits it
Jaaljava
Score risk  = fraud.invoke(order);
Limit limit = credit.invoke(order.customer());
Auth auth   = payments.invoke(AuthRequestBuilder.authRequest()
        .order(order).risk(risk).limit(limit).build());
Nothing. The compiler sees that score and check do not depend on each other, runs them in parallel, and starts authorize when both answers are back.

built today

Temporaltypescript
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';

const { fraudScore, creditCheck, authorize } =
  proxyActivities<typeof activities>({ startToCloseTimeout: '1 minute' });

export async function authorizePayment(order: Order): Promise<Auth> {
  const [risk, limit] = await Promise.all([fraudScore(order), creditCheck(order.customer)]);
  return await authorize(order, risk, limit);
}
Each call becomes an activity (separate activities file, registered on a worker) reached through proxyActivities with a timeout; the developer groups the two branches with Promise.all.

Limits: None found for this shape. General: workflow code must be deterministic (sandboxed, replayed); per-execution limits (2,000 pending activities, 51,200 history events, 4 MB gRPC message) only matter at wide fan-out. Grouping is by hand (Promise.all).

How this was adaptedDocs example is Promise.all over a homogeneous items.map(process); adapted to two different activities destructured from one proxyActivities call (the activity-basics page shows Promise.all([act2, act3]) of different activities).

docs

Restatetypescript
import * as restate from "@restatedev/restate-sdk";
import { RestatePromise } from "@restatedev/restate-sdk";

export const payments = restate.service({
  name: "payments",
  handlers: {
    authorize: async (ctx: restate.Context, order: Order) => {
      const riskP = ctx.run("fraud score", () => fraud.score(order));
      const limitP = ctx.run("credit check", () => credit.check(order.customer));
      const [risk, limit] = await RestatePromise.all([riskP, limitP]);
      return ctx.run("authorize", () => paymentsApi.authorize(order, risk, limit));
    },
  },
});
Each call wrapped in a journaled ctx.run (named); start both before awaiting and combine with RestatePromise.all, not plain Promise.all.

Limits: None found that blocks the shape. Docs direct coordination through RestatePromise combinators (they record completion order for deterministic replay); calls run concurrently only if started before being awaited. Grouping is by hand.

How this was adaptedCombined the docs' ctx.run concurrency example and RestatePromise.all example; service/handler wrapper is the standard SDK shape. Could equally be ctx.serviceClient(...) calls to other Restate services.

docs

Inngesttypescript
import { Inngest } from "inngest";
const inngest = new Inngest({ id: "payments" });

export const authorizePayment = inngest.createFunction(
  { id: "authorize-payment", triggers: { event: "order/placed" } },
  async ({ event, step }) => {
    const order = event.data.order;
    const [risk, limit] = await Promise.all([
      step.run("fraud-score", () => fraud.score(order)),
      step.run("credit-check", () => credit.check(order.customer)),
    ]);
    return step.run("authorize", () => payments.authorize(order, risk, limit));
  }
);
Each call wrapped in step.run with a stable step ID; function bound to an event trigger; Promise.all for the two branches.

Limits: None that blocks the shape. Documented behaviors: step IDs should be stable; on serverless hosts each parallel step runs as its own execution and the function is re-invoked with all results; a bare Promise.race waits for all steps unless wrapped in group.parallel(); limits of 1,000 steps, 4 MiB per step output, 32 MiB run state. Grouping is by hand.

How this was adaptedWritten from the docs' Promise.all-over-step.run example (TS SDK v4 createFunction form with triggers in the config); names adapted.

docs

Golemtypescript
// inside the PaymentAgent method implementation (defineAgent/.implement setup omitted)
import { Fraud } from './fraud-agent.js';
import { Credit } from './credit-agent.js';
import { Payments } from './payments-agent.js';

const fraud = Fraud.client.get({ orderId: order.id });
const credit = Credit.client.get({ customerId: order.customer });
const [risk, limit] = await Promise.all([
  fraud.score({ order }),
  credit.check({ customer: order.customer }),
]);
return await Payments.client.get({ orderId: order.id }).authorize({ order, risk, limit });
Each service becomes its own agent (defineAgent with zod id/method schemas, implemented separately); the orchestrator obtains clients via .client.get(id) and groups the two RPCs with Promise.all.

Restriction: 'Golem agents process invocations sequentially - a single agent cannot run work in parallel' (parallel-workers guide). Concurrency exists only across distinct agent instances, so fraud and credit must be separate agents (or instances); two calls to the same instance serialize. Awaiting RPC cycles (A awaits B awaits A) deadlock. Child agents persist after the run and must be deleted if unwanted. Docs are mid-transition (v1.5 @agent/Worker.get vs current defineAgent/.client.get).

How this was adaptedAssembled from two pages: Promise.all over child-agent calls (v1.5 parallel-workers guide, @agent + Worker.get(i) syntax) and the current client syntax Counter.client.get({...}) with object-shaped method input (call-another-agent guide). The agent definitions, zod schemas and method-body wrapper are omitted; exact current method-implementation signature not shown in docs. Assembled from two versions of the docs; not checked as one program.

docs

Resonatetypescript
import { Context, Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();

resonate.register("authorizePayment", function* (ctx: Context, order: Order) {
  const riskP = yield* ctx.beginRun(fraudScore, order);
  const limitP = yield* ctx.beginRun(creditCheck, order.customer);
  const risk = yield* riskP;
  const limit = yield* limitP;
  return yield* ctx.run(authorize, order, risk, limit);
});
// fraudScore/creditCheck/authorize are (ctx: Context, ...args) => result functions
Generator function with yield*; ctx.beginRun for each branch (ctx.run would block), then yield* on each promise; each called function takes a Context first.

Limits: None found for the shape. ctx.beginRun is same-process; to run a branch on another worker use ctx.beginRpc with ctx.options({ target }). Nondeterminism must go through ctx.date/ctx.math. Generator functions default to no retries. Grouping is by hand (begin/await ordering).

How this was adaptedDocs page describes beginRun + yield* on the returned promise; snippet written from that description with diamond names.

docs

DBOStypescript
import { DBOS } from "@dbos-inc/dbos-sdk";

async function authorizePaymentFn(order: Order) {
  const [r, l] = await Promise.allSettled([
    DBOS.runStep(() => fraud.score(order), { name: "fraudScore" }),
    DBOS.runStep(() => credit.check(order.customer), { name: "creditCheck" }),
  ]);
  if (r.status === "rejected") throw r.reason;
  if (l.status === "rejected") throw l.reason;
  return DBOS.runStep(() => payments.authorize(order, r.value, l.value), { name: "authorize" });
}
export const authorizePayment = DBOS.registerWorkflow(authorizePaymentFn);
Each call wrapped in DBOS.runStep with a name; the workflow is registered with DBOS.registerWorkflow; the two branches use Promise.allSettled plus manual unwrapping of status/reason/value.

Restriction: 'Avoid using Promise.all because of how it handles errors and rejections' - use Promise.allSettled; concurrent steps are valid only if started in a deterministic order before awaiting. A branch that is itself a chain of two or more steps (e.g. fraud.score then fraud.explain) is NOT allowed inline - the docs say to start child workflows with startWorkflow and await their handles. The one-step-per-branch diamond is fine. Grouping is by hand.

How this was adaptedCombined the docs' Promise.allSettled valid example with the DBOS.runStep(..., {name}) and DBOS.registerWorkflow forms; the rejection unwrapping is the developer's own code.

docs

Hatchettypescript
import { hatchet } from "./hatchet-client";

export const payment = hatchet.workflow<{ order: Order }>({ name: "authorize-payment" });

const fraud = payment.task({ name: "fraud-score",
  fn: async (input) => ({ risk: await fraudSvc.score(input.order) }) });
const credit = payment.task({ name: "credit-check",
  fn: async (input) => ({ limit: await creditSvc.check(input.order.customer) }) });

payment.task({ name: "authorize", parents: [fraud, credit],
  fn: async (input, ctx) => {
    const { risk } = await ctx.parentOutput(fraud);
    const { limit } = await ctx.parentOutput(credit);
    return { auth: await paymentsSvc.authorize(input.order, risk, limit) };
  } });
Each call is a declared task on a workflow; the join is expressed by parents: [fraud, credit]; outputs read back with ctx.parentOutput(task); tasks run on registered workers.

Limits: None found for the shape - it is a native DAG: parallelism follows from the declared parents edges, not hand grouping. Constraints: the DAG must be declared up front and be acyclic; a child runs only after all parents succeed; conditions limited to parent-based skip_if/cancel_if (CEL). Each task is a separately scheduled run (queue hop per step).

How this was adaptedAdapted the docs' two-task DAG (toLower -> reverse) to two parents; outputs returned as objects to match the docs' examples (docs do not state whether bare scalars are allowed).

docs

Trigger.devtypescript
import { task, batch } from "@trigger.dev/sdk";
export const fraudScore = task({ id: "fraud-score", run: async (order: Order) => fraud.score(order) });
export const creditCheck = task({ id: "credit-check", run: async (customer: string) => credit.check(customer) });

export const authorizePayment = task({
  id: "authorize-payment",
  run: async (order: Order) => {
    const { runs: [r, l] } = await batch.triggerByTaskAndWait([
      { task: fraudScore, payload: order },
      { task: creditCheck, payload: order.customer },
    ]);
    if (!r.ok || !l.ok) throw new Error("check failed");
    return payments.authorize(order, r.output, l.output);
  },
});
Each branch must become its own task (separate run); the parent fans out with batch.triggerByTaskAndWait and unwraps runs[i].ok / .output; the dependent call runs inline in the parent.

Restriction: triggerAndWait docs say 'Don't use this in parallel, e.g. with Promise.all()' - so the natural Promise.all form is forbidden. batch.triggerByTaskAndWait DOES accept two different tasks, typed per position (runs[0].output typed as childTask1's output), so the diamond is expressible, but only by promoting each branch to a separately triggered task run and reading results positionally with ok-checks. Task-wait calls must be inside a task. Grouping is by hand (the batch array).

How this was adaptedDocs' batch.triggerByTaskAndWait example uses childTask1/childTask2 with object payloads; adapted names/payloads. Import path assumes SDK v4 (@trigger.dev/sdk); v3 used @trigger.dev/sdk/v3. authorize is a plain call in the parent (could be a third task via triggerAndWait).

docs

Conductorjson
{ "name": "authorize_payment", "version": 1, "schemaVersion": 2, "tasks": [
  { "name": "fork", "taskReferenceName": "fork_ref", "type": "FORK_JOIN", "forkTasks": [
    [ { "name": "fraud_score", "taskReferenceName": "fraud_ref", "type": "SIMPLE",
        "inputParameters": { "order": "${workflow.input.order}" } } ],
    [ { "name": "credit_check", "taskReferenceName": "credit_ref", "type": "SIMPLE",
        "inputParameters": { "customer": "${workflow.input.order.customer}" } } ] ] },
  { "name": "join", "taskReferenceName": "join_ref", "type": "JOIN",
    "joinOn": ["fraud_ref", "credit_ref"] },
  { "name": "authorize", "taskReferenceName": "authorize_ref", "type": "SIMPLE",
    "inputParameters": { "order": "${workflow.input.order}",
      "risk": "${fraud_ref.output.risk}", "limit": "${credit_ref.output.limit}" } } ] }
A FORK_JOIN with one inner list per branch, a mandatory JOIN listing the task refs in joinOn, ${ref.output.x} wiring into the third task, plus a registered task definition and a polling worker for each SIMPLE task.

Restriction: 'The Fork task must be followed by a Join'; joinOn must name the refs to wait for - if omitted the Join proceeds without waiting (join-task page); the Fork itself has no output, so the dependent task reads each branch by ${taskRef.output.key} (worker outputs are key/value maps). Grouping is by hand (you author the fork block); the DAG is not derived from data references.

How this was adaptedShape follows the docs' notification fork/join example; SIMPLE task names, input wiring and output keys (risk, limit) adapted. joinOn semantics taken from the conductor-oss join-task page.

docs

Cadencego
func AuthorizePayment(ctx workflow.Context, order Order) (Auth, error) {
	ao := workflow.ActivityOptions{ScheduleToStartTimeout: time.Minute, StartToCloseTimeout: time.Minute}
	ctx = workflow.WithActivityOptions(ctx, ao)
	riskF := workflow.ExecuteActivity(ctx, FraudScore, order)
	limitF := workflow.ExecuteActivity(ctx, CreditCheck, order.Customer)
	var risk Risk
	var limit Limit
	if err := riskF.Get(ctx, &risk); err != nil { return Auth{}, err }
	if err := limitF.Get(ctx, &limit); err != nil { return Auth{}, err }
	var auth Auth
	err := workflow.ExecuteActivity(ctx, Authorize, order, risk, limit).Get(ctx, &auth)
	return auth, err
}
ActivityOptions with timeouts on the context; each call is a registered activity; start both with ExecuteActivity (returns a Future) and call Get on each later, with an error check per Future.

Limits: None found for the shape. Docs caution only that very large fan-outs can exceed the per-workflow pending-activity limit (use Batch Future). Workflow code must be deterministic (replay). Grouping is by hand (start-then-Get ordering).

How this was adaptedDocs show the options struct with cadence.ActivityOptions/cadence.WithActivityOptions aliases (older import) and describe the 'call ExecuteActivity repeatedly, Get later' pattern in prose; the code is assembled from those.

docs

Dapr Workflowtypescript
import { WorkflowContext, WorkflowActivityContext, TWorkflow } from "@dapr/dapr";

const fraudScore = async (_: WorkflowActivityContext, order: Order) => fraud.score(order);
const creditCheck = async (_: WorkflowActivityContext, customer: string) => credit.check(customer);
const authorize = async (_: WorkflowActivityContext, a: AuthInput) => payments.authorize(a.order, a.risk, a.limit);

const authorizePayment: TWorkflow = async function* (ctx: WorkflowContext, order: Order): any {
  const tasks = [ctx.callActivity(fraudScore, order), ctx.callActivity(creditCheck, order.customer)];
  const [risk, limit] = yield ctx.whenAll(tasks);
  return yield ctx.callActivity(authorize, { order, risk, limit });
};
// plus: new WorkflowRuntime(...).registerWorkflow(authorizePayment).registerActivity(...) x3
Activities as functions taking a WorkflowActivityContext and a single input; a generator workflow using yield ctx.callActivity and yield ctx.whenAll; registration of the workflow and each activity on a WorkflowRuntime.

Limits: No shape-specific restriction found. Durable-Task style: the orchestrator replays from the top, so it must be deterministic and must use ctx.whenAll (a Task combinator) rather than Promise.all; an activity takes one input, so authorize receives one object. Grouping is by hand. Note: the official SDK table does not clearly mark Workflow for JavaScript (Go, .NET, Python, Java are the marked ones).

How this was adaptedAdapted from the docs' JavaScript fan-out/fan-in tab (homogeneous ctx.callActivity map + ctx.whenAll) to two different activities; activity/registration shapes from the same tab. JS workflow SDK maturity not stated on the docs.

docs

AWS Step Functionsasl json
{ "StartAt": "Checks", "States": {
  "Checks": { "Type": "Parallel", "ResultPath": "$.checks", "Next": "Authorize",
    "Branches": [
      { "StartAt": "FraudScore", "States": { "FraudScore": { "Type": "Task",
          "Resource": "arn:aws:lambda:REGION:ACCOUNT:function:FraudScore", "End": true } } },
      { "StartAt": "CreditCheck", "States": { "CreditCheck": { "Type": "Task", "InputPath": "$.order.customer",
          "Resource": "arn:aws:lambda:REGION:ACCOUNT:function:CreditCheck", "End": true } } } ] },
  "Authorize": { "Type": "Task", "End": true,
    "Resource": "arn:aws:lambda:REGION:ACCOUNT:function:Authorize",
    "Parameters": { "order.$": "$.order", "risk.$": "$.checks[0]", "limit.$": "$.checks[1]" } } } }
A Parallel state with one self-contained sub-state-machine per branch, ResultPath to keep the original input, and JSONPath Parameters that pick each branch result by array index; each call is a separately deployed Lambda (or service integration).

Restriction: the Parallel state's output is 'an array with one element for each branch', so the dependent state addresses results positionally ($.checks[0], $.checks[1]); without ResultPath the array replaces the input and order is lost. Each branch must be self-contained (no transitions in or out). If any branch fails the whole Parallel fails and all branches stop (Lambdas already running keep running). Grouping is by hand (you author the Parallel block).

How this was adaptedBuilt on the docs' two-branch Parallel example (LookupAddress/LookupPhone); ResultPath + Parameters (JSONPath mode) are standard ASL fields listed on the page; ARNs are placeholders.

docs

AWS Lambda durable functionstypescript
import { withDurableExecution, DurableContext } from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(async (event: { order: Order }, context: DurableContext) => {
  const { order } = event;
  const checks = await context.parallel("checks", [
    async (ctx) => ctx.step("fraud-score", async () => fraud.score(order)),
    async (ctx) => ctx.step("credit-check", async () => credit.check(order.customer)),
  ]);
  checks.throwIfError();
  const [risk, limit] = checks.getResults() as [Risk, Limit];
  return context.step("authorize", async () => payments.authorize(order, risk, limit));
});
Handler wrapped in withDurableExecution; each call wrapped in a named ctx.step; the two branches go through context.parallel (each branch a function of a child DurableContext); BatchResult unwrapped with throwIfError/getResults.

Restriction: context.parallel returns a BatchResult whose getResults() is positional and typed with a single result type - the reference says 'All branches must return the same type' (Go offers ParallelMixed for different types), so in TS two heterogeneous branches need a union type or a cast. Branch failures are captured in the BatchResult and only rethrown by throwIfError(). Whether raw Promise.all over context.step is safe is not documented; the documented route is parallel/map. Durable operations cannot be nested inside a step callback. Grouping is by hand.

How this was adaptedAdapted from the reference's three-branch context.parallel example (homogeneous string results); the cast for heterogeneous results is my adaptation.

docs

Azure Durable Functionstypescript
import * as df from "durable-functions";

df.app.orchestration("authorizePayment", function* (context) {
  const order: Order = context.df.getInput();
  const [risk, limit] = yield context.df.Task.all([
    context.df.callActivity("fraudScore", order),
    context.df.callActivity("creditCheck", order.customer),
  ]);
  return yield context.df.callActivity("authorize", { order, risk, limit });
});
df.app.activity("fraudScore", { handler: (order: Order) => fraud.score(order) });
df.app.activity("creditCheck", { handler: (customer: string) => credit.check(customer) });
df.app.activity("authorize", { handler: (a: AuthInput) => payments.authorize(a.order, a.risk, a.limit) });
Generator orchestrator with yield; each call registered as a named activity and invoked by string name with one input; parallel branches via context.df.Task.all.

Restriction: 'orchestrator functions should use context.df.Task.all and context.df.Task.any instead of Promise.all and Promise.race'; the orchestrator is replayed from the start and must be deterministic; callActivity passes a single input, so authorize takes one object. Grouping is by hand.

How this was adaptedDocs example (Node v4 model) is a homogeneous fan-out over files with Task.all; adapted to two different activities.

docs

Cloudflare Workflowstypescript
import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers";

export class AuthorizePayment extends WorkflowEntrypoint<Env, { order: Order }> {
  async run(event: WorkflowEvent<{ order: Order }>, step: WorkflowStep) {
    const { order } = event.payload;
    const [risk, limit] = await Promise.all([
      step.do("fraud score", async () => fraud.score(order)),
      step.do("credit check", async () => credit.check(order.customer)),
    ]);
    return step.do("authorize", async () => payments.authorize(order, risk, limit));
  }
}
A WorkflowEntrypoint class with run(event, step); each call wrapped in step.do with a name; Promise.all for the branches; plus a wrangler binding for the workflow.

Limits: None that blocks the shape. Rules: step names are the cache key and must be deterministic (no timestamps/randomness in names); state must come only from step return values (which must be serializable); Promise.race/any over step.do should be wrapped in one outer step.do (does not apply to Promise.all). Rules also say not to call separate services in one step - consistent with three steps. Grouping is by hand.

How this was adaptedRules page shows Promise.all over two step.do calls (the cat-image example); class skeleton from the Workers API page; names adapted.

docs

Vercel Workflowtypescript
async function fraudScore(order: Order) { "use step"; return fraud.score(order); }
async function creditCheck(customer: string) { "use step"; return credit.check(customer); }
async function authorize(order: Order, risk: Risk, limit: Limit) {
  "use step"; return payments.authorize(order, risk, limit);
}

export async function authorizePayment(order: Order) {
  "use workflow";
  const [risk, limit] = await Promise.all([fraudScore(order), creditCheck(order.customer)]);
  return authorize(order, risk, limit);
}
"use step" directive on each call's function and "use workflow" on the body (compiled by the Workflow SDK); Promise.all for the branches.

Limits: None found for the shape. Documented: step inputs/outputs must be serializable; Promise.all rejects on first failure (use allSettled for partial); Promise.race does not cancel losers; very wide fan-out should be chunked; split runs before they grow past a few thousand events. Grouping is by hand.

How this was adaptedDocs example is Promise.all over three declared step functions (fetchUser/fetchOrders/fetchPreferences) with declare stubs; the "use step" bodies are written out here. Docs moved from useworkflow.dev to workflow-sdk.dev; GA status of the SDK not checked.

docs

Prefectpython
from prefect import flow, task

@task
def fraud_score(order): return fraud.score(order)
@task
def credit_check(customer): return credit.check(customer)
@task
def authorize(order, risk, limit): return payments.authorize(order, risk, limit)

@flow
def authorize_payment(order):
    risk = fraud_score.submit(order)
    limit = credit_check.submit(order["customer"])
    return authorize.submit(order, risk, limit).result()
@task on each call and @flow on the body; .submit() to get concurrency (a direct call runs synchronously); futures passed straight into the dependent task, .result() only on the terminal one.

Limits: None found for the shape - the join is inferred from the futures passed as arguments ('the downstream task won't receive the PrefectFuture'). Constraints: concurrency is opt-in via .submit() (the developer still chooses which calls go concurrent); futures must be resolved before returning from the flow; with a bounded ThreadPoolTaskRunner, calling .result() on a child from inside a parent task can deadlock; default thread runner needs thread-safe globals.

How this was adaptedCombined the docs' submit-then-pass-future example (say_hello -> print_result) with two submitted parents; names adapted.

docs

Apache Airflowpython
from airflow.sdk import dag, task

@dag(schedule=None, params={"order": {}})
def authorize_payment():
    @task
    def fraud_score(params=None): return fraud.score(params["order"])
    @task
    def credit_check(params=None): return credit.check(params["order"]["customer"])
    @task
    def authorize(risk, limit, params=None): return payments.authorize(params["order"], risk, limit)

    authorize(fraud_score(), credit_check())

authorize_payment()
@dag/@task decorators; the order arrives as a DAG param read from the injected context; passing return values (XComArgs) as arguments declares the edges.

Limits: None found for the shape - TaskFlow infers fraud_score >> authorize and credit_check >> authorize from the arguments; the parallelism is not hand-grouped. Constraints: values passed between tasks go through XCom and must be serializable (built-ins, dataclasses, or custom serialize/deserialize); each task is a separately scheduled task instance (scheduler latency per step); DAG structure is fixed at parse time.

How this was adaptedDocs example is compose_email(get_ip()); extended to two upstream tasks. Reading params via a named context kwarg is the documented context-injection mechanism, though the page does not show params specifically.

docs

LangGraphtypescript
import { StateGraph, Annotation, START, END } from "@langchain/langgraph";

const State = Annotation.Root({
  order: Annotation<Order>, risk: Annotation<Risk>, limit: Annotation<Limit>, auth: Annotation<Auth>,
});

export const graph = new StateGraph(State)
  .addNode("fraud", async (s) => ({ risk: await fraud.score(s.order) }))
  .addNode("credit", async (s) => ({ limit: await credit.check(s.order.customer) }))
  .addNode("authorize", async (s) => ({ auth: await payments.authorize(s.order, s.risk, s.limit) }))
  .addEdge(START, "fraud").addEdge(START, "credit")
  .addEdge("fraud", "authorize").addEdge("credit", "authorize")
  .addEdge("authorize", END)
  .compile();
A state schema with one channel per value; each call becomes a node returning a partial state update; edges from START fan out and two edges fan in.

Restriction: fan-in by separate edges works here only because both branches are one node long and finish in the same superstep; with branches of different lengths the target 'runs more than once' - the docs say to use a list-form edge (add_edge([a, b], target)) or defer=True, and a list-form target never runs (no error) if a listed branch is skipped. Parallel branches writing the same key need a reducer (here keys differ, so none). Graph API derives parallelism from edges; no hand grouping.

How this was adaptedDocs' JS fan-out example uses StateSchema/ReducedValue and a->b,c->d; adapted to START fan-out with Annotation.Root (documented alternative state definition) and distinct keys. The list-form edge / defer rule is quoted from the Python version of the same page.

docs

Raypython
import ray

@ray.remote
def fraud_score(order): return fraud.score(order)
@ray.remote
def credit_check(customer): return credit.check(customer)
@ray.remote
def authorize(order, risk, limit): return payments.authorize(order, risk, limit)

risk_ref = fraud_score.remote(order)
limit_ref = credit_check.remote(order["customer"])
auth = ray.get(authorize.remote(order, risk_ref, limit_ref))
@ray.remote on each function, .remote() to launch, object refs passed into the dependent task, ray.get on the final ref.

Limits: None found for the shape - passing refs as arguments makes Ray wait for both before running authorize (the dependency is inferred from data, no hand grouping). Not a durable workflow engine: Ray Core retries failed tasks but has no checkpointed workflow state across driver failure.

How this was adaptedDocs show passing one object ref into a second task; extended to two refs. No durability layer used (Ray Workflows is not part of this pattern).

docs

The short version

Plain code, no SDK constructs

Nearly every engine asks you to write its constructs: activities, step.run, ctx.run, decorators, directives, DAGs or JSON state machines. Golem is the exception that comes closest: plain code compiled to WebAssembly, durable with "no annotations, step functions, or activity wrappers". Jaal reads plain Java too (Python in progress), and adds a compiler that refuses what cannot be made durable, by name.

Parallelism without writing it

In every product we checked, parallel work is written out: futures, Promise.all, asyncio.gather, DAG edges, Parallel or Map states, ensemble nodes. In Jaal, calls that do not depend on each other run together because the compiler proved they can.

Derive, not vibe: the graph comes from the code

Elsewhere you declare the graph: nodes and edges in LangGraph, JSON states in Step Functions and Conductor, DAGs in Airflow, YAML in KServe, ensemble configs in Triton. Jaal derives it from the code you wrote, keeps it in step with every edit, and when you change the graph (make two steps a sequence) it changes the code for you.

No determinism rules

Temporal and Cadence require deterministic workflow code; Cloudflare and Hatchet's durable tasks set determinism rules, and Vercel Workflow replays in a deterministic sandbox. Jaal works out what is deterministic itself and records any call it cannot see into, so a clock read or a random number replays exactly, with nothing for you to avoid.

Resume without calling again

Temporal re-runs workflow code from the start against its history; Azure Durable Functions replays from its event history; Inngest and AWS Lambda durable functions re-run the handler with stored step results. Jaal continues from the last recorded step and never makes a settled call again. Rolling back one step and replaying only what depends on it, from the derived graph, is designed, not built.

Where others are ahead

Temporal has eight language SDKs and years in production. Several products have built-in compensation (Cloudflare, Conductor, Prefect, Golem) and run replay or time travel (LangGraph, Inngest, DBOS, Azure, Restate). Jaal's backtest of a changed version over recorded runs is still planned.

Durable execution and workflow engines

ProductHow you write itDurabilityParallel workCompensation (sagas)Replay and debuggingVersions of running workLicense and hosting
JaalPlain Java (Python in progress); a compiler derives the graphEvery step a transaction in Postgres; a crashed process is finished by anotherAutomatic: independent calls proven by the compiler; a parallel loop is one annotationDeclared per service and caller (the effect contract): built (the caller in code today)Replay after a crash from the record: built. A run's graph and timeline: built. Stepping through a run, backtest: plannedYes: a version is its contentPrivate beta
TemporalSDK constructs: deterministic Workflows plus Activities; Go, Java, Python, TS, .NET, Ruby, PHP, RustEvent History; code replayed from the startExplicit: futures, Promise.all, asyncio.gatherDocumented pattern (Saga page)Replayer over a downloaded history; VS Code replay debugger (TypeScript)Yes: Worker Versioning, patchingMIT server; Temporal Cloud
RestateSDK constructs: handlers, virtual objects, workflows, ctx.run; TS, Java, Kotlin, Python, Go, RustJournal per invocationExplicit: DurableFuture.all, SelectDocumented pattern, in regular codeRestart an invocation from a point in its journal; UI and CLIYes: immutable deploymentsServer BSL 1.1; Restate Cloud
InngestSDK constructs: step.run and other steps; TS, Python, GoStep results memoized outside the functionExplicit: Promise.all over stepsnot documentedRerun from a chosen step; bulk replayStep-ID memoizationServer SSPL with delayed Apache-2.0; cloud or self-host
GolemPlain code compiled to WebAssembly; Rust, TS, Scala, MoonBit, GoOplog at the WebAssembly boundaryExplicit: fan out to child agents, fork()Built in: transactions (not MoonBit)Oplog viewer; undo agent stateYes: automatic or manual agent updateBSL 1.1; Golem Cloud
ResonateSDK constructs: generators with context.run; TS, Python, Go, Java, RustDurable promisesExplicit: futures, Promise.allnot documentednot documentednot documentedApache-2.0
DBOSLibrary: annotated workflows and steps; Python, TS, Go, JavaCheckpoints in PostgresExplicit: durable queuesnot documentedFork a workflow from a chosen stepYes: app versions, patchingMIT; DBOS Cloud, Conductor console
HatchetSDK constructs: tasks, DAGs, durable tasks; Python, TS, Go, RubyDurable event log in PostgresExplicit: DAG dependencies, child tasksnot documentedReplay a run (new run, same input)not documentedMIT; Hatchet Cloud or self-host
Trigger.devSDK constructs: task(), triggerAndWait; TypeScriptCheckpoint and restore of the task (CRIU)Explicit: batch triggernot documentedReplay a run on the latest codeYes: a run is locked to its versionApache-2.0; cloud or self-host
Conductor (Orkes)JSON workflow definitions; workers in Java, Python, JS, Go, C#Executions persisted on the serverDeclarative: fork/join, dynamic forkBuilt in: failureWorkflowRerun from a chosen taskYes: pinned to a definition snapshotApache-2.0; Orkes. The Netflix repository was archived in Dec 2023
CadenceSDK constructs: workflows plus activities; Go, JavaEvent sourcing, history replaynot documentedDocumented (orchestration use case)Replayer, replay shadowingYes: GetVersionApache-2.0; CNCF Sandbox
Dapr WorkflowSDK constructs: workflows plus activities; Python, JS, .NET, Java, GoHistory in a state storeExplicit: when_all, Task.WhenAllDocumented patternEvent history in the dev dashboardYes: patching, named versionsApache-2.0; CNCF graduated
AWS Step FunctionsJSON state machines (Amazon States Language)Standard: exactly-once, up to a year; Express: at-least-onceDeclarative: Parallel and Map statesDocumented pattern via CatchRedrive failed runs within 14 daysYes: versions and aliasesProprietary; AWS only
AWS Lambda durable functionsSDK constructs: DurableContext step, wait, invoke; Node, Python, Java, Go, .NETCheckpoint log; handler re-run with stored resultsExplicit: parallel, mapnot documentedHistory kept 1 to 90 daysYes: pinned to the starting versionProprietary; AWS only
Azure Durable FunctionsSDK constructs: orchestrator, activity, entity functions; C#, JS, TS, Python, PowerShell, JavaEvent-sourced history replayExplicit fan-out/fan-inDocumented patternRewind a failed instanceYes: version settingsMIT extension; Azure
Cloudflare WorkflowsSDK constructs: step.do, step.sleep; TS, JS, Python (beta)Step results persisted by step nameExplicit: Promise.all; Python DAGBuilt in: rollback on a stepnot documentednot documentedProprietary; Cloudflare
Vercel WorkflowDirectives "use workflow", "use step"; TypeScriptEvent log, deterministic replayExplicit: Promise.allDocumented pattern (saga)not documentedYes: pinned to its deploymentApache-2.0; Vercel, Postgres or local
PrefectDecorators @flow, @task; PythonResult persistence and caching (off by default)Explicit: .submit(), task runnersBuilt in: transactions with rollback hooksnot documentedDeployment versions with rollbackApache-2.0; Prefect Cloud
Apache AirflowPython DAG definitionsTask state; retry or clear and rerunDeclarative: DAG dependenciesnot documentedRerun with the original code bundleYes (Airflow 3): versioned bundlesApache-2.0; managed by third parties
DagsterDecorators: assets, ops, jobs; PythonRe-execute from failureThrough the executor, over the graphnot documentedRe-execute from failurenot documentedApache-2.0; Dagster+
n8nVisual node editor; code nodes in JS or PythonExecution data saved; retry resumesnot documentednot documentedRetry with the original or current workflowWorkflow history; the published version runsSustainable Use License; cloud or self-host

Also in the field, not compared cell by cell for lack of documentation we could confirm: Flyte (Union.ai), Upstash Workflow.

Process automation and integration platforms

BPM suites, low-code automation and integration platforms. Almost all are defined in BPMN, a low-code designer or JSON/YAML; Camel and the Flowable engine are code-first. None derives the process from ordinary program code.

ProductHow you define itDurabilityParallel workCompensationVersions of running workLicense and hosting
JaalPlain Java; the graph derived by a compiler; steps can be made a sequence on the graphEvery step a transaction in Postgres; a crash finished by another processAutomatic: proven by the compilerDeclared per service and caller (the effect contract): builtHot swap inside the type contract: built (Eclipse tooling); a new contract via a router: plannedPrivate beta
Camunda 8BPMN models; job workers in Java, TypeScript, Python, C#Replicated log (Raft), state in RocksDB with snapshotsBPMN parallel gateway; multi-instanceBuilt in: BPMN compensation eventsExplicit instance migration, mapped per active elementSaaS or self-managed; Camunda License v1, Enterprise for production
Pega PlatformLow-code case types and process flowsCases saved to database tables; queue processors retrySplit Join, Split For Each shapesnot documentedNo automatic migration; manual strategiesProprietary; Pega Cloud, client cloud, on-premises
AppianLow-code Process Modeler (BPMN-style)In-memory engines persisting a transaction log to diskAND gatewaynot documentedRunning processes stay on their version; Process Upgrade via the Java APIProprietary; Appian Cloud or self-managed on Kubernetes
IBM Business Automation WorkflowBPMN in Process Designer; BPEL in the Advanced configurationInstances kept in its databasesnot documentedDocumented for BPEL processes onlyInstances stay on their snapshot or migrateProprietary; traditional install or containers
MuleSoft AnypointMule flows in XML, built visuallynot documentedScatter-Gather (parallel routes)Pattern: Try scope and error handlersnot documentedMule Kernel CPAL; Enterprise proprietary; CloudHub, Runtime Fabric
Azure Logic AppsVisual designer or JSON workflow definitionsStateful: every action's state in external storage; runs up to 90 days by defaultParallel branchesPattern: retry policies, run-after, scopesnot documentedProprietary; Consumption, Standard, hybrid
Power AutomateLow-code cloud flow designerRuns last up to 30 daysParallel branchesPattern: configure run afterDrafts and version historyProprietary SaaS (Power Platform)
Google Cloud WorkflowsYAML or JSON stepsExecutions keep state and wait up to a yearParallel step with branches; parallel fornot documentedRunning executions unaffected by an updateProprietary, managed on Google Cloud
UiPath MaestroBPMN 2.0 and DMN; Flow; Case ManagementPause, resume and retry of long-running instancesParallel and inclusive gateways; multi-instanceNot executed (per its BPMN support matrix)Instance migration to another package versionProprietary; Automation Cloud
ServiceNow Workflow StudioLow-code flows, subflows and actionsWaiting flows serialized into context recordsParallel block; one flow runs on a single threadPattern: flow error handlernot documentedProprietary SaaS
BoomiLow-code visual process canvasRetry schedules re-run failed documentsFlow Control step across threads or processesPattern: retries and error pathsnot documentedProprietary; Boomi cloud or self-hosted runtime
FlowableBPMN, CMMN, DMN engines in Java; low-code designer (commercial)Wait states committed to the database as jobs; retriesBPMN parallel gateway; multi-instanceBuilt in: BPMN compensation eventsProcess migration service (one or all instances)Engine Apache 2.0; commercial on-prem or cloud
TIBCO BPM EnterpriseBPMN in Business StudioProcess instances kept in a databasenot documentedCompensation events (documented in 4.3.2)Migration at design-time migration pointsProprietary; Kubernetes runtime
Apache CamelCode: Java, XML or YAML DSLsRoute state not persisted; Saga state via an external LRA coordinatorMulticast and Splitter parallel processingBuilt in: Saga EIP with compensation endpointsnot documentedApache 2.0, runs on any JVM

Not covered here: SAP Build Process Automation, Oracle Integration. AWS Step Functions is in the table above.

Sources

Each product's own documentation, blog or repository, read on 26 October 2026.