The problem this solves

Workforce is usually the second module implemented after Financials, and the one with the most sensitive data. It plans headcount and the full cost of employing people β€” salary, additional earnings, benefits and taxes β€” across every store, distribution center, and corporate function.

The hard part is not the arithmetic. It is that a retailer's workforce is enormous and heterogeneous: 44 stores each staffed differently by format, a distribution network with union labor, seasonal headcount that doubles in November, and sales roles with commission on top of base pay. Getting the default cost structure right for every job β€” once β€” is what makes the module usable. Getting it wrong means every planner overrides at the employee level, which defeats the point and inflates data volume.

Audience

HR business partners and FP&A teams who own headcount and compensation planning. EPM architects choosing Workforce granularity before a design workshop. Anyone who has been asked "why is Sales Associate showing Headcount 1 when Store B only has half a person scheduled there?"

A store, a year, and a scenario

Pick a store/entity, year, and scenario. The demo generates the store's staffing plan from a format-based template (Flagship, Standard, Digital, DC, Corporate, Franchise) β€” each job seeded with an FTE count, some of them deliberately fractional (0.75, 4.25) to exercise the rounding rule in the next section.

InputDrives
NLQ query"Show headcount for NYC Flagship" β€” DeepSeek extracts entity, year, and scenario and sets all three filters in one step
Store / EntityWhich staffing template applies (store format), and the FTE counts per job
Year / ScenarioA small year-over-year and Plan-vs-Actual multiplier on FTE and base pay
Job (click a row)Opens the default assignment cascade panel for that job

Headcount, FTE, and fully-loaded cost by job

  • Table: Job Γ— Pay Type Γ— Union Γ— FTE Γ— Headcount Γ— Base Γ— Benefits Γ— Taxes Γ— Earnings Γ— Fully-Loaded Cost
  • Cascade panel: click any job row to see exactly which components β€” 401(k) match, health insurance, payroll tax, bonus, commission, seasonal premium β€” apply, and their computed cost
  • KPI row: Total Headcount, Total FTE, Fully-Loaded Cost, Average Cost per FTE
  • Guardrail demo: a button injects a record mapped to the OWP_All Jobs scope member and shows the double-benefit trap being caught before it reaches the plan

Between HR data and the P&L

Workforce sits between the HR feed (source of truth for who works where) and Financials (which absorbs Store Labor, Corporate Labor, and DC Labor as OpEx lines). It is not a payroll system β€” it plans the cost, payroll executes it.

Recommended integration points

  • Budget cycle kickoff: HRBP reviews each store's job template and adjusts FTE before compensation defaults are applied
  • Seasonal planning: Store ops adds Temporary employee-type FTE for Nov–Dec, priced with Seasonal Premium Pay automatically via the job default
  • Compensation review: Finance checks fully-loaded cost per job against the retail P&A of Store Labor and Store Occupancy accounts already in the FP&A module
  • Union negotiation prep: DC Workers Union and Retail Workers Union costs are isolated by filtering on Union Code

The numbers

22
Jobs modeled
9
Compensation components
3
Union codes (incl. No Union)
7
Staffing templates by format
$0.0002
Cost per NLQ query
0ms
Client-side calc latency
ComponentChoice
GranularityEmployee and Job (demo runs at Job level; employee detail is a volume decision, not a data-model change)
Rounding ruleFTE > 0 and < 1 rounds UP to Headcount 1; FTE β‰₯ 1 rounds to nearest
GuardrailAll_ member trap detector β€” flags records assigned to a scope member instead of a real value

The honest checklist

  • βœ“You plan headcount and compensation across enough stores/jobs that employee-by-employee overrides are already a problem
  • βœ“You can define clean default cost structures by job, union, or similar grouping β€” the module runs itself once defaults are right
  • βœ“Your retail business has seasonal headcount swings that need modeling (Temporary employee type, Seasonal Premium Pay)
  • βœ“You need a member-level security design for who can see salaries β€” cost centre managers seeing only their own team is a common ask
  • βœ—You need real payroll processing β€” this plans cost, it does not run payroll
  • βœ—Job-only granularity is enough and you never need Split-Funded FTE or Merit Based Planning β€” those features require Employee or Employee and Job granularity, so confirm the feature list before the design workshop

