Skip to main content

How-to: Programmatically Interact with your First Routine

Author: Drew Shea, Created: 2025-05-29

The purpose of this document is to introduce developers to the core concepts powering the Routine C# SDK within SensibleAI Studio. It emphasizes intuitive analogies and provides clear examples to simplify understanding.

Why Routines?

We built Routines to bridge two powerful yet distinct worlds:

  • Python: the heart of AI development.
  • .NET (OneStream): our robust enterprise platform.

Routines provide a seamless way to inject advanced Python-based AI capabilities directly into the .NET ecosystem, ensuring OneStream always benefits from the latest AI innovations effortlessly.

The Anatomy of a Routine

Think of a Routine as similar to a class in any object-oriented programming language. It can have:

  • Member Variables: Holds state information.
  • Constructors: Initializes instances.
  • Methods: Execute operations, take inputs, and optionally return outputs.

The key distinction is in execution:

  • Instead of running in-process (within your application), Routines execute remotely on the AI Services compute and storage stack.
  • Inputs are provided via JSON, and outputs (called Artifacts) are returned as references to files stored remotely.

You can conceptualize a Routine as an "out-of-process class", interacting with your application through JSON inputs and filesystem-based outputs.

Example: A Bank Account Managment System

In order to get a better understanding of how and where Routine is similar and different to a standard C# class, let’s look at it through a simple example.

Imagine trying to simulate managing customer bank accounts. The goal is to represent key operations such as creating an account, depositing funds, withdrawing money, and summarizing the current state of the account.

In a traditional C# class, this logic would be implemented in-process—within the application itself, where each operation directly interacts with the internal state (like balances and transaction history).

However, if this same logic were encapsulated within a Routine, the operations would be executed remotely, using JSON to exchange input parameters and receiving outputs as references to remotely stored Artifacts.

Let’s see some code and do a compare and contrast of a standard C# class implementation versus a SensibleAI Studio Routine implementation in the following key areas:

  • Object Definition - The way in which our class (or Routine) is defined.
  • Object Instantiation - The way in which we instantiate the object
  • Method Invocation - The way in which we call a method
  • Method Result Interaction - The way in which we interact with a returned object from a method\

We will use a Standard C# Class implementation called BankAccountMgmt along with a Routine called TrainingBankAccountMgmt to do our compare and contrast. The underlying business logic in both implementations is basically the same.

1. Object Definition

Standard C# Class Example

We happen to have a standard C# class here that can simulates the management of a bank of account called BankAccountMgmt.

Standard C# Class: BankAccountMgmt
public class BankAccountMgmt
{

private string _name;
private DateTime _dateOfBirth;
private double _yearlyIncome;
private double _initialDeposit;
private string _accountType;
private double _balance;

private DateTime _creationDate;
private DateTime? _lastWithdrawalDate;
private DateTime? _lastDepositDate;


public BankAccountMgmt(string name, DateTime dateOfBirth, double yearlyIncome, double initialDeposit, string accountType)
{
// Validate inputs
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Account holder name cannot be empty.", nameof(name));

// Must be 18 years or older
if (dateOfBirth > DateTime.Now.AddYears(-18))
throw new ArgumentException("Account holder must be at least 18 years old.", nameof(dateOfBirth));

// Must have a valid yearly income
if (yearlyIncome < 0)
throw new ArgumentOutOfRangeException(nameof(yearlyIncome), "Income cannot be negative.");

// Must have a valid initial deposit
if (initialDeposit < 0)
throw new ArgumentOutOfRangeException(nameof(initialDeposit), "Initial deposit cannot be negative.");

// Ensure account type is one of "checkings", "savings", or "high-yield savings"
var validAccountTypes = new[] { "checkings", "savings", "high-yield savings" };
if (!validAccountTypes.Contains(accountType.ToLower()))
throw new ArgumentException("Invalid account type. Valid types are: checkings, savings, high-yield savings.", nameof(accountType));

// Set user defined member variables
_name = name;
_dateOfBirth = dateOfBirth;
_yearlyIncome = yearlyIncome;
_initialDeposit = initialDeposit;
_accountType = accountType;
_balance = initialDeposit;

// Save system managed member variables
_creationDate = DateTime.Now;
_lastWithdrawalDate = null;
_lastDepositDate = _creationDate;
}

public void Deposit(double amount)
{
// Logic to deposit money into the account
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive.");

_balance += amount;
}

public double Withdraw(double amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive.");

if (amount > _balance)
throw new InvalidOperationException("Insufficient funds for withdrawal.");

_balance -= amount;

return amount;
}

public AccountSummaryDto GetAccountSummary()
{
return new AccountSummaryDto
{
AccountCreationDate = _creationDate,
AccountHolder = _name,
AccountBalance = _balance,
LastWithdrawalDate = _lastWithdrawalDate,
LastDepositDate = _lastDepositDate
};
}
}


