Business Rules

Organizing Your Business Rule Code

As Business Rules grow beyond a handful of lines, keeping all logic inside Main becomes difficult to read, test, and maintain. This guide covers a standard pattern: promote runtime objects to class-level fields, then dispatch from Main to dedicated methods for each function type.

The Class-Level State Pattern

The Main method receives si, globals, api, and args as parameters. Any helper method you write needs these same objects, which means either passing them as arguments — producing long, repetitive signatures — or finding another way to share them.
The recommended approach is to declare these four objects as class-level fields and assign them at the top of Main:
1Public Class MainClass
2
3  Private si As SessionInfo
4  Private globals As BRGlobals
5  Private api As FinanceRulesApi
6  Private args As FinanceRulesArgs
7
8  Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As FinanceRulesApi, ByVal args As FinanceRulesArgs) As Object
9      Try
10          Me.si = si
11          Me.globals = globals
12          Me.api = api
13          Me.args = args
14
15          ' Dispatch logic here...
16
17          Return Nothing
18      Catch ex As Exception
19          Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
20      End Try
21  End Function
22
23End Class
Now every method in the class can access si, globals, api, and args directly — no parameter threading required.
ℹ️Info
The OneStream runtime creates a new instance of MainClass for every execution. Class-level fields are not shared between executions, so this pattern is safe — there is no risk of state leaking between Data Units or between users.

Dispatching by Function Type

With state promoted to class level, Main becomes a thin dispatcher. Its only job is to assign the fields and route to the right method based on FunctionType:
1Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As FinanceRulesApi, ByVal args As FinanceRulesArgs) As Object
2  Try
3      Me.si = si
4      Me.globals = globals
5      Me.api = api
6      Me.args = args
7
8      Select Case api.FunctionType
9          Case FinanceFunctionType.Calculate
10              RunCalculate()
11          Case FinanceFunctionType.DynamicCalcAccount
12              Return RunDynamicCalc()
13      End Select
14
15      Return Nothing
16  Catch ex As Exception
17      Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
18  End Try
19End Function
diagram

Loading diagram...

Each dedicated method focuses on a single responsibility, and because the runtime objects live at the class level, these methods can call shared helpers without any extra plumbing.

Finance Rule Example

A complete Finance Rule with class-level state, dispatcher, dedicated methods, and a shared helper:
1Imports System
2Imports OneStream.Shared.Common
3Imports OneStream.Shared.Wcf
4Imports OneStream.Finance.Engine
5Imports OneStream.Finance.Database
6
7Namespace OneStream.BusinessRule.Finance.RevenueCalc
8  Public Class MainClass
9
10      Private si As SessionInfo
11      Private globals As BRGlobals
12      Private api As FinanceRulesApi
13      Private args As FinanceRulesArgs
14
15      Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As FinanceRulesApi, ByVal args As FinanceRulesArgs) As Object
16          Try
17              Me.si = si
18              Me.globals = globals
19              Me.api = api
20              Me.args = args
21
22              Select Case api.FunctionType
23                  Case FinanceFunctionType.Calculate
24                      RunCalculate()
25                  Case FinanceFunctionType.DynamicCalcAccount
26                      Return RunDynamicCalc()
27              End Select
28
29              Return Nothing
30          Catch ex As Exception
31              Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
32          End Try
33      End Function
34
35      Private Sub RunCalculate()
36          Dim sales As Decimal = api.Data.GetDataCell(GetMemberFilter("Sales")).CellAmount
37          Dim cogs As Decimal = api.Data.GetDataCell(GetMemberFilter("COGS")).CellAmount
38
39          api.Data.SetDataCell(GetMemberFilter("GrossProfit"), sales - cogs)
40      End Sub
41
42      Private Function RunDynamicCalc() As Object
43          If api.Account.Name = "GrossMarginPct" Then
44              Dim sales As Decimal = api.Data.GetDataCell(GetMemberFilter("Sales")).CellAmount
45              If sales <> 0 Then
46                  Dim gp As Decimal = api.Data.GetDataCell(GetMemberFilter("GrossProfit")).CellAmount
47                  Return gp / sales
48              End If
49          End If
50
51          Return Nothing
52      End Function
53
54      Private Function GetMemberFilter(accountName As String) As String
55          Return "A#" & accountName & ":F#" & api.Flow.Name & ":C#" & api.Cons.Name
56      End Function
57
58  End Class
59End Namespace
Notice how GetMemberFilter uses api directly from class state. Without the class-level pattern, you would need to pass api into every helper call.

Dashboard DataSet Example

