Groovy Lab – Groovy Rules Ideas – part 3

April 3, 2026

Introduction

Welcome back to the Groovy Lab. In this post we are going one level deeper into the Groovy API, specifically into the metadata layer. If you have read the previous posts in the Groovy Lab series, you are already comfortable with orchestration patterns, substitution variable reads, calc script execution, and data interaction techniques.

 

This post focuses on a different class of Groovy capability: updating dimension member properties directly from a Groovy rule at runtime, without a metadata file load, without EDMCS, and without navigating the dimension editor. We are talking about changing things like aliases, aggregation operators, member formulas, data storage settings, and UDAs — programmatically, from within a running business rule.

Previous Groovy Lab posts for reference:

In this post, we will walk through four practical use cases with code examples:

  • Use Case 1 – Updating an Alias at Runtime
  • Use Case 2 – Changing Aggregation Operators Dynamically
  • Use Case 3 – Updating a Member Formula from a Form Cell
  • Use Case 4 – Batch Property Update from a CSV File

Each section explains the business scenario, walks through the API, provides a code example, and highlights the key things to watch for.

 

1. The Core API: getDimension, getMember, saveMember

Before jumping into use cases, it helps to understand the three core methods that underpin all Groovy-driven metadata updates. Everything in this post builds on these.

 

1.1 getDimension

getDimension() is called on the application object and returns a Dimension object for the named dimension. You can optionally pass a cube reference if you need the dimension scoped to a specific plan type.

// Get a dimension scoped to the application

Dimension acctDim = operation.application.getDimension(“Account”)

// Get a dimension scoped to a specific cube

Cube cube = operation.application.getCube(“OEP_FS”)

Dimension acctDim = operation.application.getDimension(“Account”, cube

)

1.2 getMember

getMember() is called on a Dimension object and returns a specific Member object by name. Member names are case-insensitive by default in the Groovy API.

 

// Retrieve a specific member

Member targetMember = acctDim.getMember(“4000”)

// Check it exists before acting on it

if (acctDim.hasMember(“4000”)) {

    Member targetMember = acctDim.getMember(“4000”)

    // … proceed with update

}

1.3 saveMember and the Property Map

saveMember(Map<String, Object> memberMap) is the workhorse method. It is called on the Dimension object and accepts a Map of property key-value pairs. When the Member key in the map matches an existing member name, saveMember updates that member’s properties. When the Member key does not match an existing member, a new member is created.

 

// Build the property mapMap<String, Object> props = [:]

props[“Member”] = “4000”                   // identifies which member to update

props[“Alias: Default”] = “Revenue”  // property to update

// Save (update) the member

Member updated = acctDim.saveMember(props)

println “Updated: ${updated.getName()} => alias: ${updated.getAlias(‘Default’)}”

 

⚠️  Important: Logical Model and Database Refresh

saveMember() operates on the Planning logical model, not directly on the Essbase outline.

After any saveMember() call that changes outline-level properties (aggregation, formula, data storage),

A database refresh is required to synchronize the physical OLAP model.

You can trigger this from the same Groovy rule using the REST API or EPM Automate. See Section 5.

 

1.4 Reading Existing Properties with toMap()

toMap() on a Member object returns a snapshot of all current property key-value pairs. This is invaluable for debugging — it tells you exactly what property keys and values are present, which is essential when building your update map.

// Inspect all properties for a member

Member m = acctDim.getMember(“4000”)

m.toMap().each { k, v ->

    println “${k} : ${v}”

}

A sample toMap() output for an Account dimension member looks like this:

Member                  : 4000

Alias: Default          : Revenue

Parent                  : Total Revenue

Data Storage            : store

Data Storage (OEP_FS)   : store

Aggregation (OEP_FS)    : +

Aggregation (OEP_REP)   : +

Formula                 : <none>

Formula (OEP_FS)        : <none>

Plan Type (OEP_FS)      : true

Plan Type (OEP_REP)     : true

Two Pass Calculation    : false

Data Type               : currency

UDA                     :

Solve Order (OEP_FS)    : 0