public class AccountSummaryDto
{
[Newtonsoft.Json.JsonProperty("account_creation_date")]
public DateTime AccountCreationDate { get; set; }

[Newtonsoft.Json.JsonProperty("account_holder")]
public string AccountHolder { get; set; }

[Newtonsoft.Json.JsonProperty("account_balance")]
public double AccountBalance { get; set; }

[Newtonsoft.Json.JsonProperty("last_withdrawal_date")]
public DateTime? LastWithdrawalDate { get; set; }

[Newtonsoft.Json.JsonProperty("last_deposit_date")]
public DateTime? LastDepositDate { get; set; }
}

The provided C# code defines banking account management system. The primary class, BankAccountMgmt, encapsulates all account-related data and logic.

Upon instantiation, the BankAccountMgmt class constructor ensures that essential conditions are met:

  • The account holder's name cannot be empty.
  • The holder must be at least 18 years old.
  • Income and initial deposit must not be negative.
  • The account type must be either "checkings," "savings," or "high-yield savings."

Once validated, the account details such as the holder's name, birthdate, yearly income, and initial deposit amount are stored alongside system-managed data like the account creation date, balance, and dates of the last transactions.

The class also includes key methods:

  • Deposit(double amount): Adds funds to the account, ensuring the deposit amount is positive.
  • Withdraw(double amount): Removes funds from the account after confirming sufficient balance and positive withdrawal amount.
  • GetAccountSummary(): Returns a data transfer object (AccountSummaryDto) containing essential account details, formatted neatly with attributes for easy serialization to JSON.

The AccountSummaryDto class provides a structured and cleanly serializable summary of the account state, including creation date, holder’s name, current balance, and transaction dates. This design ensures clarity, maintainability, and straightforward integration with web services or APIs.

Routine Example

As mentioned above, we have a Routine Type called TrainingBankAccountMgmt that does the exact same thing as our BankAccountMgmt class which allows us to compare and contrast easily.

Routines are implemented in Python using a specialized interface system. For the scope of this article, we are not going to be digging into the actual implementation of TrainingBankAccountMgmt but it is included in below for those who are curious.

Since how we actually code a Routine itself is outside of the scope of this article, we will just explore the Routine via the documentation.

Routine Type: TrainingBankAccountMgmt Routine
@routine
class TrainingBankAccountMgmt(BaseRoutine):
definition = RoutineDefinitionContext(
title="Training - Bank Account Mgmt",
tags=[RoutineTags_.learning_guide],
short_description="A simple Stateful Routine to demonstrate how Routines work through a bank account management use case.",
long_description=(
"This Routine is designed to demonstrate how to create a simple Stateful Routine that allows users to create a bank account, "
"deposit money, withdraw money, and get an account summary. It is part of a training series for individuals to learn how to build as "
"well as interact with Routines and their inputs/outputs."
"The Routine is designed to be simple and easy to understand, while also demonstrating the key features of Routines, "
"such as member variables, method execution, and artifact generation. It also demonstrates how to use the RoutineApi to update the "
"status of the run, log messages, and handle errors. Lastly, it also include in-memory execution for quick operations that do not "
"require heavy computation."
),
use_cases=[
RoutineDefinitionUseCaseContext(
title="Training New Routine Power Users",
description=(
"A Routine Power User is a consultant, developer, or OneStream Power User that wants to run and interact with Routines to power"
"some business process. This example Routine is designed to be simple and easy to understand so that a Routine Power User "
"can get comfortable with a Stateful Routine. A Routine Power User can use this Routine to understand what it means to: "
"1) Interact (either programmatically or through the UI) "
"2) Create a Routine Instance that takes parameters and maintains internal state. "
"3) Invoke Routine Methods on the Routine Instance to perform actions. "
"4) Visualize the Artifacts generated by running a Routine Method. "
"5) Programmatically interact with Routine Artifacts. "
"6) Leverage the advanced In-Memory Execution capabilities of certain Routine Methods (ex: deposit, get_account_summary) to run "
"them at the web-server level without needing to be run on a worker so that they may be run at the UI layer (and not require a "
"background task to run)."
),
),
RoutineDefinitionUseCaseContext(
title="Training New Routine Developers",
description=(
"A Routine Developer is a developer that wants to create Routines in Python that then Routine Power User's can use. "
"Note: The ability to create Routines is not exposed outside of the OneStream organization, so this is only for internal "
"developers that want to create Routines for their own use or for other Routine Power Users. At a later date, we will "
"expose the ability to create Routines to external developers, but for now, this is only for internal developers. "
"This example Routine is designed to be simple and easy to understand so that a Routine Developer can get comfortable with "
"what a Stateful Routine is, how to create one, and how to implement the methods that a Routine Power User can invoke. "
"A Routine Developer can use this Routine to understand what it means to: "
"1) Create a Stateful Routine that maintains internal state using member variables. "
"2) Implement Routine Methods that can be invoked by Routine Power Users. "
"3) Use the RoutineApi to update the status of the run, log messages, and handle errors. "
"4) Generate Artifacts that can be visualized by Routine Power Users. "
"5) Expose certain Routine Methods to allow in-memory execution, meaning they can be run at the web-server level. "
"6) Use the RoutineApi to access the Routine Instance and its member variables."
"7) Build Routine Method inputs as ParameterBaseModels with multiple states, dynamic options, and validation. "
),
),
],
creation_date=dt.datetime(2025, 5, 26),
author="Drew Shea",
organization=OrganizationNames_.ONESTREAM,
version=SemanticVersion(major=1, minor=0, patch=0),
)

