Lot tracking

Lot tracking (batch identifiers, manufacture date, expiration date) is a v1.6 behavior with v1 schema commitments. The full schema and ingest fields are accepted from v1 launch — the upstream can register lots and reference them on movements — but FG.AI’s putaway and pick Rules do not yet evaluate lot-aware logic until v1.6 ships (Phase 14 per WMS ADR-0033).

Serial tracking (per-unit unique identifiers) stays v2 — the v1 schema accepts serial registrations but no v1.6 behavior is planned.

Per-SKU track_by mode

Lot vs. serial vs. neither is a per-SKU configuration in PSpec, expressed on the SKU master record:

POST /wms-ingest/v1/master/skus?mode=upsert

{
  "sku": {
    "source_id": "PHARMA-001",
    "consignor_id": "pharma-co",
    "track_by": "LOT",              ◄── NONE (default) | LOT | SERIAL | LOT_AND_SERIAL
    "lot_required_at_receive": true,
    "expiry_required": true,
    "min_shelf_life_days_at_receive": 90,
    "min_shelf_life_days_at_ship": 30,
    ...
  }
}

track_by value

Inventory keyed by

Activation

NONE (default)

(account, consignor, sku, location)

v1

LOT

(account, consignor, sku, location, lot_id)

v1.6 (Phase 14)

SERIAL

per-unit

v2

LOT_AND_SERIAL

serial within lot

v2 (alongside SERIAL)

track_by is immutable after the first downstream movement for the SKU (analogous to base-UoM immutability). Choose carefully at onboarding.

The validation flags (lot_required_at_receive, expiry_required, min_shelf_life_days_at_receive, min_shelf_life_days_at_ship) ship with v1 schema but only fire on receive/ship from v1.6. In v1, they are stored and surface as lot_validation_planned flags on the SKU readback.

Registering a lot

POST /wms-ingest/v1/master/lots?mode=upsert

{
  "lot": {
    "source_id": "LOT-2026-Q2-0042",
    "consignor_id": "pharma-co",
    "sku_ref": "PHARMA-001",
    "lot_no": "LOT-2026-Q2-0042",
    "manufacture_date": "2026-04-12",
    "expiry_date": "2027-04-12",
    "vendor_ref": "supplier-bayer",
    "country_of_origin": "DE",
    "regulatory_notes": "..."
  }
}

In v1 this row is stored in the reserved lot table; behavior activates v1.6. A v1 caller can pre-load lot data at onboarding so a v1.6 cutover does not require a bulk re-ingest.

Lot reference on inventory and movements

From v1.6, every inventory snapshot row and movement carries lot_id for lot-tracked SKUs:

POST /wms-ingest/v1/inventory/snapshot?mode=bulk&scope=FULL

{
  "snapshot": {
    "warehouse_ref": "wh-tokyo-01",
    "items": [
      {
        "consignor_id": "pharma-co",
        "sku_ref":       "PHARMA-001",
        "location_ref":  "BIN-COLD-01",
        "lot_ref":       "LOT-2026-Q2-0042",   ◄── required when sku.track_by ∈ {LOT, LOT_AND_SERIAL}
        "qty":           500
      }
    ]
  }
}

For a lot-tracked SKU, a snapshot row without lot_ref is rejected (MISSING_LOT_REFERENCE). For a non-lot-tracked SKU (track_by = NONE), lot_ref must be absent (UNEXPECTED_LOT_REFERENCE).

In v1 (before v1.6 activation), this validation is a soft check — present-but-unrecognized lot references are accepted into the schema; behavior switches to strict on Phase 14 cutover, with a 60-day “soft warning” window communicated in the v1.6 release notes.

FEFO picking — a Rule, not new code

Once v1.6 is live, “pick the lot closest to expiry” (FEFO — First Expired, First Out) is a flow-service Pull Rule, not a special-cased lot-aware code path. The Rule reads lot.expiry_date and selects the qualifying inventory row whose expiry is earliest, subject to min_shelf_life_days_at_ship. The Rule lives in flow-service per WMS ADR-0029.

Per-tenant Rule configuration is out of scope for this ingest contract; the ingest API only stores the lot data the Rule will later evaluate.

Recall correlation

When a recall notice arrives, the upstream registers an affected lot range and FG.AI surfaces every movement that touched a lot in that range. The traversal uses the lot_id column on every movement row. From the ingest API surface, recall integration is the read side — the upstream subscribes to inventory.adjusted webhooks filtered by lot_id and reconstructs genealogy from movements queried via the per-lot history endpoint (v1.6).

Anti-patterns

Treating track_by as configurable post-launch. It is immutable after first movement. Plan at onboarding.

Reusing a lot_id across consignors. Lots are scoped by consignor_id; two consignors may legitimately use the same vendor lot but they’re distinct master records. Always include consignor_id in /master/lots upserts.

Sending lot data for SKUs with track_by: NONE. Quarantined as UNEXPECTED_LOT_REFERENCE. Set track_by: LOT first.

Expecting FEFO picking in v1. v1 picks are FIFO-by-receive-time only. FEFO ships v1.6.