Lifecycle Hooks

Define behavior at different execution phases of a node.

Execution Phases

Every node execution follows this sequence:

First-Time Activation (Only on first tick)

  1. 1. OnEnabled Called once when the node is first activated (after variable initialization)

Normal Execution (Every tick while running)

  1. 2. OnEnter Called once when the node starts or re-enters execution
  2. 3. OnBaseTick Defines the core behavior and returns the node's status (Success/Failure/Running)
  3. 4. OnTick Side-effect hook for logging, observations, and external systems (no status)
  4. 5. OnSuccess / OnFailure Called when the node finishes with a specific status (Success or Failure)
  5. 6. OnExit Called when the node finishes (Success or Failure)

Reset/Cleanup (Only during reset operations)

  1. 7. OnDisabled Called ONLY when the node is reset (NOT part of normal success/failure flow)

Using Lifecycle Hooks

These lifecycle methods must be called inside a node's definition. Here's an example:

Sequence(() =>
{
    OnEnter(() => Debug.Log("Entered Sequence Node!"));
    OnExit(() => Debug.Log("Exited Sequence Node!"));

    Wait(1f);
    // Rest of your nodes...
});
Sequence(() =>
{
    OnEnter(() => Debug.Log("Entered Sequence Node!"));
    OnExit(() => Debug.Log("Exited Sequence Node!"));

    Wait(1f);
    // Rest of your nodes...
});

Complete Lifecycle Flow

The complete lifecycle of a node from creation to reset looks like this:

OnEnabled Hook

Called once when the node is first activated (transitions from None to Enabling state). This happens:

  • On the first tick of the node
  • After variable initializers run
  • Before OnEnter is called
  • Sets the node's Active flag to true

OnEnabled marks the node as "alive" in the behavior tree and is perfect for one-time setup that persists across re-entries.

When OnEnabled Runs

OnEnabled runs:

  • ✅ First tick of the node
  • ✅ When tree is first started
  • ✅ After ResetImmediately() or ResetGracefully() is called and node is ticked again

OnEnabled does NOT run:

  • ❌ During re-entry (when reactive invalidation causes the node to restart)
  • ❌ When Tick(allowReEnter: true) is called on a Done node
  • ❌ After OnExit when the node completes normally

OnEnabled vs OnEnter

OnEnabled OnEnter
Called once on first activation Called every time node enters
Runs before OnEnter Runs after OnEnabled
For persistent setup For per-execution initialization
Sets Active = true Node already active
Not called on re-entry IS called on re-entry

Use OnEnabled For

  • Event subscriptions that should persist across re-entries
  • Resource allocation (NavMeshAgent setup, animation controller references)
  • System registration (registering with game managers)
  • One-time expensive setup that shouldn't repeat on re-entry

OnEnabled and Reactive Trees

In reactive behavior trees, when a node invalidates and re-enters, OnEnabled is NOT called again. Only OnEnter is called. This is intentional - the node is still "active" in the tree, just restarting its execution.

Best Practice

Use OnEnabled for expensive setup operations and resource allocation. Use OnEnter for lightweight per-execution initialization.

OnDisabled Hook

Called only during reset operations - this is the key difference from other lifecycle hooks. OnDisabled is NOT part of the normal success/failure flow.

OnDisabled runs when:

  • ResetImmediately() is called on the node or its parent
  • ResetGracefully() is called on the node or its parent
  • ✅ During reactive invalidation when subsequent nodes need to be reset
  • ✅ When the tree is destroyed (e.g., OnDestroy() calls Tree.ResetImmediately())

OnDisabled does NOT run when:

  • ❌ Node completes normally with Success or Failure
  • ❌ OnExit is called at the end of execution
  • ❌ Node transitions from Running to Done state
  • ❌ Reactive re-entry occurs (node restarts via invalidation)

Why OnDisabled is Separate

ClosureBT separates normal completion cleanup (OnExit) from reset/destroy cleanup (OnDisabled) to distinguish between two fundamentally different scenarios:

  • OnExit → "I finished my task, clean up this execution"
  • OnDisabled → "I'm being removed from the tree, clean up all resources"

This distinction is especially important for reactive trees where nodes may re-enter multiple times without being disabled.

Use OnDisabled For

  • Unsubscribing from events registered in OnEnabled
  • Releasing allocated resources (object pools, NavMeshAgents)
  • Disconnecting from systems (event buses, game managers)
  • Final cleanup that should only happen when the node is truly done (reset/destroyed)

Common Mistake

Do not expect OnDisabled to be called when a node completes normally! Use OnExit for normal completion cleanup, and OnDisabled only for reset/destroy scenarios.

Async Support

Both OnEnabled and OnDisabled support async/await with UniTask and receive CancellationToken parameters for proper cleanup:

OnEnabled(async ct =>
{
    await LoadResourcesAsync(ct);
    Debug.Log("Resources loaded");
});

OnDisabled(async ct =>
{
    await SaveStateAsync(ct);
    await UnloadResourcesAsync(ct);
    Debug.Log("Cleanup complete");
});
OnEnabled(async ct =>
{
    await LoadResourcesAsync(ct);
    Debug.Log("Resources loaded");
});

OnDisabled(async ct =>
{
    await SaveStateAsync(ct);
    await UnloadResourcesAsync(ct);
    Debug.Log("Cleanup complete");
});

OnEnterExitPair

Combines OnEnter and OnExit with matched actions:

OnEnterExitPair(
    onEnter: () => Debug.Log("Starting"),
    onExit: () => Debug.Log("Done")
);
OnEnterExitPair(
    onEnter: () => Debug.Log("Starting"),
    onExit: () => Debug.Log("Done")
);

The OnExit() will only run if its OnEnter() counterpart has run. During interruptions/cancellations, it's possible not all OnEnters() will run due to cancellation. Use this to ensure that is OnEnter() counterpart has run for its OnExit() pair

Next: Variables

Understand how to use variables in your behavior trees.

Read about Variables →