memory_capacity = 2

# region Member Variables

account_holder: MemberVar[str, MemberVarAttrs(read_only=True)]
income: MemberVar[float, MemberVarAttrs(read_only=True)]
initial_deposit: MemberVar[float, MemberVarAttrs(read_only=True)]
account_type: MemberVar[str, MemberVarAttrs(read_only=True)]

balance: MemberVar[float, MemberVarAttrs(read_only=False)]

# endregion

@rmethod(memory_capacity=2)
def __init__(self, api: RoutineApi, parameters: BankAccountSignUpConstructorParameters):
"""
The constructor for creating a new bank account at "Bank Of Routine".

This method initializes the bank account with the provided parameters, such as the account holder's name, income, initial deposit,
and account type. Note, the constructor itself ALWAYS takes a RoutineApi and the parameters for the constructor.

Args:
api (RoutineApi): The api for the routine instance. This is reused for all inner anomaly detectors.
parameters (BankAccountSignUpConstructorParameters): The input parameters needed to create a new bank account.
"""

# We can provide messages along the way execution to inform the user of progress
api.run.update_status(percent_complete=0.1, message="Creating the account...")

# Set member variables with the provided parameters
self.account_holder = parameters.name
self.income = parameters.income
self.initial_deposit = parameters.initial_deposit
self.account_type = parameters.account_type
self.balance = self.initial_deposit

api.run.update_status(
percent_complete=0.99, message=f"Account creation complete. {self.account_holder} has {self.income} in a {self.account_type} account."
)
super().__init__(api=api, parameters=parameters)

@rmethod(memory_capacity=2, allow_in_memory_execution=True)
def deposit(self, api: RoutineApi, parameters: DepositParameters) -> None:
"""
Deposit money into the bank account.

Notice how this does not return anything, but rather updates the state of the routine. Additionally, also note that it allows in-memory
execution, meaning it can be run at the web-server level without needing to be run on a worker. This is useful for quick operations that
do not require heavy computation.

Args:
api (RoutineApi): The routine api.
parameters (DepositParameters): The parameters necessary to deposit money.

Returns:
None
"""
# Update the balance with the deposit amount
self.balance += parameters.amount

# Update the status of the run to inform the user of the deposit
api.run.update_status(
percent_complete=0.99, message=f"Deposited {parameters.amount} into {self.account_holder}'s account. New balance: {self.balance}."
)

@rmethod(memory_capacity=2)
def withdraw(self, api: RoutineApi, parameters: WithdrawParameters) -> WithdrawArtifacts:
"""
Withdraw money from the bank account.

This method checks if the withdrawal amount is less than or equal to the current balance. If it is, it proceeds with the withdrawal,
updates the balance, and returns an artifact containing the amount withdrawn. If the withdrawal amount exceeds the current balance,
it raises a ValueError indicating insufficient funds.

Args:
api (RoutineApi): The routine api.
parameters (WithdrawParameters): The parameters necessary to withdraw money.

Returns:
WithdrawArtifacts: An artifact containing the amount withdrawn.
"""

