The Planning Quest Groovy · Chapter I
0 / 900 XP
📖 I–III ⚔ IV–V 👑 VI–IX 📘 Gr.I ⚡ Gr.II ⚙ Kernel 🏆 Engage 👥 WFP 📝 Quiz 🃏 Cards 🔬 Labs 🎯 Prep 🌐 REST ⚙ CLI 💻 Lab 🆕 New 🔍 Search
Groovy Mastery Series
🏠
Quest Map
Chapter I — Groovy Basics
🧩
Chapter I
Groovy for EPM
Language Fundamentals Types, Casting & Elvis Collections & Closures Null Safety Patterns Error Handling Logging & Debugging Groovy Script Skeleton
Chapter II
⚡ EPM Cloud API →
Practice
📝 Quiz 🃏 Flashcards 🔬 Enterprise Labs ⚔ Battle Saga
Full Quest Navigation
The Quest
🏠
Quest Map
Core Lessons
📖 Parts I–III (Fundamentals) ⚔ Parts IV–V (Forms + Groovy) 👑 Parts VI–IX (Workflow + Boss)
Groovy Mastery
📘 Groovy Chapter I (Basics) ⚡ Groovy Chapter II (EPM API) 💻 Groovy Lab (Interactive)
Deep Dives
⚙ Essbase Kernel (NODE s) 👥 Workforce Planning 🌐 REST API Cookbook ⚙ EPM Automate CLI Ref
Practice & Labs
📝 Practice Quiz 🃏 Flashcards 🔬 Enterprise Labs 🎯 Interview Prep
Tools & Reference
🏆 The Engagement 🆕 What's New in EPM 🔍 Search 📊 Capability Framework 🎮 Simulators
📘 Chapter I · Lesson 1 · 100 XP

🧩 Groovy Language Fundamentals

Chapter I · Groovy Basics for EPM · Lesson 1 of 7

The Sorcerer's Foundation

Before you can wield Groovy in Oracle EPM Cloud, you must understand the language itself. Groovy runs on the JVM — it is compiled Java under the hood — but it brings dynamic typing, optional syntax, and powerful collection methods that make EPM scripting dramatically more concise than Java. A senior EPM Groovy practitioner writes code that is readable, safe, and provably correct. That begins here.

Where Groovy Runs in EPM Cloud

Groovy scripts in EPM Cloud execute on Oracle's server-side JVM inside the Calculation Manager runtime. They are not run in the browser. This has critical implications:

CapabilityAvailable in EPM Groovy?Why
Standard Groovy language✅ Yes (Groovy 2.x)Server JVM execution
Java standard library✅ Most classesSame JVM
File I/O (read/write disk)❌ NoSandboxed environment
Browser APIs (localStorage, DOM)❌ NoServer-side only
Groovy 3.x features⚠ Not guaranteedEPM may lag Groovy version
println output✅ Job Console onlyNo file or console I/O
REST API calls✅ Via EPM client or HttpURLConnectionNetwork access allowed

The Three Execution Hooks

Every Groovy form script attaches to one of three lifecycle points:

HookWhen it FiresGrid Mutable?Primary Use
beforeSaveAfter user clicks Save, before Essbase commit✅ YesValidation, transformation, cancellation
afterSaveAfter Essbase write completes❌ Read-onlyAudit trail writes, notifications, triggers
onLoadWhen user opens the form✅ YesDefault values, derived pre-population
🚨Critical: operation.cancelled = true only works in beforeSave. Setting it in afterSave has no effect — the data is already committed. Always know which hook your script is attached to.

Variables: Dynamic vs Explicit Typing

Groovy variable declarations
// Dynamic typing — Groovy infers type at runtime def name = "Vision_US" // String def revenue = 2_800_000.0 // BigDecimal (note: underscores for readability) def active = true // Boolean def entities = [] // ArrayList // Explicit typing — preferred for method signatures and RTP casts String entity = "Vision_EU" Integer year = 2025 BigDecimal rate = 1.08G // G suffix = BigDecimal literal // GString interpolation — double quotes enable ${} substitution def msg = "Entity: ${entity}, Year: FY${year}" println msg // → "Entity: Vision_EU, Year: FY2025" // Multi-line strings (triple-quote) def sql = """ SELECT entity, account, value FROM planning_data WHERE year = '${year}' """
🎯 Interview Q1 [Easy] — Architecture
Where does Groovy code run in Oracle EPM Cloud, and what does that mean for what you can and cannot do?
Key signal: server-side JVM, sandboxed, no file I/O, Groovy 2.x constraint. Strong candidates also mention the difference between the three hooks.
🧩 Chapter I · Lesson 1 Tasks
Explain server-side execution and what it prevents (file I/O, browser APIs)
Distinguish the three hooks: beforeSave, afterSave, onLoad — mutability rules
Write a GString interpolating entity + year into a logging message
Use the G suffix for a BigDecimal literal and explain why not Double
🔭
The Oracle
Groovy Senior Trainer
📘 Chapter I · Lesson 2 · 125 XP

