ProcureOps ERP Solver Skill
You solve procurement / receiving / accounts-payable (AP) tasks against a read-only
ProcureOps ERP API. Each task gives you a prompt.txt, a local memo/packet payload,
and an answer_template.json. You must return ONE JSON object that matches the
template exactly. There is no answer key — derive every number/decision from the live
API and the rules below.
0. THE API IS SOURCE OF TRUTH
Base URL (overrides any 127.0.0.1:8006 / localhost in the prompt):
<remote-env-url>
GET /<collection>->{"count": N, "results": [...]}GET /<collection>/<id>-> single object (404 if missing)- Collections:
programs,suppliers,items,contracts,purchase_requisitions,purchase_orders,receipts,ap_invoices,payments,approval_events,budget_snapshots,vendor_risk_events. - Filter by any record field (case-insensitive), e.g.
GET /receipts?po_id=PO-AX17-4481,GET /ap_invoices?supplier_id=SUP-LUMA. - Date filters
start/endapply to each collection's primary date field (receipts->receipt_date, ap_invoices->invoice_date, payments->scheduled_date, purchase_orders->order_date, contracts->effective_date, purchase_requisitions->need_by, approval_events->event_date, budget_snapshots->snapshot_date, vendor_risk_events->event_date).end=<as_of_date>keeps records on/before that date (verified: it drops future-dated events). Use this for any "as of " requirement.
RULE: Local payload files (memo.md, packet.json, chargeback registers, export
values) describe the business request / watch set (which IDs to look at, what
opening balances to assume, business-control flags). They DO NOT override ERP
record values. If a payload's number disagrees with the API record, use the API.
Exception: a payload may legitimately supply data that is not in the ERP at all
(e.g. a chargeback_register_excerpt, an assumed opening_balance, a tax_rate) —
use those as given because they are inputs, not overrides.
1. RECORD SHAPES (field names that bite you)
- programs: program_id, name, owner, budget_cap, committed_amount, cost_center, priority, region, status.
- suppliers: supplier_id, name, payment_terms, region, risk_rating
(
low|medium|watch|high...), status (active...). - items: sku, description, standard_cost, preferred_supplier_id, category, uom, active.
- contracts: contract_id, supplier_id, sku, program_id, price_type
(
fixed...), unit_price, ceiling_amount, status (active|draft|...), effective_date, expiry_date, buyer. - purchase_orders: po_id, program_id, supplier_id, contract_id (MAY BE null),
requisition_id, status (
open|confirmed|partial_receipt|received|closed|cancelled), order_date, due_date, ship_to, currency, subtotal, tax, total, lines:[{line_id, sku, quantity, unit_price, description}]. - purchase_requisitions: requisition_id, program_id, sku, quantity, requester,
priority, need_by, status (
converted|approved|...). - receipts: receipt_id, po_id, supplier_id, warehouse_id, receipt_date,
packing_slip, receiver, status (
accepted|accepted_with_note|inspection_hold|...), lines:[{po_line_id, sku, quantity_received, quantity_rejected, inspection_status}]. - ap_invoices: invoice_id, po_id, supplier_id, receipt_id (MAY BE null),
invoice_date, status (
approved|on_hold|pending_receipt|paid|...), hold_code (QTY_VARIANCE|NO_RECEIPT|null|...), subtotal, freight, tax, total, lines:[{po_line_id, sku, quantity_billed, unit_price}]. - payments: payment_id, invoice_id, supplier_id, amount, scheduled_date,
status (
scheduled|paid|...). - approval_events: event_id, object_id, object_type (
requisition...), action (submitted|approved|rejected|...), actor, event_date, note_code. ⚠ KEY: filter byobject_id=REQ-..., NOTrequisition_id. (?requisition_id=returns 0 results.) - budget_snapshots: snapshot_id (e.g.
BUD-PRG-AX17), program_id, budget_cap, committed_amount, pending_invoice_amount, currency, snapshot_date. - vendor_risk_events: event_id, supplier_id, related_object_id, event_type
(
bank_change|invoice_variance|quality_hold|late_delivery|...), severity (low|medium|high|critical), status (open|monitoring|closed), event_date.
2. CORE BUSINESS RULES (transferable)
2.1 Three-way match (PO ↔ Receipt ↔ Invoice)
Match on po_id + po_line_id + sku. For each line:
- ordered_qty = PO line.quantity
- received_qty = sum of receipt line.quantity_received for the batch/scope in question (see 2.7 scoping). 0 if no receipt.
- rejected_qty = receipt line.quantity_rejected
- billed_qty = invoice line.quantity_billed
- short_qty_vs_po = ordered_qty − received_qty
- unreceived_billed_qty = max(billed_qty − received_qty, 0) (billing ahead of receipt)
- receipt_completion_ratio = received_qty / ordered_qty (round to 4 decimals)
- A clean three-way match = invoice approved AND received_qty == billed_qty AND
received_qty == ordered_qty (or PO
received) AND no price mismatch.
2.2 Price consistency / contract price
- po_unit_price = PO line.unit_price
- contract_unit_price = contract.unit_price for that sku+supplier (null/none if no contract). contract_price_match = (po_unit_price == contract_unit_price == invoice_unit_price). If there is no contract for the sku, there is no contract price to match (treat as missing_contract, not a price mismatch).
- invoice_unit_price = invoice line.unit_price.
- PRICE_MISMATCH only when the prices actually differ.
2.3 AP invoice hold vs release
- The invoice's own
statusandhold_codeare authoritative for current state:on_hold/pending_receipt=> HOLD;approved=> releasable (subject to match);paid=> already settled. - Common hold codes:
QTY_VARIANCE(billed > received),NO_RECEIPT(no receipt posted / receipt_id null). - RELEASE only when: invoice approved AND received_qty covers billed_qty AND no open blocking exception. Otherwise HOLD.
- A scheduled payment (payments.status == scheduled, scheduled_date within the
task's window) means the invoice is being paid: it reduces the close/vendor
balance and is reason code
SCHEDULED_PAYMENT_FOUND. Respect any date cutoff in the memo (e.g. "scheduled through 2026-06-30").
2.4 Quantity variance reconciliation (AP close)
- quantity_billed = invoice billed qty; quantity_received = receipt received qty
(0.00 if no receipt — say so with reason
NO_RECEIPT). - quantity_variance = quantity_billed − quantity_received.
- quantity_variance_pct = variance as a percentage of PO quantity (variance / PO_line.quantity × 100), rounded to 1 decimal — read the template; it specified "percentage of PO quantity", not of billed.
- Any nonzero variance => reason code
QTY_VARIANCEand typically HOLD. - reason_codes are a controlled enum, output alphabetical if the template says so.
2.5 Vendor balance / program rollups (AP close slice)
- opening_balance: use the value the memo tells you (often 0.00 for the slice).
- close_balance = opening_balance + invoice_total − scheduled_payments (apply the template's exact formula verbatim).
- held_invoice_total / releasable_invoice_total split by hold_decision.
- balance_status enum: FULLY_SCHEDULED (scheduled covers invoices), OPEN_HELD (has held invoices), OPEN_APPROVED (approved, not yet scheduled).
- Program summary: group by program_id; invoice_count, invoice_total, held_total, released_total, net_close_balance. Only include the TARGET invoices named in the memo — this is a "close-slice", not the whole ledger.
2.6 Sourcing nomination gates (readiness packet)
For each package line (sku + its requisition + its PO):
- selected_supplier_id = the PO's supplier (or the item's preferred_supplier_id).
- Compute blocker_codes (controlled enum, sorted ascending):
missing_contract: no active contract for that sku+supplier (PO.contract_id null AND no contract record found).supplier_watch: supplier.risk_rating ==watch(or worse, per task).open_supplier_risk: ≥1 vendor_risk_event with status in {open, monitoring} for that supplier, as of the as_of_date.ap_hold: an AP invoice on this PO is on_hold / pending_receipt.pending_receipt: PO not fully received (status open/partial_receipt) or no receipt yet but expected.late_due_date: PO due_date / requisition need_by past the as_of_date and not fulfilled.none: no blockers (use alone).
- readiness_status:
ready(no blockers) /at_risk(soft blockers like watch / monitoring, but can proceed conditionally) /not_ready(hard blockers like missing_contract, ap_hold, no receipt, open severe risk). - nomination_decision:
nominate(ready),conditional_nomination(at_risk, clearable),hold(not_ready). - Evidence lists (receipt_evidence_ids, invoice_exception_ids, risk_event_ids) are
filtered "as of as_of_date" — use the
end=<as_of_date>filter or drop records whose primary date is after as_of_date. Treat each as a SET, sort ascending. - commercial_basis_id = the contract_id backing the line (null if none).
- committee_action: bucket suppliers into nominate_now / conditional / hold by decision; send_to_committee yes/no per whether anything is ready/conditional.
2.7 Receiving closeout SCOPE (critical exclusion rule)
- A receiving-batch closeout ("batch RCV-XXX") reconciles only that batch's receipt lines — NOT the cumulative of all receipts on the PO. If the same PO has another receipt (e.g. a later RCV-#### batch), it is a SEPARATE batch: exclude it from this batch's received_qty, and list it under any "excluded_same_po_receipt_ids" field. (Seen: PO-AX17-4481 has RCV-BLUE-14 and a separate RCV-00001 — the closeout for RCV-BLUE-14 uses only 216, not 216+157.)
- A closeout review of an already-posted receipt does NOT create a new receipt; "no visible damage reported" in the memo does NOT add DAMAGE_REJECTION unless the receipt's quantity_rejected > 0.
- DAMAGE_REJECTION only when quantity_rejected > 0.
- PARTIAL_RECEIPT when received_qty < ordered_qty.
- INVOICE_QTY_EXCEEDS_RECEIPT when billed_qty > received_qty.
- SUPPLIER_WATCH_RISK when supplier.risk_rating ==
watch. - NO_EXCEPTION only when none of the above apply.
2.8 Change-control / contract amendment (release vs hold)
- Contract headroom (ceiling): noncancelled_subtotal = Σ subtotal of all POs on
the contract whose status !=
cancelled(EXCLUDE cancelled POs explicitly; list them in excluded_cancelled_po_ids). headroom_before = ceiling_amount − noncancelled_subtotal. requested_subtotal = requested_qty × contract.unit_price. headroom_after = headroom_before − requested_subtotal. ceiling_ok = (headroom_after >= 0). - Program budget: from budget_snapshots for the program. remaining_budget = budget_cap − committed_amount. requested_tax = requested_subtotal × tax_rate (tax_rate from the memo, e.g. 7.25%). requested_total = requested_subtotal + requested_tax (freight only if the memo provides freight). budget_after_change = remaining_budget − requested_total. budget_ok = (budget_after_change >= 0). max_quantity_with_current_budget = floor(remaining_budget / (unit_price × (1 + tax_rate))).
- Approval: find the latest approval_event for the source requisition
(filter
object_id=REQ-..., take max event_date). approval_ok = latest action is in the memo's "approval_good_actions" (e.g.approved). Asubmitted(not yet approved) => approval_ok=false => action obtain_final_requisition_approval. - Supplier risk: supplier_risk_ok is true unless there is an OPEN event whose
severity is severe (
high/critical). Awatchrating alone is context only (per memo) and does NOT block. severe_open_event_ids = open AND severe; open_event_ids = all status==open for that supplier. - Decision enum: combine the gates:
- all ok =>
release_amendment - budget fail
hold_for_budget - approval fail
hold_for_approval - budget AND approval fail =>
hold_for_budget_and_approval - supplier severe-risk fail =>
hold_for_supplier_risk - contract sku/price mismatch =>
reject_contract_mismatch
- all ok =>
- required_actions: enum list, sorted ascending, one per failed gate
(
raise_budget_exception_or_reduce_quantity,obtain_final_requisition_approval,resolve_supplier_risk_hold);noneif all ok. - summary.blocker_count = number of failed gates; ready_to_release = (decision == release_amendment).
2.9 Duplicate-invoice / chargeback netting (AP release file)
- A chargeback nets against the invoice it references. If chargeback.status ==
approved: net_release_amount = invoice_total − approved_chargeback_amount; decisionrelease_net_after_approved_chargeback; reason maps to the chargeback's reason_code (Underage Quantity =>approved_qty_chargeback; AP Quantity Variance =>approved_ap_quantity_variance). - If chargeback.status ==
pending_quality_review(or the receipt is on inspection_hold): decisionhold_pending_quality_chargeback; reasoninspection_hold_pending_chargeback; the amount goes to pending_chargeback_total, net_release_amount = 0.00 (held, not released). - Chargeback amount = basis_quantity × unit_cost from the register (the register is an input; use its numbers, but verify the referenced invoice/PO/receipt exist in the API).
- Invoice with receipt_id null AND no receipt on the PO: decision
hold_missing_receipt, reasonno_receipt_on_po, net 0.00. - Duplicate/second receipt on the same PO that belongs to a different invoice: list under excluded_same_po_receipt_ids and add the corresponding followup (e.g. hold_luma_duplicate_receipt_for_separate_invoice).
- authoritative_sources = the procureops_* record families you actually used; supporting_only_sources = the note/alias payloads (request notes, stale-alias notes) — they are context, not authority.
- If the packet references generated/aliased PO-73xx IDs that don't exist in the API, use the real IDs the packet maps them to (use_available_shared_ids); flag the alias note as supporting_only and add the appropriate followup.
3. OUTPUT CONVENTIONS (get these exactly right)
- Return ONLY the JSON object — no prose, no markdown fences around it.
- Match the answer_template's keys, nesting, and enums EXACTLY. Templates come in
two styles:
- Value-shape (task 1/5): keys map to placeholder strings ("string", "YYYY-MM-DD") — emit those keys with real values.
- Schema-style (task 2/3/4): keys like
required_keys,row_keys,allowed_values,item_schema,field_rulesDESCRIBE the output. Build the real object/list from those descriptions; do NOT echo the schema words.
- task_id: use the template's required/expected value verbatim
(e.g.
train_002,train_003, or the value embedded in the template). - USD amounts rounded to 2 decimals (cents). Ratios/percentages to the precision the template states (e.g. completion_ratio 4 dp, variance_pct 1 dp). Round only at output; carry full precision in intermediate math.
- Enum strings: copy VERBATIM from the template's allowed list (exact casing, underscores). Never invent a value outside the allowed set.
- List fields: treat as SETS (dedupe) and SORT ascending unless the template gives a specific ordering ("sort by po_line_id ascending", "invoice_id ascending", "alphabetical"). When the template says "set; evaluator sorts values", still emit sorted for safety.
- null vs empty: use
nullwhere the template allows it (e.g. hold_code null, commercial_basis_id null); use[]/0.00where a value is required but absent (e.g. quantity_received 0.00 when no receipt). - Booleans are real JSON booleans (true/false), not strings.
4. STEP-BY-STEP SOP FOR A NEW TASK
- Read prompt.txt + the local payload(s). Note: the as_of/close/review date, the exact target IDs (POs, receipts, invoices, suppliers, programs), the scope ("only these invoices", "this batch"), and any business-control flags (tax rate, opening balance, good actions, exclusion rules).
- Read answer_template.json end-to-end. List every output key, its type, its enum allowed-values, and its ordering rule. This defines exactly what to compute.
- Pull the live records by ID first (
/collection/<id>), then pull related sets by filter (/receipts?po_id=,/ap_invoices?po_id=,/payments?invoice_id=,/approval_events?object_id=,/vendor_risk_events?supplier_id=,/budget_snapshots?program_id=,/purchase_orders?contract_id=). - Apply date scoping: anything "as of " => filter primary date <= as_of_date
(use
end=). Future-dated records are excluded. - Apply scope inclusion/exclusion: target IDs only; exclude cancelled POs from contract usage; exclude other-batch receipts from a batch closeout; exclude non-target invoices from a close-slice.
- Run the relevant rule set from section 2 (three-way match, hold/release, variance, nomination gates, change-control gates, chargeback netting).
- Compute money with full precision, round at the end to cents.
- Assemble the JSON to the template's exact shape; verify every enum is from the allowed list and every list is deduped + correctly ordered.
- Sanity check: do the rollups add up (program totals = sum of line items; net_balance = total − scheduled; net_release = total − approved chargeback)? Does each decision enum follow from its gates?
5. COMMON MISJUDGMENTS TO AVOID
- Filtering approval_events by
requisition_id(useobject_id). - Treating
submittedas approved (it is not — approval gate fails). - Counting cancelled POs in contract usage / headroom.
- Summing all receipts on a PO when only one batch is in scope.
- Adding DAMAGE_REJECTION because a memo mentions damage — only quantity_rejected>0 counts.
- Letting a
watchsupplier rating block a change-control release — it is context only unless an OPEN severe event exists. - Including future-dated risk/approval events in an "as of" snapshot.
- variance_pct base: it is % of PO quantity (per template wording), not % of billed.
- Echoing template schema words (
required_keys,row_keys,allowed_values) into the answer instead of real values. - Overriding live API record values with stale numbers from the local memo/export.
- Releasing an invoice that is on_hold / pending_receipt or has a pending (not approved) chargeback / inspection_hold receipt.
- Forgetting to sort/dedupe list fields, or emitting an enum string with wrong casing.