Help

The seven laws, every stock type, the ledger vocabulary, and what to do when a number looks wrong · last reviewed 29 July 2026

01 The seven laws 02 16 stock types 03 Ledger vocabulary 04 Why is my number wrong 05 Phase gates 06 Authority matrix 07 Shortcuts 08 Every module 09 Troubleshooting
Read law 1 before anything else. i2 never bootstrapped opening stock, so sales deducted from zero and sellable reached -140,753 with 410 meaningless stockouts. Every number in i3 is only as good as the opening balance it was built on. Opening stock →

01 · The seven laws

Every one of them exists because something specific broke. The red line is what it prevents.

binding on all 15 modules
1
Opening balance first
No module ships until its opening state is loaded and signed off by the responsible manager. Phase 0 is a real physical count, or an accepted snapshot marked unverified.
Prevents: sellable -140,753
2
Reconcile-gated phases
A phase closes only when its ledger agrees with an external truth within tolerance for 7 consecutive days. GATE 0 to GATE 8.
Prevents: shipping on a number nobody checked
3
Events in, never edits
Append-only hash-chained ledger with balance_after. A mistake is fixed by a compensating row, never by editing history.
Prevents: silent restatement of the past
4
Two ledgers, one truth
A quantity ledger keyed (SKU x location x stock_type) and a unit ledger keyed by RFID EPC serial, continuously reconciled against each other.
Prevents: "we have 12" with no idea which 12
5
Composites are first-class
style_groups → skus → recipe_components. A composite sale posts recipe_transform component rows only. The parent never gets a ledger row.
Prevents: phantom parent stock
6
Siblings push, i3 owns
ops2, CSD, GRN, Shopify, RFID and IWMS push signed events. i3 alone decides what they mean for stock. Nothing else writes stock.
Prevents: five systems, five answers
7
Continuous-fix
Every data-quality scenario ships a rule-based fix that scans the whole table, plus a recurring sync that owns it. Never a one-shot patch on one row.
Prevents: the same bug reported twice
?
If a law is inconvenient
Then the design is wrong, not the law. Raise it in the module spec's open questions. Do not route around it in a page.
See any docs/modules/NN-*.md section 12

02 · The 16 stock types

Three dimensions. A unit is in exactly one type at any instant, at exactly one location. Moving between types is a ledger row, never an edit.

5 condition6 custody3 program2 new in i3
Stock typeDimensionSellableOwned byEnters onLeaves onWhere you see it
STOCKConditionyesi3GRN accept, return restock, transfer receivesale, transfer out, damage, write-offinventory-live
DISPLAY_GOODConditionstore onlyRetaildisplay allocation at a storequick-sell, return to stockroom, damagelocation-detail
DISPLAY_DAMAGEDConditionnoRetaildisplay unit found damagedtriage to repair, write-off or discount saledamages
QUARANTINEConditionnoQCQC hold, suspect batch, customer claimQC decision to any of 5 bucketsgrn
WRITTEN_OFFConditionnoFinanceapproved write-off onlyterminal · nothing leavesdamages
IN_TRANSIT_CUSTOMERCustodynoops2dispatch scan, gate verifydelivered, NDR, RTOrfid
IN_TRANSIT_RETURNCustodynoCSD / ops2return AWB created and picked upreturns scan at HQ_319returns
LOST_IN_TRANSITCustodynoops2carrier declares loss, or 21d no scanclaim settled, or founddrift
RTS_PENDINGCustodynoOpsstaged for return to supplierRTS shipped to supplierrts
RESERVEDCustodyheldCSDhold placed for an exchange or a VIPhold released, or converted to a salereturns
RECEIVED_NOT_INVOICEDCustodynoFinanceGRN posted before the supplier invoiceinvoice booked · 72h SLAgrn
INFLUENCER_POOLProgramnoMarketingallocation to the influencer poolloan dispatched, gifted, or returned to STOCKinfluencer
SAMPLE_POOLProgramnoMarketingsample classified as pool after QCconsumed, gifted, or returned to STOCKsamples
SAMPLE_QCProgramnoMarketingsample received from a supplierclassified within 72h to POOL or STOCKsamples
IN_PRODUCTIONCondition newnoi3PO line confirmed into factory WIPPI issued, moves to GIT_INBOUNDproduction
GIT_INBOUNDCustody newnoi3PI shipped, goods left the supplierGRN receipt at HQ_319git
In i2 these last two were virtual locations, which made "how much is in production for PO-2026-118" unanswerable. In i3 they are stock types, so they are queryable per PO line.

