ProcureOps ERP answer builder
You turn a task prompt + a local memo/payload + an answer_template.json into a
single JSON object that matches the template, grounded in the live ProcureOps
ERP API. The API is the source of truth; the local memo names anchors and
business rules but can be stale.
This is a judgment task wearing a data costume. The arithmetic is simple (sums, differences, rounding); the value is in scoping records correctly, applying the right business rule, and not including records that don't belong. Slow down on scoping and exclusions — that is where answers go wrong.
Operating loop
Read everything local first. The prompt, the
answer_template.json, and every file ininput/payloads/. The template is your contract: it names the exact keys, enums, ordering, and rounding. Build your output to mirror it field-for-field. The memo names the anchor ids (program, PO, requisition, invoice, batch, contract) and any task-specific rules (tax rate, opening balance, cutoff date, which actions count as "approved", freight handling).Identify the task family so you know the derivation pattern. Match on the template's top-level keys:
- nomination_lines / committee_action → A. sourcing nomination
- line_reconciliation / invoice_review / inspection_summary → B. receiving/AP reconciliation
- invoice_decisions / vendor_balances / payment_hold_queue → C. AP close
- contract_check / program_budget_check / approval_check → D. change-control
- release_decisions / receiving_exceptions / chargeback → E. AP release netting
You will also meet NEW templates that recombine these blocks. Don't pattern match rigidly — read the field names and the per-field notes in the template, then assemble from the building blocks in
references/derivations.md.Pull the records from the API (see "Using the API" below). Resolve each anchor id, then walk the graph the task needs: PO → its receipts + invoices + payments; supplier → risk events; requisition → approval events; program → budget snapshot + contract. The
scripts/probe.pyhelper does the common walks (bundle <po_id>,supplier <sid>) so you don't hand-roll curl.Reconcile API vs local memo. The API wins for any record you can fetch (quantities, prices, statuses, totals, dates). The memo wins only for things the API doesn't carry: task-supplied constants (tax rate, opening balance, cutoff), local registers (chargebacks), and which ids are in scope. If the memo references an id the shared API doesn't have (e.g. a "PO-73xx" alias), treat that note as supporting-only and use the real ids the packet provides.
Compute derived values with the recipes in
references/derivations.md(budget/contract headroom, quantity & price reconciliation, variance, chargeback netting, max-quantity-within-budget). Compute on raw numbers, round at the very end.Apply the as-of / cutoff scope. Almost every task has a date (
as_of,close_date,review_as_of, snapshot date). Records dated after it are out of scope. The big one: a vendor-risk event is "open as of date" whenstatus ∈ {open, monitoring}ANDevent_date <= as_of.closedis out;monitoringcounts as open.Emit JSON only. Match the template's keys exactly, fill the literal
task_id/required values it specifies, honor enums verbatim, and write nothing outside the JSON object.
Using the API
Base URL comes from the prompt; default http://127.0.0.1:8056 (mirror at
:8006 serves identical data — either works regardless of which port the prompt
prints). Read-only GET, JSON.
- Fetch one:
/<collection>/<id>returns the bare object. - List + filter:
/<collection>?field=value— exact, case-insensitive match, and it matches fields nested inside list values too (e.g./ap/invoices?sku=DRV-AX17matches the sku inside invoicelines). List responses are{"count": n, "results": [...]}— readresults. - Date range:
?start=YYYY-MM-DD&end=YYYY-MM-DDfilters each collection's primary date field (receipts.receipt_date, ap_invoices.invoice_date, payments.scheduled_date, etc.). Bounds are inclusive. - Collections: programs, suppliers, items (by sku), contracts,
purchase_requisitions, purchase_orders, receipts, ap/invoices, ap/payments,
approval_events, budget_snapshots, vendor_risk_events. Also
/manifest(counts + anchor ids) and/health. - Watch the join keys. Most children carry the parent id directly
(
po_id,supplier_id,program_id,contract_id). The exception: approval_events join onobject_id, notrequisition_id— filter with?object_id=REQ-....
Helper: python3 scripts/probe.py {get|list|bundle|supplier} ... [--base URL].
Always pass --base if the prompt names a non-default port.
Output conventions (hold these across all templates)
- Money rounds to cents (2 dp). Ratios to 4 dp, percentages to 1 dp, unless the field note says otherwise. Round once, at the end.
- List fields are sets — include each id once, order doesn't matter and the evaluator sorts — UNLESS the template says "sorted ascending"/"alphabetical"/ "sort by X", in which case you sort exactly as instructed.
- Enums verbatim. Use only the allowed values the template lists, spelled
and cased exactly. When several could apply, pick the one the business rule
selects (see derivations); for combined-blocker enums (e.g.
hold_for_budget_and_approval) compose per the template's allowed list. - Nulls where the template allows them (e.g.
commercial_basis_id/hold_codeis null when there's no contract / no hold). Don't invent ids. task_idand other required literal values come from the template (required_value) — write them exactly, don't derive them from the prompt.- Evidence/source lists: include every API record id you actually used and the local payload paths you read; classify into authoritative (API records, local registers the task says to trust) vs supporting-only (request notes, stale aliases) when the template asks for that split.
High-leverage rules and common misjudgments
- Headroom = cap − committed. Budget headroom =
budget_cap − committed_amount(program or its snapshot). Contract headroom =ceiling − sum(subtotal of non-cancelled POs on the contract). Always exclude cancelled POs from contract usage; never exclude them silently from anexcluded_*list the template asks for. - "Open risk as of date" is the most-missed scope. open OR monitoring, and
event_date <= as_of. Don't count closed events; don't forget monitoring. risk_rating≠ open risk event. A supplier can be ratedwatch/highwith no open event, or ratedlowwith an open event (both happen in this data).supplier_watchblocker keys off the rating;open_supplier_riskkeys off in-scope events. Keep them separate.- Duplicate invoices on one PO are an exception, not noise. If two invoices hit the same PO, both belong in the line's exception id set.
- Extra receipt on one PO is excluded, not summed. Pick the receipt tied to
the invoice/batch under review; route others to
excluded_same_po_receipt_idsand don't fold their quantities into the reconciliation. - Missing receipt → explicit hold. receipt_id null and no receipt for the PO
⇒ NO_RECEIPT / hold_missing_receipt / a
MISSING:<po_id>exception row; received_qty = 0.00, full quantity is the variance. - Trust the controlling status, not the stale flag. In netting/release
tasks the local chargeback register's
status(approved vs pending_quality_review) decides whether the chargeback nets now, even if the invoice's APIstatuslooks different. Confirm totals/receipts against the API. - Stay inside the named scope. When a task says "these invoices only" / "this batch only", do not pull the supplier's or program's other records into the rollup.
- Take invoice tax/freight/total verbatim from the invoice; don't recompute tax unless the template/memo explicitly defines a rate to apply (change-control is the case where you DO compute requested_tax from the memo's rate).
send_to_committee/ready_to_releaseare conservative. Only "yes"/true when nothing blocks. Any hard blocker ⇒ "no"/false.
For exact field-by-field math and the per-family recipes, read
references/derivations.md. Verify any arithmetic with a quick Python check
before writing it into the answer — a wrong cent fails the match.