The property key names in this output are exactly what you use as keys in your saveMember() map. Note that plan-type-specific properties like aggregation and formula are scoped with the plan type name in parentheses, e.g., Aggregation (OEP_FS). You must use the correct plan-type-specific key when updating those properties.

💡  Tip: Run toMap() First

Before writing any saveMember() update rule, run a quick Groovy rule that just calls toMap() on

the target member and prints the output to the job log.

This gives you the exact key names and current values for your environment, no guessing.

 

2. Use Case 1 – Updating an Alias at Runtime

2.1 Business Scenario

A common real-world situation: your organization uses numeric account codes internally (e.g., 4000, 4100) but wants to show descriptive labels to business users in forms and reports. The descriptive labels, the aliases, need to be updated whenever the business changes account names, without going through a full dimension file load.

 

With Groovy, an admin can trigger an alias update rule from a form, reading a new alias value from a text cell on the form and writing it directly to the dimension member’s alias property.

 

2.2 Code Example

/*RTPS: {AccountMember} {NewAlias} */

// Get the dimension and target member from run-time prompts

String memberName = rtps.AccountMember.toString()

String newAlias   = rtps.NewAlias.toString()

Dimension acctDim = operation.application.getDimension(“Account”)

if (!acctDim.hasMember(memberName)) {

    throwVetoException(“Member ‘${memberName}’ not found in Account dimension.”)

}

Member m = acctDim.getMember(memberName)

String currentAlias = m.getAlias(“Default”) ?: “<none>”

// Build the property map — only specify what you want to change

Map<String, Object> props = [:]

props[“Member”]         = memberName

props[“Alias: Default”] = newAlias

// Apply the update

Member updated = acctDim.saveMember(props)

// Log the change for audit

println “[Alias Update] Member: ${memberName}”

println ”  Before : ${currentAlias}”

println ”  After  : ${updated.getAlias(‘Default’)}”

2.3 Key Points

  • Only include properties you intend to change in the map. Properties not present in the map are left unchanged.
  • The alias table name must match exactly. “Alias: Default” is the standard key for the Default alias table. If you have multiple alias tables, use “Alias: YourTableName”.
  • Use hasMember() before getMember() to prevent null pointer exceptions when the member does not exist.
  • Alias changes do not require a database refresh — they are metadata-only and take effect immediately in the Planning logical model.

3. Use Case 2 – Changing Aggregation Operators Dynamically

3.1 Business Scenario

One of the more powerful runtime metadata patterns is dynamically flipping the aggregation operator on a member. Consider a planning scenario where certain account members should be excluded from rollups during specific planning cycles, for example, excluding internal transfer accounts from consolidated totals during a forecast refresh, then re-enabling them before the final submission.

 

Rather than editing the outline manually each cycle or loading a dimension file, a Groovy rule can flip the consolidation operator on a targeted list of members between + (add) and ~ (ignore) programmatically.

3.2 Code Example

/*RTPS: {PlanType} {ExcludeFlag} */// ExcludeFlag RTP: ‘Yes’ to set ~ (ignore), ‘No’ to restore + (add)

String planType   = rtps.PlanType.toString()    // e.g. “OEP_FS”

String excludeFlag = rtps.ExcludeFlag.toString() // “Yes” or “No”

String newOperator = (excludeFlag == “Yes”) ? “~” : “+”

// Members to toggle — could also be read from a form or substitution variable

List<String> targetMembers = [“4500”, “4510”, “4520”, “Intercompany_Transfer”]

Dimension acctDim = operation.application.getDimension(“Account”)

int updatedCount = 0

targetMembers.each { memberName ->

    if (acctDim.hasMember(memberName)) {

        Member m = acctDim.getMember(memberName)

        // Read current operator for logging

        String currentOp = m.toMap()[“Aggregation (${planType})”] ?: “unknown”

        Map<String, Object> props = [:]

        props[“Member”] = memberName

        props[“Aggregation (${planType})”] = newOperator

        acctDim.saveMember(props)

        println “[Agg Update] ${memberName}: ${currentOp} => ${newOperator}”

        updatedCount++

    } else {

        println “[WARN] Member not found: ${memberName} — skipped”

    }

}

println “— ${updatedCount} member(s) updated. Database refresh required. —“

3.3 Valid Aggregation Operator Values