🔢 Types, Casting & Elvis Operator

Chapter I · Groovy Basics for EPM · Lesson 2 of 7

The Type Inquisitor

The single most common production bug in EPM Groovy is a type coercion error — usually discovered during a live budget cycle. operation.parameter() always returns a String. Always. Even if you configured an Integer RTP. The '2024' + 1 = '20241' disaster has struck more than one Fortune 500 planning system. Master type safety before touching real data.

The Critical RTP Type Rule

🚨The #1 Production Bug: operation.parameter() ALWAYS returns java.lang.String, regardless of the RTP type set in Calculation Manager. String arithmetic silently concatenates instead of adding.
RTP type casting — always cast at declaration
// ❌ WRONG — silent bug in production def year = operation.parameter("Year") // String "2025" def next = year + 1 // "20251" ← string concat, NOT 2026! // ✅ CORRECT — cast immediately at declaration def year = operation.parameter("Year") as Integer def rate = operation.parameter("Rate") as BigDecimal // NOT Double — precision def flag = operation.parameter("IsActive") == "true" // String → Boolean def label = operation.parameter("Label") // String — no cast needed // Date RTP — format is EPM-locale-dependent def dateStr = operation.parameter("AsOfDate") def asOf = Date.parse("MM/dd/yyyy", dateStr) // match your EPM locale! // Optional RTP with fallback default def scenario = operation.parameter("Scenario") ?: "Actual" // Validate scope after casting if (!(scenario in ["Actual", "Budget", "Forecast"])) throw new Exception("Invalid Scenario: ${scenario}")

BigDecimal — Why Not Double?

EPM stores financial values as 64-bit IEEE doubles internally, but intermediate calculations should use BigDecimal to prevent floating-point rounding errors in percentage and ratio calculations.

❌ Double (risky)
double a = 0.1 double b = 0.2 println a + b // 0.30000000000000004 // Gross margin = 33.333333333333336%
✅ BigDecimal (safe)
def rev = (cell.data ?: 0) as BigDecimal def cost = (cogs?.data ?: 0) as BigDecimal def gm = rev == 0 ? 0 : ((rev-cost)/rev*100).setScale(2)

The Elvis Operator ?:

The Elvis operator returns the left-hand side if it is non-null and non-false; otherwise returns the right-hand default. The most-used operator in EPM Groovy.

Elvis operator patterns
// cell.data ?: 0 — the most common EPM Groovy line def rev = cell.data ?: 0 // 0 if null or missing def name = mbr?.memberName ?: "Unknown" // chained safe navigation def scen = param ?: "Budget" // RTP default // Combine with BigDecimal cast def safeRev = (cell?.data ?: 0) as BigDecimal // Warning: Elvis treats empty string "" as false too def s = "" ?: "default" // returns "default" — may surprise you // Use explicit null check if empty string is valid: def s2 = (param != null) ? param : "default"
🎯 Interview Q9 [Easy] — RTPs
What type does operation.parameter() always return, and why does this matter?
Key signal: always String regardless of CM type. Must give the '2024' + 1 = '20241' concrete example. Must state "cast at declaration" as the rule.
🔢 Chapter I · Lesson 2 Tasks
Write RTP declarations for Year(Integer), Rate(BigDecimal), IsActive(Boolean), Scenario(String with default)
Demonstrate the '2024'+1 bug and explain why it happens
Use Elvis ?: for a safe cell read returning 0 when null
Write a scope validation check after casting a Scenario RTP
🔭
The Oracle
Groovy Type System Expert
📘 Chapter I · Lesson 3 · 150 XP

📦 Collections & Closures

Chapter I · Groovy Basics for EPM · Lesson 3 of 7

The Collector's Arsenal