Try it now

The live demo runs entirely client-side β€” pick a store, click a job row, and toggle the All_ member trap simulator.

Launch Workforce Planning β†’ Start a Lab engagement

Things to try

  • Type "Payroll for the distribution network" into the NLQ bar and watch it resolve entity, year, and scenario in one step
  • Switch to LA Beverly Hills (Flagship) vs a Standard-format store and compare total headcount
  • Click Visual Merchandiser β€” note 0.75 FTE still shows Headcount 1
  • Click Simulate the All_ member trap and watch the guardrail fire
HOW WE BUILT IT

Why defaults are the whole game

A retailer with 44 stores and 22 job types has on the order of a thousand FTE positions. Pricing each one by hand is not viable. Assign benefits, taxes, and earnings by job β€” or union code, or a similar grouping β€” rather than employee by employee. Get the defaults right and the module largely runs itself; get them wrong and planners override at employee level, which is exactly the manual effort the module exists to remove.

The demo makes this concrete: change JOB_DEFAULTS['J_SM'] once in the data layer and every Store Manager FTE across all 44 stores recalculates. That single-point-of-change property is what Synchronize Defaults does in a real EPBCS application when an entity default changes.

Staffing template β†’ cascade β†’ cost

Store selected (entity.format)
1
Staffing Template
STAFFING_TEMPLATES[format] → [{job, fte}]
  • 7 formats: Flagship, Standard, Digital, Marketplace, Corporate, DC, Franchise
2
FTE → Headcount Rule
fteToHeadcount(fte)
  • (0,1) → 1 always
  • ≥ 1 → Math.round
3
Default Assignment Cascade
JOB_DEFAULTS[jobId] → component list
  • Each component priced: %-of-base, flat-per-head-monthly, or flat-per-head-yr
4
Renderer
workforce.html
  • Table + KPI row + cascade side panel
  • All_ member trap guardrail simulator

This is a genuinely separate module β€” a new data file, epm-workforce-capex-data.js, loaded alongside (not instead of) epm-data.js. It reuses ENTITIES for store format and region but adds nothing to the FP&A/SmartView/Analytics/Master Data cube. Zero risk to the first four use cases.

App UI β€” Component breakdown

ComponentBehaviour
Rule calloutStatic banner stating the FTE→Headcount rounding rule in plain language, above the table
Filter barStore/entity, year, scenario selectors; a red "Simulate the All_ member trap" button
KPI rowTotal Headcount, Total FTE, Fully-Loaded Cost, Avg Cost/FTE β€” recomputed on every filter change
Workforce tableOne row per job; clicking a row opens the cascade panel for that job
Cascade panelComponent-by-component cost breakdown with a colour-coded badge: benefit (green), tax (red), earning (indigo)
Trap alertAppears only when the simulator is toggled on; states which record triggered the guardrail and why

Employee, Job, or Employee and Job

Granularity is chosen at enablement and is the defining Workforce decision. Three options: Employee, Job, or Employee and Job. Feature dependencies constrain the choice β€” Split-Funded FTE and Merit Based Planning both require Employee or Employee and Job granularity. If merit planning is on the requirements list, Job-only was never viable; check the feature list against granularity before the design workshop, not after.

