My ERP Approval npm Library Passed 1,200 Weekly Downloads

Photo by Screenshot: the hierarchical-approval page on npm, 7 September 2026
It is an MIT-licensed, TypeScript-first engine for multi-level approval workflows in enterprise systems, aimed at developers building ERP-style flows such as purchase orders, expense claims and contract sign-off. You define a named template once and reuse it for every document of that type. It is multi-tenant, audit-ready and has only two runtime dependencies.
Yes, since version 1.0.0. Levels that share a group name open together and join before the chain advances, so Finance and Legal can review the same contract concurrently and the CEO level stays waiting until every branch approves. Rejecting any branch rejects the whole instance, and a group may also lead the chain, in which case it opens at submit.
An instance now exposes openLevels, an ascending list of every level currently collecting decisions, because a single currentLevel number could not describe the frontier once a parallel group had several branches open. currentLevel remains as the lowest open level for display and audit. On a sequential template the two always agree, so most callers need no change; move to openLevels anywhere you decide who may act or what is overdue.
Two mechanisms. Optimistic locking on a version field with a configurable retry policy handles two approvers acting at the same moment, and built-in idempotency keyed by tenant, document and template means a double-click or a network retry returns the existing instance rather than creating a second one. Every mutation is also written to an immutable audit log with an old and new state diff.
No, and npm says so openly. The counter records HTTP 200 responses served for tarball files, which includes mirrors, CI runners and analysis robots, and publishing a version guarantees a burst because every mirror fetches the new tarball. It is a directional indicator of activity rather than a user count, which is why the durable signals for this package are its 404 passing tests and its open issues.