api.run.update_status(percent_complete=0.1, message="Checking available balance...")

# Check if the withdrawal amount is less than or equal to the current balance
if parameters.amount > self.balance:
# Note this will cause a syserror. You can see that the run will fail inside SensibleAI Studio.
raise ValueError(f"Insufficient funds for withdrawal. Current balance: {self.balance}, requested: {parameters.amount}")

# Sleep to simulate bank processing time...
time.sleep(5)

# Update the balance with the withdrawal amount
self.balance -= parameters.amount

# Leverage the logger built into the RoutineApi to log info, warnings, and errors that can be viewed in SensibleAI Studio logs,
# and AI Services Activity Log.
if self.balance < 500:
api.log.warning(msg="Your balance is below $500. Please consider depositing more funds to avoid account fees.")

api.run.update_status(
percent_complete=0.99, message=f"Withdrew {parameters.amount} from {self.account_holder}'s account. New balance: {self.balance}."
)

# Return an artifact containing the current balance
return WithdrawArtifacts(dollars=parameters.amount)

@rmethod(memory_capacity=2, allow_in_memory_execution=True)
def get_account_summary(self, api: RoutineApi, parameters: XperiflowNullParameters) -> AccountSummaryArtifacts:
"""
Get the current balance of the account holder.

This method retrieves the account summary, including the account creation date, account holder's name, current balance. Additionally,
also note that it allows in-memory execution, meaning it can be run at the web-server level without needing to be run on a worker. This is
useful for quick operations that do not require heavy computation.

Args:
api (RoutineApi): The routine api.
parameters (XperiflowNullParameters): The parameters for the method. This is a placeholder and does not require any parameters.

Returns:
AccountSummaryArtifact: An artifact containing the account summary information and a request receipt.
"""

# ------- Step 1: Build up the request receipt ------
request_receipt = f"""
Request Receipt: The account holder {self.account_holder} has requested their account summary at {dt.datetime.now().isoformat()}.
"""

# ------ Step 2: Build up the account summary context ------
# Get the account creation date
account_creation_date = api.routine.creation_time

# Get the last withdrawal date and last deposit date if needed
completed_runs = api.routine.get_runs_by_status(activity_status=ExecutionActivityStatus_.COMPLETED)
completed_runs.sort(key=lambda completed_run: completed_run.creation_time, reverse=True)

# Find the last withdrawal date
last_withdrawal_date = None
for run in completed_runs:
if run.method_name == self.withdraw.__name__:
last_withdrawal_date = run.creation_time
break

# Find the last deposit date
last_deposit_date = None
for run in completed_runs:
if run.method_name == self.deposit.__name__:
last_deposit_date = run.creation_time
break

# Build up the account summary pydantic model
account_summary_context = AccountSummaryContext(
account_creation_date=account_creation_date,
account_holder=self.account_holder,
account_balance=self.balance,
last_withdrawal_date=last_withdrawal_date,
last_deposit_date=last_deposit_date,
)

return AccountSummaryArtifacts(
request_receipt=request_receipt, # Can just directly pass a string here, or you could also wrap in "Artifact(data=request_receipt)",
account_summary=account_summary_context,
)

Instead, we will use this time to go poke around the TrainingBankAccountMgmt Routine Documentation that can be found within the SensibleAI Studio.

alt text Definition Page for the TrainingBankAccountMgmt Routine

alt text Signature Page (“Constructor” View) for the TrainingBankAccountMgmt Routine

Take a moment to go find the “Training - Bank Account Mgmt” Routine or take a look at the documentation in the screenshots above. In particular, check out the “Signature” page as this tells the specific methods available as well as each method’s inputs, and outputs (artifacts).

Specifically, for the “Signature → Constructor View”, we can see that it is asking us to provide JSON inputs of:

  • “name” as a text type

  • “date_of_birth” as a datetime type

  • “income” as a decimal

  • “account_type” as a text type

  • “initial_deposit” as a decimal