The common pattern, and the one this demo models conceptually, is Employee and Job: the bulk of the population planned at Job level (this demo's store staffing), with employee detail reserved for senior or specialist roles. Employee and Job does not oblige loading employee records for everyone β€” that is a separate volume decision.

DimensionRequired whenDemo seed
JobOptional import22 retail jobs + OWP_All Jobs scope member
Skill SetMandatory for Job granularity10 skills (Customer Service, POS Operations, Buying & Planning, …)
Union CodeMandatory for Employee and Job granularityNo Union, DC Workers Union, Retail Workers Union + OWP_All Union Code
Employee TypeEmployee-only granularityRegular, Contractor, Temporary β€” Temporary drives seasonal headcount
Pay TypeEmployee-only granularityExempt, Non-Exempt
ComponentAlways β€” grades, taxes, benefits, earnings9 components: structure seeded, values configured per job

Union Code has an escape hatch: if a client doesn't track union codes, the dimension can be renamed to something meaningful β€” but a default must still be assigned to the renamed dimension. This demo keeps the real name and adds an explicit No Union member rather than leaving the field blank.

The configuration workhorse

Job = "Store Manager" β†’ 401(k) Employer Match: 4% β†’ Health Insurance: $650/mo β†’ Uniform Allowance: $200/yr β†’ Employer FICA: 7.65% β†’ State Unemployment Tax: 0.6% β†’ Store Performance Bonus: 8% ↓ Applied to every Store Manager FTE, at every store

This is the exact mechanism section 3.3 of the Workforce framework describes as "the configuration workhorse." JOB_DEFAULTS in the data layer is a direct analogue of an EPBCS entity default assignment: one row per job, listing which components apply and at what rate. genWorkforce() looks up the job's defaults and prices each line against that job's base cost β€” %-of-base components (401(k), bonus, commission, payroll tax) scale with FTE and pay; flat-per-head components (health insurance, uniform, vehicle allowance) scale with headcount, not FTE, matching how EPBCS treats per-employee benefits.

Retail-specific defaults worth noting: Sales Associate and Senior Sales Associate carry a 1.5% Sales Commission component that Cashier does not (commission attaches to roles that drive sales, not to transaction processing). Delivery Driver carries a Vehicle Allowance that no other job has. Seasonal Premium Pay is assigned to every customer-facing store job (Sales Associate, Cashier, Stockroom, Visual Merchandiser) to model November–December holiday staffing economics.

Two numbers that legitimately diverge

Headcount is the actual number of employees, and is always 1.0 for any employee whose FTE is greater than 0. FTE is full-time equivalent, where full-time is 1.0. FTE rounds to the nearest integer for headcount purposes β€” except values greater than 0 and less than 1, which always round up to 1.

FTE 0.25   β†’  Headcount 1.0    (rounds up, not to 0)
FTE 1.25   β†’  Headcount 1.0    (rounds to nearest)
FTE 4.75   β†’  Headcount 5.0    (rounds to nearest)

The demo's Visual Merchandiser row at a Standard-format store is staffed at 0.75 FTE β€” deliberately, so the rule is visible without reading documentation: Headcount still shows 1. fteToHeadcount() is a four-line function, but it is the single most support-ticket-generating behaviour in a live Workforce module. Explain it in training, or answer it every planning cycle.

The All_ member trap β€” Why some seeded members are not data locations

The failure mode: Workforce Planning adds double benefits when an employee record is assigned to OWP_All Union Code, OWP_All Jobs, or a similar All_ scope member. Oracle documents this explicitly β€” additional earnings, benefits, and taxes get applied twice.

These members exist to define a default-assignment scope β€” "this benefit applies to all jobs" β€” not as a place an employee record can live. The usual root cause is a data load mapping blank values to the All_ member as a catch-all, because it looks like a safe default. It is the opposite of safe: it silently doubles cost.

The demo's Simulate the All_ member trap button injects exactly this bad record β€” a workforce row with jobId: 'OWP_All Jobs' β€” and runs it through checkAllMemberTrap(), a guardrail that flags any record whose job or union is a scope member before it reaches the plan. The general lesson generalizes past Workforce: in any EPM module, some seeded members are configuration scope rather than data locations, and the All_ naming pattern is the tell. Before mapping a source value to a seeded member, ask what that member is for.

The correct fix for a genuine blank is not a catch-all β€” it's an explicit member. That's why this demo's Union Code dimension has a real No Union member instead of leaving the field empty.

Defaults vs Definition β€” the distinction that matters most

Rule (24.12+)Run it when
Synchronize Defaults 2.0You changed entity defaults β€” added or removed a benefit, tax, or additional earning
Synchronize Definition 2.0You changed an existing component's own definition β€” rate table, payment frequency, salary grade, maximum. Does not touch entity defaults.
Process Loaded Data with Synchronize DefaultsAfter importing compensation data β€” copies the loaded period forward and applies entity defaults
Process Loaded Data with Synchronize DefinitionSame copy-forward, but copies the properties and rates of the components being loaded
Calculate Compensation for all 2.0Recalculate across all entities, or all employees/jobs within an entity

Entity DEFAULTS = which components apply to whom β†’ Synchronize Defaults. Component DEFINITION = the component's own rates and properties β†’ Synchronize Definition. Choosing the wrong one is the classic Workforce support call: you changed a pension rate, ran Synchronize Defaults, and nothing moved β€” because the assignment didn't change, the definition did.

Both Process Loaded Data with… rules are self-sufficient β€” no additional rule is needed afterwards to compute compensation. Both also set Headcount to 1 and Partial Payment Factor to 100% for every employee unless different values were loaded at the processing month, which is exactly the demo's fteToHeadcount() behaviour reproduced at the rule level. Pre-24.12 applications use the older-named equivalents (Synchronize Defaults, Synchronize Component Definition, Calculate Compensation, Calculate Employee/Job Compensation for All Data, Process Loaded Data) β€” Oracle recommends updating to the 24.12+ rules.

Groovy templates behind the menus

Workforce ships Groovy templates behind menu items and on-save actions β€” OWP_Add Requisition_GT, OWP_Change Salary_GT, OWP_Incremental Synchronize Defaults_GT, and others. Security must be set on them or the menu items fail for users. The four Incremental templates implement the dirty-cell pattern β€” processing only changed data on save, the same idea behind this demo's cascade panel recalculating only the clicked job rather than the whole table.

Key files

FileRole
epm-nlq-src/assets/epm-workforce-capex-data.jsJOBS, COMPONENTS, JOB_DEFAULTS, STAFFING_TEMPLATES, genWorkforce(), fteToHeadcount(), checkAllMemberTrap()
epm-nlq-src/pages/workforce.htmlUI: filter bar, KPI row, workforce table, cascade side panel, trap simulator
epm-nlq-src/assets/epm-data.jsShared ENTITIES array β€” supplies store format and region for the staffing template lookup
functions/api/nlq-query.jsShared NLQ endpoint; Workforce uses the useCase: "workforce" branch β€” a compact schema of just entity/year/scenario, the smallest few-shot block of the nine use cases

NLQ prompt design

Workforce's query schema is intentionally the smallest in the whole prompt: {"intent":"workforce_query","entity":"<name|null>","year":"<FY26|FY25>","scenario":"<Plan|Actual>","confidence":<0-1>}. There is no job, union, or component field to extract β€” those are explored interactively by clicking a table row, not by asking for them in the query. Keeping the LLM's job to entity/year/scenario resolution (the same three fields FP&A and Currency Translation extract) keeps the prompt cheap and the failure surface small; the mechanism-heavy parts of this demo β€” the rounding rule, the cascade, the All_ trap β€” stay deterministic client-side logic, untouched by the LLM.

Tech stack β€” Every tool in this build

LayerToolWhy
Data layerepm-workforce-capex-data.js (vanilla JS)Deterministic mechanism (rounding, cascade, trap) β€” the LLM only resolves entity/year/scenario
LLMDeepSeek V3Shared NLQ endpoint, smallest schema of the nine use cases β€” entity/year/scenario only
Staffing modelFormat-keyed template map44 stores, 7 formats β€” one template per format, not per store, mirrors job-level defaulting
RenderingVanilla JS DOM, shared epm-nlq.cssConsistent with the other five use cases, zero framework overhead
Edge hostingCloudflare PagesStatic file, no server compute needed for this use case
BuildEleventy v3.1.5Copies epm-nlq-src/pages/ and epm-nlq-src/assets/ to _site/ verbatim

Known attack surfaces

ThreatMitigation in this build
Compensation data exposureDemo data only β€” no real employee records. Production requires member-level security so cost centre managers see only their own team, not peers'.
All_ scope member misusecheckAllMemberTrap() demonstrated as a load-time validation pattern β€” production should add this check as a hard load-reject, not a warning.
Blank union/job mappingAn explicit No Union member exists precisely so a source-system blank never falls through to a scope member as a catch-all.

Guardrails β€” What prevents bad workforce plans

  • Rounding rule enforced once, centrally: fteToHeadcount() is the only place headcount is derived β€” no page computes it independently, so the rule can never drift out of sync across views.
  • All_ member guardrail: any record whose job or union resolves to a scope member is flagged with the specific remediation (assign a real job and union; use "No Union" for genuine blanks) rather than a generic error.
  • Explicit blank handling: the Union Code dimension has no implicit default β€” every employee record must carry No Union or a real union, never a blank.
  • Single-point default changes: changing a job's compensation defaults changes it everywhere that job is staffed, in one edit β€” the same design goal as Synchronize Defaults in production EPBCS.

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 EPM Cloud 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.

The rule that governs every choice below: a user must see exactly what they would see by logging into Oracle EPM Cloud directly — no more, and no less. If this tool can surface a number the user could not retrieve themselves, it has become a privilege-escalation path, and it will be found in the first access review.

9.1 · The identity chain, end to end

Finance user opens the tool in a browser — no local account, no password held here
1
Corporate identity provider
Entra ID · Okta · OCI IAM
OIDC Authorization Code + PKCE  (or SAML 2.0)
  • 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
2
Application session
validate, never trust
  • 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
Pattern A — identity propagation
OAuth 2.0 token exchange (on-behalf-of)
  • The API is called as the user
  • EPM enforces its own security natively
  • Audit trail names the real user
  • Preferred where the API supports it
Pattern B — service account + filtering
one read-only integration account
  • 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
3
EPM identity domain
roles + dimension security
  • Roles: Service Administrator, Power User, User, Viewer — assigned to groups, never to individuals
  • Data level: Planning access permissions on Entity and a second gate on the compensation accounts — two independent checks, because entity access does not imply salary access
  • Group → role mapping lives in the platform, not in this application
Result, filtered to this user
workforce.html
  • 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 EPM Cloud does not replace your directory — it trusts it. The EPM Cloud identity domain is federated with the corporate IdP so authentication happens where it already happens, under policies security has already written.

Identity providerProtocolNotes
Microsoft Entra ID (formerly Azure AD)SAML 2.0 or OIDCThe 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.
OktaSAML 2.0 or OIDCSame pattern; Okta groups drive EPM roles through SCIM provisioning.
OCI IAM (identity domains)NativeAlready present with Oracle EPM Cloud. Can be the primary IdP for a smaller estate, or a federated spoke of Entra/Okta for a larger one.
AD FSSAML 2.0Still 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 EPM 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                    β†’  EPM role / entitlement
──────────────────────────────────────────────────────────────────
FIN-EPM-Analysts                  β†’  Planning User
FIN-EPM-Controllers-EMEA          β†’  Power User + EMEA data scope
FIN-EPM-Admins                    β†’  Service Administrator
──────────────────────────────────────────────────────────────────
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: EPBCS Workforce User, plus a separate HR/compensation entitlement for employee-level pay.

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 propagationPattern B — service account + filtering
HowThe user’s token is exchanged (OAuth 2.0 on-behalf-of) for one scoped to the EPM API; calls are made as the userA single read-only integration account calls the API; the application filters the results
Enforcement pointOracle EPM CloudThis application
Audit trail showsThe real end userThe service account — you must log the real user separately
Failure modeToken plumbing is more complex; per-user rate limits applyA filtering bug is a data breach, and the entitlement copy drifts from reality
VerdictPrefer this wherever the API supports user-token authenticationAcceptable 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: Planning access permissions on Entity and a second gate on the compensation accounts — two independent checks, because entity access does not imply salary access.
  • 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 EPM Cloud identity domain (OCI IAM, formerly IDCS), 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 Workforce Planning

ConcernAnswer for this use case
Native entitlement requiredEPBCS Workforce User, plus a separate HR/compensation entitlement for employee-level pay
Data-level controlPlanning access permissions on Entity and a second gate on the compensation accounts — two independent checks, because entity access does not imply salary access
Use-case-specific sensitivityThe highest-sensitivity data in the portal. Employee-level compensation is HR-restricted PII: mask salary below the comp role, never place employee names in a prompt, align retention with HR data policy rather than finance, and check whether works-council or GDPR consultation is required before go-live.

9.8 · Security configuration checklist

  • ✓Oracle EPM Cloud 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
Talk through your identity model → Back to the demo

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 Workforce Planning 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.

The one rule that matters most for this use case: in the demo the browser computes the result; in production Oracle computes and this layer retrieves and explains. Never ship a second calculation engine that can disagree with the system of record — the moment two numbers exist, the audit question becomes “which one is right,” and the answer must always be the EPM module.

10.1 · Production reference architecture

Finance user · corporate SSO (OIDC/SAML + MFA) · EPM role claims
1
Edge / API Gateway
WAF · rate limit · identity
  • Terminates SSO, validates the session, attaches the user’s EPM groups to the request
  • Rate limits per user, blocks anonymous access, scrubs PII patterns before anything reaches the orchestrator
2
NLQ Orchestrator
the 4-layer pipeline, hardened
L1 guardrails → L2 grounding → L3 LLM adapter → L4 eval + fallback
  • L2 grounding reads dimension metadata from EPM on a schedule — not a hardcoded schema
  • Only the schema + user query go to the model; financial values never leave the data layer
  • L4 rejects anything outside the approved member lists and falls back to the deterministic parser
Oracle OCI Generative AI
same tenancy as EPM Cloud
  • Data stays inside the OCI boundary
  • Natural fit when EPM is already in OCI
Azure OpenAI / AWS Bedrock / Vertex AI
private endpoint, zero retention
  • Use the hyperscaler the org already governs
  • Enterprise DPA, no training on prompts
Self-hosted open weights
VPC / air-gapped
  • For regulated or sovereign data
  • Highest control, highest run cost
3
EPM Data Layer
Oracle EPM REST API
  • Oracle EPBCS Workforce module — headcount, FTE, and compensation by job/employee via REST; fteToHeadcount and the default-assignment cascade run inside EPBCS, not in the browser
  • Least-privilege service account (read-only role, one app, one pod) with the token in a vault and rotated
  • Results filtered to the requesting user’s EPM security before rendering
4
Audit & Observability
append-only
  • Every query logged: user, timestamp, raw query, parsed intent JSON, model + prompt version, POV 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
Rendered result + evidence trail
workforce.html
  • The parsed JSON is shown to the user as the explanation (“AI: entity · year · scenario — 93% confident”) — the same line the demo prints today
  • Every number on screen traces to an EPM cell intersection an auditor can reproduce

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.

OptionChoose whenData posture
Oracle OCI Generative AI (Cohere Command, Llama)EPM Cloud already lives in OCI; you want one cloud boundary and one contractPrompts stay in the OCI tenancy; no training on customer data; dedicated AI clusters available for isolation
Azure OpenAI ServiceMicrosoft-first finance estate (Entra ID, Purview, Sentinel already in place)Private endpoint, regional deployment, zero-retention by default under the enterprise agreement
AWS Bedrock (Claude, Titan) / Google Vertex AI (Gemini)The org’s landing zone is AWS or GCP; VPC endpoints and IAM already auditedVPC/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 signedZDR endpoint, SSO-managed keys, SOC 2 report on file
Self-hosted open weights (Llama, Mistral, Qwen via vLLM)Sovereign or air-gapped requirements; regulated data classification forbids any external inferenceFull 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

ControlImplementation
Identity & accessCovered 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.
Service accountOne read-only EPM service account per application per pod, least-privilege role, no interactive login, credential in a vault (OCI Vault, Azure Key Vault, HashiCorp Vault), rotated on a schedule and on staff change.
Secrets & configNo secrets in code or build artifacts; environment-specific config injected at deploy; .dev.vars-style files never leave a developer machine.
NetworkPrivate endpoints to the LLM provider and to EPM where the platform supports them; egress allow-list so the orchestrator can reach exactly two hosts; TLS 1.2+ everywhere.
Prompt-injection & input guardrailsLayer 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 guardrailsLayer 4 (already in the demo) validates every returned member against the approved lists and strips unexpected keys; production adds a policy check that the resolved POV is inside the user’s security scope before the data call.
Data minimisationPrompts contain metadata and the user’s query only. No cell values, no employee names, no free-text comments from EPM. Logged prompts are classified and retained accordingly.
EncryptionIn 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 NLQ layer does not change a financial-reporting control, but it is an interface to a SOX-relevant system and 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.

RequirementHow it is satisfied
Complete, immutable audit trailAppend-only log of user, timestamp, raw query, parsed JSON, model and prompt version hash, POV returned, and row count — WORM storage, retained for the evidence period (typically 7 years), exported to the SIEM.
Change managementPrompt templates, few-shot examples, approved-member schema, 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 dutiesDevelopers cannot deploy to production; the service-account owner is not a developer; production secrets are held by platform operations.
Access recertificationQuarterly review of who can use the tool and of the service account’s EPM roles, evidenced and signed.
Testing evidenceA 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 managementAn 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 & lineageEvery displayed number traces to an EPM POV and a consolidation/calculation timestamp; an auditor can re-query the same intersection in EPM and match it.
ExplainabilityThe 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

EnvironmentEPM targetDataGate to leave
DevEPM Test pod (developer slice)Synthetic or maskedUnit tests on the data layer; lint; eval suite ≥ threshold against the Test LLM deployment
Test / UATEPM Test pod (full refresh)Masked copy of productionBusiness UAT sign-off on the golden queries; security scan; performance run (p95 latency, fallback rate)
ProdEPM Production podLiveChange ticket approved; deploy in window; smoke test; hypercare with rollback ready
  • Promoted artifacts: application build, prompt templates (versioned), approved-member schema 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. Hosting can stay on Cloudflare Pages/Workers or move to OCI Functions + API Gateway or the org’s standard platform — the code does not care.
  • Configuration: per-environment secrets and endpoints injected at deploy; the same build runs in every environment.
  • Metadata sync: a scheduled job refreshes dimension metadata into Layer 2 grounding with change detection, so a new entity or account appears in the approved lists without a code release.
  • 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.
  • 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; EPM API outage → cached metadata with a stale banner; guardrail spike → review logs for injection attempts.

10.7 · What changes for Workforce Planning

ConcernProduction answer
System of recordOracle EPBCS Workforce module — headcount, FTE, and compensation by job/employee via REST; fteToHeadcount and the default-assignment cascade run inside EPBCS, not in the browser
Read/write postureRead-only. Employee-level compensation is HR-restricted PII.
Use-case-specific controlMask salary at the employee level unless the user holds the HR/comp role; never place employee names in an LLM prompt (only job codes and entity); align retention with HR data policy, not finance’s.

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 EPM security groups; cookie gate removed
  • ✓Read-only EPM service account per pod, credential in a vault, rotation scheduled
  • ✓Prompts, schema, 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
  • ✓Metadata refresh job scheduled with change detection
  • ✓SLOs, cost caps, and the incident runbook agreed with platform operations
Plan a production rollout with us → Back to the demo