Photo by Screenshot: the hierarchical-approval page on npm, 7 September 2026
I published version 0.1.0 of hierarchical-approval on 21 June, mostly to stop rebuilding the same approval chain by hand. I had written that code three times in eighteen months for three different ERP projects, and on the third time I extracted it instead. On 7 September the npm page read 1,204 weekly downloads and version 4.0.0. That is a good week, and it is worth writing down what got there.
This post is the eleven-week story: what the library does, the release that made the API stable, the release that fixed the data model rather than the sixth symptom of it, and an honest note on what an npm counter is and is not measuring. Every figure comes from the registry API or the changelog, both linked at the end.
The badge shows the last seven days. The range endpoint shows the whole arc, and the arc is a launch, a long quiet middle where I was building rather than shipping, and a September in which the library grew up. 2,620 downloads across the first eleven weeks, ending on the best week so far.
hierarchical-approval, downloads per week since launch
21 Jun 820 ███████████████████████████ 0.1.0 - 0.3.1
28 Jun 50 ██
05 Jul 26 █
12 Jul 5 ▏
19 Jul 286 ██████████ 0.4.0, 0.5.0
26 Jul 17 ▌
02 Aug 15 ▌
09 Aug 22 ▊
16 Aug 145 █████ 0.6.0
23 Aug 26 █
30 Aug 1,020 ██████████████████████████████████ 0.7.0 - 4.0.0
----------------------------------------------------------------
2,620 downloads across the first eleven weeks.
Latest seven-day window: 1,204. Latest version: 4.0.0.The quiet stretch in July and August is the part I would defend. Nothing was being installed because nothing new was being offered: the engine worked, but it could only do sequential chains, and I was writing the parallel-branch model that 1.0.0 shipped. A library that does one thing well and is honest about the rest is a better starting point than a wide one that is wrong in six places.
The premise has not changed since the first release. An approval chain is not business logic worth rewriting per project, it is infrastructure: a named template, a document moving through it, and a set of guarantees around concurrency and audit that everyone needs and nobody enjoys building. Each of these landed for a reason I hit in production first.
| The problem in every ERP I have worked on | What the library gives you | Since |
|---|---|---|
| Approval chains hardcoded per document type | Named templates, defined once and reused anywhere | 0.1.0 |
| Two approvers clicking at the same moment | Optimistic locking on a version field, with a configurable retry policy | 0.1.0 |
| A double-click or a network retry creating two requests | Idempotency keyed by tenant, document and template | 0.1.0 |
| Tests that need a real database and a real clock | ApprovalTestKit and an injectable ManualClock, zero I/O | 0.4.0 |
| Finance and Legal reviewing one after the other for no reason | Parallel branch groups that open together and join before the chain advances | 1.0.0 |
Around those sit six pluggable adapter interfaces, for notifications, metrics, audit, scheduling, authorisation and middleware, so the engine can write to Kafka, Datadog, BullMQ or whatever your platform already runs without knowing any of them exist. The audit log is immutable and records an old and new state diff on every mutation, which is the part compliance actually asks for.
Until 1.0.0 every chain was strictly sequential, so a genuinely concurrent step had to be faked. Finance and Legal review the same contract independently, and modelling that as Finance then Legal added days of cycle time to satisfy the data model rather than the business. Levels sharing a group name now activate together and join before the chain advances.
// Before 1.0.0 this needed an arbitrary order: Finance waits on Legal,
// or Legal waits on Finance, and the cycle time pays for the guess.
levels: [
{ level: 1, name: 'Manager', approvers: [...], mode: 'any' },
{ level: 2, name: 'Finance', group: 'review', approvers: [...], mode: 'any' },
{ level: 3, name: 'Legal', group: 'review', approvers: [...], mode: 'any' },
{ level: 4, name: 'CEO', approvers: [...], mode: 'any' },
]
// Levels sharing a group name open together and join before the chain
// advances. Decisions arrive in any order; level 4 stays 'waiting' until
// every branch is approved. Rejecting any branch rejects the instance.
// Five ways a level can pass, not one:
// 'any' one approver is enough
// 'all' every listed approver must act
// 'majority' more than half
// 'quorum' a fixed N-of-M threshold (minApprovals)
// 'weighted' cumulative approver weight (threshold, weights)This was the last item on the roadmap, and the release that let me call the public API settled: breaking changes get a major version from here. The detail I was most pleased about is that all 673 tests written against the sequential engine passed unchanged, because a level without a group is simply its own group of one. Backward compatibility is easier to promise than to earn, and the test suite is where you find out which you did.
Six defects across the 3.x line shared one root cause. An instance tracked where it was with a single number, currentLevel, and that number simply cannot describe the approval frontier once a parallel group has several levels open at once. Each release corrected one more reader of it: a return that stepped back into the group it was leaving, an escalation that watched the wrong branch, a workload query that missed half the open work. Six symptoms, one bad field.
// Wrong: one number cannot describe a frontier with two branches open.
if (instance.currentLevel === myLevel) { /* may I act? */ }
// Right: ask what the instance is actually waiting on.
const open = await engine.getOpenLevels(id); // number[], ascending
// [2] a sequential chain sitting on level 2
// [2, 3] a parallel group with both branches collecting decisions
// [] terminal - approved, rejected, cancelled or expired
// currentLevel survives as the lowest open level, recomputed by the engine
// on every write. It stays for display and for the audit trail, and a
// terminal instance keeps its last value so the record still shows where
// the request stopped. On a sequential template the two always agree,
// which is why most callers needed no change at all.So 4.0.0 stopped patching readers and changed the model. Every instance write now goes through a single path that recomputes the frontier first, which means a future operation cannot persist levels without updating the open list. That is the same discipline applied to level construction in 3.0.0 and to the Postgres column list in 1.6.0, both of which had drifted for exactly the same reason: two places building the same thing, and only one of them kept current.
If you are on 3.x and reading currentLevel to decide who may act, what to notify, or what is overdue, move those reads to openLevels before upgrading. On a sequential template the two always agree, so most upgrades need no change at all. Custom storage adapters must round-trip the new field, as they already must for levels; the bundled PostgresAdapter adds the column through its own migrate step.
I checked the number before enjoying it, because npm has always been open about what it counts: HTTP 200 responses served for tarball files, which includes mirrors, CI runners and analysis robots alongside people. Publishing a version guarantees a burst, since every mirror fetches the new tarball, and an evening of releases is visible in the chart for exactly that reason. The registry's own rule of thumb puts confident signal above roughly 50 downloads a day.
That does not make the milestone less real, it makes it specific. The durable figures are the ones that do not move when I publish: 404 tests passing, twelve entry points, eleven open issues from people who read the docs closely enough to find edge cases, and a package that installs cleanly with two runtime dependencies. 1,204 is the headline; those are the reasons it is trending in the right direction, and they are what I will be watching next month.
Between 17:16 UTC on 3 September and 02:14 the next morning I published 28 versions. The content was real, every entry in the changelog is a defect with a reproduction and a named failure mode, and shipping fixes the same evening you find them is the right instinct for a library people build approvals on. The packaging of it was not right, and three things changed as a result:
None of that is a reason to be sorry about the evening itself. A library that ships 4.0.0 eleven weeks after 0.1.0 is a library being used seriously enough to find its own edges, and I would rather have found those six frontier defects myself than have someone find them inside a purchase order.
The whole package is MIT-licensed and installs in one line, with pg needed only if you want the Postgres adapter. The entry points are split so that importing the engine does not drag in NestJS, the test kit or seven plugins you are not using.
npm install hierarchical-approval
npm install pg @types/pg # peer dep, Postgres adapter only
# Twelve entry points, so you pay only for what you import:
# hierarchical-approval the engine
# hierarchical-approval/nestjs the NestJS module
# hierarchical-approval/testing ApprovalTestKit + ManualClock
# hierarchical-approval/adapters/memory zero-I/O storage
# hierarchical-approval/adapters/postgres production storage
# hierarchical-approval/plugins/audit + notify, metrics, tracing,
# webhook, scheduler, resilience
# Runtime dependencies: zod and eventemitter3. That is the whole list.You can run the library in a browser before deciding anything. RunKit gives you an instant notebook with require('hierarchical-approval') already resolved, and the StackBlitz playground boots a complete purchase-order approval chain, conditions and all, with no local install. Both links are on the npm page, and they are the fastest way to find out whether the template model fits your documents.
Eleven weeks, 37 versions, 404 tests and a best week of 1,204 downloads. The number that made me open the page is not the number that will keep the library alive, but it is a real signal that a thing I built for my own ERP work is now useful outside it, and that is the whole reason to publish anything. Version 4.0.0 is the one I would tell you to install.
hierarchical-approval on npm
MIT-licensed, TypeScript-first, multi-tenant and audit-ready. Zero runtime dependencies you do not opt into.
npmjs.com/package/hierarchical-approval