03 · Ledger vocabulary

These 18 words explain almost every "the number looks wrong" question. Learn them once.

Audit log ↗
ledger_entriescore
The append-only quantity ledger. One row per movement of one SKU at one location in one stock type. Never updated, never deleted.
quantity_deltacore
Signed change this row makes. A sale is negative, a receipt is positive. A transfer is two rows that sum to zero.
balance_aftercore
The running balance for that key immediately after this row. If balance_after disagrees with the sum of deltas, the chain is broken and everything downstream is suspect.
hash chaincore
Each row hashes its own contents plus the previous row's hash. A nightly verify job walks the chain. Tampering is detectable, not preventable.
idempotency_keycore
Uniqueness is the pair (source_system, idempotency_key), never the key alone. Two systems may legitimately use the same key string.
compensating rowcore
The only way to fix a wrong posting. A new row with the opposite delta and a link to what it reverses. History stays intact.
movement_chaincore
The ordered hops a unit must pass through. Hops cannot be skipped: a unit cannot go from STOCK to delivered without passing IN_TRANSIT_CUSTOMER.
unit_ledgerrfid
The serial-level ledger keyed by EPC. Says which physical unit, where, and when it was last seen by a reader.
recipe_transformcomposite
The posting a composite sale produces: component rows only. Selling one BX-MSR31 posts minus one of each child, and nothing at all against BX-MSR31.
set availabilitycomposite
Computed as min(child availability) at read time. Never stored. If you store it, it goes stale the moment a child moves.
sku_aliasesregistry
Old SKU strings that still resolve. Shopify keys on variant.id, not the SKU string, because renames are routine.
scenario_codeclassifier
Which of the roughly 50 rules classified this event. An unmatched event gets no posting and lands in the drift queue instead.
shadow modeclassifier
A rule that fires and records but posts nothing. Every new rule starts here until its hit rate is proven.
drift caserecon
A recorded disagreement between i3 and an external truth, with evidence attached. Open, investigating, resolved or accepted.
sellablederived
STOCK minus RESERVED at sellable locations. It is derived, so it can never be adjusted directly. Adjust the thing underneath it.
on-handderived
Every physically present type at a location. Includes DISPLAY_GOOD and QUARANTINE. Bigger than sellable, always.
authorityconflict
Who wins when two systems disagree about one fact. See the matrix below. RFID gate reads beat every claim about physical existence.
reconciliation identitygate
opening + receipts - sales +/- adjustments +/- returns = closing, with zero unexplained residual. Every gate tests it nightly.

04 · Why is my number wrong

Work down the tree. Stop at the first yes. Nine cases out of ten it is one of the first three nodes.

do not hand-edit a balance
Start here: a number on a page looks wrong A number looks wrong Node 1: has this location signed its opening balance? 1 · Opening balance signed here? No signed opening balance: every derived number at this location is meaningless. This is the i2 failure. NO → stop. Nothing here is meaningful yet. Node 2: are you comparing sellable to on-hand? 2 · Sellable or on-hand? Sellable excludes DISPLAY_GOOD, QUARANTINE, RESERVED. Most reported gaps are this. DIFFERENT → not a bug. Check display + reserved. Node 3: is this SKU a composite? 3 · Is the SKU a composite? A composite has no stock of its own. Its availability is min(children). YES → it holds no stock. Read min(children). Node 4: does balance_after equal the sum of deltas? 4 · balance_after = sum(deltas)? Chain break: escalate immediately, do not adjust anything. NO → chain break. P1. Escalate, change nothing. Everything above passed: this is a real drift case. Open it with evidence. 5 · Real drift. Open a case with evidence, do not adjust. Never do these Hand-edit a balance Delete a ledger row Re-run a webhook by hand Fix one row by SKU Median time to answer 2.4 min

What it actually turned out to be

Last 90 days · 214 "the number is wrong" reports, by root cause

214 reports
Sellable vs on-hand confusion · 71 reports · 33% Composite read against the parent · 48 · 22% Missing GRN, goods arrived but never posted · 34 · 16% Sibling event never pushed · 22 · 10% Classifier unmatched, no posting made · 17 · 8% Timing, the reader was ahead of the sync · 12 · 6% Genuine drift needing a case · 8 · 4% Hash chain break · 2 · 1%
Sellable vs
on-hand 71
Composite
parent 48
Missing
GRN 34
Sibling
silent 22
Classifier
miss 17
Timing
12
Real drift
8
Chain
break 2
55% of all reports are the first two nodes of the tree. That is a documentation problem, not a data problem, which is why this page exists.

The five steps

If the tree does not resolve it

  1. Open the SKU page and read the movement timeline, not the balance. The balance is a conclusion, the timeline is the evidence.
  2. Find the last row where the balance was right. Everything before it is fine, so only look after it.
  3. Check the event browser for that window. A missing posting usually means a missing or unclassified event, not a wrong one.
  4. If the event exists but posted nothing, it is a classifier rule gap. Open classifier and map the rule.
  5. If the event never arrived, it is a sibling contract gap. Check sibling events for silence on that source.
The fix is never one row. When you find it, the correction scans the whole table for the same pattern and ships with a recurring job that owns it. A patch on the reported order number is not a fix.
Escalate a chain break immediately to #n8n-alerts

05 · Phase gates

A phase closes only when its gate holds for 7 consecutive days. Today is GATE-1 day 6.

GATE-1 green, day 6 of 7
GatePhaseWhat it testsThresholdMeasured nowStatusPages behind it
GATE-0Foundation + opening balanceOpening balances signed per location, hash chain verifieswithin 1% of Shopify total0.31%passed 22-07opening-stock
GATE-1Sales and cancellationsi3 sellable vs Shopify available per location-day< 0.10% · classifier ≥ 99.5%0.04% · 99.7%day 6 of 7inventory-live, shopify-events
GATE-2Returns, exchanges, repairsEvery ops2 returns-scan and CSD replacement has a posting or an open casereverse drift < 0.2%not startedlockedreturns, repairs
GATE-3Inbound: PO, PI, GIT, GRN, QCGRN receipts post within 5 minutes, PO expected vs received reconciles100% within 5 minnot startedlockedinflows, grn
GATE-4Transfers, retail, consignmentIn-transit balance nets to zero per completed transferstore count vs ledger < 0.5% weeklynot startedlockedtransfers, consignment
GATE-5IWMS physical layerCarton sum vs quantity ledger in HQ zones< 0.5%not startedlockedcartons
GATE-6RFID serial layerUnit count equals quantity ledger for serialized SKUs at HQ7 days · gate catch > 99%shadow · 91% taggedshadowrfid, rfid-recon
GATE-7ProgramsInfluencer pool matches the Hub, every damage has an incidentexact matchnot startedlockedinfluencer, damages
GATE-8Intelligence and financeMonthly close reproducible from the ledger alonefinance signs one full monthnot startedlockedreports
A page behind a locked gate is not broken. It renders in gate-blocked state with its shadow data marked as not yet authoritative. See the state library.

06 · Authority matrix

When two systems disagree about one fact, this table decides. It is not negotiable per case.

RFID gate reads beat everything physical
FactAuthorityi3 does what
Order or refund existsShopifyaccepts, never invents an order
Sellable and availableShopifycross-checks daily, writes back only after cutover
Dispatch, AWB, carrier eventsops2consumes, never creates an AWB
Return intent and case linkageCSDconsumes the decision, owns the stock effect
Physical receiptGRN / IWMSposts the receipt as a ledger row
Carton, zone and bin positionIWMSmirrors, does not move cartons
Physical presence of a serialRFIDa gate read is immutable and wins
PO, PI, production statusi3owns it outright
Financial write-off approvalFinancerecords the approval, cannot self-approve
What an event means for stocki3owns it outright · law 6

Reading the matrix

The three questions it answers

"ops2 says shipped, i3 says in stock." ops2 is the authority on dispatch, so the fact is true and i3 is behind. Look for a missing or unclassified dispatch event, not for a wrong balance.

"RFID says the unit left, ops2 has no AWB." The gate read wins on physical existence. The unit is out of the building. That is a P1: something left without a shipment.

"Shopify shows 4, i3 shows 6." Shopify owns sellable. If the gap survives the daily cross-check, i3 opens a drift case rather than silently matching Shopify, because silently matching is how you lose the evidence.

Never resolved by whoever is loudest, whoever asked most recently, or whichever number is more convenient for the report due today.

Conflicts that the matrix cannot settle become drift cases with evidence attached

07 · Keyboard shortcuts

One registry. This table and the ? dialog render from the same source, so they cannot drift apart.

Command palette ↗
KeysActionScopeNeeds
⌘ KOpen the command paletteglobalviewer
/Focus the page filterany table pageviewer
?Show this shortcut listglobalviewer
EscClose the panel, palette or dialogglobalviewer
⌘ .Toggle light and darkglobalviewer
J KNext and previous rowany listviewer
EnterOpen the selected rowany listviewer
⌘ EnterOpen in the side panel without leaving the pageany listviewer
XSelect or deselect the rowany listviewer
EMark readnotificationsviewer
AAcknowledge, stops the escalation timernotificationsmanager
SSnooze until a chosen timenotificationsmanager
MMute the rule, expiry and reason requirednotificationsops_manager
VCycle saved viewsany table pageviewer
G DGo to the dashboardglobalviewer
G NGo to notificationsglobalviewer
G SGo to live inventoryglobalviewer
G FGo to driftglobalviewer
G TGo to transfersglobalviewer
G UGo to the audit logglobalviewer

Onboarding by persona

Progress is saved automatically · your persona is derived at login

Owner · Ahmad8 / 9
Store manager · 5 stores5 / 8
WH operator4 / 10
Inbound coordinator6 / 9
CS agent3 / 8
Marketing2 / 6
Finance4 / 7
Planner1 / 5
Admin9 / 12
Take the guided tour ↗ · 10 stops · about 12 minutes

08 · Every module

Fifteen modules. Each one has a specification at docs/modules/NN-*.md with its own data model, business rules and acceptance criteria.

15 modules · 72 pages
01Workspace shell
dashboard, notifications, help, login, tour, palette, states
02Inventory views
live pivot, classic table, mix, stock health
03SKU and hierarchy
grandparent, parent, children, merge, price trends
04Locations and IWMS
locations, detail, cartons, IWMS events
05Inbound PO PI production GIT
purchase orders, production, goods in transit, shipments
06GRN QC suppliers RTS
receiving, 5-bucket QC, suppliers, return to supplier
07Transfers
store to store, 2N postings, transfer detail
08Reverse logistics
returns, exchanges, repairs, CSD and ops2 events
09Damages and incidents
4-source intake, incident matching, write-offs
10Stock count and opening balance
sessions, blind count, bulk import, opening stock
11Programs
influencer, samples, consignment
12RFID
gates, unit ledger, 3-way reconcile, mobile scan
13Drift and reconciliation
cases, anomalies, conflicts, corrections, approvals
14Events and classifier rules
Shopify events, rules, alerts, outbox, audit log
15Reports, finance, ops health
reports hub, cash forecast, settings, dev console

09 · Troubleshooting

Sorted by how often it is actually asked. Every answer ends in an action, never in "contact support".

10 of 31 shown
P1 A SKU shows a negative balance. Is that a bug?

Almost never a code bug. Three causes account for nearly all of them:

  1. The opening balance for that location was never signed. Sales then deduct from zero. This is exactly what took i2 to minus 140,753. Check opening stock first.
  2. A GRN was never posted. Goods physically arrived, nobody receipted them, so every subsequent sale deducts from a balance that never got its credit.
  3. A double deduction on a replacement. The replacement dispatch deducted and the original exchange line also deducted. INV1 had 111 of these.

Do not overwrite the balance. Open a ledger correction so the compensating row and its reason are on the record.

P2 Shopify and i3 disagree by a few units. Who is right?

Shopify is the authority on sellable. i3 is the authority on what happened. Both can be correct at once, because they measure different things.

  • i3 sellable excludes RESERVED, DISPLAY_GOOD and QUARANTINE. Shopify does not know about those.
  • The daily cross-check compares them per location-day. GATE-1 requires the divergence under 0.10%.
  • If it survives the cross-check, i3 opens a drift case rather than silently matching Shopify. Silently matching destroys the evidence.
P2 A composite shows zero but its components are in stock

That is correct behaviour. A composite holds no stock of its own. Its availability is computed as min(child availability) at read time, so one exhausted child takes the whole set to zero.

Check the children on the SKU page hierarchy tree. The limiting child is highlighted. If you expected the composite itself to carry stock, the recipe is wrong, not the balance.

P2 A stock movement happened but nothing appeared in i3

Work in this order:

  1. Sibling events. Did the event arrive at all? If the source is silent, the sibling.silent rule should already have raised a P2.
  2. Event browser. It arrived but was it classified? An unmatched event posts nothing by design.
  3. Classifier. Which rule should have caught it? Map the rule in shadow mode first.
  4. Only then look at the ledger. A missing posting is upstream in 9 cases out of 10.
P3 Why did I get paged twice for the same thing?

You should not have. Every alert is unique on (rule_id, dedupe_key, throttle_bucket), so a second producer cannot create a second row. If you genuinely received two, one of these is true:

  • Two different rules matched the same underlying condition. Fix by merging the rules, not by muting one.
  • The alert escalated. An escalation is a second message by design, and it can happen only once per alert.
  • A delivery retry succeeded after appearing to fail. Check the delivery log on notifications.
P3 I muted a rule and I am still seeing the alerts

Correct. Muting suppresses delivery, never the instance. The alerts still accumulate in-app and are still counted, they simply stop reaching Slack and email.

This is deliberate: a mute that hides the record is indistinguishable from a bug, and quiet is not the same as healthy. Every suppressed delivery is logged with a reason.

P3 A page says "gate blocked". Is it broken?

No. The feature exists and its data is being collected, but its reconcile gate has not held for 7 consecutive days, so the numbers are not yet authoritative. The page shows what it has, marked as shadow.

The gate, its threshold and its current measurement are named on the page and in section 05 above.

P3 I cannot see a page that a colleague can see

Access is per app and per role. Open login and access to see exactly what you hold on app i3, and what the page requires. There are four roles: viewer, manager, ops_manager, admin.

Store-scoped pages also depend on which stores you are rostered at. A manager rostered at two stores sees two.

P3 How do I undo something?

You do not undo. You post a compensating row, which is a new ledger entry with the opposite delta and a link to what it reverses. The original stays.

This is law 3, and it is why the audit log is trustworthy. A system that lets you delete history cannot tell you what happened.

P3 What is the difference between the two ledgers?

The quantity ledger says how many, keyed (SKU x location x stock_type). The unit ledger says which ones and where they physically are, keyed by RFID EPC serial.

They are reconciled continuously. Where units exist, the quantity is the count of units. Where they disagree, the gate read wins on physical existence and a drift case records the gap. See 3-way reconcile.

Contact

Median first response 24 min, Dubai hours

recommended#n8n-updates
General questions, deltas, digests
P1 only#n8n-alerts
Active incidents and escalations
serial#n8n-rfid
Gate, count, device and write-off alerts
Never post a token or a secret to a channel

Read next

Guided tour, 10 stops ↗ Sign an opening balance ↗ The state library ↗ Search grammar ↗ Alert rules and mutes ↗ Open a drift case ↗ Verify the hash chain ↗

The four things never to do

  1. Hand-edit a balance. Post a compensating row instead.
  2. Delete a ledger row. There is no supported path and there never will be.
  3. Fix one reported order number. The fix scans the whole table or it is not a fix.
  4. Ship a data correction without the recurring job that owns it.
Law 3 and law 7

State variants

Loading. Skeletons at the exact content height, never a spinner.

Empty. The query is valid and the answer is genuinely zero.

Error. A stable error id plus "nothing was changed".

Permission. Names the role you need and offers one action.

Zero-filter. Filters hid everything. Lists them with a one-click clear.

Full library ↗
Connected to Dashboard Guided tour Notifications Command palette State library Login and access Opening stock Live inventory SKU detail Drift Classifier Rules Audit log 3-way reconcile Reports Settings