@gnldev/workflow has 460 recorded weekly downloads on npm, ranking #1560 of 2,322 tracked listings.
About @gnldev/workflow
@gnldev/workflow
Durable deterministic workflows — every step's result is journaled, so a crash → resume picks up where it left off instead of re-running the finished steps (at-most-once for side effects). Control flow: then / parallel / branch / foreach / loop. Suspendable: runResumable + sleep / waitFor (evented + scheduled).
Install:
pnpm add @gnldev/workflow— or use it from a repo clone:pnpm install && pnpm -r build.
npm i @gnldev/workflow # journal: @gnldev/durable
import { workflow, step } from '@gnldev/workflow';
import { SqliteStorage } from '@gnldev/durable/sqlite';
const fetchUser = step('fetchUser', async (id: number) => ({ id, name: 'Ada' }));
const greet = step('greet', async (u: { name: string }) => `Hello ${u.name}`);
const wf = workflow<number>().then(fetchUser).then(greet);
const out = await wf.run(1, { runId: 'w1', journal: new SqliteStorage('runs.db').runs });
// Crash → run again with the same runId → completed steps come back from the journal, continuing where it left off.
API
workflow<I>()→ builder:.then(step)·.parallel([a, b])·.branch(pred, ifStep, elseStep)·.foreach(...)·.loop(...)step(id, async (input, ctx) => out)— every step journaled; a completed step is replayed, not re-run, whatever it returned (undefinedincluded)- a step id is unique within one workflow:
.then(inc).then(inc)throws at build time. Reuse the body under a second id:.then(step('inc-2', inc.run)). The two arms of abranchand a nested workflow (asStep) may reuse an id wf.run(input, { runId, journal })— one-shotwf.runResumable(input, ctx)— for suspending steps (sleep/waitFor); suspends withWorkflowSuspended, resumes on event/time.
How it works
Each step's output is journaled under ${runId}:wf:${stepId} → the step doesn't rerun on replay. An output of undefined is stored as { __gnlStepRecord: 'void' } (a stored undefined reads back like a missing key), and an output that itself has a __gnlStepRecord key is stored wrapped, so every output replays as exactly what the step returned. Reading step records yourself? decodeStepRecord(v) from @gnldev/durable gives back the output; isVoidStepRecord(v) recognises the void record. Two step uses that would share one journal key — the same id twice, a top-level pay:check next to asStep('pay') with an inner check, a step named like a retry's <id>:attempts, or one of the engine's own keys (_suspend, _canceled, _input, _resume:*) — are refused where the workflow is built, and the error names the rename that separates them. parallel sub-steps are journaled individually; branch/loop preserve deterministic replay.
Side-effect steps (the crash window, closed)
Plain steps journal their output AFTER running — a crash between an external effect (an HTTP POST, a charge) and the journal write leaves no record, and the resume re-fires the effect. Declare the effect and the engine writes a write-ahead claim before executing:
step('charge', async (input, ctx) => {
// ctx.idempotencyKey === `${runId}:wf:charge` — carry it to the provider (Stripe Idempotency-Key)
// and the journal's dedup extends downstream, to the provider itself.
return chargeCard(input, { idempotencyKey: ctx.idempotencyKey });
}, {
sideEffect: true,
recover: async (input, { idempotencyKey }) => { // the crash-window answer: ask the provider
const found = await lookupCharge(idempotencyKey);
return found ? { done: true, output: found } : { done: false };
},
claimTtlMs: 60_000, // presume in-flight this long; calibrate ABOVE the step's worst case
});
Resume state machine: output present → replay; claim absent → run; claim live → StepRetryBlockedError
(state: 'in-flight', clears by itself); claim stale or the attempt threw → recover is asked first —
{done:true} journals the found output without re-running, {done:false} re-runs safely; no recover →
'unresolved': the engine refuses to guess. Manual exits for 'unresolved': effect LANDED →
journal.put(detail.key, output); effect NEVER fired → journal.put(detail.key + ':_claim', { startedAt: Date.now(), released: true }). A clean suspend re-runs on resume (the documented
contract); retry() carries the declaration and consults recover BETWEEN in-process attempts —
without recover, a side-effect step's first throw propagates instead of blind-re-firing.
Combinators (parallel/branch) carry the declaration through; foreach/loop bodies are bare
functions with NO durability surface — put the effect in a step(..., { sideEffect: true }) inside a
nested workflow (asStep) if an iteration needs the claim protocol.
License
Apache-2.0 — see LICENSE.
Description from the npm listing.
- ai
- agent
- llm
- typescript
- ai-sdk
- durable
- exactly-once
- orchestration
- steps
- saga
README badge
Show your rank in a README or on your website. The badge updates automatically.
Charts
Images: 30d · 1y · all time · 1600×900 dark
Adoption over time
7/30/90-day growth
Ranking history
Listing placement
Listing placement
Filled points are observed values; hollow points come from Internet Archive snapshots of the listing page. Dashed segments span periods with no observations — values between points are not interpolated.
Each band is one page of 20. Weekly downloads on a linear scale. Placement is projected from weekly downloads, so it can differ from the marketplace's own sort order. Hover a dot to see the listing.
Recent observations
Full history (1)Similar npm AI SDKs plugins
All 20 @gnldev/workflow alternativesData source
Weekly downloads (trailing 7 complete days) come from the official npm downloads API. Downloads include CI and mirrors and are not equivalent to installs or users. npm publishes no ratings.
Listing: https://www.npmjs.com/package/@gnldev/workflow. First seen Oct 20, 2026; last successful collection Oct 20, 2026. See the methodology.