Files
storybook/docs/reference/12-decorators.md
Sienna Meridian Satterwhite 16deb5d237 release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES:
- Relationship syntax now requires blocks for all participants
- Removed self/other perspective blocks from relationships
- Replaced 'guard' keyword with 'if' for behavior tree decorators

Language Features:
- Add tree-sitter grammar with improved if/condition disambiguation
- Add comprehensive tutorial and reference documentation
- Add SBIR v0.2.0 binary format specification
- Add resource linking system for behaviors and schedules
- Add year-long schedule patterns (day, season, recurrence)
- Add behavior tree enhancements (named nodes, decorators)

Documentation:
- Complete tutorial series (9 chapters) with baker family examples
- Complete reference documentation for all language features
- SBIR v0.2.0 specification with binary format details
- Added locations and institutions documentation

Examples:
- Convert all examples to baker family scenario
- Add comprehensive working examples

Tooling:
- Zed extension with LSP integration
- Tree-sitter grammar for syntax highlighting
- Build scripts and development tools

Version Updates:
- Main package: 0.1.0 → 0.2.0
- Tree-sitter grammar: 0.1.0 → 0.2.0
- Zed extension: 0.1.0 → 0.2.0
- Storybook editor: 0.1.0 → 0.2.0
2026-02-13 21:52:03 +00:00

15 KiB

Decorators

Decorators are special nodes that wrap a single child and modify its execution behavior. They enable timing control, retry logic, conditional execution, and result inversion without modifying the child node itself.

What Are Decorators?

Decorators sit between a parent and child node, transforming the child's behavior:

Parent
  └─ Decorator
       └─ Child

The decorator intercepts the child's execution, potentially:

  • Repeating it multiple times
  • Timing it out
  • Inverting its result
  • Guarding its execution
  • Forcing a specific result

Decorator Types

Storybook provides 10 decorator types:

Decorator Purpose Example
repeat Loop infinitely repeat { Patrol }
repeat(N) Loop N times repeat(3) { Check }
repeat(min..max) Loop min to max times repeat(2..5) { Search }
invert Flip success/failure invert { IsEnemy }
retry(N) Retry on failure (max N times) retry(5) { Open }
timeout(duration) Fail after duration timeout(10s) { Solve }
cooldown(duration) Run at most once per duration cooldown(30s) { Fire }
if(condition) Only run if condition true if(health > 50) { Attack }
succeed_always Always return success succeed_always { Try }
fail_always Always return failure fail_always { Test }

Syntax

<decorator> ::= <repeat-decorator>
              | <invert-decorator>
              | <retry-decorator>
              | <timeout-decorator>
              | <cooldown-decorator>
              | <if-decorator>
              | <force-result-decorator>

<repeat-decorator> ::= "repeat" <repeat-spec>? "{" <behavior-node> "}"

<repeat-spec> ::= "(" <number> ")"
                | "(" <number> ".." <number> ")"

<invert-decorator> ::= "invert" "{" <behavior-node> "}"

<retry-decorator> ::= "retry" "(" <number> ")" "{" <behavior-node> "}"

<timeout-decorator> ::= "timeout" "(" <duration-literal> ")" "{" <behavior-node> "}"

<cooldown-decorator> ::= "cooldown" "(" <duration-literal> ")" "{" <behavior-node> "}"

<if-decorator> ::= "if" "(" <expression> ")" "{" <behavior-node> "}"

<force-result-decorator> ::= ("succeed_always" | "fail_always") "{" <behavior-node> "}"

<duration-literal> ::= <number> ("s" | "m" | "h" | "d")

Repeat Decorators

Infinite Repeat: repeat

Loops the child infinitely. The child is re-executed immediately after completing (success or failure).

behavior InfinitePatrol {
    repeat {
        PatrolRoute
    }
}

Execution:

  1. Run child
  2. Child completes (success or failure)
  3. Immediately run child again
  4. Go to step 2 (forever)

Use cases:

  • Perpetual patrols
  • Endless background processes
  • Continuous monitoring

Warning: Infinite loops never return to parent. Ensure they're appropriate for your use case.

Repeat N Times: repeat N

Repeats the child exactly N times, then returns success.

behavior CheckThreeTimes {
    repeat(3) {
        CheckDoor
    }
}

Execution:

  1. Counter = 0
  2. Run child
  3. Counter++
  4. If counter < N, go to step 2
  5. Return success

Use cases:

  • Fixed iteration counts
  • "Try three times then give up"
  • Deterministic looping

Repeat Range: repeat min..max

Repeats the child between min and max times. At runtime, a specific count is selected within the range.

behavior SearchRandomly {
    repeat(2..5) {
        SearchArea
    }
}

Execution:

  1. Select count C randomly from [min, max]
  2. Repeat child C times (as in repeat N)

Use cases:

  • Variable behavior
  • Procedural variation
  • Non-deterministic actions

Validation:

  • min ≥ 0
  • max ≥ min
  • Both must be integers

Invert Decorator

Inverts the child's return value: success becomes failure, failure becomes success.

