OneStream

Forecasts and Custom Planning Solutions

A budget is a starting point — not the end of the planning cycle. Once the annual budget is approved, the FP&A team shifts to maintaining a rolling forecast that blends actual results with forward-looking projections. Beyond the core P&L, many organizations also need specialty planning modules for headcount, capital projects, or other detailed processes. This guide covers rolling forecasts, variance analysis, and the patterns for building custom CRUD-based planning solutions in OneStream.

From Budget to Forecast

A budget is a fixed plan — set once at the start of the year and locked. A forecast is a living document that updates as the year progresses. The key differences:
  • Time horizon — Budgets cover a fixed period (calendar year). Forecasts update monthly or quarterly — either covering the remaining fiscal year (YTG) or a fixed rolling window (12 or 18 months).
  • Update frequency — Budgets are built once (with possible revisions). Forecasts are updated every cycle as new actuals become available.
  • Scenario architecture — The team works in a single Forecast_Working scenario each cycle. Actuals are seeded for closed months, and projections are entered or recalculated for open months. When the cycle is approved, Forecast_Working is copied to a monthly snapshotForecast_M1 through Forecast_M12 — preserving the complete picture of each cycle for trend and accuracy analysis.
In OneStream, Forecast_Working and each Forecast_MX snapshot are configured as separate Scenario Types. Forecast_Working is the editable workspace; the snapshots use NoInputPeriods to lock the actual-seeded months from user editing (e.g., Forecast_M3 has NoInputPeriods = 2, preventing changes to January and February). Setting Workflow Tracking Frequency to "All Time Periods" on each snapshot allows the same scenario to be reused across fiscal years.
For ABC Manufacturing, Forecast_Working as of the June cycle contains:
  • January – May: Actual data (seeded from the Actual scenario)
  • June – December: Forecast data (projected by the FP&A team)
When the June cycle is approved, Forecast_Working is saved to Forecast_M6. This means ABC can later compare Forecast_M3 (what the team projected in March) against Forecast_M6 (what they projected in June) to measure forecast accuracy and track trend shifts.

The Forecast Cycle

Each forecast cycle is a three-phase process: seed, calculate, and publish. A Finance Business Rule handles the first two phases in Forecast_Working, and a separate step copies the result to the monthly snapshot.

Phase 1: Seed Forecast_Working

Copy actuals for closed months into Forecast_Working. The rule uses lastClosedPeriod to determine the cutoff — the same period-number pattern from the budget seeding guide, but writing to S#Forecast_Working.
ℹ️Info
Hybrid Scenarios: Automating the Seed Phase
OneStream provides a built-in Hybrid Scenario setting on scenario dimension members that can automate the seeding step. Instead of writing Business Rule code to copy actuals, you configure the DataBindingType property on the Forecast_Working scenario member:
  • Share Data from Source Scenario — Forecast_Working dynamically references Actual data for the defined periods. The data is read-only and not physically stored in the target — ideal for reporting scenarios that always need the latest actuals.
  • Copy Input Data from Source Scenario — Copies base-level Actual data into Forecast_Working when the scenario is calculated. After the copy, the data is editable — useful when the team needs to adjust seeded values before projecting open months.
  • Copy Input Data from Business Rule — Points to a Finance Business Rule that controls exactly which data is copied and when. This is the most flexible option — the rule can use lastClosedPeriod to copy only closed months and leave open months untouched.
By default, the hybrid data copy executes whenever the scenario is calculated (from a Cube View, calculation definition, or Calculate Data Management Step). For tighter control, set ExecuteCopyAfterCalcScenario to False on the Data Management Step so the copy only runs as part of a managed sequence. Hybrid copies can also be chained — the results in one target scenario can serve as the source for another.

Phase 2: Calculate Open Months

