How to Write a Behavior Tree from Scratch

The core of a behavior tree runtime is famously small — small enough that writing one is the single best way to actually understand tick semantics. This guide builds a complete, working core in about 100 lines of TypeScript. It ports almost line-for-line to C#, GDScript, or C++; there’s nothing language-specific in it. (If sequences and selectors aren’t second nature yet, read the node reference first — this article implements exactly what that one describes.)

The contract: one method, three answers

Everything is a node with a tick() method returning one of three statuses:

enum Status { Success, Failure, Running }

interface Node {
  tick(agent: Agent): Status;
}

Leaves do work; composites route ticks. That’s the whole architecture.

Leaves: conditions and actions

class Condition implements Node {
  constructor(private check: (agent: Agent) => boolean) {}
  tick(agent: Agent): Status {
    return this.check(agent) ? Status.Success : Status.Failure;
  }
}

class Action implements Node {
  constructor(private act: (agent: Agent) => Status) {}
  tick(agent: Agent): Status {
    return this.act(agent);
  }
}

The Action callback returning Status — not boolean — is load-bearing: a moveTo that isn’t there yet returns Running, and the tree waits. Skipping Running is classic mistake #5.

Composites: sequence and selector

class Sequence implements Node {
  constructor(private children: Node[]) {}
  tick(agent: Agent): Status {
    for (const child of this.children) {
      const status = child.tick(agent);
      if (status !== Status.Success) return status;  // Failure or Running stops the walk
    }
    return Status.Success;
  }
}

class Selector implements Node {
  constructor(private children: Node[]) {}
  tick(agent: Agent): Status {
    for (const child of this.children) {
      const status = child.tick(agent);
      if (status !== Status.Failure) return status;  // Success or Running stops the walk
    }
    return Status.Failure;
  }
}

Notice they’re mirror images — sequence bails on non-Success, selector on non-Failure. Every tick restarts the walk from child zero, which is what makes these reactive: a higher-priority selector branch reclaims control the instant its guard passes.

Memory variants and decorators

A memory sequence resumes where it left off instead of restarting — the fix for procedural checklists:

class MemSequence implements Node {
  private current = 0;
  constructor(private children: Node[]) {}
  tick(agent: Agent): Status {
    while (this.current < this.children.length) {
      const status = this.children[this.current].tick(agent);
      if (status === Status.Running) return status;
      if (status === Status.Failure) { this.current = 0; return status; }
      this.current++;
    }
    this.current = 0;
    return Status.Success;
  }
}

Decorators wrap one child and transform its result:

class Inverter implements Node {
  constructor(private child: Node) {}
  tick(agent: Agent): Status {
    const status = this.child.tick(agent);
    if (status === Status.Success) return Status.Failure;
    if (status === Status.Failure) return Status.Success;
    return Status.Running;
  }
}

Repeater, UntilSuccess, Cooldown and friends are each 5–10 lines in the same shape.

Assemble and run

The patrol/chase/attack ladder, verbatim:

const brain = new Selector([
  new Sequence([new Condition(inRange), new Action(attack)]),
  new Sequence([new Condition(canSee),  new Action(chase)]),
  new MemSequence([new Action(moveToWaypoint), new Action(wait2s), new Action(nextWaypoint)]),
]);

// game loop
for (const agent of agents) brain.tick(agent);

That’s a working behavior tree. Total: about 100 lines.

What production adds (and why you shouldn’t hand-code trees)

Three things separate this from a production runtime:

  1. A blackboard. Note the sneaky bug above: MemSequence stores current on the node, so sharing one tree across agents breaks. Production runtimes keep per-node state in a per-agent blackboard (blackboard.get(key, treeId, nodeId)), keeping the tree itself stateless and shareable.
  2. Enter/exit hooks. When a higher branch preempts a running action, that action needs an onExit/halt callback to stop the animation, cancel the path request, release the claimed resource. Interruption cleanup is the hardest 20% of a real runtime.
  3. Data-driven trees. Building trees in code (like the snippet above) buries your AI design in constructor calls. Real pipelines load trees from data — which is exactly what the free online editor exports: behavior3-format JSON listing nodes, properties, and children, ready for a loader that maps node names to your classes.

Design the tree visually, export JSON, and let your hundred lines execute it:

▶ Open the tree this article implements

Recommended reading

Books that go deeper on behavior trees and game AI.

As an Amazon Associate, behaviortrees.com earns from qualifying purchases.