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:
- Run child
- Child completes (success or failure)
- Immediately run child again
- 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:
- Counter = 0
- Run child
- Counter++
- If counter < N, go to step 2
- 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:
- Select count C randomly from [min, max]
- 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:
- Attempts = 0
- Run child
- If child succeeds → return success
- 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:
- Start timer
- Run child each tick
- If child completes before timeout → return child’s result
- If timer expires → return failure (interrupt child)
Use cases:
- Time-limited actions
- Preventing infinite loops
- Enforcing deadlines
Duration formats:
5s- 5 seconds10m- 10 minutes2h- 2 hours3d- 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:
- Check last execution time
- If (current_time - last_time) < cooldown → return failure
- 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:
- Evaluate condition expression
- If true → run child and return its result
- 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
- If checks condition
- Timeout starts timer
- Retry begins first attempt
- Cooldown checks last execution time
- 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 seconds30s- 30 seconds2m- 2 minutes10m- 10 minutes1h- 1 hour24h- 24 hours7d- 7 days
Validation:
- Number must be positive integer
- No compound durations (use
120snot2mif runtime expects seconds) - No fractional units (
1.5mnot allowed; use90s)
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
- Child required: All decorators must have exactly one child node
- Repeat count:
repeat Nrequires N ≥ 0 - Repeat range:
repeat min..maxrequires 0 ≤ min ≤ max - Retry count:
retry Nrequires N ≥ 1 - Duration positive: Timeout/cooldown durations must be > 0
- Duration format: Must match
<number><unit>(e.g.,10s,5m) - Guard expression: Guard condition must be valid expression
- 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
- Behavior Trees - Using decorators in behavior trees
- Expression Language - Guard condition syntax
- Value Types - Duration literals
- Design Patterns - Common decorator patterns
Related Concepts
- 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