Skip to main content

Behavior Trees

Behavior Trees are mathematical models for planning the execution of a complex task composed of many simple tasks. Its ease of human understanding makes them much less error prone compared to hand written state switches and they are the preferred way to define complex logic in our framework.

You can build them using our online editor and then export the trees as JSON files.

Types of Nodes

Behavior Trees are built using four types of nodes:

  • Root, which defines the entry point of the tree.
  • Composites, which have multiple children and handle the scheduling of these children.
  • Decorators, which have a single child that they transform its behavior of.
  • Action, which are the leaf nodes that actually define tasks.


  • Select nodes, represented by the ? symbol, execute each child in order until one of them succeeds.
  • Sequence nodes, represented by the -> symbol, execute each child in order until one of them fails.
  • Parallel nodes, represented by the # symbol, execute each child in parallel until all of them succeed.
  • Random nodes, represented by the * symbol, pick a random child on each pulse and propagates the result as is.

For each of these primitives there's additionally a Mem* version that remembers the last child executed in the event that the child indicates it is still running. They are represented by the same symbol as the non Mem equivalent but are instead colored in red.

Generally speaking, you should use the Mem* variants, however in certain cases such as when you want to enforce a constraint, non-memory variants are useful.

For instance consider the behavior tree below.

By using the MemSequence node instead of Sequence we introduced a bug here where the visibility is checked only once and we continue to aimbot towards the target even if it's not currently visible. Swapping this with a Sequence node will make sure that the aimAt task is halted once the target is no longer visible.


Behavior tree task primitives are implemented as either:

  • Stateless functions that only take the current context as its argument returning a BT.Result.
  • Or, any object with the appropriate pulse and reset functions.
type FailResult = false | string;
type SuccessResult = true | void | undefined | null;
type Result = SuccessResult | FailResult;
type Status = typeof RUNNING | Result;