Every EPM Groovy rule manipulates collections — lists of entities, maps of revenue data, sets of validation errors. Groovy's GDK (Groovy Development Kit) collection methods are the reason experienced EPM developers write a fraction of the code a Java developer would. But closures carry one critical trap that has broken more than one live budget submission.

Lists — Creation, Traversal, Transformation

List operations in EPM Groovy
// Creation def months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] def writes = [] // empty list — builds the batch write buffer // Add writes << ["Vision_US", "Revenue", "Jan", "FY2025", "Budget", "Working", 1_200_000.0] // .each — iterate (no return value) months.each { m -> println m } // .collect — transform list into new list def upperMonths = months.collect { it.toUpperCase() } // .findAll — filter def q4 = months.findAll { it in ["Oct","Nov","Dec"] } // .find — first match (returns single item or null) def entity = operation.grid.pov.find { it.dimensionName == "Entity" } // .sum — aggregate def totalWrite = writes.sum { it[6] ?: 0 } // .inject — fold/reduce def totalRev = entities.inject(0.0G) { acc, e -> acc + (getCell(e.getName(), "Revenue", ...) ?: 0) }

Maps — The Revenue Pre-Fetch Pattern

Map patterns for EPM data pre-fetching
// Simple map def fxRates = [USD: 1.0G, EUR: 1.08G, GBP: 1.26G, JPY: 0.0067G] println fxRates["EUR"] // 1.08 // Build revenue map from cube reads — pre-fetch BEFORE loops def revenueMap = [:] // empty map entities.each { e -> revenueMap[e.getName()] = months.collectEntries { m -> [m, (getCell(e.getName(), "Total_Revenue", m, "FY2025", "Budget", "Working") ?: 0G)] } } // Now compute in pure Groovy — ZERO Essbase calls below this line entities.each { e -> months.each { m -> def rev = revenueMap[e.getName()][m] // ... allocations, etc. } }

⚠ The Closure Return Trap

