The problem this solves
The trial balance is the ledger’s statement of itself: every account, its debits, its credits, and the proof that the two sides agree. It is also the thing that leaves the ERP and becomes the EPM load, which makes two questions matter more than any others. Is it complete — not merely balanced? And can any number on it be traced back to the journal line that created it?
Balanced and complete are different properties, and conflating them is how a period gets closed on a ledger that is missing activity. Unposted journals are excluded from the trial balance by design; the totals still tie perfectly, and the report looks finished. The close checklist exists to make that distinction impossible to miss, and the drill-back exists so that when a downstream EPM number looks wrong, the answer is two clicks away rather than a support ticket.
Audience
Close managers deciding whether a period can be closed. GL accountants investigating a balance. EPM teams who need to know whether the number they loaded was the final one — and auditors who will ask to see the journal behind a balance.
Posted journal lines, and nothing else
| View | What it builds |
|---|---|
NLQ query: “Trial balance for NYC Flagship August”, “Show the group trial balance”, or “Can we close the period?” — one view field selects the screen | |
| Entity | One company’s trial balance from its posted journal lines, with drill-back and the GL→FP&A tie-out |
| Group | All 44 companies rolled up by natural account — the number that leaves the ERP |
| Close | Five gates evaluated across every company, with the blocking companies named |
Period status is derived from the position of the period: FY26 August is Open, everything before it Closed, everything after Never Opened, and FY25 mostly Permanently Closed.
Three views, one ledger
- Entity trial balance with Dr/Cr per account, a totals row, the Dr = Cr proof, net income, and the unposted count
- Drill-back — click any row and see every journal line behind it with its batch, source and status
- Tie-out table comparing each P&L account’s GL balance to the FP&A income statement, with the difference column
- Group trial balance across 44 companies with a balance-sheet-versus-P&L chart
- Close checklist — five gates, a plain verdict, and the companies blocking it with their excluded dollars
The handover point between ERP and EPM
This is where the general ledger stops and the EPM estate begins. Journals (UC17) creates what this aggregates; Chart of Accounts (UC16) defines the combinations it aggregates by. Downstream, this trial balance is what FP&A (UC2) reports, what FCCS (UC7–9) translates and consolidates, and what ARCS (UC10) reconciles. The tie-out table proves that relationship rather than asserting it.
Recommended integration points
- Close stand-up: the checklist and the named blocking companies, every morning of the close
- EPM load validation: compare the group trial balance to what actually arrived in EPM — the completeness check ARCS (UC12) performs from the other side
- Audit fieldwork: pick a balance, drill to the journal lines, read the source — that is the walkthrough
The numbers
The honest checklist
- ✓Your close has ever proceeded with unposted journals outstanding, because nobody had the number in front of them
- ✓EPM numbers get questioned and the answer requires someone to go and look in the ERP
- ✓You want to show, not assert, that the EPM figures come from the general ledger
- ✗You need the full close task list — accruals, revaluation, allocations, roll-forward. These are five GL-integrity gates, not a close calendar
- ✗Your reporting is done directly in the ERP with no EPM layer — the tie-out has nothing to prove
Try it now
The live demo builds a trial balance for any company, the whole group, or the close checklist across all 44.
Things to try
- Scroll to the tie-out table and read the difference column — every P&L account is zero. That is the GL reproducing the FP&A income statement exactly, and it is enforced by a test rather than by hope
- Click the Store Revenue row and read the drill-back: the AR batch, its source, its status, and the code combination on each line
- Type “Can we close the period?” — four gates pass, one fails, and the blocking companies are named with the dollars that would be missing
Balanced is not complete
A trial balance that balances proves one thing: that the journals which were posted were double entries. It proves nothing at all about whether the right journals were posted. Exclude a $12,500 accrual because it is still awaiting approval and the trial balance balances exactly as before — the totals just happen to be smaller, and there is nothing on the face of the report to say so.
This is why the close checklist matters more than the balance check, and why the KPI row states the excluded value in dollars rather than a count. It is also why the same number is used in three places on the page — the KPI, the gate, and the offender list — rather than recomputed differently in each, which is how reports quietly start disagreeing with themselves.
One aggregation, three views
- Unposted batches counted and valued, then excluded — the count and dollars are returned, not discarded
- Net income derived from the P&L rows, credit-positive
- TB with drill-back
- GL → FP&A tie-out
- All 44 companies by account
- Dr = Cr on the roll-up
- Five gates across every company
- Blocking companies named
- Any trial balance row → every journal line behind it, with batch, source and status
- The same function the audit walkthrough would use
- Three panels behind one view switch, KPI row per view, chart on the group view
- Tie-out table with an explicit verdict row
App UI — component breakdown
| Component | Behaviour |
|---|---|
| View switch | Entity / Group / Close — the company selector disables itself on the two views where it is meaningless |
| KPI row | Rebuilt per view: totals and balance proof on entity, company count on group, gates and blockers on close |
| Trial balance | Account, description, type, Dr, Cr with a totals row; every row clickable |
| Drill-back | Journal lines with batch id, source, status and full code combination |
| Tie-out | Per P&L account: GL balance, FP&A value, difference, and a verdict row stating what a clean tie-out means |
| Close checklist | Five gates with pass/fail, detail, counts and values, then a plain-language verdict |
Sum the posted lines, keep the excluded ones visible
genTrialBalance(entityId, year, period) {
batches = genJournalBatches(entityId, year, period)
for each batch:
if status !== 'Posted':
unpostedCount++, unpostedValue += Σ debit // counted, then excluded
continue
for each line: rows[account].debit += line.debit, .credit += line.credit
return { rows, totalDebit, totalCredit, balanced, netIncome, unpostedCount, unpostedValue }
}
The includeUnposted option exists and is used in one place only — a unit test asserting that including them changes the totals, which is the proof that exclusion is actually happening rather than the unposted batch simply being empty. Net income is derived from the P&L rows rather than stored, so it cannot disagree with the accounts above it.
The group trial balance is the same function run across all 44 companies and summed by natural account. It balances because each company balances — and the test asserts both, because a roll-up that balanced while a component did not would mean two errors cancelling, which is worse than one error.
The capability EPM users ask for first
When a number in an EPM report looks wrong, the question is always the same: what is behind it? In a real estate that means drilling from the EPM cell to the GL balance, from the balance to the journal lines, and from the line to the subledger transaction. This demo implements the middle step, which is the one that most often has no answer.
drillToJournalLines(entityId, year, period, account) →
[{ batchId, source, category, status, co, cc, acct, prod, ic, fut, debit, credit }]
Every returned line carries its batch, its source and its status, so the answer to “where did this come from” is not just “a journal” but “the AR batch that posted August’s customer invoices, and here is its status.” The drill panel deliberately shows the full six-segment combination too, because the reason a balance is unexpected is frequently a cost centre or product that should not have been used — which loops straight back to the cross-validation rules in UC16.
Five gates, and the proof that the ledger feeds EPM
| Gate | What it checks |
|---|---|
| CL-1 Subledgers swept | Receivables, Payables, Inventory, Payroll, Assets and Treasury have transferred and posted |
| CL-2 No unposted journals | The gate that fails. Unposted batches are excluded from the trial balance, so closing now locks an incomplete ledger |
| CL-3 Balancing segment | Every journal nets to zero within each company — or the GL generated the lines to make it so |
| CL-4 Suspense clear | Account 29001 holds nothing. A suspense balance means a posting hit an invalid or unmapped combination |
| CL-5 Trial balance in balance | Debits equal credits across every company |
The checklist runs across all 44 companies and names the blockers with their excluded dollars, because “13 unposted batches” is an abstraction and “NYC Flagship, $12,500 excluded” is an action.
The tie-out
tieOutToEPM(entityId, year, period) →
per P&L account: { glNet, epmValue, diff, ties }
// glNet = the GL trial balance, sign-corrected for the account's natural side
// epmValue = genIS(entityId, year, 'Actual')[period]['A_' + account] × 1000
// asserted in CI across 5 entities × 3 periods — every account, every time
This is the strongest claim the module makes and the one most worth testing rather than asserting. The GL was built by posting journals whose amounts come from genIS(); the trial balance sums those journals; so the trial balance must reproduce genIS() exactly. It does, to the dollar, and CI fails if it ever stops. That is the honest version of “ERP feeds EPM” — not a diagram with an arrow on it, but a number you can check.
A view enum is the whole trick
The trial balance schema is {intent, entity, year, period, view, confidence}, and view does the heavy lifting: entity, group, or close. The few-shot examples teach it that “can we close the period?” and “period close checklist” both mean close, that “consolidated” and “all companies” mean group, and that naming an entity implies entity. One field turns three quite different screens into one natural-language surface, which is the pattern worth reusing whenever a page has modes rather than filters.
Cost model — cost per query
| Component | Detail | Cost |
|---|---|---|
| LLM input tokens | ~690 tokens (schema block + few-shot examples + query) | $0.000097 |
| LLM output tokens | ~45 tokens | $0.0000126 |
| Trial balance + close checklist | Client-side, deterministic, once the query resolves | $0 |
| Total per query | ~$0.0001 | |
| With 2× safety margin | ~$0.0002 |
The close checklist is the heaviest computation in the module — it builds every company’s trial balance for the period — and still completes in well under a second client-side. Production reads it from the ERP.
Key files
| File | Role |
|---|---|
epm-nlq-src/assets/epm-erp-gl-data.js | genTrialBalance(), genGroupTrialBalance(), drillToJournalLines(), genCloseChecklist(), periodStatus(), tieOutToEPM() |
epm-nlq-src/pages/trialbalance.html | UI: NLQ bar, a three-mode view switch, the trial balance with click-to-drill rows, the drill-back panel, the group roll-up with a chart, the close checklist with named blocking companies, and the GL→FP&A tie-out table |
epm-nlq-src/assets/epm-data.js | Shared ENTITIES, PERIODS and genIS() — the P&L amounts the journals post, and therefore what the trial balance reproduces |
epm-nlq-src/assets/epm-fccs-data.js | genConsolidationBS() — the balance sheet side, shared with the FCCS and ARCS modules |
functions/api/nlq-query.js | Shared NLQ endpoint; this use case uses useCase: "trialbalance" |
Why the ledger is generated from genIS(), not beside it
The tempting shortcut is to generate plausible journals independently and let the trial balance be roughly right. That would have broken the one claim this module exists to make. Instead every journal line amount is driven by genIS(), so the posted trial balance reproduces the FP&A income statement to the dollar — asserted by unit test across multiple entities and periods. It is the same discipline the FCCS module applies when it constructs a local balance sheet that balances before translation: if the synthetic data does not tie, the demo teaches the wrong lesson.
Tech stack — every tool in this build
| Layer | Tool | Why |
|---|---|---|
| Data layer | epm-erp-gl-data.js (vanilla JS) | Aggregation over the same journals UC17 generates — one ledger, three views, no parallel model |
| LLM | DeepSeek V3 | Shared NLQ endpoint; a five-field schema whose view enum selects between three screens |
| Charts | Chart.js 4.x | Shared library with the other kits |
| Edge hosting | Cloudflare Pages | Static file, no server compute needed for this use case |
| Build | Eleventy v3.1.5 | Copies epm-nlq-src/pages/ and epm-nlq-src/assets/ to _site/ verbatim |
Known attack surfaces
| Threat | Mitigation in this build |
|---|---|
| A trial balance that balances is assumed to be complete | Unposted batches are excluded by design, and both the KPI row and the close checklist state the excluded value — balanced and complete are shown as two different properties |
| A number on screen cannot be traced back to its source | Every trial balance row is clickable and drills to the journal lines behind it, each showing its batch, source and status — the capability EPM users ask for first when a number looks wrong |
| The period is closed with blockers outstanding | genCloseChecklist() evaluates five gates across all 44 companies and names the blocking companies with their excluded value; the verdict states plainly what closing now would lock in |
| The demo ledger drifts from the EPM numbers it claims to feed | tieOutToEPM() compares every P&L account against genIS() and is asserted in CI across multiple entities and periods — if the GL ever stopped tying, the test suite fails before anyone sees the page |
Guardrails — what prevents bad ledger math
- Debits equal credits for every company: asserted across all 44 entities, and again on the group roll-up — an out-of-balance trial balance is not a display bug, it is a broken ledger
- Unposted is excluded, explicitly and visibly: the same fact drives the KPI, the close gate and the offender list, so it cannot be reported inconsistently in two places
- Period status is derived, not stored:
periodStatus()computes Never Opened / Open / Closed / Permanently Closed from the period’s position relative to the current one, so the calendar cannot contradict itself - The tie-out is a test, not a claim: the page shows it, and CI enforces it — the strongest guardrail in the module
Who is asking, and what are they allowed to see?
The demo answers neither question — it has a cookie gate and no notion of a user. In production these are the two questions everything else rests on, and they have different answers: authentication is who you are, authorization is what you may see. Corporate SSO settles the first. Only Oracle ERP Cloud (Fusion) can settle the second, and the single most important rule in this section is that this application must never become the place where that decision is made.
9.1 · The identity chain, end to end
- MFA and Conditional Access are enforced here — device compliance, location, risk signals
- Returns an ID token (who the user is) and an access token (what they may call)
- Group membership arrives as a claim; the application never handles a password
- Verify signature, issuer, audience and expiry against the IdP’s published keys
- Read the group claims — there is no local user table and no local role table
- Short-lived access token with refresh-token rotation; session timeout set to the data classification
- The API is called as the user
- Fusion enforces its own security natively
- Audit trail names the real user
- Preferred where the API supports it
- The application becomes the enforcement point
- Entitlements fetched separately, applied in one audited place
- Simpler and cacheable — and a filtering bug is a data breach
- Roles: job roles (General Accountant, General Accounting Manager) composed of duty roles, plus data roles
- Data level: Data access sets scoped to ledgers and balancing segment values — the group trial balance requires access to every ledger in it, which is a much smaller population than it sounds
- Group → role mapping lives in the platform, not in this application
- The user sees exactly what they would see logging into the source system directly — no more
- Every query logged against the real end user, never a shared account
9.2 · Federating the corporate identity provider
Oracle ERP Cloud (Fusion) does not replace your directory — it trusts it. Fusion is federated with the corporate IdP so authentication happens where it already happens, under policies security has already written.
| Identity provider | Protocol | Notes |
|---|---|---|
| Microsoft Entra ID (formerly Azure AD) | SAML 2.0 or OIDC | The common case. Conditional Access, MFA and device compliance are enforced at Entra and inherited automatically. On-premises Active Directory federates through Entra Connect rather than being integrated directly. |
| Okta | SAML 2.0 or OIDC | Same pattern; Okta groups drive ERP roles through SCIM provisioning. |
| OCI IAM (identity domains) | Native | Already present with Oracle ERP Cloud (Fusion). Can be the primary IdP for a smaller estate, or a federated spoke of Entra/Okta for a larger one. |
| AD FS | SAML 2.0 | Still seen where the estate is not yet cloud-first. Works, but you inherit the on-premises availability of the token service — if AD FS is down, nobody logs in. |
For the browser application itself, use OIDC Authorization Code flow with PKCE. Not the implicit flow, which is deprecated and leaks tokens through the URL, and never a resource-owner password grant — a finance tool should not be capable of handling a password at all.
9.3 · From group membership to ERP roles
Roles are granted to groups, never to individuals, and the groups come from the directory. That one discipline is what makes joiner/mover/leaver work without anyone having to remember this application exists.
Entra ID group → ERP role / entitlement
──────────────────────────────────────────────────────────────────
FIN-EPM-Analysts → General Accountant (inquiry)
FIN-EPM-Controllers-EMEA → General Accounting Manager + EMEA data scope
FIN-EPM-Admins → Financial Application Admin
──────────────────────────────────────────────────────────────────
Provisioned by SCIM. Remove the user from the group and the
entitlement disappears on the next sync — including here.
For this use case the relevant native entitlement is: General Accountant or General Accounting Manager for the group view; opening and closing periods is a separate privilege this tool never needs.
9.4 · The architectural decision: who enforces?
This is the choice that determines whether the deployment is defensible. Both patterns appear in the diagram above; the difference is where the security boundary actually sits.
| Pattern A — identity propagation | Pattern B — service account + filtering | |
|---|---|---|
| How | The user’s token is exchanged (OAuth 2.0 on-behalf-of) for one scoped to the ERP API; calls are made as the user | A single read-only integration account calls the API; the application filters the results |
| Enforcement point | Oracle ERP Cloud (Fusion) | This application |
| Audit trail shows | The real end user | The service account — you must log the real user separately |
| Failure mode | Token plumbing is more complex; per-user rate limits apply | A filtering bug is a data breach, and the entitlement copy drifts from reality |
| Verdict | Prefer this wherever the API supports user-token authentication | Acceptable with discipline: narrowest possible service account, filtering centralised in one tested place, real user in every log line |
The shortcut to refuse. Pattern B built with a Service Administrator account and no filtering at all is the most common way this gets delivered, because it works perfectly in UAT — testers are usually over-entitled, so nobody notices that everyone can see everything. It fails at the first access review, and by then it is in production with real users depending on it.
9.5 · Data-level security is the part that matters
Role membership decides whether a user can open the application. It does not decide which rows they get back, and confusing the two is the most expensive mistake available here.
- For this use case: Data access sets scoped to ledgers and balancing segment values — the group trial balance requires access to every ledger in it, which is a much smaller population than it sounds.
- Apply it before aggregation, not after. Filtering a total that has already been computed across entities the user cannot see still leaks the total.
- The NLQ layer needs its own check. Layer 4 already validates that the resolved point of view uses approved members; production adds a second test — that the resolved POV sits inside this user’s scope — and it runs before the data call, not after. A natural-language interface is very good at asking for things politely; the authorization check must not care how the question was phrased.
- Fail closed. If entitlements cannot be resolved, return nothing and say so. An empty result is a support ticket; a permissive default is an incident.
9.6 · Provisioning, sessions and the leaver problem
- SCIM provisioning from Entra or Okta into the Fusion security console, covering joiner, mover and leaver. The mover is the case people forget — somebody changing region should lose the old scope, not accumulate both.
- No local user store. If this application keeps its own copy of who may do what, a leaver keeps access until somebody remembers to update it. Nobody ever does.
- Short-lived access tokens with refresh-token rotation; align session timeout with the data classification rather than with convenience.
- MFA and Conditional Access at the IdP — not reimplemented here. Device compliance and location policy come free with federation.
- Quarterly recertification of both the groups that grant access and the service account’s own entitlements, evidenced and signed.
- Break-glass access is a named, monitored, time-boxed account — never a shared credential in a password manager.
9.7 · What this means for Trial Balance & Period Close
| Concern | Answer for this use case |
|---|---|
| Native entitlement required | General Accountant or General Accounting Manager for the group view; opening and closing periods is a separate privilege this tool never needs |
| Data-level control | Data access sets scoped to ledgers and balancing segment values — the group trial balance requires access to every ledger in it, which is a much smaller population than it sounds |
| Use-case-specific sensitivity | The group trial balance is the single most sensitive report in the estate: it is the unpublished results. Restrict it to consolidation and leadership, log every access, and treat an export as a material-information event with the disclosure implications that carries. |
9.8 · Security configuration checklist
- ✓Oracle ERP Cloud (Fusion) federated with the corporate IdP over SAML 2.0 or OIDC; the cookie gate removed entirely
- ✓Browser app uses OIDC Authorization Code + PKCE — no implicit flow, no password grant
- ✓MFA and Conditional Access enforced at the IdP, not reimplemented in the application
- ✓Roles granted to directory groups, never to individuals; SCIM covers joiner, mover and leaver
- ✓Enforcement pattern chosen deliberately — Pattern A where the API supports it, or Pattern B with filtering centralised and tested
- ✓Data-level security applied before aggregation, and the resolved POV checked against the user’s scope before the data call
- ✓No local user table and no local role table anywhere in the application
- ✓Every query logged against the real end user, even when a service account makes the call
- ✓Authorization failures fail closed and are logged as security events rather than swallowed
- ✓Quarterly recertification of access groups and of the service account’s own entitlements
From demo to a governed enterprise deployment
Everything above runs on synthetic data, a public LLM API key, a cookie gate, and no audit trail — deliberately, so the mechanics are inspectable. Taking Trial Balance & Period Close to production is not a rewrite; the 4-layer pipeline and the data-layer contract survive intact. It is a controlled-change program across six workstreams: architecture, LLM platform, security, SOX/audit, environment promotion, and operations. This section is the checklist we run with clients.
10.1 · Production reference architecture
- Terminates SSO, validates the session, attaches the user’s ERP responsibilities and data-access sets to the request
- Rate limits per user, blocks anonymous access, scrubs PII patterns before anything reaches the orchestrator
- L2 grounding reads the live chart of accounts and segment value sets on a schedule — not a hardcoded schema
- Only the schema + user query go to the model; ledger amounts never leave the data layer
- L4 rejects any segment value outside the approved value sets and falls back to the deterministic parser
- Data stays inside the OCI boundary
- Natural fit when Fusion ERP is already in OCI
- Use the hyperscaler the org already governs
- Enterprise DPA, no training on prompts
- For regulated or sovereign ledgers
- Highest control, highest run cost
- Oracle ERP Cloud — account balances, period statuses and the close checklist via the ERP REST API or BI Publisher; the GL owns the balances and the period lifecycle
- Least-privilege read-only integration user, scoped to the required ledgers and data-access sets, credential in a vault and rotated
- Results filtered to the requesting user’s own ledger and data-access-set security before rendering
- Every query logged: user, timestamp, raw query, parsed intent JSON, model + prompt version, ledger/period returned, latency, cost
- Exported to the SIEM; retained per the SOX evidence schedule
- Dashboards for fallback rate, eval pass rate, guardrail hits, p95 latency, spend
- The parsed JSON is shown to the user as the explanation — the same line the demo prints today
- Every number on screen drills to a journal line an auditor can reproduce in the ERP
10.2 · Choosing the LLM platform
The demo’s DeepSeek call is a placeholder for a single adapter, callLLM(system, user), behind Layer 3. Swapping the provider changes one function and zero business logic. Pick the platform the organisation already governs — the security and procurement review is the long pole, not the integration.
| Option | Choose when | Data posture |
|---|---|---|
| Oracle OCI Generative AI (Cohere Command, Llama) | Fusion ERP already lives in OCI; you want one cloud boundary and one contract | Prompts stay in the OCI tenancy; no training on customer data; dedicated AI clusters available for isolation |
| Azure OpenAI Service | Microsoft-first finance estate (Entra ID, Purview, Sentinel already in place) | Private endpoint, regional deployment, zero-retention by default under the enterprise agreement |
| AWS Bedrock / Google Vertex AI | The org’s landing zone is AWS or GCP; VPC endpoints and IAM already audited | VPC/PSC private access, no data used for training, CloudTrail/Cloud Audit Logs integration |
| Direct enterprise API (Anthropic, OpenAI) | Fastest model access; acceptable when a zero-data-retention agreement and DPA are signed | ZDR endpoint, SSO-managed keys, SOC 2 report on file |
| Self-hosted open weights (Llama, Mistral, Qwen via vLLM) | Sovereign or air-gapped requirements; the ledger’s data classification forbids external inference | Full control; you own patching, eval, and capacity — budget for an MLOps owner |
Put a model gateway in front of whichever you choose (Azure API Management, OCI API Gateway, Kong AI Gateway, LiteLLM, or Portkey): it owns key custody, per-team spend caps, routing and fallback between models, prompt/response logging, and lets you retire a deprecated model without touching the application.
10.3 · Security controls
| Control | Implementation |
|---|---|
| Identity & access | Covered in full in section 09 — corporate SSO, group-to-role mapping, and the decision about who enforces data-level security. Listed here because it is a production gate, not because it is optional. |
| Integration user | One read-only ERP integration user per application per environment, scoped to the required ledgers, no interactive login, credential in a vault (OCI Vault, Azure Key Vault, HashiCorp Vault), rotated on a schedule and on staff change. |
| Secrets & config | No secrets in code or build artifacts; environment-specific config injected at deploy; .dev.vars-style files never leave a developer machine. |
| Network | Private endpoints to the LLM provider and to the ERP where the platform supports them; egress allow-list so the orchestrator can reach exactly two hosts; TLS 1.2+ everywhere. |
| Prompt-injection & input guardrails | Layer 1 (already in the demo) blocks instruction-override patterns, enforces length and scope; extend with a classifier on the gateway and log every rejection. |
| Output guardrails | Layer 4 (already in the demo) validates every returned segment value against the approved value sets and strips unexpected keys; production adds a check that the resolved ledger and period are inside the user’s data-access set before the query runs. |
| Data minimisation | Prompts contain metadata and the user’s query only. No journal amounts, no supplier or customer names, no journal descriptions. Logged prompts are classified and retained accordingly. |
| Encryption | In transit (TLS) and at rest (provider-managed KMS); audit logs on immutable storage with customer-managed keys where policy requires. |
10.4 · SOX, audit, and model-risk controls
A read-only window onto the general ledger does not change a financial-reporting control, but the GL is the most SOX-relevant system in the estate and this lands squarely in ITGC scope. Treat prompts, schemas, and eval sets as code — that single decision satisfies most of what an auditor will ask for.
| Requirement | How it is satisfied |
|---|---|
| Complete, immutable audit trail | Append-only log of user, timestamp, raw query, parsed JSON, model and prompt version hash, ledger/period/account returned, and row count — WORM storage, retained for the evidence period (typically 7 years), exported to the SIEM. |
| Change management | Prompt templates, few-shot examples, approved segment value sets, and code are version-controlled; every change follows ticket → peer review → test evidence → CAB approval → deploy. A prompt edit is a code change. |
| Segregation of duties | Developers cannot deploy to production; the integration-user owner is not a developer; production secrets are held by platform operations. Critically, this tool grants no posting ability — it cannot create, approve or post a journal. |
| Access recertification | Quarterly review of who can use the tool and of the integration user’s ERP roles and ledger access, evidenced and signed. |
| Testing evidence | A golden-query regression suite (the few-shot examples plus a larger labelled set) runs in CI before every release; pass rate and diffs are archived as release evidence. |
| Model risk management | An inventory entry (intended use, limitations, owner, validation date) in the model-risk register — the SR 11-7 pattern for financial services; periodic re-validation when the model or prompt changes. |
| Reproducibility & lineage | Every displayed number drills to a journal line, a batch and a source; an auditor can re-run the same query in the ERP and match it. |
| Explainability | The parsed intent JSON is the explanation and is shown to the user on every response — no hidden reasoning between the query and the data call. |
10.5 · Dev → Test → Prod promotion
| Environment | ERP target | Data | Gate to leave |
|---|---|---|---|
| Dev | ERP Dev instance | Synthetic or masked | Unit tests on the data layer; lint; eval suite ≥ threshold against the Test LLM deployment |
| Test / UAT | ERP Test instance (post-refresh) | Masked copy of production | Business UAT sign-off on the golden queries; security scan; performance run (p95 latency, fallback rate) |
| Prod | ERP Production | Live | Change ticket approved; deploy in window; smoke test; hypercare with rollback ready |
- Promoted artifacts: application build, prompt templates (versioned), approved segment-value snapshot, eval set, infrastructure config (IaC) — all from the same Git tag.
- Pipeline: branch → PR review → CI (tests + evals) → deploy to Test → UAT sign-off → CAB → deploy to Prod → smoke test.
- Chart of accounts drift: a scheduled job refreshes segment value sets and the cross-validation rule list into Layer 2 grounding with change detection, so a new cost centre or company appears in the approved lists without a code release — and an unmapped one is surfaced rather than silently accepted.
- Rollback: previous build and previous prompt version retained; rollback is a redeploy, and because prompts are versioned it also reverts a prompt regression.
10.6 · Operating it
- SLOs: p95 latency, availability of the read path (the deterministic fallback keeps it alive when the LLM is down — already built), fallback rate as a quality signal, eval pass rate per release.
- Close-window load: GL queries spike hard in the first five working days. Size for the close, not the average, and cache the chart of accounts aggressively — it barely changes.
- Cost governance: per-user and per-team token budgets at the gateway; alert on anomalies; the unit-cost model earlier in this kit is the baseline.
- Model lifecycle: providers retire models on a schedule — re-run the eval suite on the successor before switching, and record the switch as a change.
- Incident runbook: LLM outage → fallback parser; ERP API outage → cached metadata with a stale banner; guardrail spike → review logs for injection attempts.
10.7 · What changes for Trial Balance & Period Close
| Concern | Production answer |
|---|---|
| System of record | Oracle ERP Cloud — account balances, period statuses and the close checklist via the ERP REST API or BI Publisher; the GL owns the balances and the period lifecycle |
| Read/write posture | Read-only. Opening, closing and permanently closing a period are ERP actions under change control; this layer reports readiness and never changes a status. |
| Use-case-specific control | A group trial balance is the single most sensitive report in the estate — it is the unpublished results. Restrict the group view to consolidation and leadership roles, log every access, and treat an export as a material-information event. |
10.8 · Production readiness checklist
- ✓LLM platform selected from the governed list, DPA / zero-retention terms on file, gateway in front of it
- ✓SSO integrated; authorisation derived from ERP roles, ledgers and data-access sets; cookie gate removed
- ✓Read-only ERP integration user per environment, credential in a vault, rotation scheduled, no posting privilege
- ✓Prompts, segment value sets, and eval set version-controlled and under change management
- ✓Append-only audit log wired to the SIEM with the agreed retention
- ✓Golden-query eval suite passing in CI; results archived as release evidence
- ✓Dev / Test / Prod pipeline with gates, IaC, and a rehearsed rollback
- ✓Model-risk register entry and owner named; first re-validation date set
- ✓Chart-of-accounts refresh job scheduled with change detection for new and unmapped values
- ✓SLOs sized for the close window, cost caps, and the incident runbook agreed with platform operations