interface ITask<T> {
pulse(this: any, ctx: T): Status;
reset(this: any): void;
type SimpleTask<T> = (this: void, ctx: T) => Result;
type Task<T> = SimpleTask<T> | ITask<T>;
-- BT status codes.
BTStatus BT.SUCCESS -- Indicates success.
BTStatus BT.FAIL -- Indicates failure.
BTStatus BT.RUNNING -- Indicates async progress, halts the tree.

-- Checks if the given BTStatus is equivalent to SUCCESS / FAIL.
bool BT.IsSuccess(BTStatus status)
bool BT.IsFail(BTStatus status)

-- Converts a BTStatus to a string.
string BT.ToString(BTStatus status)

Here's a sample task that suspends the tree for a single tick on each visit and then succeeds if the context is "hello".

bt.bind("myTask", {
pulse: function (ctx) {
if (!this.ranBefore) {
this.ranBefore = true;
print("#1 = ", ctx);
return BT.RUNNING;
} else {
print("#2 = ", ctx);
return ctx == "hello" ? BT.SUCCESS : BT.FAIL;
reset: function () {
this.ranBefore = false;

A more elegant way to define tasks are using coroutines with generators. We can rewrite the previous sample as a coroutine like demonstrated below.

bt.bindGen("myTask", function* (ctx) {
print("#1 = ", ctx);
ctx = yield;
print("#2 = ", ctx);
if (ctx != "hello") {
return "fail";

You can also use an async function that takes a single context argument as a task after wrapping using BT.WrapFn or binding using bindAsync.

bt.bindAsync("myTask", async (ctx, cancellationToken) => {
await Event.OnGamePause;

Construction and members

Although you can use to create a new empty instance, behavior trees are generally constructed from their JSON definitions using BT.From or using the file path with BT.Load. You can also use variables in all node properties (e.g. timeout duration of <property> in a waiter node) and then specify a resolver in the BT.From function to remap it.

-- Creates an empty behavior tree.

-- Loads a behaviour tree from the given path to it's JSON descriptor.
BehaviorTree BT.Load(string path, (function<string key>|table)? variableResolver)

-- Loads a behaviour tree from the given JSON descriptor.
BehaviorTree BT.From(string json, (function<string key>|table)? variableResolver)

-- Properties as set in the constructing descriptor.
string BehaviorTree.title
string BehaviorTree.description

-- Root node.
BTNode? BehaviorTree.root

-- Names of every task referenced by the tree.
string[] BehaviorTree:getTasks()

-- Names of every undefined task referenced by the tree.
string[] BehaviorTree:getMissingTasks()

-- Whether or not the tree definition is complete.
bool BehaviorTree:isComplete()

-- Clones the behavior tree instance.
BehaviorTree BehaviorTree:clone()

-- Gets a bound task.
any? BehaviorTree:get(string taskName)

-- Binds a task.
void BehaviorTree:bind(string taskName, Task? t)
void BehaviorTree:bindAsync(string taskName, AsyncTask? asyncT)
void BehaviorTree:bindGen(string taskName, GeneratorTask? genT)

-- Pulses the tree and returns the status.
BTStatus BehaviorTree:pulse(any ctx)

-- Resets the tree memory.
void BehaviorTree:reset()

-- Enumerates each node in the tree.
void BehaviorTree:forEach(function<Node n> cb)
import * as BT from "@Core/BT";

let bt = BT.Load("test-tree");

bt.bind("taskOk1", () => {});
bt.bind("taskOk2", () => true);
bt.bind("taskFail1", () => false);
bt.bind("taskFail2", () => "error");
bt.bind("taskFail3", undefined);

// Equivalent to testTree.title

The BT.From function only allows loading of tree-scoped exports so make sure you use the Tree as JSON option in the editor. Given an invalid description the function may throw.

Dynamic composites

An aimbot node might need further children nodes for each module to implement, however since behavior-tree definitions are static JSON files, this wouldn't be possible without a way to dynamically create composite nodes. To solve this issue, you can instantiate all composite nodes manually.

-- Creates the composite nodes with the given children.
BTNode BT.NewSelect(Task[]? children)
BTNode BT.NewSequence(Task[]? children)
BTNode BT.NewRandom(Task[]? children)
BTNode BT.NewParallel(Task[]? children)
BTNode BT.NewMemSelect(Task[]? children)
BTNode BT.NewMemSequence(Task[]? children)
BTNode BT.NewMemRandom(Task[]? children)

Nodes have the following members.

-- Properties as set in the constructing descriptor.
string BTNode.title
string BTNode.description

-- Resets the node memory.
void BTNode:reset()

-- If composite: List of children.
BTNode[] BTNode.children
-- If composite: Inserts a child at the end of the list.
BTNode BTNode:push(Task t)
-- If composite: Inserts a child at the beginning of the list.
BTNode BTNode:unshift(Task t)

You can see an example use of this functionality below.

import * as BT from "@Core/BT";

const aimbotTask = BT.NewMemSelect();
export function AddAimbot(x: BT.Task) {
export const Tree = BT.Load("my-tree");
Tree.bind("aimbotTask", aimbotTask);
local Logic = require("@Logic")
Logic.AddAimbot(|ctx| do
-- ...
import * as BT from "@Core/BT";
import * as Logic from "@Logic";

const selNode = Logic.Tree.get("aimbot") as BT.Select;
selNode.unshift(() => {
// ...



import * as BT from "@Core/BT";

// Load the behavior tree.
const bt = BT.Load<number>("sample-tree");
print("Loaded behavior tree:", bt); // Prints 'Loaded behavior tree: A behavior tree'

// Enumerate all required tasks.
for (const k of bt.tasks) {
print("Task:", k);
// Prints: 'Task: neverFinish2'
// 'Task: neverFinish1'
print(bt.isComplete); // Prints 'false'

// Define the tasks.
bt.bindGen("neverFinish1", function* (arg: number) {
for (let n = 0; ; ++n) {
print(`=> neverFinish1(${arg} | ${n})`);
arg = yield;

bt.bind("neverFinish2", {
pulse: (arg) => {
print(`=> neverFinish2(${arg})`);
return BT.RUNNING;
reset: () => {},

print(bt.isComplete); // Prints 'true'

// Tick.
for (let i of $range(1, 9)) {
print(`--- Tick ${i} ---`);
const result = bt.pulse(i);

--- Tick 1 ---
=> neverFinish2(1)
--- Tick 2 ---
=> neverFinish2(2)
--- Tick 3 ---
=> neverFinish2(3)
--- Tick 4 ---
=> neverFinish2(4)
--- Tick 5 ---
=> neverFinish2(5)
--- Tick 6 ---
=> neverFinish2(6)
--- Tick 7 ---
=> neverFinish1(7 | 0)
--- Tick 8 ---
=> neverFinish1(8 | 1)
--- Tick 9 ---