Operator Symbol Description
Add + Aggregates upward — standard for most P&L accounts
Subtract Subtracts from parent — useful for expense sign reversal
Multiply * Multiplies into parent — rare; used in rate/volume models
Divide / Divides into parent — rare
Percent % Treats member as a percentage contribution
Ignore ~ Excluded from parent rollup — data is stored but not aggregated
Never Share ^ Non-aggregating; used for attribute dimensions and SmartLists

 

3.4 Key Points

  • Aggregation operator changes affect the Essbase outline. A database refresh is required after this rule runs.
  • The property key is plan-type specific: “Aggregation (OEP_FS)”. You must update each plan type independently if the member exists in multiple plan types.
  • This pattern is particularly powerful when combined with a substitution variable that flags the current planning cycle state — the rule reads the variable, determines the operator, and applies it in one pass.
⚠️  Refresh Required After Aggregation Changes

After changing aggregation operators via saveMember(), a database refresh is required.

Without it, the Essbase outline does not reflect the change and calculations will produce incorrect results.

See Section 5 for how to trigger the refresh from within the same Groovy rule.

 

4. Use Case 3 – Updating a Member Formula Dynamically

4.1 Business Scenario

Member formulas in EPM Planning are traditionally static, you write them in the outline and they stay until someone edits them manually or loads an updated dimension file. But there are real-world scenarios where the formula logic needs to change dynamically based on business inputs.

 

A practical example: a rate-driven allocation model where the allocation percentage for a cost center is stored as a cell value in a Planning form. When a planner updates the rate in the form and saves, a Groovy rule reads that cell value and writes it directly into the member formula for the corresponding account member, making the Essbase formula formula reflect the latest approved rate without any manual outline editing.

 

4.2 Code Example

/*RTPS: {CostCenterMember} */// Reads the allocation rate from the current form grid and writes

// it into the member formula of the target account member.

String costCenter = rtps.CostCenterMember.toString()

String planType   = “OEP_FS”

// Step 1 — Read the allocation rate from the form grid

// The rate is stored in the ‘Alloc_Rate’ account member for the selected cost center

double allocRate = 0.0

operation.grid.dataCellIterator(“Alloc_Rate”).each { cell ->

    if (cell.getMemberName(“Entity”) == costCenter) {

        allocRate = cell.data ?: 0.0

    }

}

if (allocRate == 0.0) {

    throwVetoException(“Allocation rate for ${costCenter} is zero or missing. Update aborted.”)

}

// Step 2 — Build the new formula string

// Formula: multiply the shared cost pool by the rate read from the form

String newFormula = “\”Shared_Cost_Pool\” * ${allocRate}”

// Step 3 — Apply to the target member

String targetAccount = “Alloc_” + costCenter

Dimension acctDim = operation.application.getDimension(“Account”)

if (!acctDim.hasMember(targetAccount)) {

    throwVetoException(“Target account ‘${targetAccount}’ not found.”)

}

Map<String, Object> props = [:]

props[“Member”]                   = targetAccount

props[“Formula (${planType})”]    = newFormula

props[“Two Pass Calculation”]     = true   // recommended for formula members

Member updated = acctDim.saveMember(props)

println “[Formula Update] ${targetAccount}”

println ”  New formula  : ${newFormula}”

println ”  Rate applied : ${allocRate}”

println ”  Refresh required to activate in Essbase.”

4.3 Key Points

  • Formula keys are plan-type specific: “Formula (OEP_FS)”. A member in multiple plan types may need separate formula updates per plan type.
  • Member references inside a formula string must be wrapped in escaped double quotes: “\”MemberName\”” in Groovy produces “MemberName” in the formula, which is what Essbase expects.
  • Setting “Two Pass Calculation”: true is recommended for members that reference other calculated members in their formula — otherwise you may see stale values in the first calculation pass.
  • Formula changes require a database refresh. The formula is written to the logical model and must be pushed to Essbase before calculations reflect the new logic.
  • This pattern is most powerful when combined with an approval or lock workflow — only update the formula after a manager has approved the rate in the form, using Groovy validation logic to gate the update.

5. Use Case 4 – Batch Property Update from a CSV File

5.1 Business Scenario

