Conditions¶
Conditional logic can be applied to any document. Currently, we use it to run Game Events. Any condition starts with one of 2 logical operators And / Or. Then you can add any other types of conditions. The conditions can be as complex and nested as you want.
- Dates Range, Day Of The Week, and Time Of The Day are conditions based on a specific time.
- ABTest Condition checks if the user was assigned to a specific A/B Test and Variant.
- Active Event triggers only if the specified GameEvent is active right now.
- Segment Condition checks if the user belongs to a specific Segment.
- Revenue checks if the user has generated a specific amount of in-app/ads revenue(count) during certain days.
- Was Item Purchased checks if a StoreItem was purchased at least once.
- Was Product Purchased checks if a Product was purchased at least once.
- Primitive allows you to use any custom User Properties.

- Country checks if the user is from the specified country.
Note: Please use this Country condition under the System category instead of getting the custom Country User Property from Primitive condition.

Logic Class - Direct Condition API¶
When you add a Condition parameter to your documents (like GameEvents, Offers, or custom templates), you get access to the Logic class - a powerful API for working with conditions directly at runtime.
How to Access Logic¶
Conditions are accessed as parameters from parent documents, not by ID:
// Get event and access its condition
var weekendEvent = Balancy.CMS.GetModelByUnnyId<GameEvent>("789"); // Use actual numeric ID
var eventCondition = weekendEvent?.Condition; // Logic instance
if (eventCondition != null && eventCondition.CanPass())
{
Debug.Log("Weekend event is available - show to player");
ShowWeekendSale();
}
Available Methods¶
CanPass()¶
Check if the condition passes right now.
bool CanPass()
Returns: true if condition passes, false otherwise
Example:
var vipOffer = Balancy.CMS.GetModelByUnnyId<GameOffer>("1234");
var offerCondition = vipOffer?.Condition;
if (offerCondition != null && offerCondition.CanPass())
{
Debug.Log("VIP offer is available - show to player");
ShowVIPOffer(vipOffer);
}
else
{
Debug.Log("VIP offer not available");
}
SubscribeForStatusChange()¶
Get notified when the condition status changes (passes or fails).
void SubscribeForStatusChange(Action<bool> callback)
Parameters:
callback: Action<bool>- Function called when condition status changestrue= condition just passedfalse= condition just failed
Example:
private Balancy.Models.SmartObjects.Conditions.Logic _eventCondition;
void Start()
{
// Get weekend event and access its condition
var weekendEvent = Balancy.CMS.GetModelByUnnyId<GameEvent>("789");
_eventCondition = weekendEvent?.Condition;
if (_eventCondition != null)
{
// Subscribe to condition changes
_eventCondition.SubscribeForStatusChange(OnWeekendStatusChanged);
// Check initial state
UpdateWeekendUI(_eventCondition.CanPass());
}
}
void OnWeekendStatusChanged(bool isActive)
{
Debug.Log($"Weekend event is now: {(isActive ? "ACTIVE" : "INACTIVE")}");
UpdateWeekendUI(isActive);
if (isActive)
{
ShowWeekendBanner();
PlayEventMusic();
}
else
{
HideWeekendBanner();
RestoreNormalMusic();
}
}
void OnDestroy()
{
// Clean up subscription
if (_eventCondition != null)
{
_eventCondition.UnsubscribeFromStatusChange();
}
}
UnsubscribeFromStatusChange()¶
Remove subscription to prevent memory leaks.
void UnsubscribeFromStatusChange()
Important: Always unsubscribe when the component is destroyed or no longer needs updates.
Example:
void OnDestroy()
{
// Clean up
if (_flashSaleCondition != null)
{
_flashSaleCondition.UnsubscribeFromStatusChange();
}
}
GetSecondsLeftBeforeDeactivation()¶
Get time remaining before the condition becomes false (useful for countdowns).
int GetSecondsLeftBeforeDeactivation()
Returns:
- Seconds remaining (0+)
- int.MaxValue if condition has no time limit
Example:
var flashSale = Balancy.CMS.GetModelByUnnyId<GameEvent>("456");
var saleCondition = flashSale?.Condition;
if (saleCondition != null && saleCondition.CanPass())
{
int secondsLeft = saleCondition.GetSecondsLeftBeforeDeactivation();
if (secondsLeft > 0 && secondsLeft < int.MaxValue)
{
// Show countdown
TimeSpan timeLeft = TimeSpan.FromSeconds(secondsLeft);
countdownText.text = $"Sale ends in: {timeLeft:hh\\:mm\\:ss}";
// Update every second
InvokeRepeating(nameof(UpdateCountdown), 1f, 1f);
}
}
GetSecondsBeforeActivation()¶
Get time until the condition becomes true (useful for "coming soon" timers).
int GetSecondsBeforeActivation()
Returns:
- Seconds until activation (0+)
- int.MaxValue if condition has no start time
Example:
var upcomingEvent = Balancy.CMS.GetModelByUnnyId<GameEvent>("2001");
var eventCondition = upcomingEvent?.Condition;
if (eventCondition != null && !eventCondition.CanPass())
{
int secondsUntil = eventCondition.GetSecondsBeforeActivation();
if (secondsUntil > 0 && secondsUntil < int.MaxValue)
{
// Show "coming soon" timer
TimeSpan timeUntil = TimeSpan.FromSeconds(secondsUntil);
comingSoonText.text = $"Starts in: {timeUntil:dd\\:hh\\:mm\\:ss}";
}
}
Complete Example: Dynamic Event UI¶
Here's a complete example showing all Logic methods working together:
public class DynamicEventUI : MonoBehaviour
{
[Header("Event Configuration")]
public string eventUnnyId = "1234"; // Set in Inspector
[Header("UI References")]
public GameObject eventPanel;
public Text statusText;
public Text countdownText;
private Logic _eventCondition;
void Start()
{
// Get event and access its condition
var gameEvent = Balancy.CMS.GetModelByUnnyId<GameEvent>(eventUnnyId);
_eventCondition = gameEvent?.Condition;
if (_eventCondition != null)
{
// Subscribe to condition changes
_eventCondition.SubscribeForStatusChange(OnEventStatusChanged);
// Check initial state
UpdateUI(_eventCondition.CanPass());
}
else
{
Debug.LogWarning($"Event {eventUnnyId} or its condition not found");
eventPanel.SetActive(false);
}
}
void OnEventStatusChanged(bool isActive)
{
Debug.Log($"Event status changed: {isActive}");
UpdateUI(isActive);
}
void UpdateUI(bool isActive)
{
if (isActive)
{
ShowActiveState();
}
else
{
ShowInactiveState();
}
}
void ShowActiveState()
{
eventPanel.SetActive(true);
statusText.text = "EVENT LIVE NOW!";
statusText.color = Color.green;
// Check if event has time limit
int secondsLeft = _eventCondition.GetSecondsLeftBeforeDeactivation();
if (secondsLeft > 0 && secondsLeft < int.MaxValue)
{
// Show countdown
countdownText.gameObject.SetActive(true);
InvokeRepeating(nameof(UpdateCountdown), 0f, 1f);
}
else
{
// No time limit
countdownText.gameObject.SetActive(false);
}
}
void ShowInactiveState()
{
CancelInvoke(nameof(UpdateCountdown));
// Check if event will start soon
int secondsUntil = _eventCondition.GetSecondsBeforeActivation();
if (secondsUntil > 0 && secondsUntil < int.MaxValue)
{
// Show "coming soon"
eventPanel.SetActive(true);
statusText.text = "COMING SOON";
statusText.color = Color.yellow;
countdownText.gameObject.SetActive(true);
InvokeRepeating(nameof(UpdateComingSoon), 0f, 1f);
}
else
{
// Event not scheduled
eventPanel.SetActive(false);
}
}
void UpdateCountdown()
{
int secondsLeft = _eventCondition.GetSecondsLeftBeforeDeactivation();
if (secondsLeft > 0 && secondsLeft < int.MaxValue)
{
TimeSpan timeLeft = TimeSpan.FromSeconds(secondsLeft);
countdownText.text = $"Ends in: {timeLeft:hh\\:mm\\:ss}";
}
else
{
CancelInvoke(nameof(UpdateCountdown));
countdownText.gameObject.SetActive(false);
}
}
void UpdateComingSoon()
{
int secondsUntil = _eventCondition.GetSecondsBeforeActivation();
if (secondsUntil > 0 && secondsUntil < int.MaxValue)
{
TimeSpan timeUntil = TimeSpan.FromSeconds(secondsUntil);
countdownText.text = $"Starts in: {timeUntil:dd\\:hh\\:mm\\:ss}";
}
else
{
CancelInvoke(nameof(UpdateComingSoon));
// Event should be active now, will be handled by OnEventStatusChanged
}
}
void OnDestroy()
{
// Clean up subscriptions
if (_eventCondition != null)
{
_eventCondition.UnsubscribeFromStatusChange();
}
CancelInvoke();
}
}
Use Cases¶
- Event UI Management - Show/hide event banners when conditions change
- Countdown Timers - Display time remaining for limited-time offers
- Coming Soon Timers - Show when upcoming content will unlock
- Feature Gates - Enable/disable features based on player conditions
- Dynamic Content - Load different content when conditions change (time-of-day, player level)
Best Practices¶
// ✅ GOOD - defensive null checks
var premiumOffer = Balancy.CMS.GetModelByUnnyId<GameOffer>("3456");
var condition = premiumOffer?.Condition;
if (condition != null && condition.CanPass())
{
ShowPremiumFeature();
}
// ❌ BAD - crash if null
var premiumOffer = Balancy.CMS.GetModelByUnnyId<GameOffer>("3456");
if (premiumOffer.Condition.CanPass()) // NullReferenceException!
{
ShowPremiumFeature();
}
// ✅ GOOD - always unsubscribe
void OnDestroy()
{
if (_condition != null)
{
_condition.UnsubscribeFromStatusChange();
}
}
// ❌ BAD - memory leak
void OnDestroy()
{
// Forgot to unsubscribe!
}
Custom Conditions¶
Built-in conditions cover common cases (time ranges, profile fields, segments, etc.). Custom Conditions let you define evaluation logic that only your game code can provide — "player completed the tutorial", "player is in a guild", etc.
How It Works¶
- In the CMS dashboard, create a condition of type Custom. Add any parameters you need.
- Run code generation — the SDK generates a class for your condition.
- In your game code, override three methods:
CanPassCustom()— evaluation logic (returnstrueorfalse)Subscribe()— start listening to game events that affect this conditionUnsubscribe()— stop listening
- When your game events fire, call
ForceUpdate()to trigger re-evaluation.
Custom conditions combine with built-in conditions using And/Or/Not:
And
├── Custom("MyVIPCondition") → Player is VIP (your code)
├── TimeRange(start, end) → During holiday week (built-in)
└── ProfileField(level >= 5) → Player level >= 5 (built-in)
Implementation¶
Create a second partial class file with the same name as the auto-generated one:
// MyCustomCond.CanPass.cs (your file)
namespace Balancy.Models
{
public partial class MyCustomCond
{
public override bool CanPassCustom()
{
// Use auto-generated parameters
return PlayerData.Level >= ParamInt;
}
public override void Subscribe()
{
PlayerData.OnLevelChanged += OnLevelChanged;
}
public override void Unsubscribe()
{
PlayerData.OnLevelChanged -= OnLevelChanged;
}
private void OnLevelChanged(int newLevel)
{
// Trigger re-evaluation
ForceUpdate();
}
}
}
Override methods in a subclass and register with the factory:
import { SmartObjectsConditionsCustom } from '@balancy/core';
class MyCustomCond extends SmartObjectsConditionsCustom {
private boundHandler = () => this.forceUpdate();
public canPassCustom(): boolean {
return gameState.level >= this.getIntParam("paramInt");
}
public subscribe(): void {
gameState.on('levelChanged', this.boundHandler);
}
public unsubscribe(): void {
gameState.off('levelChanged', this.boundHandler);
}
}
// Register so the SDK instantiates your class
CMS.onTypeRequested = (templateName: string) => {
if (templateName === "MyCustomCond") return new MyCustomCond();
return null;
};
ForceUpdate Without Subscribe¶
If you don't need reactive subscriptions, skip Subscribe/Unsubscribe and call ForceUpdate directly:
// By UnnyId (static helper)
Balancy.CustomConditions.ForceUpdate("condition-unny-id");
// Or from the condition instance
myCondition.ForceUpdate();
API.CustomConditions.forceUpdate("condition-unny-id");
// Or from the condition instance
myCondition.forceUpdate();
API Reference¶
| Method | Description |
|---|---|
CanPassCustom() |
Override to provide evaluation logic. Returns true/false. Default: false. |
Subscribe() |
Override to subscribe to game events. Called when condition enters evaluation tree. |
Unsubscribe() |
Override to unsubscribe from game events. Called when condition leaves evaluation tree. |
ForceUpdate() |
Triggers re-evaluation and notifies all subscribers. Call from your event handlers. |
Technical Details
CanPassCustom()is called synchronously — keep it fast. No network calls or heavy computation.Subscribe()is called at most once per condition instance.Unsubscribe()is only called ifSubscribe()was previously called.- If
CanPassCustom()throws an exception, the SDK catches it and treats the condition asfalse. - Parameters defined in the CMS are available as typed properties (C#) or via
getIntParam()/getStringParam()(TypeScript).
Conditional Template¶
ConditionalTemplate is a special template type that automatically activates and deactivates documents based on conditions. All our built-in features: GameEvent, In-Game Shop, Segmentation, Overrides and A/B tests are built on top of it. Balancy automatically tracks the Conditions and Priorities of all Conditional Templates and notifies you when something changes.
How It Works¶
- Create multiple documents from your ConditionalTemplate (e.g., Summer Sale, Winter Sale, Default Offer)
- Add conditions to each document (e.g., date ranges, player level, A/B test)
- Set priorities for manual sorting (optional - SDK does NOT filter by priority)
- Balancy continuously evaluates conditions and returns ALL documents with passing conditions
Important: The SDK returns ALL active documents - it does NOT automatically filter by priority. The priority field is available for YOUR use (sorting, selection logic). Only when ConditionalTemplate is used as a Singleton base does the SDK automatically use priority to select a single document.
Priority Behavior¶
- A/B Tests and Overrides use priority to determine which override takes precedence when multiple overrides target the same parameter. The override with the HIGHEST priority wins.
- In-Game Store uses ConditionalTemplate to support multiple stores. If multiple stores have passing conditions, the SDK returns all of them. You can use priority to sort and select which store to display.
- Standalone ConditionalTemplates - SDK returns ALL documents with passing conditions. Priority is a field for manual sorting if needed.
- Singletons (see below) - SDK automatically uses priority to select the highest-priority document with a passing condition.
API Usage¶
1. Get All Active Documents¶
// Get ALL currently active documents (SDK returns all with passing conditions)
var activeOffers = Balancy.CMS.GetActiveConditionalTemplates<SeasonalOffer>(includeChildren: true);
Console.WriteLine($"Active offers: {activeOffers.Length}");
// Sort by priority manually (priority is for YOUR use)
var sortedOffers = activeOffers.OrderByDescending(o => o.Priority);
foreach (var offer in sortedOffers)
{
Console.WriteLine($" {offer.Name}: Priority {offer.Priority}, Discount {offer.Discount}%");
}
// Show highest priority offer
if (sortedOffers.Any())
{
ShowOffer(sortedOffers.First());
}
2. Subscribe to Activation/Deactivation¶
// Subscribe to be notified when documents activate or deactivate
Balancy.CMS.SubscribeConditionalTemplate<SeasonalEvent>((active, deactivated) =>
{
// Handle newly activated events
foreach (var evt in active)
{
Debug.Log($"Event activated: {evt.Name}");
StartEvent(evt);
ShowEventNotification(evt);
}
// Handle deactivated events
foreach (var evt in deactivated)
{
Debug.Log($"Event deactivated: {evt.Name}");
EndEvent(evt);
HideEventBanner(evt);
}
});
// Don't forget to unsubscribe when no longer needed
Balancy.CMS.UnsubscribeConditionalTemplate<SeasonalEvent>();
Use Cases¶
- Multiple active seasonal events - Summer Sale + Weekend Bonus both active simultaneously
- Player-specific offers - Multiple offers eligible for the same player
- A/B test variants - Multiple variants with passing conditions
- Feature flags - Multiple features enabled at once
- Difficulty variants - Different reward tiers based on player level
Singletons¶
Singleton is a template type with exactly one active instance per player. There are two types:
1. Regular Singleton¶
Always returns the same document. Use for static global settings that never change.
var gameSettings = Balancy.CMS.GetSingleton<GameSettings>();
var current = gameSettings.Get(); // Always returns the same document
2. ConditionalTemplate Singleton¶
Automatically switches between documents based on conditions and priority. This is the powerful option for dynamic configurations.
How It Works¶
- Create a Singleton template that inherits from ConditionalTemplate
- Create multiple documents (e.g., Summer Theme, Winter Theme, Default Theme)
- Add conditions to each document (e.g., date ranges)
- Set priorities (higher = takes precedence)
- Balancy automatically selects the highest-priority document with a passing condition
When conditions change (e.g., season changes), Balancy automatically switches to the new highest-priority document and triggers the OnChanged callback.
API Usage¶
// Get singleton wrapper
var gameSettings = Balancy.CMS.GetSingleton<GameSettings>();
// Access current value (SDK auto-selects by priority + condition for ConditionalTemplate singletons)
var currentSettings = gameSettings.Get();
if (currentSettings != null)
{
Debug.Log($"Current theme: {currentSettings.Theme}");
Debug.Log($"Difficulty: {currentSettings.Difficulty}");
}
// Subscribe to changes (for ConditionalTemplate singletons)
// OnChanged triggers when conditions change and different document becomes active
gameSettings.OnChanged += settings =>
{
Debug.Log($"Settings changed to: {settings?.UnnyId}");
if (settings != null)
{
ApplyNewSettings(settings);
Debug.Log($"New theme applied: {settings.Theme}");
}
};
Example: Seasonal Theme Singleton¶
After creating a custom Singleton, inherit it from ConditionalTemplate to enable automatic switching:

Setup:
- SummerTheme (Priority: 100, Condition: June-August)
- WinterTheme (Priority: 100, Condition: December-February)
- DefaultTheme (Priority: 0, Condition: none - fallback)
Behavior:
- During summer: SDK automatically selects SummerTheme (highest priority + passing condition)
- During winter: SDK automatically selects WinterTheme (highest priority + passing condition)
- Other times: SDK selects DefaultTheme (no condition, always passes)
- When season changes:
OnChangedcallback fires with new theme
Example: A/B Testing Singleton Configuration¶
You can A/B Test your singleton configurations. For instance, to test a special config for non-payers:

Setup:
- Default Config (Priority: 0, Condition: none)
- VIP Config (Priority: 50, Condition: Player spent > $10)
- AB Test Config (Priority: 100, Condition: Non-payer + A/B Test Group A)
The SDK will automatically select the highest-priority config with a passing condition.
Helpful information
Conditional Templates and Singletons are initialized during the Init all Managers. You can start using them as soon as Balancy is ready.
Static Conditions (AppVersion, Country, Platform, A/B Test): These don't change during a session. Once Balancy is ready, you have accurate data.
Dynamic Conditions (Time, User Properties like level): These can change during a session. Balancy will notify you through callbacks:
- ConditionalTemplates: Use
SubscribeConditionalTemplateto track activation/deactivation - Singletons: Use
OnChangedcallback to track when the active document switches
Best Practice: Don't cache Singletons. Always access them directly via DataEditor.GameConfig.Get() to ensure you have the most up-to-date data.