Business Rules

Debugging Business Rules

Debugging Business Rules is harder than typical application code. Rules fire once per Data Unit — potentially thousands of times during a single calculation — and there is no step debugger.
The recommended approach is to collect trace messages in a nullable StringBuilder, controlled by a debug flag, and output the full trace as a single error log entry through XFException. When debug is disabled, the pattern has zero performance overhead.

The Debug Logging Pattern

1Imports System
2Imports System.Text
3Imports OneStream.Shared.Common
4Imports OneStream.Shared.Wcf
5Imports OneStream.Finance.Engine
6Imports OneStream.Finance.Database
7
8Namespace OneStream.BusinessRule.Finance.MyRuleName
9  Public Class MainClass
10
11      Private log As StringBuilder = Nothing
12      Private Const ENABLE_DEBUG As Boolean = True
13
14      Public Function Main(ByVal si As SessionInfo, ByVal globals As BRGlobals, ByVal api As FinanceRulesApi, ByVal args As FinanceRulesArgs) As Object
15          Try
16              If ENABLE_DEBUG Then log = New StringBuilder()
17
18              If api.FunctionType = FinanceFunctionType.Calculate Then
19                  log?.AppendLine("Entity: " & api.Entity.Name & " | Time: " & api.Time.Name)
20
21                  Dim amount As Decimal = api.Data.GetDataCell("A#Sales").CellAmount
22                  log?.AppendLine("Sales amount: " & amount.ToString())
23
24                  api.Data.SetDataCell("A#AdjustedSales", amount * 1.1D)
25                  log?.AppendLine("Set AdjustedSales to " & (amount * 1.1D).ToString())
26              End If
27
28              Return Nothing
29          Catch ex As Exception
30              Throw ErrorHandler.LogWrite(si, New XFException(si, log?.ToString(), String.Empty, ex))
31          End Try
32      End Function
33
34  End Class
35End Namespace
Key elements of the pattern:
  • log declared at the class level — Accessible throughout Main and any helper methods you add to the class.
  • ENABLE_DEBUG / EnableDebug constant — A compile-time flag. Set to True during development, False for production. Because it's a constant, the compiler can optimize away the dead code paths.
  • If ENABLE_DEBUG Then log = New StringBuilder() — Only allocates the StringBuilder when debug is on. When off, log stays Nothing/null.
  • log?.AppendLine(...) — The null-conditional operator. When log is Nothing/null, the entire call is skipped. When log exists, the message is appended.
  • XFException in the Catch block — Wraps the accumulated log as the generalMsg parameter. If an error occurs, the full trace appears in the error log as a single entry alongside the exception details.

Why This Works — Classes and Functions

Class-Level Fields

When OneStream executes your rule, it instantiates your MainClass and calls Main(). Fields declared outside Main — at the class level — are created when the class is instantiated and remain accessible to every method in the class.
This matters because best practice is to keep Main lean: it should check api.FunctionType and dispatch to dedicated methods for each function type. Declaring log at the class level means those helper methods can append to the same StringBuilder without passing it as a parameter.

Nullable References

Declaring log As StringBuilder = Nothing means the variable exists but holds no object. No StringBuilder is allocated in memory, no methods can run on it — it's just an empty reference. The debug constant decides whether to actually create the object with New StringBuilder().

The Null-Conditional Operator (?.)

log?.AppendLine("msg") means: if log is not Nothing/null, call AppendLine; otherwise, skip the entire call. When debug is disabled, log stays Nothing/null — every log?.AppendLine() call becomes a no-op with zero performance overhead. No string allocations, no method calls, no cost.

Controlling Where Execution Stops

Let It Fail Naturally

The most common approach: don't catch exceptions mid-rule. When an error occurs, it bubbles up to the outer Try/Catch block, which passes the accumulated log through XFException. You see the full trace up to the point of failure.

Throw Intentionally

Sometimes you need to inspect state at a specific point without waiting for an error. Throw a simple Exception to halt execution — the outer Catch block will catch it and output the accumulated log through XFException automatically:
1' Halt execution here — the Catch block will output the log
2Throw New Exception("Intentional stop — check log")
This is useful when your rule completes successfully but produces unexpected results — throw at the point you want to inspect and the Catch block handles the rest, packaging your accumulated log into the error output.

Quick Messages with BRApi.ErrorLog

For simple one-off informational messages — not debug tracing — BRApi.ErrorLog.LogMessage writes a single entry to the application error log:
1' Simple informational message
2BRApi.ErrorLog.LogMessage(si, "Import completed for " & entityName)
3
4' Message with explicit error level and detail
5BRApi.ErrorLog.LogMessage(si, XFErrorLevel.Warning, "Missing rate for " & entityName, "Defaulting to 1.0")
🛑Danger
Never place BRApi.ErrorLog.LogMessage inside a loop or in code that executes per Data Unit.
Each call inserts a separate row into the error log table. If your rule loops through thousands of members or runs across hundreds of entities, you'll generate that many database rows per execution.
After only a few kick-offs, the error log table can grow large enough to degrade application performance or crash the server entirely.
Use the StringBuilder pattern above for any logging that may repeat, and reserve LogMessage for one-time events like process completion or configuration warnings.
⚠️Warning
When writing to the error log, avoid including sensitive or confidential information. OneStream attempts to filter and redact sensitive data, but you should design your logging with security in mind.