Skip to content

Proposal: abortable steps, buffered messages and browser support (happy to implement) #913

Open

Description

@Zyles

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 AbortSignal anywhere.
  • 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 timeout passes;
    • 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.ts and sqlite/sqlite.ts import node:crypto, and timers are typed NodeJS.Timeout.
  • The Backend interface is only reachable through openworkflow/internal.

Proposal:

  • Use globals: globalThis.crypto.randomUUID() / getRandomValues() (available in Node 19+, Bun and every browser), and ReturnType<typeof setTimeout> for timers.
  • CI check: a browser smoke test, or a bundling check that fails on node:* imports outside sqlite/ and postgres/.
  • Backend export: land feat(openworkflow): export the backend API and test suite #893, so third-party backends can import Backend and testBackend.
  • 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

  1. Do you want these changes in OpenWorkflow? Which fit your direction, and which would you rather stay outside the library?
  2. For item 1: are you OK with { signal } as the step-function argument, and with abort-on-cancel semantics?
  3. For item 3: would you accept browser compatibility of the core and keep it covered in CI?
  4. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions