---
title: "Example: Inventory Tracker · RUAL Documentation"
description: "Stock movements as an append-only log with derived levels."
canonical: https://docs.rual.nl/examples/inventory-tracker
language: en
---

# Example: Inventory Tracker

Items, locations, movements, and low-stock alerts. A complete warehouse tracker where the ledger is always right.

Inventory is a ledger problem: current stock is a sum of movements, and every movement must be recorded once, in order. This example builds that ledger with alerts on top.

### Data Model

- **items**: sku, name, unit, reorder_level.

- **locations**, warehouse/zone/bin.

- **movements**. One append-only document per change:

```
{
  "_meta": {
    "cms": 1782166695055,
    "created": 1782166695,
    "entity": 1,
    "expiry": -1,
    "guid": "08193b4a5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708",
    "removed": 0,
    "ums": 1782166695092,
    "update_hash": "5e0d38c7a1f294b6037ce8a51d9f27b046a3c81e",
    "updated": 1782166695
  },
  "actor_guid": "b9ee6a631b24e78d1aa48aef0dc067fff7bfbacd24a56ef4ed1f08e537d48b6bd02",
  "item_name": "essential cotton t-shirt",
  "location": "wh-1/zone-a/bin-04",
  "note": "po-10482",
  "quantity": 24,
  "sku": "TSHIRT-BLK-M",
  "type": "receive"
}
```

Movement types: `receive`, `ship`, `adjust`, `transfer` (two linked movements: out + in). Never update movements: corrections are new `adjust` movements (the activity-log principle from [Storage Examples](https://docs.rual.nl/blueprints/storage-examples#activity-logs)).

### Current Stock, Two Ways

- **Ledger sum (exact)**: aggregate movements per sku+location when you need the precise figure.

- **Counter document (fast)**. A `stock` storage with one doc per sku+location; every movement flow also applies [`mutations_increment_by_field`](https://docs.rual.nl/block-types/mutations/mutations_increment_by_field) to it via `function_update_document` (sequential, race-free. See [mutations](https://docs.rual.nl/blueprints/storage#mutations)). Lists and alerts read only counters.

Audit job: a daily [`schedule_repeating_event`](https://docs.rual.nl/block-types/events/schedule_repeating_event) recomputes the ledger sum against the counter and logs drift. You will catch a missed movement within a day, not at year-end.

### The Flows

- **Receive**: form or API → create movement → increment counter. Idempotent via a source reference (`po-10482`): re-receiving the same reference is skipped.

- **Ship**: same, negative increment, with a guard when the counter would drop below zero (hold as `backorder` movement instead of going negative).

- **Transfer**. One flow writes the out-movement and in-movement as two linked documents sharing a transfer guid; the counters move in the same flow.

- **Count check**. An `adjust` movement with the counted quantity difference; the counter follows the same path as everything else.

### Low-Stock Alerts

- After every counter change, compare against the item's `reorder_level`.

- Crossing the threshold (and only then: track previous state on the counter to avoid alert spam) creates an alert document and emails the buyer via the [email pattern](https://docs.rual.nl/tutorials/email-system).

- Alert page: open alerts sorted by severity, closed with a status mutation once the receive lands.

### Next Steps
