npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@demystify/workflow

v0.1.0

Published

The DAG plan shape and its scheduler for agent work. Validates the graph at parse time (dangling edges, duplicate ids, cycles named in the error), returns every step whose dependencies are satisfied so fan-out is possible, distinguishes blocked from faile

Readme

@demystify/workflow — the DAG plan shape and its scheduler

@demystify/agent-kernel runs a linear plan: Step[] and a cursor. Real agent work is a graph — fan out to six invoices, fan back in to rank them, branch when one fails, compensate what already ran. This package is that graph: the plan shape, its validation, and the pure function that says which steps are ready.

Zero runtime dependencies. Pure, deterministic, no clock, no I/O, no model call. Node ≥ 22, ESM, TypeScript strict.

What this is NOT

Not a durable execution runtime. Temporal, DBOS, Inngest and Restate exist, are mature, and solve a genuinely hard problem: surviving machines. Workers die mid-step, a process is evicted between two effects, a retry must not re-charge a card. None of that is solved here. If you need cross-machine durability, run one of those underneath this package and let it own the retries, the timers and the event history. This computes the next set of steps; something else keeps the process alive long enough to perform them.

Not a transport. Same rule as agent-kernel, for the same reason:

There is no sender here. No execute, no dispatch, no callback, no chokepoint.

The package computes which steps are ready. The host performs them. A chokepoint is still a place where sending happens, so it is still a target: compromise the plan and the chokepoint obediently sends. The stronger property is that nothing here holds a reference through which anything can leave the process. If there is nowhere for a message to go, no amount of prompt injection can make one be sent — not because a check refused, but because the capability was never in the room.

test/no-transport.test.ts enforces that rather than trusting it, and both routes back in were verified by deliberately re-introducing them:

adding `fetch(...)` to the scheduler          -> ✗ "never uses fetch"
adding `onReady: (step) => void` to a type    -> ✗ "declares no function-typed
                                                   field — a callback IS a transport"
adding `notify()` to an exported interface    -> ✗ "declares no field or method
                                                   whose name implies an outbound effect"

Not a queue, a worker pool, or a retry policy. It tells you six steps are ready. How many you run at once, on what threads, with what backoff, is yours.

Install

npm install @demystify/workflow

No keys, no database, no Docker, no env vars. It is a function of its arguments.

The loop

import { readySteps, workflowStatus, validateWorkflow } from "@demystify/workflow";

const plan = validateWorkflow({
  steps: [
    { id: "fetch", kind: "invoice.list", estimateMinor: 20 },
    { id: "score", kind: "risk.rank", dependsOn: ["fetch"], estimateMinor: 400 },
    { id: "draft", kind: "message.draft", dependsOn: ["fetch"], estimateMinor: 300 },
    { id: "review", kind: "human.review", dependsOn: ["score", "draft"] },
  ],
});

// The state is the host's, not the package's: three sets of ids.
let state = { succeeded: [] as string[], failed: [] as string[] };

for (;;) {
  const report = workflowStatus(plan, state);
  if (report.status !== "running") break;   // succeeded, blocked or failed

  const batch = readySteps(plan, state);    // may be SEVERAL — that is fan-out
  const results = await Promise.all(batch.map((step) => host.perform(step)));
  //                                              ^^^^ THE PACKAGE CANNOT DO THIS

  state = fold(state, batch, results);      // append ids to succeeded / failed
}

Between readySteps and the next call, the host does the work. The package never had a way to do it.

What it guarantees

| # | Guarantee | How | |---|---|---| | 1 | An invalid graph never half-runs | Dangling edges, duplicate ids, duplicate edges, bad compensators and cycles are all refused at parse time. A cycle found at step 40 has already spent money on 39 steps. | | 2 | A cycle is NAMED | dependency cycle: a -> b -> c -> a, and CycleError.cycle carries the closed path. "Cycle detected" sends a human to read a graph too large to read. | | 3 | Fan-out is a set, not a cursor | readySteps returns every step whose edges are satisfied. Six at once is normal. | | 4 | blocked is distinct from failed | Steps remain, none can ever run, nothing is in flight. A cursor-based runner reports this as "running" forever. | | 5 | Unreachable steps are LISTED | With the root cause on each, so a host renders "3 steps will never run because s2 failed" instead of hanging. | | 6 | Compensation is reported, never run | compensationPlan returns ids in reverse-topological order. Undoing is an effect; effects belong to the host. | | 7 | Deterministic | Pure function of (workflow, state). Same input, same output, on any machine, in any order. No clock, no RNG, no environment. | | 8 | Money is integer minor units | With an explicit currency, always. No float exists in the code. | | 9 | Structurally incapable of sending | See above. |

Conditional edges

An edge may be a bare string or an object with a condition:

