ERP Finance Expense-Control Skill (task_group_005)
Reusable workflow rules for expense-control / AP / prepaid-close / vendor-compliance
tasks against the shared ERP finance API. Distilled from reflect training.
1. API contract
- Base URL:
<remote-env-url> (runner-provided; treat remote API as system of record).
GET /endpoints lists all paths. All list endpoints support exact-match query params by
field name, plus limit/offset.
- Resource paths (use the
/api/... form):
- Claims:
GET /api/claims?claim_id=<id>
- AP bills:
GET /api/ap/bills?claim_id=<id> (also queryable by bill_id)
- AP payments:
GET /api/ap/payments?bill_id=<bill_id>
- Vendors:
GET /api/vendors?vendor_id=<id>
- Compliance:
GET /api/compliance/objects?business_id=<id> (returns bank, license, PEP,
sanctions, shell, tax_id, ubo_list, risk_score, review_status, missing_fields in one object)
- Prepaid invoices:
GET /api/prepaids/invoices?prepaid_invoice_id=<id>
- GL balances:
GET /api/prepaids/gl-balances?account=<acct> (returns per-period ending_balance)
- Close logs:
GET /api/close/logs (area, period, related_account, status, message)
- Efficiency: batch curl calls; query by specific ID/param only; never dump whole collections.
- Currency: report USD to two decimals unless a task explicitly says otherwise. When a prompt
says "USD cents" but the schema field says
unit: USD, precision: 2, emit the USD value to
2 decimals (the schema wins).
2. General reconciliation SOP
- Read the task's
payloads/answer_template.json FIRST — it defines required keys, enums,
ordering, and unit/precision. Conform exactly (extra props often disallowed; ordering enforced).
- Fetch the candidate IDs' current records from the API (claims, bills, payments, vendors,
compliance, prepaids, GL, close-logs as relevant). Ignore stale local snapshots/prompts that
conflict — the API is the system of record.
- For each candidate, classify using the rules below, then aggregate (totals, flag lists,
status) per the template.
- Sort every ID list exactly as the template specifies (ascending by id unless stated).
- Keep candidate answers minimal-valid JSON.
3. Expense-claim reimbursement close (claim -> AP bill -> payment)
Classify each batch claim into exactly one bucket:
- paid: a matching AP bill (bill.amount == claim.amount AND bill.vendor_id ==
claim.vendor_id) with status
paid AND a cleared payment (payment.status == cleared) for
that amount. A processing/in-flight payment does NOT count as paid.
- payable (can remain in AP queue): claim approved, and a valid OPEN (scheduled/approved,
not paid/void) AP bill that matches claim amount + vendor. In-flight (processing) payment
leaves it payable; the open AP balance still equals the bill amount.
- blocked: not paid and cannot be released — no matching bill, amount/vendor mismatch,
voided bill, unapproved claim, or expense-case issue. These need owner/AP-link cleanup.
Derived fields:
ap_open_balance_total = sum of valid open matching AP bill amounts for payable claims only
(USD 2dp). Exclude mismatched, voided, and already-paid bills.
crm_required_claim_ids = blocked claims requiring expense-case or AP-link remediation
(equals the blocked set when all blockers are cleanup-type).
batch_status: blocked if any claim blocked; else open_payables if valid unpaid AP bills
remain; else ready_to_close.
reviewed_claim_count = number of batch claims reviewed.
Multiple bills per claim: pick the one matching claim amount+vendor as the "real" bill; stray
bills (wrong vendor/amount) are ignored for balance but noted as AP-link issues.
4. Vendor onboarding finance-risk release (per business)
Input: compliance object + vendor record per business_id. As-of date from batch payload.
reportable_ubo_counts = count of UNIQUE beneficial-owner names in ubo_list with
ownership_pct >= 25 (the 25% reporting threshold). Deduplicate by name; a name qualifies if
ANY of its entries is >= 25%. (24% does NOT count; 30%+ does. This threshold was confirmed:
using 10% drops the score.)
hard_stop_flags (alphabetical; empty list when none) — derive from compliance object:
bank_closed : bank_account_status == "closed"
bank_name_mismatch : bank_account_status == "name_mismatch"
confirmed_pep : pep_status == "confirmed_pep" (possible_pep / not_run do NOT set this)
expired_license : license_expiry < as_of_date
missing_required_documents : missing_fields non-empty
sanctions_confirmed: sanctions_check_status indicates a confirmed hit
screening_not_run : sanctions_check_status == "not_run" OR pep_status == "not_run"
shell_company_suspected: shell_company_suspected == true
vendor_on_hold : vendor.status == "on_hold"
Note: tax_id mismatch between compliance and vendor is NOT one of the hard_stop enum values.
decision {approve, awaiting_information, escalate}:
escalate if ANY serious flag: confirmed_pep, sanctions_confirmed, shell_company_suspected,
bank_closed, bank_name_mismatch. (PEP/shell/bank issues escalate — confirmed: downgrading
them to awaiting_information drops the score.)
- else
awaiting_information if any remediable flag: missing_required_documents,
screening_not_run, expired_license, vendor_on_hold.
- else
approve (no hard stops).
follow_up_business_ids = businesses whose decision is not approve (ascending).
overall_release_ready = true only if every business is approve.
Common misjudgments:
- Treating escalate as sanctions-only (too narrow) — PEP/shell/bank must escalate.
- Using a 10% UBO threshold — use 25%.
- Forgetting screening_not_run also covers pep_status == "not_run".
5. Prepaid close reconciliation (per account, per period)
Input: scoped prepaid invoice IDs + accounts + close period + variance_threshold_abs.
Per invoice (straight-line monthly amortization):
march_amortization (period month) = the invoice's monthly_amortization FIELD when the
period month falls within [service_start month .. service_end month]; else 0. Use full-month
convention: the service_start month counts as a full month (a 03-15 start amortizes all of
March). *** Use the monthly_amortization FIELD, NOT original_amount/term *** — exact
original/term recomputation drops the score.
cumulative_amortization_through_march = monthly_amortization FIELD × number of months
elapsed from service_start month through the close month (inclusive).
ending_balance = original_amount − cumulative_amortization_through_march (2dp).
default_missing_term_flag = true iff missing_contract_dates is in data_quality_flags.
exception_flag = true iff data_quality_flags is NON-EMPTY (any flag — superset, not just
rounded_amount). default_missing_term is a subset of exceptions.
Per account rollup (sum over the account's selected invoices):
- original_amount_total, march_amortization_total, cumulative_amortization_through_march,
schedule_ending_balance (= original_total − cumulative_total).
gl_ending_balance = the GL balance record whose period == close period for that account.
variance_amount = schedule_ending_balance − gl_ending_balance (2dp; can be negative).
variance_flag = |variance_amount| > variance_threshold_abs.
has_default_missing_term_flag = any selected invoice in the account has missing_term.
account_status:
requires_reconciliation if has_default_missing_term_flag (data-quality prevents a clean tie).
- else
variance_review if variance_flag.
- else
reconciled.
Output lists: default_missing_term_invoice_ids and exception_invoice_ids ascending by
invoice id (note: "PPD-2025-..." sorts before "PPD-AUR-..." because '2' < 'A').
selected_invoice_ids and invoice_results keep the SAME order as the scope file.
Common misjudgments:
- Recomputing cumulative as original×months/term (exact) instead of monthly_field×months — drops.
- Using disjoint exception (only rounded_amount) instead of superset — drops.
- Prorating mid-month starts — use full-month.
6. AP batch reconciliation from a stale export (claim, bill, payment, close-log)
For each candidate claim, compare the stale snapshot to CURRENT API data:
- eligible_claim_ids: approved claim with a valid OPEN AP bill matching amount+vendor (amount
& vendor match), and no blocking issue. An in-flight (processing) payment keeps it eligible;
its open AP balance = the bill amount (payment not yet cleared).
- not_ready_claim_ids: paid (already settled), amount/vendor mismatch, voided bill, or
unapproved claim.
- ap_balance_by_claim (USD 2dp): valid matching OPEN bills minus CLEARED payments, EXCLUDING
mismatched and voided bills. A paid claim => 0. A claim whose only bill mismatches => 0 (the
mismatched bill is excluded, NOT counted). A voided bill => 0. *** Do NOT count every open
non-voided bill regardless of match — counting mismatched bills drops the score. ***
- stale_snapshot_corrections (per claim, enum):
mark_in_flight_payment : snapshot showed no payment but current shows a processing payment.
replace_with_matched_paid_bill: snapshot referenced a wrong (mismatched) scheduled bill; current
has a matched PAID bill + cleared payment.
exclude_amount_or_vendor_mismatch: bill amount or vendor does not match the claim.
ignore_void_bill : the linked bill is void in the current API.
block_unapproved_claim : claim status is not approved (e.g. needs_receipt).
current_snapshot_ok : snapshot already matches current state.
- batch_status:
needs_ap_refresh when the batch originated from a stale AP export and must
be reconciled before sending (default for stale-snapshot batches with mixed eligibility).
ready_to_send only when all candidates eligible. blocked is for hard blocks — do not use
it merely because some claims are not ready (using "blocked" here drops the score).
- close_log_required: {required: bool, ids: [close_log_id ascending]}. Inspect /api/close/logs
for AP-area entries relevant to the claim bill accounts/period; include unresolved
(open/ready_for_review/blocked) log ids when the batch needs close-log documentation. (This
field's exact trigger was not fully resolved in training — verify against current logs.)
Common misjudgments:
- Counting mismatched open bills in ap_balance (drops).
- Setting batch_status="blocked" instead of "needs_ap_refresh" (drops).
7. Payment release after vendor account-change (per business)
Input: compliance object + vendor record + account-change ticket per business. Review/as_of date
from payload. Enumerate blocking conditions, then decide.
Derived flag lists (ascending business_id):
bank_mismatch_ids = bank_account_status == "name_mismatch" (closed is NOT a mismatch).
invalid_tax_ids = compliance.tax_id != vendor.tax_id, or tax_id has invalid format
(e.g. contains non-digits / placeholder like all-9s).
expired_license_ids = license_expiry < as_of_date (strict date compare; a license expiring
the day AFTER as_of is NOT expired).
risk_score_override_flags = risk_score >= 70.
review_queue_ids = businesses not released (hold or escalate).
decisions {release, hold, escalate} — scored as a whole object (ALL businesses must match
exactly; a single wrong decision fails the entire decisions field):
escalate: any serious condition — confirmed_pep, sanctions_confirmed,
shell_company_suspected, bank_closed. (Payment to a closed bank / confirmed PEP escalates.)
hold: remediable/blocking conditions without a serious one — bank_name_mismatch, invalid_tax,
expired_license, missing license, screening_not_run, risk_score >= 70. (High risk needs an
override before release => hold, not escalate.)
release: no blocking conditions (bank verified, license valid, sanctions clear, no PEP, tax
valid, vendor active, risk < 70).
Common misjudgments:
- Escalating bank_name_mismatch / risk>=70 (these are hold, not escalate).
- Holding confirmed_pep / bank_closed (these escalate).
- Forgetting the decisions field is all-or-nothing — verify every business.
- Counting a license expiring the next day as expired.
8. Cross-task pitfalls (sharpened by judge feedback)
- Obey the answer_template's enum vocabularies and ordering literally; do not invent values.
- Currency: schema unit/precision wins over loose "cents" wording in the prompt.
- "System of record" = remote API. Stale CSV/snapshot payloads are context only; always
re-fetch current claim/bill/payment/compliance/GL state.
- Match logic for AP bills: amount AND vendor_id must equal the claim's. Stray/legacy bills
linked to a claim are ignored (not counted in balances), but drive the correction enum.
- A
processing payment is NOT cleared — it does not settle a claim or reduce AP balance.
- When a round's score drops after a change, revert; the prior interpretation was closer.
Judge feedback is coarse (single score), so change one concept at a time, not many at once.
- Avoid over-correcting from arithmetic reverse-engineering: confirm a hypothesis by a score
RISE before keeping it.
1---2name: reflect-3-attempt-01-123description: ERP Finance Expense-Control Skill (task_group_005)4---5# ERP Finance Expense-Control Skill (task_group_005)67Reusable workflow rules for expense-control / AP / prepaid-close / vendor-compliance8tasks against the shared ERP finance API. Distilled from reflect training.910## 1. API contract1112- Base URL: `<remote-env-url>` (runner-provided; treat remote API as system of record).13- `GET /endpoints` lists all paths. All list endpoints support exact-match query params by14 field name, plus `limit`/`offset`.15- Resource paths (use the `/api/...` form):16 - Claims: `GET /api/claims?claim_id=<id>`17 - AP bills: `GET /api/ap/bills?claim_id=<id>` (also queryable by bill_id)18 - AP payments: `GET /api/ap/payments?bill_id=<bill_id>`19 - Vendors: `GET /api/vendors?vendor_id=<id>`20 - Compliance: `GET /api/compliance/objects?business_id=<id>` (returns bank, license, PEP,21 sanctions, shell, tax_id, ubo_list, risk_score, review_status, missing_fields in one object)22 - Prepaid invoices: `GET /api/prepaids/invoices?prepaid_invoice_id=<id>`23 - GL balances: `GET /api/prepaids/gl-balances?account=<acct>` (returns per-period ending_balance)24 - Close logs: `GET /api/close/logs` (area, period, related_account, status, message)25- Efficiency: batch curl calls; query by specific ID/param only; never dump whole collections.26- Currency: report USD to two decimals unless a task explicitly says otherwise. When a prompt27 says "USD cents" but the schema field says `unit: USD, precision: 2`, emit the USD value to28 2 decimals (the schema wins).2930## 2. General reconciliation SOP31321. Read the task's `payloads/answer_template.json` FIRST — it defines required keys, enums,33 ordering, and unit/precision. Conform exactly (extra props often disallowed; ordering enforced).342. Fetch the candidate IDs' current records from the API (claims, bills, payments, vendors,35 compliance, prepaids, GL, close-logs as relevant). Ignore stale local snapshots/prompts that36 conflict — the API is the system of record.373. For each candidate, classify using the rules below, then aggregate (totals, flag lists,38 status) per the template.394. Sort every ID list exactly as the template specifies (ascending by id unless stated).405. Keep candidate answers minimal-valid JSON.4142## 3. Expense-claim reimbursement close (claim -> AP bill -> payment)4344Classify each batch claim into exactly one bucket:45- **paid**: a matching AP bill (bill.amount == claim.amount AND bill.vendor_id ==46 claim.vendor_id) with status `paid` AND a cleared payment (payment.status == `cleared`) for47 that amount. A `processing`/in-flight payment does NOT count as paid.48- **payable (can remain in AP queue)**: claim approved, and a valid OPEN (scheduled/approved,49 not paid/void) AP bill that matches claim amount + vendor. In-flight (processing) payment50 leaves it payable; the open AP balance still equals the bill amount.51- **blocked**: not paid and cannot be released — no matching bill, amount/vendor mismatch,52 voided bill, unapproved claim, or expense-case issue. These need owner/AP-link cleanup.5354Derived fields:55- `ap_open_balance_total` = sum of valid open matching AP bill amounts for payable claims only56 (USD 2dp). Exclude mismatched, voided, and already-paid bills.57- `crm_required_claim_ids` = blocked claims requiring expense-case or AP-link remediation58 (equals the blocked set when all blockers are cleanup-type).59- `batch_status`: `blocked` if any claim blocked; else `open_payables` if valid unpaid AP bills60 remain; else `ready_to_close`.61- `reviewed_claim_count` = number of batch claims reviewed.6263Multiple bills per claim: pick the one matching claim amount+vendor as the "real" bill; stray64bills (wrong vendor/amount) are ignored for balance but noted as AP-link issues.6566## 4. Vendor onboarding finance-risk release (per business)6768Input: compliance object + vendor record per business_id. As-of date from batch payload.6970**reportable_ubo_counts** = count of UNIQUE beneficial-owner names in `ubo_list` with71`ownership_pct >= 25` (the 25% reporting threshold). Deduplicate by name; a name qualifies if72ANY of its entries is >= 25%. (24% does NOT count; 30%+ does. This threshold was confirmed:73using 10% drops the score.)7475**hard_stop_flags** (alphabetical; empty list when none) — derive from compliance object:76- `bank_closed` : bank_account_status == "closed"77- `bank_name_mismatch` : bank_account_status == "name_mismatch"78- `confirmed_pep` : pep_status == "confirmed_pep" (possible_pep / not_run do NOT set this)79- `expired_license` : license_expiry < as_of_date80- `missing_required_documents` : missing_fields non-empty81- `sanctions_confirmed`: sanctions_check_status indicates a confirmed hit82- `screening_not_run` : sanctions_check_status == "not_run" OR pep_status == "not_run"83- `shell_company_suspected`: shell_company_suspected == true84- `vendor_on_hold` : vendor.status == "on_hold"85Note: tax_id mismatch between compliance and vendor is NOT one of the hard_stop enum values.8687**decision** {approve, awaiting_information, escalate}:88- `escalate` if ANY serious flag: confirmed_pep, sanctions_confirmed, shell_company_suspected,89 bank_closed, bank_name_mismatch. (PEP/shell/bank issues escalate — confirmed: downgrading90 them to awaiting_information drops the score.)91- else `awaiting_information` if any remediable flag: missing_required_documents,92 screening_not_run, expired_license, vendor_on_hold.93- else `approve` (no hard stops).9495**follow_up_business_ids** = businesses whose decision is not `approve` (ascending).96**overall_release_ready** = true only if every business is `approve`.9798Common misjudgments:99- Treating escalate as sanctions-only (too narrow) — PEP/shell/bank must escalate.100- Using a 10% UBO threshold — use 25%.101- Forgetting screening_not_run also covers pep_status == "not_run".102103## 5. Prepaid close reconciliation (per account, per period)104105Input: scoped prepaid invoice IDs + accounts + close period + variance_threshold_abs.106107Per invoice (straight-line monthly amortization):108- `march_amortization` (period month) = the invoice's `monthly_amortization` FIELD when the109 period month falls within [service_start month .. service_end month]; else 0. Use full-month110 convention: the service_start month counts as a full month (a 03-15 start amortizes all of111 March). *** Use the monthly_amortization FIELD, NOT original_amount/term *** — exact112 original/term recomputation drops the score.113- `cumulative_amortization_through_march` = monthly_amortization FIELD × number of months114 elapsed from service_start month through the close month (inclusive).115- `ending_balance` = original_amount − cumulative_amortization_through_march (2dp).116- `default_missing_term_flag` = true iff `missing_contract_dates` is in data_quality_flags.117- `exception_flag` = true iff data_quality_flags is NON-EMPTY (any flag — superset, not just118 rounded_amount). default_missing_term is a subset of exceptions.119120Per account rollup (sum over the account's selected invoices):121- original_amount_total, march_amortization_total, cumulative_amortization_through_march,122 schedule_ending_balance (= original_total − cumulative_total).123- `gl_ending_balance` = the GL balance record whose period == close period for that account.124- `variance_amount` = schedule_ending_balance − gl_ending_balance (2dp; can be negative).125- `variance_flag` = |variance_amount| > variance_threshold_abs.126- `has_default_missing_term_flag` = any selected invoice in the account has missing_term.127- `account_status`:128 - `requires_reconciliation` if has_default_missing_term_flag (data-quality prevents a clean tie).129 - else `variance_review` if variance_flag.130 - else `reconciled`.131132Output lists: `default_missing_term_invoice_ids` and `exception_invoice_ids` ascending by133invoice id (note: "PPD-2025-..." sorts before "PPD-AUR-..." because '2' < 'A').134`selected_invoice_ids` and `invoice_results` keep the SAME order as the scope file.135136Common misjudgments:137- Recomputing cumulative as original×months/term (exact) instead of monthly_field×months — drops.138- Using disjoint exception (only rounded_amount) instead of superset — drops.139- Prorating mid-month starts — use full-month.140141## 6. AP batch reconciliation from a stale export (claim, bill, payment, close-log)142143For each candidate claim, compare the stale snapshot to CURRENT API data:144- **eligible_claim_ids**: approved claim with a valid OPEN AP bill matching amount+vendor (amount145 & vendor match), and no blocking issue. An in-flight (processing) payment keeps it eligible;146 its open AP balance = the bill amount (payment not yet cleared).147- **not_ready_claim_ids**: paid (already settled), amount/vendor mismatch, voided bill, or148 unapproved claim.149- **ap_balance_by_claim** (USD 2dp): valid matching OPEN bills minus CLEARED payments, EXCLUDING150 mismatched and voided bills. A paid claim => 0. A claim whose only bill mismatches => 0 (the151 mismatched bill is excluded, NOT counted). A voided bill => 0. *** Do NOT count every open152 non-voided bill regardless of match — counting mismatched bills drops the score. ***153- **stale_snapshot_corrections** (per claim, enum):154 - `mark_in_flight_payment` : snapshot showed no payment but current shows a processing payment.155 - `replace_with_matched_paid_bill`: snapshot referenced a wrong (mismatched) scheduled bill; current156 has a matched PAID bill + cleared payment.157 - `exclude_amount_or_vendor_mismatch`: bill amount or vendor does not match the claim.158 - `ignore_void_bill` : the linked bill is void in the current API.159 - `block_unapproved_claim` : claim status is not approved (e.g. needs_receipt).160 - `current_snapshot_ok` : snapshot already matches current state.161- **batch_status**: `needs_ap_refresh` when the batch originated from a stale AP export and must162 be reconciled before sending (default for stale-snapshot batches with mixed eligibility).163 `ready_to_send` only when all candidates eligible. `blocked` is for hard blocks — do not use164 it merely because some claims are not ready (using "blocked" here drops the score).165- **close_log_required**: {required: bool, ids: [close_log_id ascending]}. Inspect /api/close/logs166 for AP-area entries relevant to the claim bill accounts/period; include unresolved167 (open/ready_for_review/blocked) log ids when the batch needs close-log documentation. (This168 field's exact trigger was not fully resolved in training — verify against current logs.)169170Common misjudgments:171- Counting mismatched open bills in ap_balance (drops).172- Setting batch_status="blocked" instead of "needs_ap_refresh" (drops).173174## 7. Payment release after vendor account-change (per business)175176Input: compliance object + vendor record + account-change ticket per business. Review/as_of date177from payload. Enumerate blocking conditions, then decide.178179Derived flag lists (ascending business_id):180- `bank_mismatch_ids` = bank_account_status == "name_mismatch" (closed is NOT a mismatch).181- `invalid_tax_ids` = compliance.tax_id != vendor.tax_id, or tax_id has invalid format182 (e.g. contains non-digits / placeholder like all-9s).183- `expired_license_ids` = license_expiry < as_of_date (strict date compare; a license expiring184 the day AFTER as_of is NOT expired).185- `risk_score_override_flags` = risk_score >= 70.186- `review_queue_ids` = businesses not released (hold or escalate).187188**decisions** {release, hold, escalate} — scored as a whole object (ALL businesses must match189exactly; a single wrong decision fails the entire decisions field):190- `escalate`: any serious condition — confirmed_pep, sanctions_confirmed,191 shell_company_suspected, bank_closed. (Payment to a closed bank / confirmed PEP escalates.)192- `hold`: remediable/blocking conditions without a serious one — bank_name_mismatch, invalid_tax,193 expired_license, missing license, screening_not_run, risk_score >= 70. (High risk needs an194 override before release => hold, not escalate.)195- `release`: no blocking conditions (bank verified, license valid, sanctions clear, no PEP, tax196 valid, vendor active, risk < 70).197198Common misjudgments:199- Escalating bank_name_mismatch / risk>=70 (these are hold, not escalate).200- Holding confirmed_pep / bank_closed (these escalate).201- Forgetting the decisions field is all-or-nothing — verify every business.202- Counting a license expiring the next day as expired.203204## 8. Cross-task pitfalls (sharpened by judge feedback)205206- Obey the answer_template's enum vocabularies and ordering literally; do not invent values.207- Currency: schema unit/precision wins over loose "cents" wording in the prompt.208- "System of record" = remote API. Stale CSV/snapshot payloads are context only; always209 re-fetch current claim/bill/payment/compliance/GL state.210- Match logic for AP bills: amount AND vendor_id must equal the claim's. Stray/legacy bills211 linked to a claim are ignored (not counted in balances), but drive the correction enum.212- A `processing` payment is NOT `cleared` — it does not settle a claim or reduce AP balance.213- When a round's score drops after a change, revert; the prior interpretation was closer.214 Judge feedback is coarse (single score), so change one concept at a time, not many at once.215- Avoid over-correcting from arithmetic reverse-engineering: confirm a hypothesis by a score216 RISE before keeping it.