The data flows in, the cube calculates, but Vision's planners still have no way to enter their forecasts. You must build their camp — the planning forms where hundreds of users will input their budgets and rolling forecasts. Build them wrong, and the mutiny will be swift.
Planning Form Types
Form Type
Description
Vision Use Case
Simple Form
Single grid, one plan type
Department expense input by month
Composite Form
Multiple grids + tabs
Full P&L with driver-based inputs
Ad-Hoc Grid
Flexible analysis, free pivoting
FP&A analyst variance exploration
Smart Form
Custom HTML/JS UI overlay
Complex driver input with visual business logic
Rolling 12-Month Forecast
Vision uses a 12-month rolling forecast — always forecasting 12 months ahead regardless of where in the fiscal year we are.
📊Rolling vs Static: A static forecast closes at Dec 31. In November, you have 2 months of visibility. A rolling 12-month forecast means in November 2025, you're already forecasting through October 2026. CFOs and investors strongly prefer rolling forecasts for capital allocation decisions.
Rolling Forecast Form Column Setup
// Form Page: Scenario=Forecast, Version=Working// Column range — rolls forward each month automatically&CurPeriod:&CurPeriod+11 // 12 months from current period subvar// Lock Actual months — read-only so planners can't overwrite// In form: set column property "Read Only" for Actual periods// Use: @ISANCESTOR("Actual", @CURRMBR(Scenario))
Action Menus are Vision's chain of command — the right-click menus that trigger business rules, launch workflows, copy data, and execute complex operations with a single click. Build them well and planners will call you a wizard.
Action Menus
Action Menus appear when a user right-clicks on a form cell, row, or column. They trigger: business rules, navigation to other forms, data copy operations, or external URL launches.
💡FP&A Power Move: Build an "Spread Annual to Monthly" action menu item. One click triggers a Groovy rule that takes the annual total a planner entered and distributes it to 12 months using historical seasonality. This eliminates one of the most tedious manual tasks in budgeting.
Budget Approval Workflow
✏
Draft
Planner enters
📤
Submit
Sends to mgr
🔍
Review
Manager checks
↩
Push Back
Rejected w/ note
✅
Approved
Finance locks
Planning Unit Hierarchy (PUH) defines the approval chain. Each Entity+Scenario combination is a Planning Unit. States flow upward through manager layers until Corporate Finance gives final approval.
⛺ Quest Tasks — Lesson 10
Create an Action Menu item "Copy Budget to Working"
Build "Spread Annual to Monthly" Action Menu item
Set up Planning Unit Hierarchy for Vision entities
Test the Draft → Submit → Approve workflow for one entity
🔭
The Oracle
Workflow Expert
⛺ Part IV · Lesson 11 · 200 XP
🪟 Forms 2.0 — The New Standard
Part IV · Planning Camps · Lesson 3 of 3 · EPB-MOD-04-L3
The Architect's Upgrade
Oracle quietly changed the rules. Forms 2.0 — released progressively from EPM 25.06 — is not just a visual refresh. It is a stateless, API-driven rendering engine that replaces the classic form framework entirely. If you deploy a composite form today without understanding Forms 2.0, you will hit walls you cannot explain. Master it, and you build forms that are faster, more flexible, and future-proof.
Classic Forms vs Forms 2.0 — What Actually Changed
Forms 2.0 is Oracle's complete rewrite of the planning form rendering layer. The business user sees a similar grid, but the architecture underneath is fundamentally different — and those differences have direct consequences for how you configure, debug, and optimize forms.
Dimension
Classic Forms
Forms 2.0
Architecture
Stateful — server holds form session state between interactions
Stateless — each interaction is an independent API call with full context
Rendering engine
Legacy ADF (Oracle Application Development Framework)
Oracle JET 2.0 — modern JavaScript component library
Composite forms
Multiple grids share a session; context passes implicitly
Each grid is independent; context must be explicitly passed via Apply Context
Runtime Prompts
Evaluated once at form load; cached in session
Evaluated per-API-call; must be explicitly re-passed to each grid in a composite
Member formula display
Dynamic Calc members shown inline, re-evaluated on each cell change
Dynamic Calc members retrieved via separate API call — may require explicit refresh
Performance profile
Slow initial load (full session setup); faster navigation within session
Fast initial load (stateless); each interaction is a targeted API call
Mobile support
Limited — ADF was not designed for mobile viewports
Fully responsive — JET 2.0 is mobile-first
Available from
All EPM versions
EPM 25.06+ (full Forms 2.0 with composite support)
⚠Stateless is the key mental shift. In classic forms, when a planner changes a page member selector, the server remembers the new context for all subsequent interactions in that session. In Forms 2.0, the page member change fires an API call — and that API call must carry the full POV context every time. If your composite form has Grid A and Grid B and the planner changes the entity selector in Grid A, Grid B does NOT automatically know about it unless you have configured Apply Context.
The 12-Component Dashboard Limit — NODE Z Governor
Forms 2.0 enforces a hard limit of 12 components per dashboard. This is a system governor — not a recommendation. Exceeding it causes the dashboard to fail silently on load in some versions, or throw an error in others. Plan your dashboard layout before building.
Component Type
Counts Toward 12?
Notes
Planning form grid
✅ Yes
Each grid = 1 component regardless of size
Chart
✅ Yes
Each chart = 1 component
Infolet
✅ Yes
Each infolet = 1 component
Tile (static text/image)
✅ Yes
Even static tiles count
Navigation flow shortcut
❌ No
Navigation items are not dashboard components
🚨NODE Z Hard Limit: 12 components per dashboard. This is not a soft guideline — it is a system ceiling. Vision Corp's original CFO dashboard design had 16 components. Rebuilding it to fit within 12 required consolidating 4 separate entity charts into a single multi-series chart and merging 2 KPI infolets. Design to the limit from the start.
Composite Forms and Apply Context — The Critical Configuration
A composite form in Forms 2.0 contains multiple grids displayed together. The challenge: each grid is stateless and independent. When a planner selects Entity = Vision_US in Grid A's page dimension, Grid B does not automatically filter to Vision_US — unless you configure Apply Context.
How Apply Context Works
Apply Context is a Forms 2.0 configuration that creates a dependency between grids. When the page member of the source grid changes, the target grid's matching dimension is automatically updated and the grid re-queries. This replaces the implicit session state sharing of classic forms.
Scenario
Classic Forms Behaviour
Forms 2.0 Behaviour
Planner changes Entity page selector in Grid A
All grids in composite automatically use the new entity (shared session)
Only Grid A updates. Grid B stays on previous entity unless Apply Context is configured
RTP (Runtime Prompt) changes value
New RTP value propagates to all grids automatically
Each grid must have Apply Context configured to receive the new RTP value
Master-Detail relationship (Grid A drives Grid B)
Implicit — selection in Grid A flows to Grid B
Explicit — configure "Master" grid, "Detail" grid, and the dimension that links them
💡Apply Context Setup: In the composite form designer, select a grid → Properties → Apply Context. Set: Source grid = "Master", Target dimension = Entity (or whichever dimension should sync). The target grid's Entity page selection will now mirror the master grid whenever it changes. For Vision Corp's full P&L composite: Grid A (Revenue inputs) is master, Grid B (Cost inputs) and Grid C (KPI summary) are both targets on the Entity dimension.
Runtime Prompts in Forms 2.0 — The Stateless Trap
Runtime Prompts (RTPs) in Forms 2.0 behave differently from classic forms in one critical way: because each grid interaction is an independent API call, the RTP value must be explicitly re-passed on each call. This is handled automatically by Forms 2.0 when RTPs are properly configured — but fails silently when they are not.
Situation
Classic Forms
Forms 2.0
RTP defined at form level
Value shared across all grids automatically
Value must be mapped to each grid that uses it via Apply Context or form-level RTP binding
RTP value not passed to a grid
Grid uses cached session value (often correct)
Grid uses the RTP default value — silently ignores the user's selection
Multi-grid RTP dependencies
One RTP, all grids automatically update
Configure RTP binding explicitly for each grid in the composite form designer
🚨Silent RTP Failure Pattern: In Forms 2.0, if an RTP is not properly bound to a grid, the grid uses the RTP default value instead of the user's selection — with no error message. A planner selects "FY2026" from the Year RTP, but Grid B (not bound) silently shows FY2025 data. The planner doesn't notice. This is the #1 Forms 2.0 support ticket in production environments. Always test every grid's RTP binding explicitly after configuring a composite form.
Classic vs Forms 2.0 Feature Gap — What's Not There Yet
As of EPM 25.11, Forms 2.0 has not fully replicated every classic form feature. Know the gaps before committing to a Forms 2.0 migration.
⚠ Partial — some dependency chains require classic
Custom JavaScript overlays (Smart Forms)
✅ Supported
⚠ Under review — check latest EPM release notes
Vision Corp Forms 2.0 Migration — Decision Framework
Not every form needs to be migrated to Forms 2.0 immediately. Use this decision framework to prioritise.
Migrate to Forms 2.0 First
Keep on Classic (for now)
New forms being built from scratch
Existing complex composite forms that are stable and not causing issues
Mobile-accessed forms (field finance teams)
Forms using Smart Form (custom JS overlay) functionality
Forms accessed by 50+ concurrent users (performance benefit)
Forms relying on complex dependent dropdown cascades
Dashboard-embedded forms (12-component limit applies)
Forms where Supporting Detail formatting is critical
Any new form built after EPM 25.06
Forms scheduled for decommission within 12 months
📊FP&A Production Reality: Vision Corp's migration timeline: Q1 FY2026 — all new forms built in Forms 2.0. Q2 FY2026 — simple input forms migrated. Q3 FY2026 — composite forms migrated after the July forecast cycle completes (never migrate during an active planning cycle). Q4 FY2026 — decommission all classic forms. This 12-month runway respects the budget and forecast cycle constraints that govern when form changes are safe to deploy.
Forms 2.0 in the Vision Corp CFO Dashboard
Vision Corp's CFO Dashboard uses Forms 2.0 with a 4-grid composite layout — exactly at the 12-component limit when combined with 4 charts and 4 infolets.
#
Component
Type
Apply Context Role
1
Entity Selector Grid
Form grid (1 row, Entity page)
Master — drives all other grids
2
P&L Summary Grid
Form grid (Descendants Net_Income)
Detail — Entity syncs from master
3
Revenue vs Budget Chart
Chart
Detail — Entity syncs from master
4
EBITDA Margin Chart
Chart
Detail — Entity syncs from master
5–8
4 KPI Infolets
Infolet ×4
Detail — Entity syncs from master
9
Entity Waterfall Chart
Chart
Detail — Entity syncs from master
10
Headcount Summary Grid
Form grid
Detail — Entity syncs from master
11
Rolling Forecast Grid
Form grid
Detail — Entity + Year sync from master
12
Alerts Tile
Tile (static + conditional format)
N/A (static)
💡Design Trick — The Entity Selector Grid: Rather than putting the Entity page selector on every grid independently, Vision Corp uses a single 1-row grid as a dedicated "master selector". This grid has Entity in its page dimension and displays just one or two high-level KPI accounts as data. Its only purpose is to drive the Apply Context chain. When the planner changes Entity here, all 11 other components update simultaneously. This pattern solves the most common Forms 2.0 UX problem — context synchronisation across a complex dashboard.
🎯 Interview Q — Senior EPBCS Architect
A client has a composite form where changing the Entity page selector in Grid A doesn't update Grid B. They're on EPM 25.08. Diagnose and fix.
Strong answer: (1) Confirm they are using Forms 2.0 — this is a Forms 2.0 Apply Context issue, not a classic form bug. (2) Navigate to the composite form designer → Grid B properties → Apply Context tab → confirm Entity dimension is NOT configured as a target from Grid A. (3) Fix: set Grid A as Master, Grid B as Detail, dimension = Entity. (4) Test: change Entity in Grid A, verify Grid B refreshes. Strong hire adds: check RTP bindings at the same time — if any RTPs are used, verify all grids have them bound.
⛺ Quest Tasks — Lesson 11
Explain the stateless architecture of Forms 2.0 and why Apply Context is necessary
Identify what the 12-component dashboard limit covers and how to stay within it
Configure Apply Context between two grids in a composite form: entity selector → P&L grid
Test the silent RTP failure pattern — change an RTP and verify all grids respond
Apply the migration decision framework to Vision Corp's 50-form library — identify which 10 to migrate first
🔭
The Oracle
Forms 2.0 & JET Architecture Expert
⚔ Part V · Lesson 11 · 175 XP
⚔ Business Rules & Groovy Primer
Part V · Groovy Battle Saga · Lesson 1 of 5
The Sorcerer Awakens
Deep in the engine room of EPBCS lives a power most admins fear to touch: Groovy scripting. Where Essbase calc scripts end, Groovy begins — server-side Java capable of reading and writing every cell in the cube, calling REST APIs, and executing complex financial logic. Master it, and you become the most dangerous person in your finance organisation.
Groovy vs Essbase Calc Scripts
Capability
Essbase Calc
Groovy
Read/Write cube
✓
✓
Conditional logic
Limited IF/ENDIF
Full Java if/else/switch/ternary
Loops & iteration
FIX/ENDFIX only
for, while, each, collect, find
REST API calls
✗
✓ (call any HTTP endpoint)
Dynamic member lists
✗
✓ (query at runtime from cube)
Error handling
✗
✓ try/catch/finally
String manipulation
✗
✓ full Java string operations
Essential Groovy Objects & APIs
Core Groovy Objects
// The cube — your gateway to everythingdef essbase = cube.getEssbaseCube()
// Read a cell (one network call)def rev = getCell("Vision_US", "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working")
// Write a cell — use sparingly, never in loops
setCell(1234567.89, "Vision_US", "Product_Revenue", "Jan", "FY2025", "Budget", "Working")
// Batch write — ONE call for ALL values (critical for performance)
operation.grid.setDataCellValues([["Vision_US", "Prod_Rev", "Jan", "FY2025", "Budget", "Working", 1200000.0],
["Vision_US", "Prod_Rev", "Feb", "FY2025", "Budget", "Working", 980000.0]])
// Get all leaf members under a parentdef entities = essbase.getMember("Entity", "Total_Vision").getLevelZeroMembers()
// Get current POV from form contextdef entity = operation.grid.pov.find { it.dimensionName == "Entity" }.memberName
⚠Performance Rule #1: Never call setCell() inside a loop. Build a list of writes and call setDataCellValues() once. For 50 entities, this is the difference between 50 network round-trips (slow) and 1 (fast).
⚔ Quest Tasks — Lesson 11
Create your first Groovy business rule and attach it to a form
Use getCell() to read a value and log it with println()
Use getLevelZeroMembers() to iterate all leaf Entity members
Implement batch setDataCellValues() for efficient writes
🔭
The Oracle
Groovy Sorcerer
⚔ Part V · Battle 1 · 200 XP
⚔ Battle 1 — Seasonal Revenue Spread
Part V · Groovy Battle Saga · Battle 1 of 4
⚔ Groovy Battle — The Revenue Sorcerer
Spread Annual Revenue Using Historical Seasonality
Mission: Sales enters annual revenue targets as a single total. Your Groovy rule must distribute this total to 12 months using prior year actual seasonality weights. Without this, planners manually enter 12 months — inconsistently and slowly.
The Algorithm
Read the annual budget total entered by the planner
Calculate prior year monthly proportions (Jan Actual / Full Year Actual)
💡Rounding Fix: Due to decimal rounding, 12 months may not sum exactly to the annual total. The last line assigns the rounding difference to December — keeping the audit clean.
⚔ Battle 1 Tasks
Implement the seasonality spread Groovy rule
Add fallback logic for entities with no prior year actuals (equal distribution)
Attach the rule to an Action Menu on the annual total form
Implement rounding reconciliation so months sum exactly to annual
🔭
The Oracle
Groovy Battle Coach
⚔ Part V · Battle 2 · 225 XP
⚔ Battle 2 — Dynamic Cost Allocations
Part V · Groovy Battle Saga · Battle 2 of 4
⚔ Groovy Battle — The Overhead Distributor
Revenue-Proportional IT Shared Cost Allocation
Mission: Corporate IT has an $8.5M shared cost pool. Finance requires this cost allocated to 8 business entities proportional to each entity's revenue. Must run dynamically — as revenue changes in Budget, allocated costs auto-update.
💡FP&A Enterprise Pattern: Shared services allocation (HR, IT, Finance, Legal) is one of the top 5 most-automated use cases in EPBCS across Fortune 500 companies. This two-pass pattern handles all of them — just change the pool account and allocation basis.
⚔ Battle 2 Tasks
Implement two-pass proportional allocation in Groovy
Add zero-revenue guard to prevent division by zero
Extend to also allocate HR and Finance shared cost pools
Add monthly spreading so annual pool distributes to each month
🔭
The Oracle
Allocation Expert
⚔ Part V · Battle 3 · 250 XP
⚔ Battle 3 — Validation Guards
Part V · Groovy Battle Saga · Battle 3 of 4
⚔ Groovy Battle — The Gatekeeper
Business Rule Validation with Clear User Feedback
Mission: Build Groovy validations that prevent impossible plan submissions: negative headcount, margin below floor, revenue below committed backlog. Each violation must produce a clear, actionable error — not a cryptic system error.
Groovy — Collect-All-Errors Validation Pattern
import com.hyperion.calcmgr.common.groovyrules.exceptions.RuleStoppedException
def entity = operation.grid.pov.find { it.dimensionName == "Entity" }.memberName
def errors = [] // Collect ALL errors before throwing — better UX// Validation 1: Headcount must be positive and sanedef hc = getCell(entity, "Total_Headcount", "Full_Year", "FY2025", "Budget", "Working") ?: 0
if (hc < 0) errors << "❌ Headcount cannot be negative (${entity}): ${hc}"if (hc > 10000) errors << "⚠ Headcount >10,000 — likely a data entry error: ${hc}"// Validation 2: Gross margin must be above thresholddef rev = getCell(entity, "Total_Revenue", "Full_Year", "FY2025", "Budget", "Working") ?: 0
def gp = getCell(entity, "Gross_Profit", "Full_Year", "FY2025", "Budget", "Working") ?: 0
if (rev > 0 && (gp / rev) * 100 < 10)
errors << "❌ Gross margin ${Math.round((gp/rev)*100)}% is below the 10% minimum for ${entity}"// Validation 3: Forecast revenue cannot be below committed backlogdef backlog = getCell(entity, "Committed_Backlog", "Full_Year", "FY2025", "Actual", "Final") ?: 0
if (rev < backlog) errors << "❌ Revenue forecast (${rev}) < committed backlog (${backlog}) for ${entity}"// Throw once with ALL errors — user sees everything at onceif (!errors.isEmpty()) throw newRuleStoppedException(errors.join("\n"))
💡UX Best Practice: Collect all errors before throwing. If 3 validations fail, the user sees all 3 at once and fixes them in one go. Throwing on the first error means fix-resubmit-fix-resubmit — a maddening experience that breeds distrust in the system.
⚔ Battle 3 Tasks
Implement headcount range validation with clear error messages
Add gross margin floor validation (10% minimum)
Build backlog vs forecast check to prevent impossible plans
Use collect-all-errors pattern before throwing RuleStoppedException
🔭
The Oracle
Groovy Validation Expert
⚔ Part V · Battle 4 · 300 XP
⚔ Battle 4 — Debug & Optimize
Part V · Groovy Battle Saga · Battle 4 of 4
⚔ Groovy Battle — The Performance Inquisitor
Diagnosing and Fixing a Slow Groovy Rule
Mission: A Groovy allocation rule that worked in testing is taking 45 minutes in production with full data. Diagnose the bottleneck, identify the anti-patterns, and refactor for sub-5-minute execution.
❌ The Slow Code (45 minutes)
Anti-Pattern — N network calls in a loop
// WRONG: setCell inside nested loops = hundreds of round-trips
entities.each { entity ->
months.each { month ->
def val = computeValue(entity, month)
setCell(val, entity, "IT_Alloc", month, "FY2025", "Budget", "Working")
// 8 entities × 12 months = 96 individual network calls
}
}
// WRONG: Reading inside an O(n²) nested loop
entities.each { e ->
entities.each { other -> // n × n reads = terribledef v = getCell(e.getName(), "Rev", ...)
}
}
✅ The Fixed Code (under 2 minutes)
Optimized Pattern — Pre-fetch, compute in memory, batch write
def startTime = System.currentTimeMillis()
// Step 1: Pre-fetch all data into a map (reads happen BEFORE writes)def revenueMap = [:]
entities.each { e ->
revenueMap[e.getName()] = months.collectEntries { month ->
[month, getCell(e.getName(), "Total_Revenue", month, "FY2025", "Actual", "Final") ?: 0]
}
}
// Step 2: Compute in pure Groovy (zero Essbase calls here)def writes = []
entities.each { e ->
months.each { month ->
def allocated = computeShare(revenueMap, e.getName(), month)
writes << [e.getName(), "IT_Alloc", month, "FY2025", "Budget", "Working", allocated]
}
}
// Step 3: ONE batch write for all cells
operation.grid.setDataCellValues(writes)
println("Done in ${System.currentTimeMillis() - startTime}ms. ${writes.size()} cells written.")
💡FP&A Enterprise Impact: A 45-minute rule blocks the overnight batch — planners arrive to stale numbers every morning. A 2-minute rule runs reliably in the nightly window. This is the difference between a system Finance trusts and one they work around.
⚔ Battle 4 Tasks
Identify all anti-patterns in the slow code
Implement pre-fetch data map pattern (reads before writes)
Refactor to batch setDataCellValues for all writes
Add timing logs (System.currentTimeMillis()) to measure before/after