Building Behavior Trees in Go with go-bt
Behavior trees are common in game AI, robotics, and agent-like systems. They let you compose decision-making from small nodes: sequences, selectors, decorators, and leaves.
rvitorper/go-bt is a compact Go implementation worth reading because it avoids a class hierarchy. The current API is generic, context-aware, and built around one small interface.
The core interface
The central type is core.Command[T]:
type Command[T any] interface {
Run(ctx *BTContext[T]) int
}
T is the blackboard type: the shared state your tree reads and mutates. Runtime state does not live in exported Children fields or global variables. It is passed through BTContext[T], which embeds Go’s normal context.Context and carries the blackboard pointer.
ctx := core.NewBTContext(context.Background(), &state)
code := tree.Run(ctx)
The return value is an int. In the library’s examples and tests, 1 means success, 0 means running, and -1 means failure.
Leaves are functions over context
Leaf actions are created with leaf.NewAction:
attack := leaf.NewAction(func(ctx *core.BTContext[NPC]) int {
ctx.Blackboard.Attacks++
return 1
})
That is the Go pattern to notice. Instead of defining a struct type for every tiny behaviour, you pass a function with the right shape. The function receives a typed context, so it can access cancellation, timing hooks, and blackboard state without package-level globals.
Sequences and selectors compose commands
Composite nodes accept children that satisfy core.Command[T]:
tree := composite.NewSelector[NPC](
composite.NewSequence[NPC](
leaf.NewAction(func(ctx *core.BTContext[NPC]) int {
if ctx.Blackboard.SeesEnemy {
return 1
}
return -1
}),
leaf.NewAction(func(ctx *core.BTContext[NPC]) int {
if ctx.Blackboard.HasAmmo {
return 1
}
return -1
}),
attack,
),
leaf.NewAction(func(ctx *core.BTContext[NPC]) int {
ctx.Blackboard.Patrolling = true
return 1
}),
)
A sequence runs children until one fails or reports running. A selector runs children until one succeeds or reports running. Because everything is a Command[T], composites and leaves can be nested freely.
The children slice is internal to the composite. Users build trees through constructors such as composite.NewSequence and composite.NewSelector rather than mutating exported fields.
Context is part of the design
BTContext[T] is more than a bag of state. It carries the parent context.Context, so cancellation and deadlines can flow into tree execution. The README also calls out testability: temporal logic can be controlled through context functions rather than forcing tests to wait on real time.
That is a practical choice for behaviour trees. Timeouts, sleeps, retries, and long-running actions are exactly where naive tree implementations become flaky.
Decorators wrap commands
Decorators such as inverter, optional, repeat, and timeout wrap another command and alter how its result is interpreted. This is the same composition model as middleware: take a child command, return a command with different behaviour.
That keeps the library small. Leaf actions do work. Composites choose which child runs. Decorators modify child results. The generic Command[T] interface ties it together.
What to take from go-bt
The useful Go lessons are:
- model nodes with one small generic interface
- keep shared state typed through a blackboard
- pass runtime state through context rather than globals
- create leaves from functions
- build composites through constructors
- make time and cancellation testable
That is a clean fit for Go: small interfaces, explicit state, and composition over inheritance.