behavior AvoidEnemies {
    invert {
        IsEnemyNearby  // Success if NOT nearby
    }
}

Truth table:

Child returns Decorator returns
Success Failure
Failure Success
Running Running (unchanged)

Use cases:

  • Negating conditions ("if NOT X")
  • Inverting success criteria
  • Converting "found" to "not found"

Example:

behavior SafeExploration {
    choose safe_actions {
        // Only explore if NOT dangerous
        then explore {
            invert { IsDangerous }
            ExploreArea
        }

        // Default: Stay put
        Wait
    }
}

Retry Decorator

Retries the child up to N times on failure. Returns success if any attempt succeeds, failure if all N attempts fail.

behavior PersistentDoor {
    retry(5) {
        OpenLockedDoor
    }
}

Execution:

  1. Attempts = 0
  2. Run child
  3. If child succeeds → return success
  4. If child fails:
    • Attempts++
    • If attempts < N, go to step 2
    • Else return failure

Use cases:

  • Unreliable actions (lockpicking, persuasion)
  • Network/resource operations
  • Probabilistic success

Example with context:

behavior Thief_PickLock {
    then attempt_entry {
        // Try to pick lock (may fail)
        retry(3) {
            PickLock
        }

        // If succeeded, enter
        EnterBuilding

        // If failed after 3 tries, give up
    }
}

Validation:

  • N must be ≥ 1

Timeout Decorator

Fails the child if it doesn't complete within the specified duration.

behavior TimeLimitedPuzzle {
    timeout(30s) {
        SolvePuzzle
    }
}

Execution:

  1. Start timer
  2. Run child each tick
  3. If child completes before timeout → return child's result
  4. If timer expires → return failure (interrupt child)

Use cases:

  • Time-limited actions
  • Preventing infinite loops
  • Enforcing deadlines

Duration formats:

  • 5s - 5 seconds
  • 10m - 10 minutes
  • 2h - 2 hours
  • 3d - 3 days

Example:

behavior QuickDecision {
    choose timed_choice {
        // Give AI 5 seconds to find optimal move
        timeout(5s) {
            CalculateOptimalStrategy
        }

        // Fallback: Use simple heuristic
        UseQuickHeuristic
    }
}

Notes:

  • Timer starts when decorator is first entered
  • Timer resets if decorator exits and re-enters
  • Child node should handle interruption gracefully

Cooldown Decorator

Prevents the child from running more than once per cooldown period. Fails immediately if called within cooldown.

behavior SpecialAbility {
    cooldown(30s) {
        FireCannon
    }
}

Execution:

  1. Check last execution time
  2. If (current_time - last_time) < cooldown → return failure
  3. Else:
    • Run child
    • Record current_time as last_time
    • Return child's result

Use cases:

  • Rate-limiting abilities
  • Resource cooldowns (spells, items)
  • Preventing spam

Example:

behavior Mage_SpellCasting {
    choose spells {
        // Fireball: 10 second cooldown
        cooldown(10s) {
            CastFireball
        }

        // Lightning: 5 second cooldown
        cooldown(5s) {
            CastLightning
        }

        // Basic attack: No cooldown
        MeleeAttack
    }
}

State management:

  • Cooldown state persists across behavior tree ticks
  • Each cooldown decorator instance has independent state
  • Cooldown timers are per-entity (not global)

If Decorator

Only runs the child if the condition is true. Fails immediately if condition is false.

behavior ConditionalAttack {
    if(health > 50) {
        AggressiveAttack
    }
}

Execution:

  1. Evaluate condition expression
  2. If true → run child and return its result
  3. If false → return failure (do not run child)

Use cases:

  • Preconditions ("only if X")
  • Resource checks ("only if have mana")
  • Safety checks ("only if safe")

Expression syntax: See Expression Language for complete syntax.

Examples:

behavior GuardedActions {
    choose options {
        // Only attack if have weapon and enemy close
        if(has_weapon and distance < 10) {
            Attack
        }

        // Only heal if health below 50%
        if(health < (max_health * 0.5)) {
            Heal
        }

        // Only flee if outnumbered
        if(enemy_count > ally_count) {
            Flee
        }
    }
}

Comparison with bare if conditions:

// Using bare 'if' condition (checks every tick, no body)
then approach_and_attack {
    if(enemy_nearby)
    Approach
    Attack
}

// Using 'if' decorator with body (precondition check, fails fast)
if(enemy_nearby) {
    then do_attack {
        Approach
        Attack
    }
}

The if decorator with a body is more efficient for gating expensive subtrees.

Force Result Decorators

succeed_always

Always returns success, regardless of child's actual result.

behavior TryOptionalTask {
    succeed_always {
        AttemptBonus  // Even if fails, we don't care
    }
}

Use cases:

  • Optional tasks that shouldn't block progress
  • Logging/monitoring actions
  • Best-effort operations

Example:

behavior QuestSequence {
    then main_quest {
        TalkToNPC

        // Try to find secret, but don't fail quest if not found
        succeed_always {
            SearchForSecretDoor
        }

        ReturnToQuestGiver
    }
}

fail_always