dependsOn: ["fetch"]                              // shorthand for when: "succeeded"
dependsOn: [{ id: "charge", when: "failed" }]     // fires ONLY if charge failed
dependsOn: [{ id: "charge", when: "always" }]     // fires once charge settles, either way

when: "failed" is how compensation and rollback are expressed. The refund step becomes ready precisely because the charge failed — no separate error channel, no exception handler, just an edge in the same graph the rest of the run lives in.

An edge that can no longer be satisfied makes its step permanently unreachable, and that is reported rather than waited on: a when: "failed" step whose dependency succeeded will never run, and workflowStatus says so.

Status, and why blocked matters

const report = workflowStatus(plan, { succeeded: ["s1"], failed: ["s2"] });

report.status;        // "blocked"  — not "running", not "failed"
report.remaining;     // ["s3", "s4", "s5"]
report.unreachable;   // [{ id: "s3", because: "s2", reason: 'step "s3" needs "s2" to succeed, but it failed' }, …]
report.failed;        // ["s2"]
  • running — at least one step is ready now, or in flight.
  • blocked — nothing can run and at least one step never will. Terminal.
  • failed — every step settled and at least one failed. Terminal.
  • succeeded — every step settled successfully. Terminal.

Checked in that order, which is why a run whose compensation branch completed cleanly still reports failed: the compensation succeeding does not mean the workflow did.

because is the root cause, not the immediate parent. Three steps orphaned behind one failure all name that failure, which is the sentence a human wants.

Compensation

{ id: "refund", kind: "payment.refund", compensates: "charge",
  dependsOn: [{ id: "charge", when: "always" }] }
compensationPlan(plan, { succeeded: ["reserve", "charge"], failed: ["ship"] });
// [ { stepId: "charge",  compensatedBy: "refund" },
//   { stepId: "reserve", compensatedBy: "unreserve" } ]
  • Only steps that succeeded are compensated. A step that failed did not complete, and undoing work that never happened is how a rollback does damage of its own.
  • Ordered reverse-topologically: dependents are undone before what they depended on. There are no timestamps in this package, so the order comes from the graph — and is therefore deterministic.
  • Reversal is append-only in spirit. Compensating does not erase the compensated step's record: charge stays in state.succeeded after refund runs, because it did succeed and money moved. A reversed payment is two rows in a ledger, never zero.
  • The package does not run it. It cannot.

Cost

estimateCost(plan, { currency: "INR" });
// { total:          { amountMinor: 116, currency: "INR" },   // ₹1.16 if it all runs
//   criticalPath:   { amountMinor: 111, currency: "INR" },   // the chain parallelism cannot shorten
//   criticalPathIds: ["a", "b", "d"],
//   unknownSteps:   0 }

Two numbers because a DAG has two honest answers. Budgeting spend wants total; budgeting a serial resource — a rate-limited model, a wall clock — wants criticalPath.

Both are worst cases: conditions are ignored, so a when: "failed" compensation branch is counted even though a healthy run never takes it. That is the safe direction for a ceiling. Steps with no estimateMinor count as 0 and are reported in unknownSteps, so the figure is never read as more precise than it is.

Using it with @demystify/agent-kernel

There is no dependency between the two packages, deliberately. Step's id, kind, input, requires and estimateMinor fields mirror the kernel's so the same object can be handed to either, but the types are copied rather than imported: the graph logic stands alone and neither package version-locks the other.

A host that wants both runs the DAG here and the ledger there — readySteps picks the batch, the kernel records each step, and the capability gate and cost ceiling stay where they already are.

Standards

  • Agnostic core. No database, no HTTP client, no framework, no identity model.
  • Zero-config, keyless, offline. Nothing to configure; nothing to mock.
  • Money is integer minor units with the currency alongside. No float exists in the code, and a non-integer estimate is rejected at the boundary — and again in estimateCost, for a workflow assembled in code rather than parsed.
  • Nothing mutates its inputs. Frozen workflows and frozen states work.

Known limits

  • In-memory only. State is the host's to persist. This package stores nothing and remembers nothing between calls — which is exactly what makes it resumable.
  • No timers, no sleep, no scheduling in time. "Run this in 10 minutes" is a runtime concern. See "not a durable execution runtime".
  • No dynamic fan-out. The graph is fixed at parse time; a step cannot spawn N children discovered at run time. Build the wider graph and refuse the steps you do not need, or parse a new workflow for the second phase.
  • No cost ceiling enforcement. estimateCost reports; it does not halt. Halting on spend is agent-kernel's job, and it does it before each step.
  • unknownSteps > 0 means both cost figures are floors, not forecasts.

Testing

pnpm test   # 137 tests, 100% statements

Coverage thresholds are enforced by pnpm test, not merely configured. The no-transport suite was verified red-green: each route back to a sender was deliberately re-introduced and confirmed to fail the build, not merely to pass with it absent.

MIT.