🚨The Closure Return Bug: return inside .each{} only exits the current iteration (like Java's continue). It does NOT exit the script. This is the most commonly misunderstood Groovy behaviour in EPM production scripts.
❌ Wrong — doesn't cancel
entities.each { e -> if (e == "INVALID") return // only skips THIS one! process(e) // rest still run }
✅ Correct — pre-filter
entities .findAll { it != "INVALID" } .each { e -> process(e) } // or use boolean flag + check after .each
🎯 Interview Q11 [Medium] — RTPs
What is the difference between return inside an .each{} closure versus return at the script level?
Must explain closure scope: return only exits the closure iteration. Dangerous in EPM when combined with validation/cancellation logic. Strong candidates offer: pre-filter with .findAll{} OR boolean flag pattern.
📦 Chapter I · Lesson 3 Tasks
Build a writes=[] batch list with << operator for 12 months
Use collectEntries to build a revenue Map from entity → month → value
Demonstrate the closure return bug and show two correct fixes
Use .find{} to extract Entity member from operation.grid.pov
🔭
The Oracle
Groovy Collections Expert
📘 Chapter I · Lesson 4 · 150 XP

🛡 Null Safety — The Guard Chain

Chapter I · Groovy Basics for EPM · Lesson 4 of 7

The Null Slayer

In Essbase, #Missing and null are different from zero. Arithmetic on a null throws NullPointerException. Arithmetic on a zero gives wrong business results. A script that runs perfectly in test — where all cells have data — detonates in production when a single entity has no prior-year actuals. The Guard Chain is your protection.

The Guard Chain — Order Matters

Order is not optional: null → readOnly → noData → edited. Check edited before null crashes. Check noData before treating null data as zero gives wrong arithmetic.
Complete null safety guard chain
// The canonical EPM Groovy guard pattern — memorise this operation.grid.dataCellIterator().each { cell -> if (cell == null) { println "SKIP: null cell"; return } if (cell.readOnly) { return } // locked intersection if (cell.noData) { return } // #Missing — not zero if (!cell.edited) { return } // not changed this save // Only reach here for genuinely user-edited, writable, non-missing cells processCell(cell) }

Safe Navigation Operator ?.

Safe navigation — prevents NullPointerException chains
// Without safe nav — crashes if cell or memberNames is null def name = cell.memberNames.first() // NullPointerException risk // With safe nav — returns null at first null in chain def name = cell?.memberNames?.first() // null if cell or memberNames is null def val = grid?.getCellAt(0,0)?.data // chained: 3 potential nulls handled // Combine ?. with ?: for a complete safe expression def rev = (cell?.data ?: 0G) as BigDecimal // null-safe + typed

assert — Business Preconditions

assert for business rule preconditions
// assert throws AssertionError (not Exception) — surfaces to EPM save dialog assert cell.data instanceof Number : "Expected numeric, got: ${cell.data?.class?.simpleName}" assert year in (2020..2030) : "Year ${year} is outside allowed range 2020–2030" assert totalRev > 0 : "Total revenue is zero — allocation aborted" // requireNonNull — programming errors (not business conditions) Objects.requireNonNull(cell, "Cell at [0,0] is null — check form configuration")

Vision Corporation Scenario: Null-Safe Revenue Read

🏗 Progressive Scenario Build — Step 1 of 5
Vision US Revenue Spread — Foundation Layer

We are building Vision Corporation's seasonal revenue spread rule step-by-step. This lesson: lay the null-safe read foundation.

// STEP 1: Null-safe foundation — reads annual total safely // Vision Corporation — revenue spread Groovy rule (building incrementally) def scenario = operation.parameter("Scenario") ?: "Budget" def year = operation.parameter("Year") ?: "FY2025" def entity = operation.grid.pov.find { it.dimensionName == "Entity" }?.memberName // Guard: entity must be in POV Objects.requireNonNull(entity, "No Entity in form POV — attach this rule correctly") // Null-safe annual revenue read def annualRev = (getCell(entity, "Total_Revenue", "Full_Year", year, scenario, "Working") ?: 0G) as BigDecimal if (annualRev == 0) { println "Guard: ${entity} has zero annual revenue — no spread applied"; return } println "Vision Spread Step 1 — Entity: ${entity}, Annual: ${annualRev}" // → Next lesson: add seasonality calculation on top of this foundation
🎯 Interview Q6 [Easy] — Cell API
Why should you always check cell.edited before processing a cell in a beforeSave script?
Two answers needed: (1) performance — ALL cells in a form are included in every save, (2) correctness — side effects fire even when nothing changed, producing phantom audit records. Must mention combining with readOnly and noData guards.
🛡 Chapter I · Lesson 4 Tasks
Write the full 4-step guard chain in the correct order
Use safe navigation ?. to chain cell → memberNames → first() without crashing
Write 2 assert statements for Vision: year range and positive revenue
Implement Step 1 of Vision US Revenue Spread with null-safe reads
🔭
The Oracle
Null Safety & Defensive Coding Expert
📘 Chapter I · Lesson 5 · 150 XP

⚠ Error Handling — Production Patterns

Chapter I · Groovy Basics for EPM · Lesson 5 of 7

The Error Architect

A Groovy script that crashes gracefully is worth more than one that crashes cryptically. The difference between "Revenue cannot be negative — you entered -5000 in Entity Vision_EU, Account Total_Revenue, Period Jan" and a Java stack trace dumped at a planner is the difference between user trust and an angry ticket to IT at 9pm during budget close.

AssertionError vs Exception — When to Use Each

MechanismWhen to UseUser SeesEPM Behaviour
assert condition : "msg"Business rule precondition (invalid data/params)The assert messageSave cancelled, message shown
throw new Exception("msg")Unexpected system failure, catch blocksMessage if caught + cancelledRule fails with error
operation.cancelled = trueControlled user-facing cancellationcancellationMessageSave blocked cleanly
Bare returnSilent skip — no error neededNothing (save continues)Script exits cleanly

The Production Error Wrapper — 4 Layers

Q24 Expert — Production error wrapper (Vision Corporation)
def log = { String lvl, String msg -> println "[${new Date().format('HH:mm:ss')}][${lvl.padRight(5)}] ${msg}" } def outcome = "UNKNOWN" try { // ── Layer 1: Input validation (AssertionError on failure) ── def year = operation.parameter("Year") as Integer assert year in 2020..2030 : "Year ${year} out of range 2020–2030" def scenario = operation.parameter("Scenario") ?: "Budget" assert scenario in ["Actual","Budget","Forecast"] : "Bad scenario: ${scenario}" // ── Layer 2: Business logic (with DataGridBuilder — see Ch II) ── log("INFO", "Processing ${scenario}/FY${year} for Vision Corporation") // ... DataGridBuilder.build(), eachRow mutations ... // ... grid.save() ← ALWAYS the last line in the try block ... outcome = "SUCCESS" } catch (AssertionError ae) { // ── Layer 3a: Business violation — user-facing cancel ── outcome = "VALIDATION_CANCEL" operation.cancelled = true operation.cancellationMessage = ae.message // shown in EPM dialog log("WARN", "Validation: ${ae.message}") } catch (Exception e) { // ── Layer 3b: System failure — log + rethrow ── outcome = "SYSTEM_ERROR" log("ERROR", e.message) e.stackTrace.take(5).each { log("ERROR", " at ${it}") } throw e // rethrow — never swallow system errors } finally { // ── Layer 4: Guaranteed outcome logging ── log("INFO", "Script outcome: ${outcome}") }
⚠ Partial Write Danger: A script writing 500 entity intersections crashes at iteration 250. The first 249 are committed; 251 are not. This is a data integrity disaster. Solution: use grid.save() as the LAST line in the try block — all mutations are held in memory until that single atomic commit. See Chapter II.
🎯 Interview Q22 [Medium] + Q24 [Expert] — Error Handling
What is the difference between AssertionError and Exception in EPM scripts? / Design a production-grade error handling wrapper.
Q22: assert throws AssertionError (not Exception) — semantically for business rule violations. Q24: 4 layers — input validation, business logic, error classification (separate catch blocks), guaranteed finally logging. grid.save() as last line in try block is the atomicity guarantee.
⚠ Chapter I · Lesson 5 Tasks
Write the 4-layer error wrapper for Vision's budget validation rule
Use separate catch blocks for AssertionError vs Exception with different responses
Implement cancellationMessage with entity name + actual value + guidance
Explain why system exceptions must be rethrown, not swallowed
🔭
The Oracle
Error Handling Expert
📘 Chapter I · Lesson 6 · 125 XP

🔍 Logging & Debugging in EPM Cloud

Chapter I · Groovy Basics for EPM · Lesson 6 of 7

The Diagnostic Scribe

println is your only output channel. There is no real-time streaming, no debugger for form-attached scripts, no file I/O. A production script without structured logging is impossible to diagnose when it fails at 2am during close week. Build the log helper into every script from the first line.

Where Logs Appear

Log LocationWhat It ShowsWhen to Use
Job ConsoleAll println output, in sequencePrimary. Jobs → Business Rule → Show Details
Calc Manager DebuggerStep-through with variable inspectionManually launched rules only — not form saves
EPM System DiagnosticsServer-level errors (timeout, OOM, disconnect)Infrastructure issues not visible in Job Console
OCI Log StreamsApplication-level Oracle Cloud logsEscalation to Oracle Support

Structured Log Helper

Production logging pattern — copy into every rule
import java.text.SimpleDateFormat // Structured log helper — timestamps + levels def log = { String level, String msg -> def ts = new SimpleDateFormat("HH:mm:ss").format(new Date()) println "[${ts}][${level.padRight(5)}] ${msg}" } log("INFO", "=== Vision Revenue Spread START | user=${operation.getUserName()} ===") log("INFO", "Entity: ${entity}, Scenario: ${scenario}, Year: ${year}") // Timing profiler — three-phase approach def t = System.currentTimeMillis() def lap = { String label -> def now = System.currentTimeMillis() log("PERF", "${label}: ${now - t}ms") t = now } // Use in rule: // ... build phase ... → lap("Retrieval") // ... mutation phase ... → lap("Computation") // ... grid.save() ... → lap("Commit") // Total reveals bottleneck: retrieval vs compute vs write // Per-entity log inside loop (WARN: expensive in large loops) entities.each { e -> log("DEBUG", " entity=${e.getName()} rev=${revenueMap[e.getName()]}") } // Summary instead of per-cell — production best practice log("INFO", "=== COMPLETE: ${writes.size()} cells written | ${totalRev} total rev spread ===")
⚠ Don't Log in Tight Loops: Every println is a string allocation + I/O flush. Logging per cell in a loop over 10,000 intersections bloats the job log, may trigger truncation, and measurably slows execution. Log counts and summaries — not per-cell detail in production.
🎯 Interview Q20 [Expert] — Performance
Describe how you would profile a slow Groovy script and systematically identify whether the bottleneck is retrieval, computation, or commit.
Three-phase approach with System.currentTimeMillis() around each phase. Build time → if high: tighten POV spec. Computation → if high: getMember() in loop, check getCellByIntersection inside eachRow. Commit → if high: block fragmentation, concurrent writes. Must say "measure before AND after a fix".
🔍 Chapter I · Lesson 6 Tasks
Build the log() helper with timestamp and padded level labels
Implement the 3-phase profiler with lap() function
Add script start/end summary logs with entity count and user identity
Diagnose: if Retrieval phase = 35s, Commit = 1s — what is the bottleneck and fix?
🔭
The Oracle
Debugging & Performance Expert
📘 Chapter I · Lesson 7 · 100 XP

📋 The Universal Groovy Script Skeleton

Chapter I · Groovy Basics for EPM · Lesson 7 of 7 · Chapter Complete!

The Master Template

Every expert EPM Groovy developer starts from a battle-tested skeleton. Not from a blank file. Every production rule at Vision Corporation is born from this template — RTPs typed correctly, grid built once, mutations in memory, single atomic commit, four-layer error wrapper, structured logging. This is the crystallisation of everything in Chapter I.

The "10 Lines" Universal Skeleton

Universal EPM Groovy skeleton — paste into every new rule
// ════════════════════════════════════════════════════════════════ // THE PLANNING QUEST — Universal Groovy Script Skeleton // Copy this. Adapt the content. Never start from blank. // ════════════════════════════════════════════════════════════════ import java.text.SimpleDateFormat // ── Structured logger ────────────────────────────────────────── def log = { String l, String m -> println "[${new SimpleDateFormat('HH:mm:ss').format(new Date())}][${l.padRight(5)}] ${m}" } def t = System.currentTimeMillis() def lap = { String lbl -> def n=System.currentTimeMillis(); log("PERF","${lbl}: ${n-t}ms"); t=n } def outcome = "UNKNOWN" try { // ── 1. RTPs — cast at declaration, validate immediately ──────── def scenario = operation.parameter("Scenario") ?: "Budget" def year = operation.parameter("Year") ?: "FY2025" assert scenario in ["Actual","Budget","Forecast"] : "Invalid scenario: ${scenario}" log("INFO", "START | user=${operation.getUserName()} | ${scenario} ${year}") // ── 2. Resolve entities from POV or RTP ─────────────────────── def entityParam = operation.parameter("Entity") ?: "" def entities = entityParam.tokenize(",")*.trim().findAll{it}.unique() if (entities.isEmpty()) entities = [ operation.grid.pov.find{it.dimensionName=="Entity"}?.memberName ?: "Total_Vision" ] // ── 3. Build DataGrid — ONE call, before any loop (see Ch II) ─ // def grid = DataGridBuilder.create(operation) ← completed in Ch II // ── 4. Mutation phase ───────────────────────────────────────── // grid.eachRow { row -> ... } ← mutations in memory // ── 5. Single atomic commit ─────────────────────────────────── // grid.save() ← ALWAYS last in try lap("Total"); outcome = "SUCCESS" } catch (AssertionError ae) { outcome = "VALIDATION_CANCEL" operation.cancelled = true operation.cancellationMessage = ae.message log("WARN", "Validation: ${ae.message}") } catch (Exception e) { outcome = "SYSTEM_ERROR" log("ERROR", e.message) e.stackTrace.take(5).each { log("ERROR", " ${it}") } throw e } finally { log("INFO", "=== OUTCOME: ${outcome} ===") }

Chapter I — What You've Mastered

TopicKey PrincipleInterview Coverage
Execution ModelServer-side JVM, 3 hooks, hook mutability rulesQ1, Q2, Q3
Types & Castingoperation.parameter() always String — cast at declarationQ9
CollectionsPre-fetch into Map, batch writes[], closure return ≠ script returnQ10, Q11
Null SafetyGuard chain order: null→readOnly→noData→editedQ6, Q7
Error Handling4-layer wrapper, AssertionError vs Exception, rethrow system errorsQ22, Q23, Q24
LoggingStructured logger, 3-phase profiler, summaries not per-cellQ20, Q21
💡Chapter II awaits: DataGridBuilder API, Cell Properties, RTPs multi-select, Cross-Cube REST, Performance patterns, and the complete Vision Corporation Revenue Spread built step-by-step to production-grade.
📋 Chapter I · Lesson 7 · Completion
Memorise the skeleton structure — can you write it from memory?
Identify where DataGridBuilder.build() and grid.save() slots in
Adapt the skeleton for a Vision Corporation headcount validation rule
Complete all 7 lessons in Chapter I — 900 XP earned
🔭
The Oracle
Chapter I Complete — Ready for Chapter II