Always returns failure, regardless of child's actual result.

behavior TestFailure {
    fail_always {
        AlwaysSucceedsAction  // But we force it to fail
    }
}

Use cases:

  • Testing/debugging
  • Forcing alternative paths
  • Disabling branches temporarily

Example:

behavior UnderConstruction {
    choose abilities {
        // Temporarily disabled feature
        fail_always {
            NewExperimentalAbility
        }

        // Fallback to old ability
        ClassicAbility
    }
}

Combining Decorators

Decorators can nest to create complex behaviors:

behavior ComplexPattern {
    // Repeat 3 times, each with 10 second timeout
    repeat(3) {
        timeout(10s) {
            SolveSubproblem
        }
    }
}

More complex nesting:

behavior ResilientAction {
    // If: Only if health > 30
    if(health > 30) {
        // Timeout: Must complete in 20 seconds
        timeout(20s) {
            // Retry: Try up to 5 times
            retry(5) {
                // Cooldown: Can only run once per minute
                cooldown(1m) {
                    PerformComplexAction
                }
            }
        }
    }
}

Execution order: Outside → Inside

  1. If checks condition
  2. Timeout starts timer
  3. Retry begins first attempt
  4. Cooldown checks last execution time
  5. Child action runs

Duration Syntax

Timeout and cooldown decorators use duration literals:

<duration-literal> ::= <number> <unit>

<unit> ::= "s"    // seconds
         | "m"    // minutes
         | "h"    // hours
         | "d"    // days

<number> ::= <digit>+

Examples:

  • 5s - 5 seconds
  • 30s - 30 seconds
  • 2m - 2 minutes
  • 10m - 10 minutes
  • 1h - 1 hour
  • 24h - 24 hours
  • 7d - 7 days

Validation:

  • Number must be positive integer
  • No compound durations (use 120s not 2m if runtime expects seconds)
  • No fractional units (1.5m not allowed; use 90s)

Comparison Table

Decorator Affects Success Affects Failure Affects Running Stateful
repeat Repeat Repeat Wait Yes (counter)
repeat N Repeat Repeat Wait Yes (counter)
repeat min..max Repeat Repeat Wait Yes (counter)
invert → Failure → Success Unchanged No
retry N → Success Retry or fail Wait Yes (attempts)
timeout dur → Success → Success → Failure if expired Yes (timer)
cooldown dur → Success → Success → Success Yes (last time)
if(expr) → Success → Success → Success No
succeed_always → Success → Success → Success No
fail_always → Failure → Failure → Failure No

Stateful decorators maintain state across ticks. Stateless decorators evaluate fresh every tick.

Validation Rules

  1. Child required: All decorators must have exactly one child node
  2. Repeat count: repeat N requires N ≥ 0
  3. Repeat range: repeat min..max requires 0 ≤ min ≤ max
  4. Retry count: retry N requires N ≥ 1
  5. Duration positive: Timeout/cooldown durations must be > 0
  6. Duration format: Must match <number><unit> (e.g., 10s, 5m)
  7. Guard expression: Guard condition must be valid expression
  8. No empty decorators: repeat { } is invalid (missing child)

Use Cases by Category

Timing Control

  • timeout: Prevent infinite loops, enforce time limits
  • cooldown: Rate-limit abilities, prevent spam
  • repeat: Continuous processes, patrols

Reliability

  • retry: Handle unreliable actions, probabilistic success
  • if: Precondition checks, resource validation
  • succeed_always: Optional tasks, best-effort

Logic Control

  • invert: Negate conditions, flip results
  • fail_always: Disable branches, testing

Iteration

  • repeat N: Fixed loops, deterministic behavior
  • repeat min..max: Variable loops, procedural variation

Best Practices

1. Use Guards for Expensive Checks

Avoid:

then expensive {
    if(complex_condition)
    ExpensiveOperation
}

Prefer:

if(complex_condition) {
    ExpensiveOperation  // Only runs if condition passes
}

2. Combine Timeout with Retry

Avoid: Infinite retry loops

Prefer:

timeout(30s) {
    retry(5) {
        UnreliableAction
    }
}

3. Use Cooldowns for Rate Limiting

Avoid: Manual timing in actions

Prefer:

cooldown(10s) {
    FireCannon
}

4. Invert for Readable Conditions

Avoid:

choose options {
    then branch_a {
        if(not is_dangerous)
        Explore
    }
}

Prefer:

choose options {
    then branch_a {
        invert { IsDangerous }
        Explore
    }
}

5. succeed_always for Optional Tasks

Avoid:

then quest {
    MainTask
    choose optional {
        BonusTask
        DoNothing  // Awkward fallback
    }
    NextTask
}

Prefer:

then quest {
    MainTask
    succeed_always { BonusTask }  // Try bonus, don't fail quest
    NextTask
}

Cross-References

  • Composability: Decorators can nest for complex control flow
  • Separation of concerns: Decorators handle control flow, children handle logic
  • State management: Stateful decorators (repeat, retry, timeout, cooldown) persist across ticks
  • Performance: Guards prevent unnecessary child execution