Running Custom Logic at Save Time in OneStream Line Item Modeling
OneStream Line Item Modeling (LIM) is a driver-based engine that lets planners enter detailed data — workforce records, project lines, contract schedules — directly against the financial data model, without the spreadsheet sprawl. Most of the calculation logic that runs against LIM data lives where you’d expect it: in the Calculate & Analyze step, executed on demand or as part of a Workflow.
But there’s a category of logic that shouldn’t wait until Calculate & Analyze runs.
If a planner enters a row against city 111 - Mumbai, they probably want to see the parent Region (West India) and Country (India) populated immediately — not after they save, navigate away, kick off a calculation, and come back. The same applies to derived descriptions, default values pulled from member properties, validation messages, or anything else where the user expects instant feedback at the point of entry.
For that, you need to hook into the save event itself — the moment the user clicks the save icon on the Dynamic Grid. This post shows how.
The scenario
Take an Entity hierarchy with cities at the base, regions as their parents, and country at the top:
The goal: when a planner enters or modifies a row referencing a base-level city, automatically populate the Region and Country columns from the city’s parent members on the same save action — using each member’s Description, not its name.
Where to put the code
Save-time logic for a Dynamic Grid lives in the assembly that backs the grid component. In a typical Planning solution, that’s WsDynamicGridService.cs, found under:
Maintenance Units → CodeOnly (PLN) → CustomEventHandler_PLN → WsDynamicGridService.cs
Inside that file, OneStream provides a MyCustomBeforeEvent method stub. This is the hook that fires before the save commits — exactly the right place to inspect the edited rows and inject derived values.
The code
The pattern is straightforward: iterate over the edited rows, filter for inserts and updates, look up the city member, walk up to its parent (Region), and walk up again to the grandparent (Country). Write each parent’s Description back into the corresponding column on the row.
<pre class="wp-block-code alignwide"><code>private static void MyCustomBeforeEvent(SessionInfo si, DashboardDynamicGridArgs args)
{
foreach (XFEditedDataRow row in args.SaveDataArgs.EditedDataRows)
{
if (row.InsertUpdateOrDelete == DbInsUpdateDelType.Update
|| row.InsertUpdateOrDelete == DbInsUpdateDelType.Insert)
{
var dimPk = BRApi.Finance.Dim.GetDimPk(si, "DimName");
var city = Convert.ToString(row.ModifiedDataRow.Items["Colname of city"]);
var cityMemberId = Convert.ToInt32(
BRApi.Finance.Members.GetMemberId(si, dimPk.DimTypeId, city));
var regions = BRApi.Finance.Members.GetParents(
si, dimPk, cityMemberId, false, null);
foreach (var region in regions)
{
row.ModifiedDataRow.Items["Colname of Region"] = region.Description;
var regionId = Convert.ToInt32(
BRApi.Finance.Members.GetMemberId(si, dimPk.DimTypeId, region.Name));
var countries = BRApi.Finance.Members.GetParents(
si, dimPk, regionId, false, null);
foreach (var country in countries)
{
row.ModifiedDataRow.Items["Colname of Country"] = country.Description;
}
}
}
}
}
</pre>
Replace "DimName" with the actual Entity dimension name, and "Colname of city", "Colname of Region", and "Colname of Country" with the column names defined in your Register Definition.
A few things to be aware of
- Register Definition is the source of truth for column names. The
row.ModifiedDataRow.Items[…]lookups use the column names exactly as defined in the Register — rename a column there and this code breaks silently. GetParentsreturns a collection. The nestedforeachloops handle the enumerable, but if your Entity allows shared members or alternate hierarchies, the last parent written wins.- This runs on every save, on every edited row. Cache the dimension PK outside the row loop and short-circuit if the city column hasn’t actually changed.
- Description vs. Name. The code writes
member.Description. If you need the member name, switch tomember.Name. - Errors here block the save. Anything thrown inside
MyCustomBeforeEventsurfaces as a save failure to the user — wrap the lookups in defensive checks.
When to use this vs. Calculate & Analyze
Use save-time logic when…
- The user expects to see the result the moment they save the row
- The derivation is a simple lookup or transformation, not an expensive calculation
- You want to validate or enrich the row before it commits to the database
Use Calculate & Analyze when…
- The logic depends on data across multiple rows or other cube intersections
- The calculation is expensive enough that running it per-save would hurt UX
- The result is consumed by reports or downstream cubes rather than by the data entry user
The two patterns are complementary, not competing. A typical LIM solution uses save-time logic for immediate enrichment of the entered row, and Calculate & Analyze for the heavy lifting that produces aggregated, model-wide outputs.
Related: see our walkthrough of building a Snowflake REST API connector for OneStream for another OneStream extensibility deep-dive.
Need Help with OneStream?
James & Monroe is a specialist OneStream implementation partner. Whether you’re building Line Item Modeling solutions, customising Dynamic Grids, or designing a full OneStream deployment, our team can help you get the architecture right the first time.