The three previous use cases handle targeted, single-member or small-list updates. For larger-scale changes — updating aliases or formulas across dozens or hundreds of members at once — the most practical approach is a CSV-driven batch update pattern.

 

This pattern is particularly useful during chart-of-accounts redesigns, year-start alias refreshes, or post-migration cleanup. An admin uploads a CSV to the EPM inbox, then runs a single Groovy rule that reads the file and applies all updates in one pass.

 

5.2 CSV File Format

Upload the file to the EPM inbox via Application > Inbox/Outbox or via EPM Automate uploadFile. A minimal two-column format for alias updates:

MemberName,NewAlias

4100,Revenue – Product A

4200,Revenue – Product B

4300,Revenue – Services

5100,Direct Costs – Materials

5200,Direct Costs – Labour

For multi-property updates, extend the columns to include additional property values:

MemberName,NewAlias,Aggregation_OEP_FS,Formula_OEP_FS4100,Revenue Product A,+,

4500,Interco Transfer,~,

9000,Derived Margin,~,”4100″ – “5100”

5.3 Code Example

/*RTPS: {Dimension} {FileName} */

// Dimension RTP: name of the dimension to update (e.g. Account)

// FileName RTP:  CSV file name in EPM inbox (e.g. alias_updates.csv)

String dimName  = rtps.Dimension.toString()

String fileName = rtps.FileName.toString()

String planType = “OEP_FS”

Dimension dim = operation.application.getDimension(dimName)

int updated = 0

int skipped = 0

int errors  = 0

// Read the CSV from the EPM inbox

operation.application.readFile(fileName).eachLine { line, lineNum ->

    // Skip the header row

    if (lineNum == 1) return

    List<String> cols = line.split(“,”).collect { it.trim() }

    if (cols.size() < 2) return   // skip malformed rows

    String memberName = cols[0]

    String newAlias   = cols[1]

    if (!dim.hasMember(memberName)) {

        println “[SKIP] Line ${lineNum}: ‘${memberName}’ not found in ${dimName}”

        skipped++

        return

    }

    try {

        Map<String, Object> props = [:]

        props[“Member”]         = memberName

        props[“Alias: Default”] = newAlias

        // Optional: handle additional columns if present

        if (cols.size() >= 3 && cols[2]) {

            props[“Aggregation (${planType})”] = cols[2]

        }

        if (cols.size() >= 4 && cols[3]) {

            props[“Formula (${planType})”] = cols[3]

        }

        dim.saveMember(props)

        println “[OK] Line ${lineNum}: ‘${memberName}’ updated.”

        updated++

    } catch (Exception e) {

        println “[ERROR] Line ${lineNum}: ‘${memberName}’ — ${e.message}”

        errors++

    }

}

println “”

println “=== Batch Update Complete ===”

println ”  Updated : ${updated}”

println ”  Skipped : ${skipped} (member not found)”

println ”  Errors  : ${errors}”

if (errors > 0) {

    throwVetoException(“Batch completed with ${errors} error(s). Review job log.”)

}

5.4 Key Points

  • operation.application.readFile(fileName) reads a file from the EPM inbox. The file must be uploaded to inbox before the rule runs.
  • Wrapping each saveMember() call in a try-catch ensures one bad row does not abort the entire batch. Log the error and continue to the next row.
  • The rule above uses run-time prompts for the dimension name and file name, making it reusable across different dimensions without code changes.
  • For very large batches (hundreds of members), add a counter and log a progress line every 50 updates so you can track progress in the job console.
  • If you include aggregation or formula updates in the CSV, a database refresh is required after the rule completes.

 

6. Triggering a Database Refresh After Metadata Updates

Any saveMember() call that modifies outline-level properties — aggregation operators, member formulas, data storage, two-pass calculation — needs a database refresh to synchronize the Essbase physical model. You can trigger this from within the same Groovy rule using the REST API, keeping the full workflow in one place.

6.1 Via REST API

// Trigger a database refresh via REST API after metadata updatesConnection con = operation.application.getConnection(“LocalConnection”)

