Northwind ERP Fulfillment Decision Skill
Executable experience for solving ERP fulfillment evaluation tasks against the
shared Northwind ERP API. Covers six business families: expedite-queue dispatch,
BOM replenishment, supplier incident scorecards, allocation/transfer decisions,
and procurement quality-hold review.
Remote API Access
Base URL: <remote-env-url> (the prompt may reference a local
http://127.0.0.1:8007; prefer the remote URL given in environment_access.md.)
Always use curl -sS --max-time 30 '<url>'. The server speaks HTTP/1.0 and
closes each connection after every request. Parse responses with python3 -c "import json; ..." or jq.
Endpoints (GET unless noted)
| Endpoint |
Purpose |
GET /health |
Manifest with record counts. |
GET /products / GET /products/<sku> |
SKU master: sku, name, category, active, safety_stock, overstock_threshold, unit_cost, weight_lb, supplier_id. |
GET /customers / GET /customers/<id> |
Customer master: customer_id, name, account_status, risk_flag, tier, margin_band. |
GET /warehouses |
warehouse_id, name, zip, region. |
GET /inventory?warehouse_id=&sku= |
on_hand, reserved, quarantined, last_count_date. |
GET /purchase_orders?supplier_id=&sku=&status= |
po_id, sku, quantity, eta, status, warehouse_id, supplier_id. |
GET /orders?wave=&required_date=&customer_id= / GET /orders/<id> |
order_id, customer_id, warehouse_id, warehouse_id, shipping_speed, destination_zip, required_date, priority, wave, lines[{line_id, sku, quantity, unit_price}]. |
GET /shipping/quote?warehouse_id=&destination_zip=&weight_lb=&speed= |
Returns base_rate, fuel_surcharge_rate, total_cost, service_days, zone_distance, carrier. |
GET /incidents?start=&end=&supplier_id=&sku=&incident_type=&status= |
incident_id, supplier_id, sku, incident_type, severity, status, open_date, close_date, resolution_cost, root_cause, warehouse_id. |
GET /suppliers |
supplier_id, name, quality_status, region. |
GET /boms / GET /boms/<bom_id> |
bom_id, name, warehouse_id, target_date, components[{sku, quantity_per_kit}]. |
Efficient data gathering
Download all collections up front, then process in Python:
curl -sS --max-time 30 '<remote-env-url>/products' > /tmp/products.json
curl -sS --max-time 30 '<remote-env-url>/customers' > /tmp/customers.json
curl -sS --max-time 30 '<remote-env-url>/warehouses' > /tmp/warehouses.json
curl -sS --max-time 30 '<remote-env-url>/suppliers' > /tmp/suppliers.json
curl -sS --max-time 30 '<remote-env-url>/boms' > /tmp/boms.json
curl -sS --max-time 30 '<remote-env-url>/inventory' > /tmp/inventory.json
curl -sS --max-time 30 '<remote-env-url>/purchase_orders' > /tmp/purchase_orders.json
curl -sS --max-time 30 '<remote-env-url>/incidents' > /tmp/incidents.json
Core Business Rules
1. Effective Available Stock
effective_available = on_hand - reserved - quarantined - safety_stock
safety_stock comes from the product master, not the inventory record.
- Effective stock can be negative (more committed/protected than on hand).
Always use the raw value for
target_effective_available and gap calculations.
Do NOT cap at zero — capping caused score drops in testing.
- No inventory record for a (warehouse, sku) pair ⇒ treat effective as 0 for
that pair.
2. Customer Account / Risk Overrides (checked BEFORE inventory)
Customer fields: account_status (active, blocked, review_required) and
risk_flag (none, fraud_watch, credit_watch).
Exception precedence (pick the first that applies):
| Condition |
customer_exception |
Decision impact |
| account_status == blocked |
account_blocked |
reject_hold / hold_credit_or_fraud |
| risk_flag == fraud_watch |
fraud_watch |
manual_review / hold_credit_or_fraud |
| account_status == review_required |
review_required |
manual_review / send_account_review |
| risk_flag == credit_watch |
credit_watch |
manual_review / send_account_review |
| else |
none |
proceed to product / inventory check |
The account/risk check happens before the inventory check in the decision
flow, but inventory_status is still computed and reported for every order.
3. Product Status Check (after account, before inventory)
- If a product has
active == false (inactive), the line gets
manual_review / escalate_product_master.
- This check applies per-line, not per-order (in allocation tasks an order
can have some inactive-SKU lines and some normal lines).
4. Inventory Status Classification
For each SKU line at the order's warehouse, compute effective_available:
| Condition |
Classification |
| SKU inactive AND effective < ordered |
inactive_and_shortage |
| SKU inactive (effective ≥ ordered) |
inactive_sku |
| effective < ordered (SKU active) |
shortage |
| effective ≥ ordered AND (effective − ordered) < safety_stock |
low_stock |
| effective ≥ ordered AND (effective − ordered) ≥ safety_stock |
ready |
shortage_skus: SKUs where effective < ordered quantity.
low_stock_skus: SKUs where effective ≥ ordered but remaining after
allocation is below safety_stock.
inactive_skus: SKUs where product active == false.
All SKU lists must be sorted ascending and de-duplicated.
5. Decision Precedence (per order or per line)
- account_blocked → reject_hold, hold_credit_or_fraud
- review_required / fraud_watch / credit_watch → manual_review, send_account_review (or hold_credit_or_fraud for fraud)
- inactive product → manual_review, escalate_product_master
- any shortage → backorder, create_backorder
- low stock (no shortage) → delayed_release, delay_and_monitor
- all available → ship_now, release_to_pick
6. Shipping Quotes
total_weight = Σ (product.weight_lb × line.quantity) for all lines in the order
Call: GET /shipping/quote?warehouse_id=<wh>&destination_zip=<zip>&weight_lb=<weight>&speed=<speed>
Pass the full-precision weight (do not round before sending). The endpoint
returns total_cost, service_days, zone_distance. Round total_cost to 2
decimals for the answer. Use the order's shipping_speed field as speed.
Compute the shipping quote for every order in the queue, regardless of the
dispatch decision (even blocked/reviewed orders need a quote). The summary
total_shipping_cost_usd is the sum of all shipping quotes.
7. Rounding
- Currency: round to 2 decimal places.
- Percentages: round to 1 decimal place.
- Durations: round to 2 decimal places.
- All other quantities: integers (no rounding needed).
Task Family SOPs
Family A: Expedite Queue Dispatch (e.g. wave TRAIN_EXPEDITE_A)
Input: a memo listing order_ids to process, an as_of_date.
Output shape (answer_template.json):
{
"wave_id": "<wave>",
"records": [ {order_id, inventory_status, customer_exception,
final_decision, next_action, shortage_skus, inactive_skus,
low_stock_skus, shipping_quote:{zone_distance, service_days,
total_cost_usd}} ],
"summary": {order_count, decision_counts:{ship_now, delayed_release,
manual_review, backorder, reject_hold}, total_shipping_cost_usd,
blocked_order_ids, manual_review_order_ids, backorder_order_ids,
inactive_sku_order_ids}
}
Procedure:
- For each order_id in the memo (not all orders in the wave — only the memo
list), fetch the order detail.
- Check customer account_status/risk_flag → customer_exception.
- For each line, compute effective stock at the order's warehouse → classify
shortage / low_stock / inactive.
- Determine inventory_status (combine across lines).
- Apply decision precedence: account → product → inventory.
- Compute shipping quote for all orders.
- Sort records ascending by order_id.
blocked_order_ids = orders with customer_exception == account_blocked.
manual_review_order_ids = orders with final_decision == manual_review.
backorder_order_ids = orders with final_decision == backorder.
inactive_sku_order_ids = orders that have at least one inactive SKU.
Key: The memo lists specific order_ids — only process those, not the entire
wave. Keep records sorted by order_id.
Family B: BOM Replenishment (e.g. kit builds at a warehouse)
Input: a production memo with target_builds (bom_id, target_build_quantity,
target_build_date) and a planning_site (warehouse_id).
Output shape:
{
"task_id": "<task_id>",
"plan_date": "YYYY-MM-DD",
"kit_targets": [{bom_id, kit_name, warehouse_id, build_quantity, build_date}],
"component_plan": [{sku, total_required, target_effective_available,
timely_po_qty, transfer_qty, purchase_requisition_qty, final_action,
coverage_po_ids, exclusion_reason}],
"transfer_requests": [{sku, from_warehouse_id, to_warehouse_id, quantity,
needed_by}],
"purchase_requisitions": [{sku, supplier_id, warehouse_id, quantity,
needed_by, unit_cost, extended_cost}],
"excluded_components": [{sku, reason, supporting_po_ids}],
"summary": {component_count, total_purchase_units, total_purchase_cost,
total_transfer_units, timely_po_covered_units}
}
Procedure:
- Compute demand: For each BOM, multiply each component's
quantity_per_kit by the build quantity. Sum across all builds for each SKU
→ total_required.
- Earliest build date: For each SKU, the earliest
target_build_date
among all builds containing it → needed_by for transfers and purchases.
- Effective stock at the planning site →
target_effective_available
(raw, can be negative).
- Timely POs: Filter purchase orders where
status in (open, confirmed),
warehouse_id == planning site, sku matches, and eta <= needed_by.
timely_po_qty = sum of all eligible PO quantities.
coverage_po_ids = sorted list of eligible PO IDs.
- Gap calculation:
total_available = target_effective_available + timely_po_qty
gap = max(0, total_required - total_available)
- Exclusion checks (if no gap to fill):
- If
effective >= total_required:
- If
effective >= product.overstock_threshold → target_overstock,
overstock_excluded
- Else →
stocked_no_gap, no_action_stocked
- If
effective < total_required but total_available >= total_required
→ timely_po_covers_gap, timely_po_covered
- Transfer (if gap > 0): For each other warehouse, compute spare effective
stock (same formula). Sort by spare descending. Transfer from one warehouse
at a time (largest spare first), taking
min(spare, remaining_gap).
- Purchase requisition: Remaining gap after transfers.
unit_cost from product master, extended_cost = unit_cost × quantity.
supplier_id from product master.
- final_action:
overstock_excluded / no_action_stocked / timely_po_covered: excluded
transfer_only: gap fully covered by transfers
purchase_required: purchase needed (with or without transfers)
- Summary:
total_purchase_units = sum of purchase_requisition_qty
total_purchase_cost = sum of extended_cost, 2 decimals
total_transfer_units = sum of transfer quantities
timely_po_covered_units = for each excluded timely_po_covers_gap
component, min(timely_po_qty, max(0, total_required - effective)).
NOT the raw total PO quantity — use the gap-coverage amount.
Key learnings:
- Effective stock stays raw (negative allowed). Capping at zero drops the score.
timely_po_covered_units = the gap the timely PO actually fills, not the
total PO quantity. For a component needing 90 with effective −16 and a 335-unit
PO, the covered amount is 106 (90 − (−16)), not 335.
kit_targets uses the memo's target_build_date, not the BOM record's
target_date.
- Sort
component_plan by sku. Sort transfer_requests by sku, then quantity
descending, then from_warehouse_id ascending. Sort purchase_requisitions
and excluded_components by sku.
Family C: Supplier Incident Scorecard (e.g. Q1 review)
Input: a scorecard request with incident date filter (field, start, end,
inclusive), analysis_date, duration rule, percentage rule, recommendation
policy.
Output shape:
{
"analysis_window": {start_date, end_date, analysis_date},
"summary": {filtered_incident_count, supplier_count,
total_resolution_cost, overall_rma_count, overall_work_order_count},
"supplier_scorecard": [{supplier_id, supplier_name, incident_count,
incident_percentage, total_resolution_cost, avg_duration_days,
rma_count, work_order_count, open_incident_count, severe_incident_count,
recommendation_code}],
"top_escalation_suppliers": [supplier_id, ...],
"highest_cost_supplier_id": "string",
"highest_share_supplier_id": "string"
}
Procedure:
- Filter incidents on
open_date (per the request's field) within
[start_date, end_date] inclusive (string comparison works for
YYYY-MM-DD).
- Overall summary:
filtered_incident_count = count of filtered incidents
supplier_count = distinct suppliers with ≥1 filtered incident
total_resolution_cost = sum of resolution_cost, 2 decimals
overall_rma_count / overall_work_order_count = counts by type
- Per-supplier rows (sort by supplier_id ascending):
incident_count = count of filtered incidents for supplier
incident_percentage = incident_count / filtered_incident_count × 100,
1 decimal
total_resolution_cost = sum, 2 decimals
avg_duration_days: for each incident, duration = (close_date −
open_date).days if closed, else (analysis_date − open_date).days. Average
across the supplier's incidents, 2 decimals.
rma_count / work_order_count = by type
open_incident_count = status == open
severe_incident_count = severity in {high, critical}
- Recommendation code (precedence: ESCALATE_SUPPLIER > PROCESS_REVIEW >
WATCHLIST > MONITOR):
- ESCALATE_SUPPLIER: supplier
quality_status == quality_hold AND
incident_count >= 3, OR has any critical RMA (incident_type == RMA and
severity == critical), OR (rma_count >= 3 AND total_resolution_cost >=
15000.00)
- PROCESS_REVIEW: work_order_count >= 3 AND work_order_count >
rma_count
- WATCHLIST: quality_status in {watch, quality_hold}, OR
incident_count >= 4, OR total_resolution_cost >= 12000.00, OR
severe_incident_count >= 2
- MONITOR: none of the above
- top_escalation_suppliers: supplier_ids with ESCALATE_SUPPLIER, ordered by
incident_count descending, then total_resolution_cost descending, then
supplier_id ascending.
- highest_cost_supplier_id: supplier with max total_resolution_cost
(ties broken by supplier_id ascending).
- highest_share_supplier_id: supplier with max incident_count
(ties broken by supplier_id ascending).
Key: The filter uses open_date, not close_date. Duration for open
incidents uses the analysis_date, not today's date. The percentage is a
percentage number (e.g. 23.7), not a fraction (0.237). Severe = {high,
critical}.
Family D: Allocation / Transfer Decision (e.g. wave TRAIN_TRANSFER_B)
Input: a wave ID, order data, customer/product/inventory masters, an
allocation memo.
Output shape:
{
"wave_id": "<wave>",
"line_actions": [{order_id, line_id, sku, requested_warehouse,
requested_effective_available, action, ship_quantity, transfer_from,
transfer_quantity, backorder_quantity, primary_reason}],
"transfer_requests": [{order_id, line_id, sku, from_warehouse,
to_warehouse, quantity}],
"blocked_orders": [order_id, ...],
"order_rollup": [{order_id, outcome}],
"summary": {total_orders, total_lines, ship_lines, transfer_lines,
backorder_lines, manual_review_lines, blocked_orders, transfer_units,
backorder_units}
}
Procedure:
- For each order in the wave, determine the order-level exception:
- account_blocked → all lines
manual_review, primary_reason =
account_blocked
- fraud_watch → all lines
manual_review, primary_reason = fraud_watch
- review_required → all lines
manual_review, primary_reason =
account_review_required
- For orders with no account exception, check each line:
- If product
active == false → manual_review, reason inactive_product
- Else compute
effective_available at the requested warehouse:
- If effective ≥ quantity →
ship, ship_quantity = quantity
- If effective < quantity:
usable = max(0, effective) (cannot ship negative stock)
remaining = quantity − usable
- Check other warehouses: if any has
effective >= remaining
(spare effective with safety stock preserved), transfer from the
warehouse with the largest effective
→ transfer, ship_quantity = usable, transfer_from = that warehouse,
transfer_quantity = remaining
- If no single warehouse can cover the full remaining →
backorder,
ship_quantity = 0, backorder_quantity = quantity,
primary_reason = insufficient_effective_stock
requested_effective_available = raw effective stock (can be negative).
- blocked_orders: ALL orders stopped at account or customer-risk level
(account_blocked, review_required, fraud_watch). NOT inactive-product-only
orders. Sort ascending.
- order_rollup outcomes:
- All lines ship →
ready_to_ship
- Ship + transfer only (no backorder, no manual_review) →
needs_transfer
- Any backorder (no manual_review) →
has_backorder
- All lines manual_review →
manual_review
- manual_review mixed with other actions, or other mixed combinations →
mixed_actions
- Summary: counts and unit totals.
transfer_units = sum of
transfer_quantity, backorder_units = sum of backorder_quantity.
Key: For backorder lines, ship_quantity = 0 and backorder_quantity = full
quantity (the line cannot be partially shipped). For transfer lines, the full
uncovered portion must be coverable by a single warehouse — if no single
warehouse can cover, it's a backorder. Transfer source = warehouse with the
largest effective stock that can cover. Sort line_actions by order_id then
line_id. Sort transfer_requests by order_id then line_id.
Family E: Procurement Quality-Hold Review
Input: a memo with analysis_window (start, end), decision_choices
(freeze_new_replenishment, buyer_review_required, monitor_only),
target_supplier_ids.
Output shape:
{
"analysis_window": {start, end},
"supplier_decisions": [{supplier_id, supplier_name, quality_status,
recent_incident_count, recent_rma_count, severe_or_critical_count,
open_incident_count, affected_skus, sample_incident_ids, decision,
held_po_ids}],
"held_po_ids": [po_id, ...],
"release_supplier_ids": [supplier_id, ...],
"summary": {suppliers_reviewed, freeze_count, buyer_review_count,
monitor_count, held_po_count, total_recent_incidents}
}
Procedure:
- Filter incidents for each target supplier:
open_date within
[start, end] inclusive.
- Per supplier, compute:
recent_incident_count = filtered incident count
recent_rma_count = RMA-type count
severe_or_critical_count = severity in {high, critical}
open_incident_count = status == open
affected_skus = sorted unique SKUs from filtered incidents
sample_incident_ids = sorted incident IDs, capped at 5
- Decision logic (best-effort thresholds from training):
- freeze_new_replenishment: quality_hold AND recent_incident_count >= 3,
OR any critical RMA, OR (rma_count >= 3 AND total_resolution_cost >=
- buyer_review_required: quality_hold AND incidents < 3, OR
(watch AND (incidents >= 4 OR total_resolution_cost >= 15000 OR any
critical incident))
- monitor_only: none of the above
- held_po_ids per supplier: all open/confirmed purchase order IDs for
that supplier (regardless of decision — the template says "open or
confirmed purchase order ids" without a held qualifier).
- Top-level held_po_ids: union of held_po_ids from suppliers whose
decision is freeze or buyer_review only (sorted, unique).
- release_supplier_ids: suppliers with monitor_only (sorted).
- Summary counts.
Key uncertainty: The exact decision thresholds for buyer_review vs
monitor_only were not fully resolved in training (all rounds scored 0.556).
The most likely split observed: quality_hold + significant incidents → freeze;
watch + moderate-to-high risk → buyer_review; watch + minor risk → monitor.
Treat recent_incident_count >= 4 or total_resolution_cost >= 15000 or
any critical incident as the buyer_review trigger for watch suppliers. Per-
supplier held_po_ids should list all open/confirmed POs, not just held ones.
Common Misjudgments and Exclusion Rules
Don't cap effective stock at zero. Negative effective stock is valid
and must be used in gap calculations. Capping dropped the BOM score from
0.78 to 0.50.
timely_po_covered_units is the gap covered, not total PO qty. If a
component needs 90, has effective −16, and a timely PO of 335 units, the
covered amount is 106 (the shortfall), not 335.
Only process orders listed in the memo, not the entire wave. The
/orders?wave= endpoint returns all orders in the wave, but the task memo
specifies which order_ids to process.
blocked_orders includes ALL account/risk-stopped orders (blocked,
review_required, fraud_watch), not just account_blocked. Inactive-product
orders are NOT in blocked_orders (that's a line-level product issue).
For allocation transfers, one warehouse must cover the FULL uncovered
quantity. If no single warehouse can cover the entire remaining amount,
the line goes to backorder, not a multi-warehouse transfer.
For allocation backorder lines, ship_quantity = 0. The entire quantity
is backordered — no partial ship.
Incident filter uses open_date, not close_date. Duration for open
incidents uses analysis_date, not today. Percentages are percentage
numbers (23.7), not fractions (0.237).
Recommendation precedence is strict. ESCALATE > PROCESS_REVIEW >
WATCHLIST > MONITOR. A supplier meeting WATCHLIST criteria but also
PROCESS_REVIEW criteria gets PROCESS_REVIEW. A supplier on quality_hold
with >= 3 incidents gets ESCALATE even if it also meets PROCESS_REVIEW.
Kit build dates come from the production memo, not the BOM record's
target_date. Use the memo's target_build_date for needed_by and
build_date fields.
Shipping weight uses full precision. Don't round the weight before
passing to /shipping/quote. Round only the returned total_cost to 2
decimals.
Transfer source selection = largest effective first. Among warehouses
that can cover the uncovered quantity, choose the one with the largest
effective stock.
Overstock exclusion: target_overstock when effective >= total_required
AND effective >= product.overstock_threshold. If effective >= total_required
but below the threshold, it's stocked_no_gap.
Reusable SOP (applies to unseen test tasks)
- Read the prompt and answer template carefully. The template defines
exact field names, types, ordering, and enum values. Match them precisely.
- Download all data collections up front via the API endpoints listed above.
- Compute effective stock = on_hand − reserved − quarantined − safety_stock
(safety_stock from product master). Keep raw (negative allowed).
- Apply account/risk overrides before inventory for any dispatch,
allocation, or expedite task.
- Check product active status per line before inventory for allocation
tasks.
- Filter incidents on open_date inclusive within the analysis window.
- Compute durations using close_date for closed, analysis_date for open.
- Apply the recommendation/decision precedence strictly
(ESCALATE > PROCESS_REVIEW > WATCHLIST > MONITOR for scorecards;
account > product > inventory for dispatch/allocation).
- Round correctly: currency 2 decimals, percentages 1 decimal, durations 2
decimals.
- Sort all lists as specified in the template (by order_id, line_id, sku,
supplier_id, etc.).
- Verify output shape against the template before submitting: all required
top-level keys, all item required keys, all enum values valid, all
orderings correct.
- Use the remote API (
<remote-env-url>) with
curl -sS --max-time 30 for every call.
1---2name: reflect-3-attempt-03-143description: Northwind ERP Fulfillment Decision Skill4---5# Northwind ERP Fulfillment Decision Skill67Executable experience for solving ERP fulfillment evaluation tasks against the8shared Northwind ERP API. Covers six business families: expedite-queue dispatch,9BOM replenishment, supplier incident scorecards, allocation/transfer decisions,10and procurement quality-hold review.1112## Remote API Access1314Base URL: `<remote-env-url>` (the prompt may reference a local15`http://127.0.0.1:8007`; prefer the remote URL given in `environment_access.md`.)1617Always use `curl -sS --max-time 30 '<url>'`. The server speaks HTTP/1.0 and18closes each connection after every request. Parse responses with `python3 -c19"import json; ..."` or `jq`.2021### Endpoints (GET unless noted)2223| Endpoint | Purpose |24|---|---|25| `GET /health` | Manifest with record counts. |26| `GET /products` / `GET /products/<sku>` | SKU master: sku, name, category, active, safety_stock, overstock_threshold, unit_cost, weight_lb, supplier_id. |27| `GET /customers` / `GET /customers/<id>` | Customer master: customer_id, name, account_status, risk_flag, tier, margin_band. |28| `GET /warehouses` | warehouse_id, name, zip, region. |29| `GET /inventory?warehouse_id=&sku=` | on_hand, reserved, quarantined, last_count_date. |30| `GET /purchase_orders?supplier_id=&sku=&status=` | po_id, sku, quantity, eta, status, warehouse_id, supplier_id. |31| `GET /orders?wave=&required_date=&customer_id=` / `GET /orders/<id>` | order_id, customer_id, warehouse_id, warehouse_id, shipping_speed, destination_zip, required_date, priority, wave, lines[{line_id, sku, quantity, unit_price}]. |32| `GET /shipping/quote?warehouse_id=&destination_zip=&weight_lb=&speed=` | Returns base_rate, fuel_surcharge_rate, total_cost, service_days, zone_distance, carrier. |33| `GET /incidents?start=&end=&supplier_id=&sku=&incident_type=&status=` | incident_id, supplier_id, sku, incident_type, severity, status, open_date, close_date, resolution_cost, root_cause, warehouse_id. |34| `GET /suppliers` | supplier_id, name, quality_status, region. |35| `GET /boms` / `GET /boms/<bom_id>` | bom_id, name, warehouse_id, target_date, components[{sku, quantity_per_kit}]. |3637### Efficient data gathering3839Download all collections up front, then process in Python:4041```bash42curl -sS --max-time 30 '<remote-env-url>/products' > /tmp/products.json43curl -sS --max-time 30 '<remote-env-url>/customers' > /tmp/customers.json44curl -sS --max-time 30 '<remote-env-url>/warehouses' > /tmp/warehouses.json45curl -sS --max-time 30 '<remote-env-url>/suppliers' > /tmp/suppliers.json46curl -sS --max-time 30 '<remote-env-url>/boms' > /tmp/boms.json47curl -sS --max-time 30 '<remote-env-url>/inventory' > /tmp/inventory.json48curl -sS --max-time 30 '<remote-env-url>/purchase_orders' > /tmp/purchase_orders.json49curl -sS --max-time 30 '<remote-env-url>/incidents' > /tmp/incidents.json50```5152---5354## Core Business Rules5556### 1. Effective Available Stock5758```59effective_available = on_hand - reserved - quarantined - safety_stock60```6162- `safety_stock` comes from the **product master**, not the inventory record.63- Effective stock **can be negative** (more committed/protected than on hand).64 Always use the raw value for `target_effective_available` and gap calculations.65 Do NOT cap at zero — capping caused score drops in testing.66- No inventory record for a (warehouse, sku) pair ⇒ treat effective as 0 for67 that pair.6869### 2. Customer Account / Risk Overrides (checked BEFORE inventory)7071Customer fields: `account_status` (active, blocked, review_required) and72`risk_flag` (none, fraud_watch, credit_watch).7374**Exception precedence** (pick the first that applies):7576| Condition | `customer_exception` | Decision impact |77|---|---|---|78| account_status == blocked | `account_blocked` | reject_hold / hold_credit_or_fraud |79| risk_flag == fraud_watch | `fraud_watch` | manual_review / hold_credit_or_fraud |80| account_status == review_required | `review_required` | manual_review / send_account_review |81| risk_flag == credit_watch | `credit_watch` | manual_review / send_account_review |82| else | `none` | proceed to product / inventory check |8384The account/risk check happens **before** the inventory check in the decision85flow, but `inventory_status` is still computed and reported for every order.8687### 3. Product Status Check (after account, before inventory)8889- If a product has `active == false` (inactive), the line gets90 `manual_review` / `escalate_product_master`.91- This check applies **per-line**, not per-order (in allocation tasks an order92 can have some inactive-SKU lines and some normal lines).9394### 4. Inventory Status Classification9596For each SKU line at the order's warehouse, compute `effective_available`:9798| Condition | Classification |99|---|---|100| SKU inactive AND effective < ordered | `inactive_and_shortage` |101| SKU inactive (effective ≥ ordered) | `inactive_sku` |102| effective < ordered (SKU active) | `shortage` |103| effective ≥ ordered AND (effective − ordered) < safety_stock | `low_stock` |104| effective ≥ ordered AND (effective − ordered) ≥ safety_stock | `ready` |105106**shortage_skus**: SKUs where effective < ordered quantity.107**low_stock_skus**: SKUs where effective ≥ ordered but remaining after108allocation is below safety_stock.109**inactive_skus**: SKUs where product `active == false`.110111All SKU lists must be **sorted ascending** and de-duplicated.112113### 5. Decision Precedence (per order or per line)1141151. account_blocked → **reject_hold**, hold_credit_or_fraud1162. review_required / fraud_watch / credit_watch → **manual_review**, send_account_review (or hold_credit_or_fraud for fraud)1173. inactive product → **manual_review**, escalate_product_master1184. any shortage → **backorder**, create_backorder1195. low stock (no shortage) → **delayed_release**, delay_and_monitor1206. all available → **ship_now**, release_to_pick121122### 6. Shipping Quotes123124```125total_weight = Σ (product.weight_lb × line.quantity) for all lines in the order126```127128Call: `GET /shipping/quote?warehouse_id=<wh>&destination_zip=<zip>&weight_lb=<weight>&speed=<speed>`129130Pass the **full-precision** weight (do not round before sending). The endpoint131returns `total_cost`, `service_days`, `zone_distance`. Round `total_cost` to 2132decimals for the answer. Use the order's `shipping_speed` field as `speed`.133134Compute the shipping quote for **every** order in the queue, regardless of the135dispatch decision (even blocked/reviewed orders need a quote). The summary136`total_shipping_cost_usd` is the sum of all shipping quotes.137138### 7. Rounding139140- Currency: round to 2 decimal places.141- Percentages: round to 1 decimal place.142- Durations: round to 2 decimal places.143- All other quantities: integers (no rounding needed).144145---146147## Task Family SOPs148149### Family A: Expedite Queue Dispatch (e.g. wave TRAIN_EXPEDITE_A)150151**Input**: a memo listing order_ids to process, an `as_of_date`.152153**Output shape** (`answer_template.json`):154```155{156 "wave_id": "<wave>",157 "records": [ {order_id, inventory_status, customer_exception,158 final_decision, next_action, shortage_skus, inactive_skus,159 low_stock_skus, shipping_quote:{zone_distance, service_days,160 total_cost_usd}} ],161 "summary": {order_count, decision_counts:{ship_now, delayed_release,162 manual_review, backorder, reject_hold}, total_shipping_cost_usd,163 blocked_order_ids, manual_review_order_ids, backorder_order_ids,164 inactive_sku_order_ids}165}166```167168**Procedure**:1691. For each order_id in the memo (not all orders in the wave — only the memo170 list), fetch the order detail.1712. Check customer account_status/risk_flag → customer_exception.1723. For each line, compute effective stock at the order's warehouse → classify173 shortage / low_stock / inactive.1744. Determine inventory_status (combine across lines).1755. Apply decision precedence: account → product → inventory.1766. Compute shipping quote for all orders.1777. Sort records ascending by order_id.1788. `blocked_order_ids` = orders with customer_exception == account_blocked.179 `manual_review_order_ids` = orders with final_decision == manual_review.180 `backorder_order_ids` = orders with final_decision == backorder.181 `inactive_sku_order_ids` = orders that have at least one inactive SKU.182183**Key**: The memo lists specific order_ids — only process those, not the entire184wave. Keep records sorted by order_id.185186---187188### Family B: BOM Replenishment (e.g. kit builds at a warehouse)189190**Input**: a production memo with `target_builds` (bom_id, target_build_quantity,191target_build_date) and a `planning_site` (warehouse_id).192193**Output shape**:194```195{196 "task_id": "<task_id>",197 "plan_date": "YYYY-MM-DD",198 "kit_targets": [{bom_id, kit_name, warehouse_id, build_quantity, build_date}],199 "component_plan": [{sku, total_required, target_effective_available,200 timely_po_qty, transfer_qty, purchase_requisition_qty, final_action,201 coverage_po_ids, exclusion_reason}],202 "transfer_requests": [{sku, from_warehouse_id, to_warehouse_id, quantity,203 needed_by}],204 "purchase_requisitions": [{sku, supplier_id, warehouse_id, quantity,205 needed_by, unit_cost, extended_cost}],206 "excluded_components": [{sku, reason, supporting_po_ids}],207 "summary": {component_count, total_purchase_units, total_purchase_cost,208 total_transfer_units, timely_po_covered_units}209}210```211212**Procedure**:2131. **Compute demand**: For each BOM, multiply each component's214 `quantity_per_kit` by the build quantity. Sum across all builds for each SKU215 → `total_required`.2162. **Earliest build date**: For each SKU, the earliest `target_build_date`217 among all builds containing it → `needed_by` for transfers and purchases.2183. **Effective stock** at the planning site → `target_effective_available`219 (raw, can be negative).2204. **Timely POs**: Filter purchase orders where `status` in (open, confirmed),221 `warehouse_id` == planning site, `sku` matches, and `eta <= needed_by`.222 `timely_po_qty` = sum of all eligible PO quantities.223 `coverage_po_ids` = sorted list of eligible PO IDs.2245. **Gap calculation**:225 ```226 total_available = target_effective_available + timely_po_qty227 gap = max(0, total_required - total_available)228 ```2296. **Exclusion checks** (if no gap to fill):230 - If `effective >= total_required`:231 - If `effective >= product.overstock_threshold` → `target_overstock`,232 `overstock_excluded`233 - Else → `stocked_no_gap`, `no_action_stocked`234 - If `effective < total_required` but `total_available >= total_required`235 → `timely_po_covers_gap`, `timely_po_covered`2367. **Transfer** (if gap > 0): For each other warehouse, compute spare effective237 stock (same formula). Sort by spare descending. Transfer from one warehouse238 at a time (largest spare first), taking `min(spare, remaining_gap)`.2398. **Purchase requisition**: Remaining gap after transfers.240 `unit_cost` from product master, `extended_cost = unit_cost × quantity`.241 `supplier_id` from product master.2429. **final_action**:243 - `overstock_excluded` / `no_action_stocked` / `timely_po_covered`: excluded244 - `transfer_only`: gap fully covered by transfers245 - `purchase_required`: purchase needed (with or without transfers)24610. **Summary**:247 - `total_purchase_units` = sum of purchase_requisition_qty248 - `total_purchase_cost` = sum of extended_cost, 2 decimals249 - `total_transfer_units` = sum of transfer quantities250 - `timely_po_covered_units` = for each excluded `timely_po_covers_gap`251 component, `min(timely_po_qty, max(0, total_required - effective))`.252 **NOT** the raw total PO quantity — use the gap-coverage amount.253254**Key learnings**:255- Effective stock stays raw (negative allowed). Capping at zero drops the score.256- `timely_po_covered_units` = the gap the timely PO actually fills, not the257 total PO quantity. For a component needing 90 with effective −16 and a 335-unit258 PO, the covered amount is 106 (90 − (−16)), not 335.259- `kit_targets` uses the **memo's** `target_build_date`, not the BOM record's260 `target_date`.261- Sort `component_plan` by sku. Sort `transfer_requests` by sku, then quantity262 descending, then from_warehouse_id ascending. Sort `purchase_requisitions`263 and `excluded_components` by sku.264265---266267### Family C: Supplier Incident Scorecard (e.g. Q1 review)268269**Input**: a scorecard request with incident date filter (field, start, end,270inclusive), analysis_date, duration rule, percentage rule, recommendation271policy.272273**Output shape**:274```275{276 "analysis_window": {start_date, end_date, analysis_date},277 "summary": {filtered_incident_count, supplier_count,278 total_resolution_cost, overall_rma_count, overall_work_order_count},279 "supplier_scorecard": [{supplier_id, supplier_name, incident_count,280 incident_percentage, total_resolution_cost, avg_duration_days,281 rma_count, work_order_count, open_incident_count, severe_incident_count,282 recommendation_code}],283 "top_escalation_suppliers": [supplier_id, ...],284 "highest_cost_supplier_id": "string",285 "highest_share_supplier_id": "string"286}287```288289**Procedure**:2901. **Filter incidents** on `open_date` (per the request's field) within291 `[start_date, end_date]` **inclusive** (string comparison works for292 YYYY-MM-DD).2932. **Overall summary**:294 - `filtered_incident_count` = count of filtered incidents295 - `supplier_count` = distinct suppliers with ≥1 filtered incident296 - `total_resolution_cost` = sum of resolution_cost, 2 decimals297 - `overall_rma_count` / `overall_work_order_count` = counts by type2983. **Per-supplier rows** (sort by supplier_id ascending):299 - `incident_count` = count of filtered incidents for supplier300 - `incident_percentage` = incident_count / filtered_incident_count × 100,301 1 decimal302 - `total_resolution_cost` = sum, 2 decimals303 - `avg_duration_days`: for each incident, duration = (close_date −304 open_date).days if closed, else (analysis_date − open_date).days. Average305 across the supplier's incidents, 2 decimals.306 - `rma_count` / `work_order_count` = by type307 - `open_incident_count` = status == open308 - `severe_incident_count` = severity in {high, critical}3094. **Recommendation code** (precedence: ESCALATE_SUPPLIER > PROCESS_REVIEW >310 WATCHLIST > MONITOR):311 - **ESCALATE_SUPPLIER**: supplier `quality_status == quality_hold` AND312 `incident_count >= 3`, OR has any critical RMA (incident_type == RMA and313 severity == critical), OR (rma_count >= 3 AND total_resolution_cost >=314 15000.00)315 - **PROCESS_REVIEW**: work_order_count >= 3 AND work_order_count >316 rma_count317 - **WATCHLIST**: quality_status in {watch, quality_hold}, OR318 incident_count >= 4, OR total_resolution_cost >= 12000.00, OR319 severe_incident_count >= 2320 - **MONITOR**: none of the above3215. **top_escalation_suppliers**: supplier_ids with ESCALATE_SUPPLIER, ordered by322 incident_count descending, then total_resolution_cost descending, then323 supplier_id ascending.3246. **highest_cost_supplier_id**: supplier with max total_resolution_cost325 (ties broken by supplier_id ascending).3267. **highest_share_supplier_id**: supplier with max incident_count327 (ties broken by supplier_id ascending).328329**Key**: The filter uses `open_date`, not `close_date`. Duration for open330incidents uses the `analysis_date`, not today's date. The percentage is a331percentage number (e.g. 23.7), not a fraction (0.237). Severe = {high,332critical}.333334---335336### Family D: Allocation / Transfer Decision (e.g. wave TRAIN_TRANSFER_B)337338**Input**: a wave ID, order data, customer/product/inventory masters, an339allocation memo.340341**Output shape**:342```343{344 "wave_id": "<wave>",345 "line_actions": [{order_id, line_id, sku, requested_warehouse,346 requested_effective_available, action, ship_quantity, transfer_from,347 transfer_quantity, backorder_quantity, primary_reason}],348 "transfer_requests": [{order_id, line_id, sku, from_warehouse,349 to_warehouse, quantity}],350 "blocked_orders": [order_id, ...],351 "order_rollup": [{order_id, outcome}],352 "summary": {total_orders, total_lines, ship_lines, transfer_lines,353 backorder_lines, manual_review_lines, blocked_orders, transfer_units,354 backorder_units}355}356```357358**Procedure**:3591. For each order in the wave, determine the order-level exception:360 - account_blocked → all lines `manual_review`, primary_reason =361 `account_blocked`362 - fraud_watch → all lines `manual_review`, primary_reason = `fraud_watch`363 - review_required → all lines `manual_review`, primary_reason =364 `account_review_required`3652. For orders with no account exception, check each line:366 - If product `active == false` → `manual_review`, reason `inactive_product`367 - Else compute `effective_available` at the requested warehouse:368 - If effective ≥ quantity → `ship`, ship_quantity = quantity369 - If effective < quantity:370 - `usable = max(0, effective)` (cannot ship negative stock)371 - `remaining = quantity − usable`372 - Check other warehouses: if any has `effective >= remaining`373 (spare effective with safety stock preserved), transfer from the374 warehouse with the **largest** effective375 → `transfer`, ship_quantity = usable, transfer_from = that warehouse,376 transfer_quantity = remaining377 - If no single warehouse can cover the full remaining → `backorder`,378 ship_quantity = 0, backorder_quantity = quantity,379 primary_reason = `insufficient_effective_stock`3803. `requested_effective_available` = raw effective stock (can be negative).3814. **blocked_orders**: ALL orders stopped at account or customer-risk level382 (account_blocked, review_required, fraud_watch). NOT inactive-product-only383 orders. Sort ascending.3845. **order_rollup** outcomes:385 - All lines ship → `ready_to_ship`386 - Ship + transfer only (no backorder, no manual_review) → `needs_transfer`387 - Any backorder (no manual_review) → `has_backorder`388 - All lines manual_review → `manual_review`389 - manual_review mixed with other actions, or other mixed combinations →390 `mixed_actions`3916. **Summary**: counts and unit totals. `transfer_units` = sum of392 transfer_quantity, `backorder_units` = sum of backorder_quantity.393394**Key**: For backorder lines, ship_quantity = 0 and backorder_quantity = full395quantity (the line cannot be partially shipped). For transfer lines, the full396uncovered portion must be coverable by a single warehouse — if no single397warehouse can cover, it's a backorder. Transfer source = warehouse with the398largest effective stock that can cover. Sort line_actions by order_id then399line_id. Sort transfer_requests by order_id then line_id.400401---402403### Family E: Procurement Quality-Hold Review404405**Input**: a memo with `analysis_window` (start, end), `decision_choices`406(freeze_new_replenishment, buyer_review_required, monitor_only),407`target_supplier_ids`.408409**Output shape**:410```411{412 "analysis_window": {start, end},413 "supplier_decisions": [{supplier_id, supplier_name, quality_status,414 recent_incident_count, recent_rma_count, severe_or_critical_count,415 open_incident_count, affected_skus, sample_incident_ids, decision,416 held_po_ids}],417 "held_po_ids": [po_id, ...],418 "release_supplier_ids": [supplier_id, ...],419 "summary": {suppliers_reviewed, freeze_count, buyer_review_count,420 monitor_count, held_po_count, total_recent_incidents}421}422```423424**Procedure**:4251. **Filter incidents** for each target supplier: `open_date` within426 `[start, end]` inclusive.4272. Per supplier, compute:428 - `recent_incident_count` = filtered incident count429 - `recent_rma_count` = RMA-type count430 - `severe_or_critical_count` = severity in {high, critical}431 - `open_incident_count` = status == open432 - `affected_skus` = sorted unique SKUs from filtered incidents433 - `sample_incident_ids` = sorted incident IDs, capped at 54343. **Decision logic** (best-effort thresholds from training):435 - **freeze_new_replenishment**: quality_hold AND recent_incident_count >= 3,436 OR any critical RMA, OR (rma_count >= 3 AND total_resolution_cost >=437 15000)438 - **buyer_review_required**: quality_hold AND incidents < 3, OR439 (watch AND (incidents >= 4 OR total_resolution_cost >= 15000 OR any440 critical incident))441 - **monitor_only**: none of the above4424. **held_po_ids** per supplier: all open/confirmed purchase order IDs for443 that supplier (regardless of decision — the template says "open or444 confirmed purchase order ids" without a held qualifier).4455. **Top-level held_po_ids**: union of held_po_ids from suppliers whose446 decision is freeze or buyer_review only (sorted, unique).4476. **release_supplier_ids**: suppliers with monitor_only (sorted).4487. **Summary** counts.449450**Key uncertainty**: The exact decision thresholds for buyer_review vs451monitor_only were not fully resolved in training (all rounds scored 0.556).452The most likely split observed: quality_hold + significant incidents → freeze;453watch + moderate-to-high risk → buyer_review; watch + minor risk → monitor.454Treat `recent_incident_count >= 4` or `total_resolution_cost >= 15000` or455any critical incident as the buyer_review trigger for watch suppliers. Per-456supplier `held_po_ids` should list all open/confirmed POs, not just held ones.457458---459460## Common Misjudgments and Exclusion Rules4614621. **Don't cap effective stock at zero.** Negative effective stock is valid463 and must be used in gap calculations. Capping dropped the BOM score from464 0.78 to 0.50.4654662. **`timely_po_covered_units` is the gap covered, not total PO qty.** If a467 component needs 90, has effective −16, and a timely PO of 335 units, the468 covered amount is 106 (the shortfall), not 335.4694703. **Only process orders listed in the memo, not the entire wave.** The471 `/orders?wave=` endpoint returns all orders in the wave, but the task memo472 specifies which order_ids to process.4734744. **blocked_orders includes ALL account/risk-stopped orders** (blocked,475 review_required, fraud_watch), not just account_blocked. Inactive-product476 orders are NOT in blocked_orders (that's a line-level product issue).4774785. **For allocation transfers, one warehouse must cover the FULL uncovered479 quantity.** If no single warehouse can cover the entire remaining amount,480 the line goes to backorder, not a multi-warehouse transfer.4814826. **For allocation backorder lines, ship_quantity = 0.** The entire quantity483 is backordered — no partial ship.4844857. **Incident filter uses `open_date`, not `close_date`.** Duration for open486 incidents uses `analysis_date`, not today. Percentages are percentage487 numbers (23.7), not fractions (0.237).4884898. **Recommendation precedence is strict.** ESCALATE > PROCESS_REVIEW >490 WATCHLIST > MONITOR. A supplier meeting WATCHLIST criteria but also491 PROCESS_REVIEW criteria gets PROCESS_REVIEW. A supplier on quality_hold492 with >= 3 incidents gets ESCALATE even if it also meets PROCESS_REVIEW.4934949. **Kit build dates come from the production memo**, not the BOM record's495 `target_date`. Use the memo's `target_build_date` for `needed_by` and496 `build_date` fields.49749810. **Shipping weight uses full precision.** Don't round the weight before499 passing to `/shipping/quote`. Round only the returned `total_cost` to 2500 decimals.50150211. **Transfer source selection = largest effective first.** Among warehouses503 that can cover the uncovered quantity, choose the one with the largest504 effective stock.50550612. **Overstock exclusion**: `target_overstock` when effective >= total_required507 AND effective >= product.overstock_threshold. If effective >= total_required508 but below the threshold, it's `stocked_no_gap`.509510---511512## Reusable SOP (applies to unseen test tasks)5135141. **Read the prompt and answer template carefully.** The template defines515 exact field names, types, ordering, and enum values. Match them precisely.5162. **Download all data collections** up front via the API endpoints listed above.5173. **Compute effective stock** = on_hand − reserved − quarantined − safety_stock518 (safety_stock from product master). Keep raw (negative allowed).5194. **Apply account/risk overrides before inventory** for any dispatch,520 allocation, or expedite task.5215. **Check product active status per line** before inventory for allocation522 tasks.5236. **Filter incidents on open_date inclusive** within the analysis window.5247. **Compute durations** using close_date for closed, analysis_date for open.5258. **Apply the recommendation/decision precedence** strictly526 (ESCALATE > PROCESS_REVIEW > WATCHLIST > MONITOR for scorecards;527 account > product > inventory for dispatch/allocation).5289. **Round correctly**: currency 2 decimals, percentages 1 decimal, durations 2529 decimals.53010. **Sort all lists** as specified in the template (by order_id, line_id, sku,531 supplier_id, etc.).53211. **Verify output shape** against the template before submitting: all required533 top-level keys, all item required keys, all enum values valid, all534 orderings correct.53512. **Use the remote API** (`<remote-env-url>`) with536 `curl -sS --max-time 30` for every call.