As for Artifacts, there are none (just like in standard C# classes, you can’t return objects from a constructor).

2. Object Instantiation

Now that we have an understanding of how our Standard C# Class and Routine Definitions look, let’s now understand how we can instantiate the object.

Standard C# Class Example

In a standard C# class (that is not static), there will be one or more constructors that are defined. In the case of our BankAccountMgmt class, we only have one. In order to create an instance of BankAccountMgmt, we do the following:

var name = "John Doe";
var dateOfBirth = new DateTime(1985, 2, 5);
var income = 50000.0;
var initialDeposit = 1000.0;
var accountType = "savings";
// Create an instance of the BankAccountMgmt class directly in C#
var dotNetClassInstance = new BankAccountMgmt(name, dateOfBirth, income, initialDeposit, accountType);

As seen in the code above, we now have an instance of BankAccountMgmt in the variable dotNetClassInstance.

Routine Example

In order to instantiate a Routine, we have a bit more work to do

var name = "John Doe";
var dateOfBirth = new DateTime(1985, 2, 5);
var income = 50000.0;
var initialDeposit = 1000.0;
var accountType = "savings";
// Get the Routine Client which provides access to the routines available in SensibleAI Studio
IRoutineClient routineClient = XBRApi.Routines.GetRoutineClient(si);
// Create the routine instance (the class itself - this does not run the constructor)
var routineInstance = routineClient.CreateRoutineInstanceAsync(
"TrainingBankAccountMgmt", // The type of the routine (the class itself)
"1.0.0", // The verison of the routine to execute (new versions may be added over time)
null // The name of the routine (null for default name)
).Result;
// Call the constructor of the routine instance
var ctorRun = routineInstance.CreateConstructorRunAsync(
new JObject() // The actual input parameters (as json) required to run the constructor
{
{"name", name },
{"date_of_birth", dateOfBirth },
{"income", income },
{"account_type", accountType },
{"initial_deposit", initialDeposit },
{"gender", null }
}
).Result;
// Execute the constructor run and wait for it to succesfully complete
var ctorRunResult = ctorRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;

Here is the following code breakdown:

  • Line 8: We need to retrieve the RoutineClient which provides access to the available Routines found within SensibleAI Studio.
  • Line 11 - 15: Leveraging that RoutineClient, we can create an instance of the Routine Type “TrainingBankAccountMgmt”
    • Note: This is where it may feel a bit odd. We have created an “instance” of TrainingBankAccountMgmt but we have not actually run it’s constructor yet. That happens below.
  • Line 18 - 28: This is where we actually create a “Run” of the constructor with the appropriate parameters (as specified by the Routine documentation).
  • Line 31: Finally, we can now run our Constructor and wait for it to successfully complete.

In short, it’ll take a bit of getting used to two things:

  1. When you create an instance of a Routine, the constructor doesn’t automatically get called. You have to go create it, and then run it after you create the Routine Instance.
  2. You have to create a Method Run (Lines 18 - 28), and then you can “run” that Method Run (Line 31)

3. Method Invocation

Now that we have instances of BankAccountMgmt standard C# class and our TrainingBankAccountMgmt Routine, now let’s deposit some monthly income into John Doe’s bank account.

Standard C# Class Example This is simple - just call the method with the appropriate parameters.

dotNetClassInstance.Deposit(income / 12); // Deposit a monthly income amount

Routine Example

For calling a Routine Method, it’s fairly straightforward. It should feel very similar to how we called the constructor of the Routine Instance.

// Create a run to deposit money into the account
var depositRun = routineInstance.CreateMethodRunAsync(
"deposit", // The name of the method to execute (I can get this from the Routine Documentation)
new JObject() // The actual input parameters (as json) required to run the deposit method
{
{"amount", income / 12 } // Deposit a monthly income amount
}
).Result;
// Execute the deposit run and wait for it to successfully complete
var depositRunResult = depositRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;
  • Line 2 - 8: Create a Method Run object.
  • Line 11: Run the Method Run object and wait for it to successfully complete.

4. Method Result Interaction

Now let’s call a method that will return something to us. Let’s use the GetAccountSummary and get_account_summary found on the standard C# class and Routine, respectively.

Both of these should give back some metadata about the holder’s account.

Standard C# Class Example

For a standard C# Class method that returns an object, we can call the method and then set it equal to a variable like so:

// Get the account summary from the DotNet class
AccountSummaryDto accountSummaryDotNet = dotNetClassInstance.GetAccountSummary();
BRApi.ErrorLog.LogObject(si, "Account Summary from DotNet Class", accountSummaryDotNet);

The log statement will provide:

Description: Account Summary from DotNet Class
{
"account_creation_date": "2025-05-26T20:00:41.4146951-04:00",
"account_holder": "John Doe",
"account_balance": 5166.666666666667,
"last_withdrawal_date": null,
"last_deposit_date": "2025-05-26T20:00:41.4146951-04:00"
}

Routine Example

When calling a method that returns Artifacts, there are a few things I have to do.

First, I should go read the Routine Documentation to determine what types of articles are expected to come back. In the case of the get_account_summary, we see the following:

alt text Notice the “Account Summary” Artifact and its Qualified Key Annotation of “account_summary”

alt text For Artifacts of a certain type, it may be able to display more information - For example, the “Account Summary” Artifact can tell us the expected JSON Schema.

We can see an account summary artifact, which is expected to be a json payload containing account summary information. We can also see some other important information that we should be aware of:

  • Qualified Key Annotation: This is the unique identifier of the Artifact within this method run. We can use this key to extract our Artifact in code as you’ll see below
  • File Annotations: There are cases where we may want to directly read the data stored in the files that the Artifact represents, we can get an understanding of what the file path will look like:
    • @ = Artifact
    • <int> = index variable (signifying multiple files)
    • <str> = string variable (signifying multiple files)
  • Schema: Depending on the Artifact Type, we may have additional ifnormation available in the documentation. In the case of our “Account Summary” Artifact, we can see the exact JSON schema that we should be expecting to if we were to deserialize this Artifact data into a .NET object or .NET JObject.
// Create a run to get the account summary
var summaryRun = routineInstance.CreateMethodRunAsync("get_account_summary").Result; // The name of the method to execute
// Execute the summary run and wait for it to successfully complete
var summaryRunResult = summaryRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;
// Get the "account_summary" artifact from the routine instance.
// Note: I can find the artifact qualified key ("account_summary" in this case) in the Routine Documentation
var accountSummaryArtifact = summaryRunResult.GetArtifactInfo("account_summary")?.GetArtifactAsync().Result;
// Deserialize the account summary artifact into an AccountSummaryDto object
var accountSummaryJson = accountSummaryArtifact?.GetDataAsObjectAsync<JObject>().Result;
var accountSummaryJsonDto = accountSummaryJson?.ToObject<AccountSummaryDto>();
BRApi.ErrorLog.LogObject(si, "Account Summary from Routine Instance", accountSummaryJsonDto);

The log statement will provide:

Description: Account Summary from Routine Instance
{
"account_creation_date": "2025-05-27T00:00:41.48486",
"account_holder": "John Doe",
"account_balance": 5166.666666666667,
"last_withdrawal_date": null,
"last_deposit_date": "2025-05-27T00:01:11.9"
}

Below is a line-by-line walkthrough of the Routine-based version of Get Account Summary. Each step maps directly to the numbered comments in the snippet.

StepPurposeKey Take-aways
1var summaryRun = routineInstance.CreateMethodRunAsync("get_account_summary").Result;Creates a Method Run object for the get_account_summary Routine method. Nothing actually executes yet—this call just prepares a run record in AI Services.
2var summaryRunResult = summaryRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;Starts the run on the worker cluster and blocks until the run reaches a Completed status. When this call returns, all declared Artifacts have been pushed to object storage and are ready for retrieval.
3var accountSummaryArtifact = summaryRunResult.GetArtifactInfo("account_summary")?.GetArtifactAsync().Result;Looks up the Artifact whose qualified key is account_summary (visible on the Routine’s Signature page), then downloads the metadata wrapper that points to its underlying file(s).
4var accountSummaryJson = accountSummaryArtifact?.GetDataAsObjectAsync<JObject>().Result;Opens the Artifact’s primary file, reads the JSON payload into a JObject, but still keeps it in a generic, schema-agnostic container.
5var accountSummaryJsonDto = accountSummaryJson?.ToObject<AccountSummaryDto>();Converts that generic JSON into the strongly-typed AccountSummaryDto defined earlier, giving compile-time property access.
6BRApi.ErrorLog.LogObject(si, "Account Summary from Routine Instance", accountSummaryJsonDto);Writes the fully materialised DTO to the OneStream error log so it can be inspected in the Business Rule tester or activity log.

Why this extra ceremony?

Unlike the in-process C# call—which simply returns the DTO—Routines run out-of-process. Every return value is persisted (or can be directly returned in a JSON serializable format called “In-Memory” execution - A more advanced topic for another time) as an Artifact so it can be:

  • Downloaded from the AI Stack.
  • Versioned independently of the calling code.
  • Shared with other consumers (dashboards, pipelines, notebooks).

Consequently, the calling pattern is always:

  1. Create the run object.
  2. Start & wait until completion.
  3. Pull back the desired Artifacts by qualified key.
  4. Deserialize or otherwise handle the Artifact’s content to get it into Memory in C#

Once this mental model clicks, switching between traditional C# classes and Routines becomes straightforward—the main difference is where the work happens and how the results come back.

Appendix

Entire Example Business Rule

Below is the entire code example that was walked through in the article above.

Full Code Example

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.CSharp;
using OneStream.Finance.Database;
using OneStream.Finance.Engine;
using OneStream.Shared.Common;
using OneStream.Shared.Database;
using OneStream.Shared.Engine;
using OneStream.Shared.Wcf;
using OneStream.Stage.Database;
using OneStream.Stage.Engine;
using Workspace.XBR.Xperiflow.MetaDB;
using Workspace.XBR.Xperiflow;
using Workspace.XBR.Xperiflow.Core.Session;
using Workspace.XBR.Xperiflow.Utilities.AdoDataTable.Extensions;
using Workspace.XBR.Xperiflow.MetaFileSystem;
using Workspace.XBR.Xperiflow.Routines.Instances;
using Workspace.XBR.Xperiflow.Routines.Runs;
using Workspace.XBR.Xperiflow.Routines.Artifacts;
using Newtonsoft.Json.Linq;
using Workspace.XBR.Xperiflow.Utilities.Logging.Extensions;
using Antlr4.Runtime.Atn;
using Newtonsoft.Json;
using Workspace.XBR.Xperiflow.Routines;
using System.Reflection;

namespace OneStream.BusinessRule.Extender.AnatomyOfARoutine
{
public class MainClass
{
public object Main(SessionInfo si, BRGlobals globals, object api, ExtenderArgs args)
{
try
{

var name = "John Doe";
var dateOfBirth = new DateTime(1985, 2, 5);
var income = 50000.0;
var initialDeposit = 1000.0;
var accountType = "savings";

//////////////////////////////////////////////
/// Example 1A: Create a Bank Account Instance (From a DotNet Class)
//////////////////////////////////////////////

// Create an instance of the BankAccountMgmt class directly in C#
var dotNetClassInstance = new BankAccountMgmt(name, dateOfBirth, income, initialDeposit, accountType);


//////////////////////////////////////////////
/// Example 1B: Create a Instance (From the TrainingBankAccountMgmt Routine Type)
//////////////////////////////////////////////

// Get the Routine Client which provides access to the routines available in SensibleAI Studio
IRoutineClient routineClient = XBRApi.Routines.GetRoutineClient(si);

// Create the routine instance (the class itself - this does not run the constructor)
var routineInstance = routineClient.CreateRoutineInstanceAsync(
"TrainingBankAccountMgmt", // The type of the routine (the class itself)
"1.0.0", // The verison of the routine to execute (new versions may be added over time)
null // The name of the routine (null for default name)
).Result;

// Call the constructor of the routine instance
var ctorRun = routineInstance.CreateConstructorRunAsync(
new JObject() // The actual input parameters (as json) required to run the constructor
{
{"name", name },
{"date_of_birth", dateOfBirth },
{"income", income },
{"account_type", accountType },
{"initial_deposit", initialDeposit },
{"gender", null }
}
).Result;

// Execute the constructor run and wait for it to succesfully complete
var ctorRunResult = ctorRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;


// -----------------------------------------------------------------------------------------------------------

//////////////////////////////////////////////////
/// Example 2A: Deposit Money into the Bank Account (Against the DotNet Class)
//////////////////////////////////////////////////

dotNetClassInstance.Deposit(income / 12); // Deposit a monthly income amount



////////////////////////////////////////////////////
/// Example 2B: Deposit Money into the Bank Account (Against the Routine Instance)
////////////////////////////////////////////////////

// Create a run to deposit money into the account
var depositRun = routineInstance.CreateMethodRunAsync(
"deposit", // The name of the method to execute (I can get this from the Routine Documentation)
new JObject() // The actual input parameters (as json) required to run the deposit method
{
{"amount", income / 12 } // Deposit a monthly income amount
}
).Result;

// Execute the deposit run and wait for it to successfully complete
var depositRunResult = depositRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;


//////////////////////////////////////////////////
/// Example 3A: Get Account Summary (Against the DotNet Class)
//////////////////////////////////////////////////

// Get the account summary from the DotNet class
var accountSummaryDotNet = dotNetClassInstance.GetAccountSummary();

BRApi.ErrorLog.LogObject(si, "Account Summary from DotNet Class", accountSummaryDotNet);


//////////////////////////////////////////////////
/// Example 3B: Get Account Summary (Against the Routine Instance)
//////////////////////////////////////////////////

// Create a run to get the account summary
var summaryRun = routineInstance.CreateMethodRunAsync("get_account_summary").Result; // The name of the method to execute


// Execute the summary run and wait for it to successfully complete
var summaryRunResult = summaryRun.StartAndWaitForSuccessfulCompletionResultAsync().Result;

// Get the account summary artifact from the routine instance
var accountSummaryArtifact = summaryRunResult.GetArtifactInfo("account_summary")?.GetArtifactAsync().Result;

// Deserialize the account summary artifact into an AccountSummaryDto object
var accountSummaryJson = accountSummaryArtifact?.GetDataAsObjectAsync<JObject>().Result;
var accountSummaryJsonDto = accountSummaryJson?.ToObject<AccountSummaryDto>();

BRApi.ErrorLog.LogObject(si, "Account Summary from Routine Instance", accountSummaryJsonDto);

return null;
}
catch (Exception ex)
{
throw ErrorHandler.LogWrite(si, new XFException(si, ex));
}
}
}

public class BankAccountMgmt
{

private string _name;
private DateTime _dateOfBirth;
private double _yearlyIncome;
private double _initialDeposit;
private string _accountType;
private double _balance;

private DateTime _creationDate;
private DateTime? _lastWithdrawalDate;
private DateTime? _lastDepositDate;


public BankAccountMgmt(string name, DateTime dateOfBirth, double yearlyIncome, double initialDeposit, string accountType)
{
// Validate inputs
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Account holder name cannot be empty.", nameof(name));

// Must be 18 years or older
if (dateOfBirth > DateTime.Now.AddYears(-18))
throw new ArgumentException("Account holder must be at least 18 years old.", nameof(dateOfBirth));

// Must have a valid yearly income
if (yearlyIncome < 0)
throw new ArgumentOutOfRangeException(nameof(yearlyIncome), "Income cannot be negative.");

// Must have a valid initial deposit
if (initialDeposit < 0)
throw new ArgumentOutOfRangeException(nameof(initialDeposit), "Initial deposit cannot be negative.");

// Ensure account type is one of "checkings", "savings", or "high-yield savings"
var validAccountTypes = new[] { "checkings", "savings", "high-yield savings" };
if (!validAccountTypes.Contains(accountType.ToLower()))
throw new ArgumentException("Invalid account type. Valid types are: checkings, savings, high-yield savings.", nameof(accountType));

// Set user defined member variables
_name = name;
_dateOfBirth = dateOfBirth;
_yearlyIncome = yearlyIncome;
_initialDeposit = initialDeposit;
_accountType = accountType;
_balance = initialDeposit;

// Save system managed member variables
_creationDate = DateTime.Now;
_lastWithdrawalDate = null;
_lastDepositDate = _creationDate;
}

public void Deposit(double amount)
{
// Logic to deposit money into the account
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive.");

_balance += amount;
}

public double Withdraw(double amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive.");

if (amount > _balance)
throw new InvalidOperationException("Insufficient funds for withdrawal.");

_balance -= amount;

return amount;
}

public AccountSummaryDto GetAccountSummary()
{
return new AccountSummaryDto
{
AccountCreationDate = _creationDate,
AccountHolder = _name,
AccountBalance = _balance,
LastWithdrawalDate = _lastWithdrawalDate,
LastDepositDate = _lastDepositDate
};
}
}

public class AccountSummaryDto
{
[Newtonsoft.Json.JsonProperty("account_creation_date")]
public DateTime AccountCreationDate { get; set; }

[Newtonsoft.Json.JsonProperty("account_holder")]
public string AccountHolder { get; set; }

[Newtonsoft.Json.JsonProperty("account_balance")]
public double AccountBalance { get; set; }

[Newtonsoft.Json.JsonProperty("last_withdrawal_date")]
public DateTime? LastWithdrawalDate { get; set; }

[Newtonsoft.Json.JsonProperty("last_deposit_date")]
public DateTime? LastDepositDate { get; set; }
}
}

You should be able to create a Business Rule called “AnatomyOfARoutine” and copy/paste the code above in if you are on a v4.0.0+ version of Xperiflow.

info

Do not forget to set the “Referenced Assemblies” in your business rule to point to Xperiflow Business Rules: WS\Workspace.XBR.Xperiflow

Was this page helpful?