Repository navigation
Proposal: abortable steps, buffered messages and browser support (happy to implement) #913
Description
Hi! I'm building a local-first project where everything runs on the user's own machine: there's no server. Most of it runs in a background process, and part of it runs entirely in the browser inside a Web Worker. The project needs a durable workflow engine for long-running jobs:
- AI generation steps that can take minutes;
- approval steps where a person responds hours or days later;
- agent steps that run for 30 minutes or more;
- many queued runs with limits per provider.
OpenWorkflow is the closest fit I've found. It's embeddable, needs no extra infrastructure, runs on SQLite through node:sqlite, and its step checkpointing is solid. I'd much rather use it and contribute than build around it.
Per CONTRIBUTING, here's what I'd need, before opening any PRs. Would you want these changes in OpenWorkflow? If so, I'm happy to implement them, one PR per item, with tests in testBackend where the backend is involved. Where an existing issue already covers something, I've linked it rather than opening a duplicate.
What I need most
1. Abortable steps and a bounded stop()
Today:
- A step function takes no arguments, and there's no
AbortSignalanywhere. cancelWorkflowRun()changes the run's status, but a running step continues until it finishes on its own; its result is then rejected as stale.worker.stop()waits for every active execution, with no upper bound.
With steps that run for 30+ minutes, cancel needs to actually stop the work, and shutting down can't wait indefinitely.
Proposal (additive):
await step.run({ name: "generate", timeout: "5m" }, async ({ signal }) =>
provider.generate(input, { signal }),
);
await worker.stop({ timeoutMs: 10_000 }); // abort remaining steps after 10 s- Step functions receive
{ signal }. Existing zero-argument functions keep working. - The signal aborts when:
- the run is canceled (the worker notices on its next heartbeat, or immediately if the cancel came through the same client);
- an optional per-step
timeoutpasses; stop({ timeoutMs })reaches its deadline.
- An aborted step is recorded as failed with an
AbortError. A step aborted by shutdown re-runs on the next claim, exactly as after a lease expiry today, so delivery semantics don't change.
2. Run-addressed, buffered messages (+1 to #894)
Today: sendSignal only reaches steps that are waiting at that moment. My approvals and completion callbacks race with the run reaching waitForSignal, the same race described in #877.
Proposal: the design in #894 covers my cases exactly: addressed to a run, buffered, FIFO, consumed once, with idempotent sends. I'd be glad to help implement it once you've settled on a design.
3. A browser-compatible core and a public Backend (+1 to #893)
Today: I need the worker to run inside a Web Worker, with a browser-side SQLite (WASM) backend. Two things block that:
worker/worker.tsandsqlite/sqlite.tsimportnode:crypto, and timers are typedNodeJS.Timeout.- The
Backendinterface is only reachable throughopenworkflow/internal.
Proposal:
- Use globals:
globalThis.crypto.randomUUID()/getRandomValues()(available in Node 19+, Bun and every browser), andReturnType<typeof setTimeout>for timers. - CI check: a browser smoke test, or a bundling check that fails on
node:*imports outsidesqlite/andpostgres/. - Backend export: land feat(openworkflow): export the backend API and test suite #893, so third-party backends can import
BackendandtestBackend. - The WASM backend: I'd maintain it out of tree, as you suggested for the Drizzle backend in Proposal: Drizzle backend (
openworkflow/drizzle) #880, unless you'd prefer it in-tree later.
I know Backend is still evolving. I'd pin exact versions and run testBackend in my CI.
Strongly wanted
4. Concurrency keys and priority (#20, #56)
Per-key limits, such as at most 2 concurrent runs per AI provider, plus a priority on runs. With SQLite and a single worker, this can be enforced inside the claim transaction. I saw you're weighing Postgres-only vs. Redis in #20. I'd be happy to start with an implementation in the claim query for SQLite and Postgres, if that fits.
5. Per-step timeouts
step.run({ name, timeout }), which goes hand in hand with item 1. Today only the run-level deadlineAt exists.
6. A run-event subscription API
Today, observers poll (result() polls every 1 s). Something like ow.subscribe({ workflowRunId } | { workflowName }, handler) that receives status changes and step completions would help. It would be event-driven within the same process, and fall back to polling across processes. It would drive a live UI and let a caller wait for a long run without busy-polling.
Nice to have
| # | Item | Related |
|---|---|---|
| 7 | Retention API: delete finished runs older than N days or beyond N per workflow | #303 |
| 8 | A configurable WORKFLOW_STEP_LIMIT, or a continueAsNew() for long-lived loops |
— |
| 9 | A parent-close policy for child runs (cancel, abandon or wait), also applied when a runWorkflow timeout passes. Today both leave the child running on its own; this would also address #714 |
#714 |
| 10 | Adding workflows to a running worker (your option 1 in #826 works for me) | #826 |
| 11 | Starting a linked child without waiting, e.g. step.startWorkflow() returning a handle the parent can await later. Today a child can only be started and awaited in one call, and starting a run from inside a step isn't linked to the parent |
— |
| 12 | Starting a run when another run finishes (a completion trigger, e.g. runWorkflow(spec, input, { after: workflowRunId })), so separate runs can be chained without a caller polling result() |
— |
Questions
- Do you want these changes in OpenWorkflow? Which fit your direction, and which would you rather stay outside the library?
- For item 1: are you OK with
{ signal }as the step-function argument, and with abort-on-cancel semantics? - For item 3: would you accept browser compatibility of the core and keep it covered in CI?
- For Proposal: messages, run-addressed and buffered (alongside signals) #894 and item 4: should I wait for your design, or propose PRs against a draft?
Thanks for building OpenWorkflow. I'd like to use it in production and contribute.
Activity