For developers
A .NET Standard 2.1 runtime built for maximum compatibility.
Your game owns the loop — the engine hands you data, you decide what to do with it.
Pull-based API
The runtime returns data. Your game decides what to do with it. No callback wiring, no event handlers — just a loop the game drives.
while (true) {
foreach (var e in runtime.GetEventQueue()) {
HandleEvent(e);
}
var choices = runtime.GetChoices();
if (choices.IsComplete) break;
var picked = await ui.PresentChoices(choices.Choices);
runtime.SelectChoice(picked.Uuid);
} Typed events
Polymorphic C# event objects, not untyped dictionaries.
SequenceEvent, HookEvent,
ActivityEvent, VariableChangedEvent,
ActChangedEvent. Pattern-match them.
hook.ParamsAs<T>() gives type-safe access to custom
parameters.
foreach (var e in runtime.GetEventQueue()) {
switch (e) {
case SequenceEvent seq:
await media.Play(seq.File);
break;
case HookEvent hook when hook.Name == "show_meter":
var p = hook.ParamsAs<ShowMeterParams>();
ui.ShowMeter(p.Value, p.Min, p.Max);
break;
case VariableChangedEvent v:
ui.UpdateVariable(v.Key, v.NewValue, v.Min, v.Max);
break;
}
} Preload the next content
PeekSequences() tells you, for every currently visible
choice, which media would play first if picked — without advancing
state. Prewarm videos, load audio, decode images. Zero-latency
transitions.
foreach (var peek in runtime.PeekSequences()) {
if (peek.Type == MediaItemType.Video) {
media.Prewarm(peek.File);
}
} Video annotations and story beats
GetStoryBeats() returns beats from media the player has
actually seen, in play order, with importance that decays the further
you move past each beat (each beat sets its own decay value). Build
"previously on…" recaps, character-aware ambience, or analytics — from
data already in your story.
var beats = runtime.GetStoryBeats();
foreach (var beat in beats) {
if (beat.Importance >= StoryBeatImportance.Major) {
recapBuilder.Add(beat.MediaId, beat.At);
}
} Activity system
Delegate control to the game for minigames, puzzles, or QTEs. Typed
variable wiring with constraints and modifiers. Set each return value
via SetField() and call Complete() —
constraints are validated on set, mandatory fields on commit.
case ActivityEvent activity:
var result = await minigame.Run(activity.Name, activity.Fields);
foreach (var field in result.Values) {
activity.SetField(field.Id, field.Value);
}
activity.Complete();
break; Save and restore
Opaque save state. SaveToJson() gives you a string;
LoadFromJson(save) puts everything back — play history
included, so story-beat recall survives reloads. Your save system
doesn't need to know about conversation internals.
var save = runtime.SaveToJson();
playerSave.Write(save);
// later…
var loaded = playerSave.Read();
runtime.LoadFromJson(loaded); CLI for CI
storybonsai validate checks every expression and reference
in your project. storybonsai pathtest runs randomised
playthroughs with configurable seed and strategy. Non-zero exit on
failure. Pipe --format json into dashboards, Slack bots,
or archive it with the build.
One file per node
Every node is its own JSON file. Small, focused diffs. Merge-conflict-free collaboration between writers.
Single-player or multiplayer. Native from day one.
Characters are tagged with a player ID, so each visible choice routes
to the right player automatically. SelectChoices(choiceA, choiceB)
submits every player's pick at once and the runtime applies them
in a single pass. Sequences and activities also carry player IDs,
so media and input reach the right screen. Network transport is
your call — the runtime is network-agnostic.
var p1 = await ui.PresentChoices(choicesForP1);
var p2 = await ui.PresentChoices(choicesForP2);
runtime.SelectChoices(p1.Uuid, p2.Uuid); Platform support
Unity, Godot (.NET), MonoGame. .NET Standard 2.1 for maximum compatibility with any .NET-capable engine.