Run the same driver-based calculations as the budget for the open months in Forecast_Working. The calculation logic for open months mirrors the budget calculation from the previous guide — many implementations share calculation logic between Budget and Forecast scenarios by extracting shared helper methods or parameterizing the same rule to work for both.
1If api.FunctionType = FinanceFunctionType.Calculate Then
2  Dim scenario As String = api.Pov.Scenario.MemberName
3
4  If scenario.XFEqualsIgnoreCase("Forecast_Working") Then
5      ' Determine the current period number (1 = Jan, 2 = Feb, etc.)
6      Dim periodNum As Integer = api.Time.GetPeriodNumFromId(api.Pov.TimeDimPk.MemberId)
7
8      ' Last closed period — typically stored in an application setting
9      ' For this example, assume March (period 3) is the last closed month
10      Dim lastClosedPeriod As Integer = 3
11
12      If periodNum <= lastClosedPeriod Then
13          ' Closed month: seed with actuals
14          api.Data.Calculate("S#Forecast_Working = S#Actual")
15      Else
16          ' Open month: run forecast calculations (same drivers as budget)
17          api.Data.Calculate("A#SalaryExpense = RemoveZeros(A#FTE * A#AvgSalary)")
18          api.Data.Calculate("A#ProductRevenue = RemoveZeros(A#SalesVolume * A#PricePerUnit)")
19          api.Data.Calculate("A#COGSAmount = RemoveZeros(A#ProductRevenue * A#COGSPercent)")
20          api.Data.Calculate("A#GrossProfit = A#Revenue - A#COGSAmount")
21          api.Data.Calculate("A#OpEx = A#SalaryExpense + A#BenefitsExpense + A#OtherOpEx")
22          api.Data.Calculate("A#NetIncome = A#GrossProfit - A#OpEx")
23      End If
24  End If
25End If
💡Tip
In production, the lastClosedPeriod value should not be hard-coded. Common approaches include reading it from a substitution variable, a dedicated cube cell (e.g., A#LastClosedPeriod:E#Settings), or an application database table. This lets the finance team advance the cutoff each month without touching the Business Rule. Some implementations also use api.Scenario.Text(2) to store a reference to the prior forecast scenario — useful when seeding open months from the previous cycle's projections instead of recalculating from scratch.

Phase 3: Publish Snapshot

Once the forecast cycle is approved, copy Forecast_Working to the corresponding monthly snapshot. For a March cycle, this publishes to Forecast_M3:
1' Publish Forecast_Working to the current month's snapshot
2' curMonth is determined from the cycle (e.g., 3 for March)
3Dim curMonth As Integer = 3
4api.Data.Calculate("S#Forecast_M" & curMonth.ToString() & " = S#Forecast_Working")
This publish step is typically run as a Data Management step or triggered by a Workflow event after the forecast is approved — not embedded in the Finance Business Rule that handles seeding and calculation.

Isolating Phases with CustomCalculate

The Calculate approach shown above runs inside the DUCS — it fires on every calculation and consolidation pass. For production forecast workflows, split each phase into a separate CustomCalculate function so the FP&A team has granular, on-demand control: seed without recalculating, recalculate without republishing.
1If api.FunctionType = FinanceFunctionType.CustomCalculate Then
2  Dim fn As String = args.CustomCalculateArgs.FunctionName
3  Dim periodNum As Integer = api.Time.GetPeriodNumFromId(api.Pov.TimeDimPk.MemberId)
4  Dim lastClosedPeriod As Integer = 3 ' Read from an application setting in production
5
6  If fn.XFEqualsIgnoreCase("SeedForecast") Then
7      ' Phase 1 — Seed closed months with actuals
8      api.Data.ClearCalculatedData(True, True, True, True, "")
9      If periodNum <= lastClosedPeriod Then
10          api.Data.Calculate("S#Forecast_Working = S#Actual", True)
11      End If
12
13  ElseIf fn.XFEqualsIgnoreCase("CalcForecast") Then
14      ' Phase 2 — Run driver-based calculations for open months
15      api.Data.ClearCalculatedData(True, True, True, True, _
16          "A#SalaryExpense,A#ProductRevenue,A#COGSAmount,A#GrossProfit,A#OpEx,A#NetIncome")
17      If periodNum > lastClosedPeriod Then
18          api.Data.Calculate("A#SalaryExpense = RemoveZeros(A#FTE * A#AvgSalary)", True)
19          api.Data.Calculate("A#ProductRevenue = RemoveZeros(A#SalesVolume * A#PricePerUnit)", True)
20          api.Data.Calculate("A#COGSAmount = RemoveZeros(A#ProductRevenue * A#COGSPercent)", True)
21          api.Data.Calculate("A#GrossProfit = A#Revenue - A#COGSAmount", True)
22          api.Data.Calculate("A#OpEx = A#SalaryExpense + A#BenefitsExpense + A#OtherOpEx", True)
23          api.Data.Calculate("A#NetIncome = A#GrossProfit - A#OpEx", True)
24      End If
25
26  ElseIf fn.XFEqualsIgnoreCase("PublishForecast") Then
27      ' Phase 3 — Copy Forecast_Working to the monthly snapshot
28      Dim curMonth As Integer = lastClosedPeriod
29      api.Data.ClearCalculatedData(True, True, True, True, "")
30      api.Data.Calculate("S#Forecast_M" & curMonth.ToString() & " = S#Forecast_Working", True)
31  End If
32End If
⚠️Warning
Durable storage is critical. Every api.Data.Calculate call above passes True as the second argument. Without Durable storage, data written by SeedForecast would be cleared when CalcForecast runs — each Custom Calculate step clears non-durable calculated data by default.
Each function maps to its own Custom Calculate Data Management step. The step's Business Rule property points to the Finance BR; the Function Name property is set to SeedForecast, CalcForecast, or PublishForecast. The three steps can run in a sequence (seed → calculate → publish) or independently — for example, running just CalcForecast after updating driver assumptions without re-seeding actuals.
Each Custom Calculate function can also be triggered from a Dashboard Parameter Component server task action. The arguments follow the format {BusinessRuleName},{FunctionName},{NameValuePairs} — for example, a "Seed Actuals" button with arguments {ForecastCalc},{SeedForecast},{}. A forecast management dashboard can present one button per phase, giving the FP&A team point-and-click control over the entire cycle.
ℹ️Info
For the full Calculate vs. CustomCalculate comparison, ClearCalculatedData details, and Durable storage mechanics, see Writing Calculations.
ℹ️Info
Hybrid Approaches: YTG vs Rolling Forecast
The Forecast_Working + snapshot architecture supports two common forecast variants:
  • YTG (Year-To-Go) Forecast — The plan duration shrinks each month. Forecast_M1 has 12 projected months and 0 actual months; Forecast_M12 has 1 projected month and 11 actual months. This is the most common approach for organizations with a fixed fiscal year.
  • Rolling Forecast — A fixed 12-month window that always looks 12 months ahead regardless of where you are in the year. The June cycle would project July 2026 through June 2027, extending into the next fiscal year.
The architecture is the same for both — only the Scenario Type time span configuration differs. YTG forecasts use a fixed fiscal-year time span; rolling forecasts extend the time span into the next year.

Variance Reporting

Variance analysis — comparing Budget vs. Actual or Forecast vs. Actual — is one of the primary outputs of any FP&A process. OneStream supports variance reporting through both calculated cube data and dashboard components.
The simplest approach is calculating variance accounts directly in the cube:
1If api.FunctionType = FinanceFunctionType.Calculate Then
2  ' Budget vs. Actual variance (favorable = positive)
3  api.Data.Calculate("A#BudgetVariance:S#Actual = A#Actual:S#Actual - A#Actual:S#Budget")
4
5  ' Forecast vs. Actual variance (compare against a specific monthly snapshot)
6  api.Data.Calculate("A#ForecastVariance:S#Actual = A#Actual:S#Actual - A#Actual:S#Forecast_M3")
7
8  ' Variance percentage (using Data Buffer to avoid divide-by-zero)
9  Dim budgetBuf As DataBuffer = api.Data.GetDataBufferUsingFormula("A#Actual:S#Budget")
10  Dim actualBuf As DataBuffer = api.Data.GetDataBufferUsingFormula("A#Actual:S#Actual")
11
12  For Each cell As DataBufferCell In actualBuf.DataBufferCells.Values
13      Dim key As String = cell.GetLocalUniqueKeyString()
14      If budgetBuf.DataBufferCells.ContainsKey(key) Then
15          Dim budgetAmt As Decimal = budgetBuf.DataBufferCells(key).CellAmount
16          If budgetAmt <> 0D Then
17              cell.CellAmount = (cell.CellAmount - budgetAmt) / budgetAmt
18          Else
19              cell.CellAmount = 0D
20          End If
21      Else
22          cell.CellAmount = 0D
23      End If
24  Next
25
26  api.Data.FormulaVariables.SetDataBufferVariable("varPct", actualBuf, False)
27  api.Data.Calculate("A#BudgetVariancePct:S#Actual = $varPct")
28End If
ℹ️Info
An alternative to storing variance in the cube is computing it dynamically in Dashboard DataSet rules or Cube View configurations. This approach avoids creating additional calculated accounts but requires more dashboard-level logic. The choice depends on how many places the variance data is consumed — if it appears in multiple reports, storing it in the cube is more efficient.
Dashboards then present variance data using conditional formatting (red/green for unfavorable/favorable), waterfall charts, and drill-down navigation that lets the CFO click from the company total down to a specific entity, department, and account to understand the root cause of a variance.

Specialty Planning: Headcount

The core P&L budget uses aggregate driver accounts — FTE and AvgSalary at the department level. But many organizations need employee-level detail: individual names, exact salaries, start/end dates, benefit elections, and department transfers. This exceeds what the cube is designed for.
The solution is a relational table that stores employee-level detail, combined with a Business Rule that reads the table, aggregates the data, and writes summary totals to the cube.
The pattern:
  1. Relational table — A custom database table (or MarketPlace People Planning table) with columns for EmployeeID, Name, Entity, Department, AnnualSalary, StartDate, EndDate, BenefitsRate, etc.
  2. Custom dashboard — An XFBR-powered form where HR and department managers can add, edit, and remove employee records (CRUD operations).
  3. Aggregation rule — A Spreadsheet or Extender Business Rule that reads the employee table, sums salary by Entity × Department × Month, and writes the totals to the cube.
Here is a simplified aggregation rule:
1' Spreadsheet Business Rule — Aggregate People Plan to Cube
2' Reads from the PeoplePlan table, sums salary by Entity/Department/Month
3Dim dt As DataTable = BRApi.Database.ExecuteSql(si, "SELECT Entity, Department, AnnualSalary, StartDate, EndDate FROM PeoplePlan WHERE Year = 2026", False)
4
5Dim salaryByKey As New Dictionary(Of String, Decimal)
6Dim fteByKey As New Dictionary(Of String, Decimal)
7
8For Each row As DataRow In dt.Rows
9  Dim entity As String = row("Entity").ToString()
10  Dim dept As String = row("Department").ToString()
11  Dim annualSalary As Decimal = CDec(row("AnnualSalary"))
12  Dim startDate As DateTime = CDate(row("StartDate"))
13  Dim endDate As DateTime = If(IsDBNull(row("EndDate")), New DateTime(2026, 12, 31), CDate(row("EndDate")))
14
15  ' Distribute across months the employee is active
16  For month As Integer = 1 To 12
17      Dim monthStart As New DateTime(2026, month, 1)
18      Dim monthEnd As DateTime = monthStart.AddMonths(1).AddDays(-1)
19
20      If startDate <= monthEnd AndAlso endDate >= monthStart Then
21          Dim key As String = $"{entity}|{dept}|2026M{month}"
22          Dim monthlySalary As Decimal = annualSalary / 12D
23          If salaryByKey.ContainsKey(key) Then
24              salaryByKey(key) += monthlySalary
25              fteByKey(key) += 1D
26          Else
27              salaryByKey(key) = monthlySalary
28              fteByKey(key) = 1D
29          End If
30      End If
31  Next
32Next
33
34' Write aggregated data to cube via api or BRApi calls
35For Each kvp In salaryByKey
36  Dim parts() As String = kvp.Key.Split("|"c)
37  Dim pov As String = $"E#{parts(0)}:U1#{parts(1)}:T#{parts(2)}:S#Budget:A#SalaryExpense"
38  ' Write to cube using appropriate API method
39Next
⚠️Warning
This is a simplified example showing the aggregation pattern. A production implementation would handle proration for mid-month start/end dates, multiple currency entities, benefits calculations, and error handling. The OneStream MarketPlace People Planning solution provides a full-featured version of this pattern out of the box.

Specialty Planning: Capital Projects

Capital project planning follows the same relational-to-cube pattern as headcount, but with different detail fields:
  • Project name, ID, and category
  • Total cost and spend timeline (by month or quarter)
  • Useful life and depreciation method (straight-line, declining balance)
  • Start date and in-service date
A custom dashboard lets project managers enter and maintain their capital project list. An Extender or Spreadsheet Business Rule reads the project table, calculates monthly depreciation for each asset, and writes the results to the cube — both the CapEx spend (in the period it occurs) and the ongoing depreciation expense (spread over the useful life).
This pattern is especially valuable because depreciation calculations are tedious in a flat cube model (you would need separate accounts for every asset or complex time-shifting formulas). The relational table handles the detail, and the Business Rule distills it into the handful of cube accounts the P&L needs.

CRUD Patterns in OneStream

Both headcount and capital project planning follow the same architectural pattern: a relational table for detail, Business Rules for processing, the cube for aggregated results, and a dashboard for user interaction. This CRUD pattern (Create, Read, Update, Delete) generalizes to any specialty planning module.
diagramCRUD Pattern for Specialty Planning

Loading diagram...

The specific Business Rule types used at each step:
CRUD StepRule TypePurpose
Dashboard displayDashboard DataSetRead the relational table and return data for the grid
Save / editDashboard ExtenderHandle save events, validate input, write to relational table
DeleteDashboard ExtenderHandle delete events, remove rows from relational table
Aggregate to cubeSpreadsheet or ExtenderRead table, perform calculations, write results to cube cells
Trigger from workflowData Management ExtenderRun the aggregation rule as part of a Data Management sequence
ℹ️Info
The OneStream MarketPlace includes pre-built solutions for People Planning, Capital Planning, Account Reconciliations, and other specialty modules. These follow the CRUD pattern described here and can be extended with custom Business Rules. Before building a custom module from scratch, check whether a MarketPlace solution covers your requirements.

ABC Example: Rolling Forecast and People Plan

ABC Manufacturing implements two extensions beyond their annual budget:

Rolling Forecast

Each month, after the close, ABC runs a Data Management sequence that:
  1. Loads actuals for the closed month into the Actual scenario (using the integration pipeline from the Data Integration guide)
  2. Seeds actuals into Forecast_Working for closed months — replacing prior projections with what actually happened
  3. Recalculates Forecast_Working for open months — using the same driver-based logic as the budget, but with updated assumptions
  4. Publishes the snapshot — Once the forecast cycle is approved, copies Forecast_Working to the corresponding Forecast_MX (e.g., Forecast_M3 for the March cycle)
The Finance Business Rule handles steps 2 and 3 using the period-number comparison pattern shown earlier in this guide. The lastClosedPeriod value is stored in a settings table and updated by the FP&A team each month. It controls both the seeding cutoff and which snapshot is the publish target.

People Plan

ABC's People Plan tracks 500 employees across three entities. The module provides:
  • A dashboard grid where HR managers can view, add, edit, and terminate employee records. Each row shows employee name, entity, department, annual salary, benefits rate, start date, and optional end date.
  • An aggregation rule that runs nightly (or on demand) to read the employee table, calculate monthly salary and benefits by Entity × Department, and write the totals to the cube under the Forecast scenario.
  • Integration with the P&L — The aggregated SalaryExpense and BenefitsExpense from the People Plan replace the simple FTE × AvgSalary driver calculation for entities that have completed their people planning. Entities still in draft mode fall back to the driver-based approach.
This hybrid approach — detailed people planning where it is ready, driver-based estimates where it is not — is a common pattern in large implementations. The Finance Business Rule checks a flag (stored in the cube or a settings table) for each entity to determine which calculation path to use.
💡Tip
Start with the driver-based approach (FTE × AvgSalary) and layer in the detailed People Plan later. This lets you deliver a working budget quickly while the more complex module is developed and tested. The same principle applies to any specialty planning area: get the simple version working first, then add detail.