Three agents. Formal states.
§5.0 State Machine Overview
Intercompany Reconciliation Agent.
Closes intercompany books at month-end across six legal entities via SAP BAPI extraction, ML-based mismatch detection, and controller-gated journal posting[cite: 8]. Every correction write requires a HITL approval token — no autonomous ERP mutation at any confidence level[cite: 8].
| State | Type | Entry Action | Exit Condition | Next State(s) | Audit Event | HITL Required |
|---|---|---|---|---|---|---|
| IDLE | Start | Await Pub/Sub trigger or ad-hoc query[cite: 8] | Trigger received[cite: 8] | EXTRACTING[cite: 8] | state_init[cite: 8] | — |
| EXTRACTING | Process | Pull IC entries from all 6 SAP entities via OData; parallelised by entity[cite: 8] | All entities returned, or timeout after 30min[cite: 8] | MATCHING · ERROR[cite: 8] | extract_start · extract_complete[cite: 8] | — |
| MATCHING | Process | ML anomaly detector scores each IC pair; confidence interval computed per mismatch[cite: 8] | All pairs scored; ≥1 mismatch flagged[cite: 8] | HITL-IC-01 · COMPLETE (clean)[cite: 8] | match_complete · mismatch_flagged[cite: 8] | — |
| HITL-IC-01 | ⏸ PAUSE | Dispatch review task to Group Controller UI; 4h SLA timer starts; escalation to CFO at 3h50m[cite: 8] | Approve or Reject decision received with hitl_id token[cite: 8] | POSTING (approved) · IDLE (rejected) · SLA_BREACH (4h elapsed)[cite: 8] | hitl_dispatched · hitl_resolved[cite: 8] | ALWAYS[cite: 8] |
| SLA_BREACH | ⚠ Timeout | Escalate to CFO + Finance Director; set compliance_flag=true; block POSTING until decision received[cite: 8] | Human decision received OR manual override by CFO[cite: 8] | POSTING (decision=APPROVE) · IDLE (decision=REJECT)[cite: 8] | sla_breached · escalation_fired · compliance_flagged[cite: 8] | CFO override[cite: 8] |
| POSTING | Write | SAP BAPI journal correction write; hitl_id embedded in SAP document header[cite: 8]. Idempotent: run_id deduplication guard — duplicate run_id returns existing confirmation, no re-write[cite: 8] | BAPI returns success or fault[cite: 8] | COMPLETE · ERROR[cite: 8] | sap_bapi_write · post_confirmed[cite: 8] | hitl_id required[cite: 8] |
| COMPLETE | Terminal | Close period reconciliation record; notify month-end dashboard[cite: 8] | — | — | recon_complete[cite: 8] | — |
| ERROR | Stable | Log structured error; page on-call engineer; if BAPI partial write → transition to ROLLBACK[cite: 8]. No auto-retry from ERROR — requires explicit RETRYING transition with backoff[cite: 8]. | On-call engineer initiates retry or rollback[cite: 8] | ROLLBACK (if partial write) · RETRYING (transient fault, max 3)[cite: 8] | error_logged · escalation_fired[cite: 8] | — |
| RETRYING | Recovery | Exponential backoff retry (×3 max); re-attempt BAPI write with same run_id (idempotent)[cite: 8] | BAPI success OR max retries exceeded[cite: 8] | COMPLETE (success) · ERROR (max retries)[cite: 8] | retry_attempt · retry_exhausted[cite: 8] | — |
| ROLLBACK | Recovery | Execute SAP compensating journal entry; revert all partial writes from this run[cite: 8] | Compensating entries confirmed[cite: 8] | IDLE[cite: 8] | rollback_exec · rollback_confirmed[cite: 8] | — |
// S — State set[cite: 8] S = { IDLE, EXTRACTING, MATCHING, HITL-IC-01, SLA_BREACH, POSTING, RETRYING, COMPLETE, ERROR, ROLLBACK }[cite: 8] // s₀ — Initial state · F — Terminal states[cite: 8] s₀ = IDLE F = { COMPLETE } Dead-ends = { ERROR (unrecovered) }[cite: 8] // δ(current_state, event [guard]) → next_state[cite: 8] δ(IDLE, pubsub_trigger) → EXTRACTING[cite: 8] δ(EXTRACTING, all_entities_returned) → MATCHING[cite: 8] δ(EXTRACTING, timeout_30m OR odata_fault) → ERROR[cite: 8] δ(MATCHING, [mismatches = 0]) → COMPLETE[cite: 8] δ(MATCHING, [mismatches ≥ 1]) → HITL-IC-01[cite: 8] δ(HITL-IC-01, decision=APPROVE, hitl_id_present) → POSTING[cite: 8] δ(HITL-IC-01, decision=REJECT) → IDLE[cite: 8] δ(HITL-IC-01, [sla_elapsed ≥ 3h50m] AND no_decision) → SLA_BREACH // escalation fires[cite: 8] δ(SLA_BREACH, decision=APPROVE, hitl_id_present) → POSTING[cite: 8] δ(SLA_BREACH, decision=REJECT) → IDLE[cite: 8] δ(POSTING, bapi_success, [run_id not duplicate]) → COMPLETE // idempotent[cite: 8] δ(POSTING, bapi_fault, [no partial write]) → ERROR[cite: 8] δ(POSTING, bapi_fault, [partial write confirmed]) → ROLLBACK[cite: 8] δ(ERROR, engineer_initiates_retry, [retry_count < 3]) → RETRYING[cite: 8] δ(RETRYING, bapi_success) → COMPLETE[cite: 8] δ(RETRYING, bapi_fault, [retry_count = 3]) → ERROR // dead-letter[cite: 8] δ(ROLLBACK, compensating_entry_confirmed) → IDLE[cite: 8]
Cash & Treasury Agent.
Aggregates real-time cash positions from bank APIs and FX feeds, generates a 7-day liquidity forecast with confidence intervals, and proposes FX hedge instructions[cite: 8]. Daily cash positioning reports are autonomous — hedge execution always requires Treasury Manager sign-off[cite: 8].
| State | Type | Entry Action | Exit Condition | Next State(s) | Audit Event | HITL Required |
|---|---|---|---|---|---|---|
| IDLE | Start | Await daily scheduler or FX alert[cite: 8] | Scheduled trigger fires[cite: 8] | FETCHING[cite: 8] | state_init[cite: 8] | — |
| FETCHING | Process | Pull positions from bank APIs (SWIFT gpi, open banking); pull live FX rates[cite: 8] | All feeds returned or timeout 15min[cite: 8] | FORECASTING · ERROR[cite: 8] | fetch_start · fetch_complete[cite: 8] | — |
| FORECASTING | Process | Run 7-day cash model; compute confidence interval; generate FX hedge recommendation[cite: 8] | Model returns forecast + confidence[cite: 8] | ANOMALY_CHECK[cite: 8] | forecast_run · forecast_complete[cite: 8] | — |
| ANOMALY_CHECK | Decision | Evaluate: confidence < 0.75 OR proposed hedge > €500K?[cite: 8] | Decision threshold evaluated[cite: 8] | HITL-TR-01 (triggered) · COMPLETE (auto-report)[cite: 8] | anomaly_check_result[cite: 8] | Conditional[cite: 8] |
| HITL-TR-01 | ⏸ PAUSE | Dispatch forecast + hedge recommendation to Treasury Manager; 2h SLA; escalation to CFO at 1h45m[cite: 8]. Compound trigger: if both conf < 0.75 AND notional > €500K fire simultaneously, a single consolidated HITL task is created with both conditions surfaced in the review UI — one hitl_id issued[cite: 8]. | Approve or Reject with hitl_id[cite: 8] | EXECUTING (approved) · IDLE (rejected) · SLA_BREACH (2h elapsed)[cite: 8] | hitl_dispatched · hitl_resolved[cite: 8] | ALWAYS (when triggered)[cite: 8] |
| SLA_BREACH | ⚠ Timeout | Escalate to CFO + Head of Treasury; set compliance_flag=true; hedge execution blocked until decision received[cite: 8] | Human decision received or CFO override[cite: 8] | EXECUTING (decision=APPROVE) · IDLE (decision=REJECT)[cite: 8] | sla_breached · escalation_fired · compliance_flagged[cite: 8] | CFO override[cite: 8] |
| EXECUTING | Write | Send hedge instruction to bank API; hitl_id attached to instruction record[cite: 8]. Idempotent: instruction_id deduplication — duplicate instruction_id returns existing confirmation without re-executing[cite: 8] | Bank API confirms or faults[cite: 8] | COMPLETE · ERROR[cite: 8] | hedge_instruction · exec_confirmed[cite: 8] | hitl_id required[cite: 8] |
| COMPLETE | Terminal | Publish daily treasury dashboard; archive forecast record[cite: 8] | — | — | treasury_complete[cite: 8] | — |
| ERROR | Stable | Log structured error; alert Treasury Operations[cite: 8]. No auto-retry — requires explicit operator-initiated RETRYING transition with exponential backoff (max 3 attempts)[cite: 8]. | Operator initiates retry[cite: 8] | RETRYING (transient, max 3) · IDLE (unrecoverable, manual reset)[cite: 8] | error_logged · escalation_fired[cite: 8] | — |
| RETRYING | Recovery | Exponential backoff retry (×3 max); re-attempt bank API call with same instruction_id (idempotent)[cite: 8] | API success OR max retries exceeded[cite: 8] | COMPLETE (success) · ERROR (max retries)[cite: 8] | retry_attempt · retry_exhausted[cite: 8] | — |
S = { IDLE, FETCHING, FORECASTING, ANOMALY_CHECK, HITL-TR-01, SLA_BREACH, EXECUTING, RETRYING, COMPLETE, ERROR }[cite: 8]
s₀ = IDLE F = { COMPLETE }[cite: 8]
// δ(current_state, event [guard]) → next_state[cite: 8]
δ(IDLE, daily_scheduler_trigger) → FETCHING[cite: 8]
δ(FETCHING, all_feeds_returned) → FORECASTING[cite: 8]
δ(FETCHING, timeout_15m OR api_fault) → ERROR[cite: 8]
δ(FORECASTING, model_complete) → ANOMALY_CHECK[cite: 8]
δ(ANOMALY_CHECK, [conf ≥ 0.75 AND notional ≤ €500K]) → COMPLETE // auto-report[cite: 8]
δ(ANOMALY_CHECK, [conf < 0.75 OR notional > €500K]) → HITL-TR-01[cite: 8]
δ(HITL-TR-01, decision=APPROVE, hitl_id_present) → EXECUTING[cite: 8]
δ(HITL-TR-01, decision=REJECT) → IDLE[cite: 8]
δ(HITL-TR-01, [sla_elapsed ≥ 1h45m] AND no_decision) → SLA_BREACH[cite: 8]
δ(SLA_BREACH, decision=APPROVE, hitl_id_present) → EXECUTING[cite: 8]
δ(SLA_BREACH, decision=REJECT) → IDLE[cite: 8]
δ(EXECUTING, bank_api_success, [instruction_id not duplicate]) → COMPLETE // idempotent[cite: 8]
δ(EXECUTING, bank_api_fault) → ERROR[cite: 8]
δ(ERROR, operator_retry, [retry_count < 3]) → RETRYING[cite: 8]
δ(RETRYING, bank_api_success) → COMPLETE[cite: 8]
δ(RETRYING, bank_api_fault, [retry_count = 3]) → ERROR // dead-letter[cite: 8]
AP Exception Agent.
Parses invoice documents via Document AI, classifies each against five exception categories, auto-routes clean approvals, and pauses on exceptions for AP Lead review[cite: 8]. 77% of monthly volume is approved autonomously; 23% routes to HITL-AP-01[cite: 8].
| State | Type | Entry Action | Exit Condition | Next State(s) | Audit Event | HITL Required |
|---|---|---|---|---|---|---|
| IDLE | Start | Await invoice ingest feed event[cite: 8] | Invoice batch arrives[cite: 8] | INGESTING[cite: 8] | state_init[cite: 8] | — |
| INGESTING | Process | Document AI OCR parse; extract PO number, line items, vendor, amount, cost centre[cite: 8] | Parse complete or Document AI fault[cite: 8] | CLASSIFYING · ERROR[cite: 8] | ingest_start · ingest_complete[cite: 8] | — |
| CLASSIFYING | Process | Exception classifier assigns label: PO_MISMATCH · PRICE_VARIANCE · MISSING_CC · DUPLICATE · APPROVED[cite: 8]. APPROVED label requires classifier confidence ≥ 0.92 AND no rule-based exception flags — below threshold routes to HITL regardless of label[cite: 8]. | Label assigned with confidence score[cite: 8] | HITL-AP-01 (exceptions or conf < 0.92) · POSTING-AUTO (APPROVED, conf ≥ 0.92)[cite: 8] | classification · routing_decision[cite: 8] | — |
| HITL-AP-01 | ⏸ PAUSE | Route to AP Lead review queue with exception label, original invoice, PO diff, and confidence score; 8h SLA timer starts; escalation to Finance Controller at 7h[cite: 8] | Approve or Reject with hitl_id[cite: 8] | POSTING (approved) · IDLE (rejected) · SLA_BREACH (8h elapsed)[cite: 8] | hitl_dispatched · hitl_resolved[cite: 8] | ALWAYS (exceptions)[cite: 8] |
| SLA_BREACH | ⚠ Timeout | Escalate to Finance Controller + AP Manager; set compliance_flag=true; invoice remains parked; supplier notified of hold extension[cite: 8] | Human decision received[cite: 8] | POSTING (decision=APPROVE) · IDLE (decision=REJECT)[cite: 8] | sla_breached · escalation_fired · supplier_notified[cite: 8] | Controller override[cite: 8] |
| POSTING | Write | ERP write; hitl_id attached for exception invoices; null hitl_id for APPROVED-auto path[cite: 8]. Idempotent: invoice_id deduplication guard — duplicate invoice_id returns existing ERP confirmation, no re-write[cite: 8] | ERP confirms or faults[cite: 8] | COMPLETE · ERROR[cite: 8] | erp_write · post_confirmed[cite: 8] | hitl_id for exceptions[cite: 8] |
| COMPLETE | Terminal | Mark invoice processed; update AP dashboard; trigger payment run eligibility[cite: 8] | — | — | invoice_complete[cite: 8] | — |
| ERROR | Stable | Log structured error; flag invoice for manual triage; alert AP Operations[cite: 8]. No auto-retry — explicit operator-initiated RETRYING transition required[cite: 8]. | Operator initiates retry or manual triage[cite: 8] | RETRYING (transient, max 3) · IDLE (unrecoverable)[cite: 8] | error_logged · invoice_flagged[cite: 8] | — |
| RETRYING | Recovery | Exponential backoff retry (×3 max); re-attempt ERP write with same invoice_id (idempotent)[cite: 8] | ERP success OR max retries exceeded[cite: 8] | COMPLETE (success) · ERROR (max retries)[cite: 8] | retry_attempt · retry_exhausted[cite: 8] | — |
S = { IDLE, INGESTING, CLASSIFYING, HITL-AP-01, SLA_BREACH, POSTING, RETRYING, COMPLETE, ERROR }[cite: 8]
s₀ = IDLE F = { COMPLETE }[cite: 8]
// δ(current_state, event [guard]) → next_state[cite: 8]
δ(IDLE, invoice_batch_arrives) → INGESTING[cite: 8]
δ(INGESTING, parse_complete) → CLASSIFYING[cite: 8]
δ(INGESTING, document_ai_fault) → ERROR[cite: 8]
δ(CLASSIFYING, [label ∈ {PO_MISMATCH,PRICE_VARIANCE,MISSING_CC,DUPLICATE}]) → HITL-AP-01[cite: 8]
δ(CLASSIFYING, [label = APPROVED AND conf < 0.92]) → HITL-AP-01 // conf gate[cite: 8]
δ(CLASSIFYING, [label = APPROVED AND conf ≥ 0.92]) → POSTING // auto-path[cite: 8]
δ(HITL-AP-01, decision=APPROVE, hitl_id_present) → POSTING[cite: 8]
δ(HITL-AP-01, decision=REJECT) → IDLE // invoice parked[cite: 8]
δ(HITL-AP-01, [sla_elapsed ≥ 7h] AND no_decision) → SLA_BREACH[cite: 8]
δ(SLA_BREACH, decision=APPROVE, hitl_id_present) → POSTING[cite: 8]
δ(SLA_BREACH, decision=REJECT) → IDLE[cite: 8]
δ(POSTING, erp_success, [invoice_id not duplicate]) → COMPLETE // idempotent[cite: 8]
δ(POSTING, erp_fault) → ERROR[cite: 8]
δ(ERROR, operator_retry, [retry_count < 3]) → RETRYING[cite: 8]
δ(RETRYING, erp_success) → COMPLETE[cite: 8]
δ(RETRYING, erp_fault, [retry_count = 3]) → ERROR // dead-letter[cite: 8]
§5.4 Shared HITL Specification
| Checkpoint ID | Agent | Trigger Condition | SLA | SLA Breach → State | Approver | Escalation | Escalation At | hitl_id Required | Rejection Route |
|---|---|---|---|---|---|---|---|---|---|
| HITL-IC-01 | IC Recon Agent | Any flagged IC mismatch detected by ML anomaly detector; Hard unconditional loop[cite: 8] | 4 Hours | SLA_BREACH · compliance_flag=true · CFO notified · POSTING blocked[cite: 8] | Group Controller | CFO + Finance Director | 3h 50m | SAP BAPI header (required for all ERP writes)[cite: 8] | IDLE · no journal posted[cite: 8] |
| HITL-TR-01 | Treasury Agent | Forecast confidence < 0.75 OR proposed FX hedge notional > €500,000[cite: 8]. Either condition triggers; compound True creates single consolidated track[cite: 8]. | 2 Hours | SLA_BREACH · compliance_flag=true · hedge blocked · CFO + Head of Treasury notified[cite: 8] | Treasury Manager | CFO + Head of Treasury | 1h 45m | Bank API hedge instruction record (required)[cite: 8] | IDLE · no hedge executed[cite: 8] |
| HITL-AP-01 | AP Exception Agent | Exception classifier assigns label PO_MISMATCH, PRICE_VARIANCE, MISSING_CC, or DUPLICATE — OR — any APPROVED-labelled invoice where confidence < 0.92[cite: 8]. | 8 Hours | SLA_BREACH · compliance_flag=true · invoice parked · Finance Controller + AP Manager notified[cite: 8] | AP Lead | Finance Controller + AP Manager | 7h 00m | ERP write record (required for exception class; null for APPROVED class ≥ 0.92)[cite: 8] | IDLE · invoice parked[cite: 8] |