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:

DecoratorPurposeExample
repeatLoop infinitelyrepeat { Patrol }
repeat(N)Loop N timesrepeat(3) { Check }
repeat(min..max)Loop min to max timesrepeat(2..5) { Search }
invertFlip success/failureinvert { IsEnemy }
retry(N)Retry on failure (max N times)retry(5) { Open }
timeout(duration)Fail after durationtimeout(10s) { Solve }
cooldown(duration)Run at most once per durationcooldown(30s) { Fire }
if(condition)Only run if condition trueif(health > 50) { Attack }
succeed_alwaysAlways return successsucceed_always { Try }
fail_alwaysAlways return failurefail_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 returnsDecorator returns
SuccessFailure
FailureSuccess
RunningRunning (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

DecoratorAffects SuccessAffects FailureAffects RunningStateful
repeatRepeatRepeatWaitYes (counter)
repeat NRepeatRepeatWaitYes (counter)
repeat min..maxRepeatRepeatWaitYes (counter)
invert→ Failure→ SuccessUnchangedNo
retry N→ SuccessRetry or failWaitYes (attempts)
timeout dur→ Success→ Success→ Failure if expiredYes (timer)
cooldown dur→ Success→ Success→ SuccessYes (last time)
if(expr)→ Success→ Success→ SuccessNo
succeed_always→ Success→ Success→ SuccessNo
fail_always→ Failure→ Failure→ FailureNo

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