← BlogScenario DSL (OpenSCENARIO)· 4/4

How an OSC Scenario Becomes a Behavior Tree — Compiling serial and parallel into a Tree

How a human-readable OSC scenario text becomes an executable Behavior Tree: serial maps to Sequence, parallel maps to Parallel, and each action becomes a leaf that emits SUCCESS/RUNNING/FAILURE.

~4 min
OpenSCENARIObehavior-treepy_treescompilationexecution

How an OSC Scenario Becomes a Behavior Tree

A scenario written in OpenSCENARIO DSL is human-readable text. Computers can't execute text directly. The bridge between the two is compilation into a Behavior Tree (BT) — the process by which a declarative scenario is transformed into an executable tree.

Grammar Becomes Tree Nodes

The key is that OSC's structural syntax maps 1:1 to BT node types.

Rendering diagram…
  • serial → Sequence node: executes children in order; each must complete before the next begins.
  • parallel → Parallel node: executes children simultaneously.
  • Each action (spawn, move, emit, assert, etc.) → leaf node: a single concrete behavior.

After compilation, the scenario the author wrote becomes the execution tree — with its structure preserved intact.

The Leaf Language — SUCCESS / RUNNING / FAILURE

The elegance of a BT comes from three simple signals. Every leaf (action) returns one of three values on every tick:

Rendering diagram…
  • SUCCESS — Action complete. The parent Sequence advances to the next child.
  • RUNNING — Still in progress (e.g., "moving to goal"). Re-evaluated next tick.
  • FAILURE — Failed. The parent routes to a recovery branch or terminates the scenario.

These three signals, combined, express sequencing, concurrency, repetition, and recovery across arbitrarily complex scenario flows. This is exactly the same mechanism Nav2 uses to coordinate navigation with a BT — the domain is different, the principle is identical.

A Sequence node's tick is this simple.

# serial -> Sequence: children in order, next only after the previous succeeds
def tick_sequence(children):
    for child in children:
        status = child.tick()
        if status != SUCCESS:
            return status        # RUNNING -> wait, FAILURE -> stop
    return SUCCESS               # succeeds only if all succeed

Why Compile to BT?

💡 Separation of declaration and execution — OSC handles "what" (declaration); the BT handles "how to tick and execute" (execution). This separation means scenario authors don't need to know about tree ticking, and the execution engine doesn't need to understand scenario semantics. It's also what makes self-containment achievable — the compilation layer is clean enough to keep the two sides independent.

⚠️ Don't mistake RUNNING for SUCCESS — Treating an asynchronous operation (movement, waiting) as synchronous and immediately resolving RUNNING as SUCCESS causes the next step to begin before the current one finishes. "Return RUNNING until movement completes" must be implemented accurately for the scenario to behave as intended.

In a nutshell

📌 OSC text is compiled into a Behavior Tree for execution — serial→Sequence, parallel→Parallel, and each action becomes a leaf that returns SUCCESS/RUNNING/FAILURE every tick. The combination of these three signals expresses sequencing, concurrency, and recovery; the separation of declaration (OSC) from execution (BT) is what keeps the DSL clean.

See standard test results with videos and verdict evidence on the imSim Portal.

View public results →