Dashboard DataSet rules use args.FunctionType (a DashboardDataSetFunctionType) to determine what the platform is requesting. The api parameter is typed as Object — it is not used in Dashboard rules the way FinanceRulesApi is used in Finance rules.
1Imports System
2Imports System.Collections.Generic
3Imports System.Data
4Imports OneStream.Shared.Common
5Imports OneStream.Shared.Wcf
6
7Namespace OneStream.BusinessRule.DashboardDataSet.SummaryReport
8  Public Class MainClass
9
10      Private si As SessionInfo
11      Private globals As BRGlobals
12      Private args As DashboardDataSetArgs
13
14      Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As Object, ByVal args As DashboardDataSetArgs) As Object
15          Try
16              Me.si = si
17              Me.globals = globals
18              Me.args = args
19
20              Select Case args.FunctionType
21                  Case DashboardDataSetFunctionType.GetDataSetNames
22                      Dim names As New List(Of String)()
23                      names.Add("SummaryReport")
24                      Return names
25                  Case DashboardDataSetFunctionType.GetDataSet
26                      If args.DataSetName.XFEqualsIgnoreCase("SummaryReport") Then
27                          Return GetSummaryReport()
28                      End If
29              End Select
30
31              Return Nothing
32          Catch ex As Exception
33              Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
34          End Try
35      End Function
36
37      Private Function GetSummaryReport() As DataTable
38          Dim dt As New DataTable()
39          dt.Columns.Add("Entity", GetType(String))
40          dt.Columns.Add("Amount", GetType(Decimal))
41
42          Dim entityFilter As String = String.Empty
43          If args.NameValuePairs.ContainsKey("EntityFilter") Then
44              entityFilter = args.NameValuePairs("EntityFilter")
45          End If
46
47          ' Build and populate the DataTable using BRApi calls
48          ' ...
49
50          Return dt
51      End Function
52
53  End Class
54End Namespace
The GetSummaryReport method reads args.NameValuePairs directly from class state — no need to thread the args object through the call.

Dashboard Extender Example

Dashboard Extender rules handle UI lifecycle events. The args.FunctionType is a DashboardExtenderFunctionType:
1Imports System
2Imports System.Collections.Generic
3Imports OneStream.Shared.Common
4Imports OneStream.Shared.Wcf
5
6Namespace OneStream.BusinessRule.DashboardExtender.MyDashboard
7  Public Class MainClass
8
9      Private si As SessionInfo
10      Private globals As BRGlobals
11      Private args As DashboardExtenderArgs
12
13      Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As Object, ByVal args As DashboardExtenderArgs) As Object
14          Try
15              Me.si = si
16              Me.globals = globals
17              Me.args = args
18
19              Select Case args.FunctionType
20                  Case DashboardExtenderFunctionType.LoadDashboard
21                      Return HandleLoadDashboard()
22                  Case DashboardExtenderFunctionType.ComponentSelectionChanged
23                      Return HandleSelectionChanged()
24              End Select
25
26              Return Nothing
27          Catch ex As Exception
28              Throw ErrorHandler.LogWrite(si, New XFException(si, ex))
29          End Try
30      End Function
31
32      Private Function HandleLoadDashboard() As Object
33          If args.LoadDashboardTaskInfo.Reason = LoadDashboardReasonType.Initialize AndAlso _
34             args.LoadDashboardTaskInfo.Action = LoadDashboardActionType.BeforeFirstGetParameters Then
35
36              Dim result As New XFLoadDashboardTaskResult()
37              ' Set default parameter values, configure initial state
38              ' ...
39              Return result
40          End If
41
42          Return Nothing
43      End Function
44
45      Private Function HandleSelectionChanged() As Object
46          Dim taskInfo As XFSelectionChangedTaskInfo = args.SelectionChangedTaskInfo
47
48          ' React to user selections — refresh components, update filters
49          ' ...
50
51          Return Nothing
52      End Function
53
54  End Class
55End Namespace
Each function type has its own method with a clear name, making it easy to find and modify specific behavior.

When To Extract Further

The class-level pattern works well for a single rule file, but as your codebase grows you may find:
  • Methods are reused across multiple rules — The same helper logic appears in several Business Rules.
  • A single rule file exceeds a few hundred lines — Scrolling through one large class becomes cumbersome.
  • You want unit-testable logic — Business Rule classes cannot be tested outside of OneStream.
When this happens, move shared logic into a Workspace Assembly — a separate class file that multiple Business Rules can reference. This keeps each rule lean and your shared logic in one place.
See the Getting Started with Workspace Assemblies guide for how to set up assemblies and reference them from your rules.