HttpResponse<String> refreshResp = con.post(

    “/rest/v3/applications/YourAppName/jobs”)

    .header(“Content-Type”, “application/json”)

    .body(“””

    {

        \”jobType\”: \”CubeRefresh\”,

        \”jobName\”: \”YourAppName\”

    }

    “””)

    .asString()

println “Cube refresh triggered: ${refreshResp.status} — ${refreshResp.body}”

6.2 Via EPM Automate

Alternatively, use the epmAutomate object available in Groovy (requires admin privileges):

// Trigger database refresh using EPM Automate from Groovydef automate = epmAutomate()

automate.execute(“refreshCube”, “YourAppName”)

println “Cube refresh via EPM Automate complete.”

 

💡  Tip: Separate Rules for Large-Scale Updates

For large batch updates affecting many members, consider separating the saveMember() rule

from the refresh rule and chaining them via a Groovy orchestration rule or a Data Integration Pipeline.

This gives you better visibility into each step and makes debugging easier if the refresh fails.

 

7. Quick Reference: Common saveMember() Property Keys

The table below lists the most commonly used property keys in saveMember() calls. Use toMap() on an actual member in your environment to confirm the exact key names, they can vary slightly depending on your application configuration and plan type names.

Property Example Key Example Value Notes
Member name Member 4000 Required — identifies the member to update or create
Default alias Alias: Default Revenue – Product Use alias table name after colon
Secondary alias Alias: French Revenu Produit One entry per alias table
Parent member Parent Total Revenue Moves the member to a different parent
Data Storage Data Storage store store, never share, dynamic calc, label only
Plan-type storage Data Storage (OEP_FS) store Plan-type scoped storage override
Aggregation Aggregation (OEP_FS) + +, -, *, /, %, ~, ^
Member formula Formula (OEP_FS) “Rev” * 1.1 Plan-type specific formula
Two-pass calc Two Pass Calculation true true or false
Data type Data Type currency currency, non-currency, percentage, unspecified
UDA UDA CAPEX Replaces all UDAs — include all if adding to existing
SmartList Smart List StatusList Links member to a SmartList
Plan type enabled Plan Type (OEP_FS) true true or false — enables/disables in plan type

 

8. Common Pitfalls and How to Avoid Them

Pitfall What Happens How to Avoid
Not checking hasMember() before getMember() Null pointer exception if member does not exist; rule crashes Always guard getMember() calls with hasMember(), especially in batch loops
Using the wrong property key name saveMember() silently ignores unknown keys; update appears to succeed but nothing changes Run toMap() first to confirm exact key names for your environment and plan types
Forgetting the plan-type suffix on scoped properties Aggregation or formula update does not apply to the intended plan type Use “Aggregation (OEP_FS)” not “Aggregation” for plan-type-specific properties
Overwriting UDAs by setting only one UDA value The UDA key in saveMember() replaces all existing UDAs on the member Read existing UDAs with getUdas() first, then build a complete list including new UDA
Skipping the database refresh after formula or aggregation changes Essbase outline is stale; calculations run on old formula/operator logic Always trigger a CubeRefresh job after outline-level property updates
Running metadata updates from a non-admin user context saveMember() for admin-scoped changes throws a SecurityException Ensure the rule is executed by a Service Administrator or use a proxy admin connection

 

Conclusion

Groovy-driven metadata updates open up a class of automation that was simply not possible in the old Hyperion on-premises world. The ability to read a form cell, inspect a member’s current properties, update an alias, flip an aggregation operator, or rewrite a member formula — all from within a running business rule — gives EPM developers a level of dynamic control that reduces manual admin work and makes applications more self-maintaining.

 

The four use cases in this post cover the most common patterns:

  • Targeted alias updates — clean, no refresh required
  • Aggregation operator toggling — powerful for cycle-driven exclusion logic, refresh required
  • Formula injection from form data — ties live planner input to outline logic, refresh required
  • CSV-driven batch updates — scalable, reusable, audit-friendly

 

The core principle across all of them is the same: build the property map with only the keys you want to change, guard every getMember() call with hasMember(), log every update to the job console, and always trigger a database refresh when outline-level properties have changed.

 

As always, run toMap() first on any member you plan to update, it will save you debugging time and make sure your key names are exactly right. See you in the next Groovy Lab post.

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *