Behavior Trees
Behavior trees (BTs) are the execution engine behind every agent in AgentLoop. They pair deterministic control flow with intelligent, model-driven decision nodes — neural, symbolic, and learned — so agent behavior stays reproducible and auditable while still adapting to the work in front of it.
This page explains the core concepts: ticking, the blackboard, status, and the node types you can use. When you are ready to write one, head to Authoring Behavior Trees; for the exact file format, see Behavior Tree JSON.
What a behavior tree does
A behavior tree is a hierarchical structure that defines how an agent carries out work. The agent runs a simple control loop: it ticks the tree repeatedly, evaluating one node at a time. Each tick advances the tree by a single step, and the node it lands on returns a status. The tree keeps ticking until it reaches a terminal result.
Agents run their trees in one of two modes:
- Reactive — task-handling agents (such as the engineer or QA tester) wait at a blocking action until work arrives, process it to completion, then loop back to wait.
- Proactive — the orchestrator runs on a timer, continuously scanning for work and assigning tasks without waiting for an external signal.
The blackboard
The blackboard is a key-value store shared by every node in a tree. Nodes read from it to make decisions and write to it to record results. It holds three broad kinds of state:
- Input context — the task description, codebase analysis, comments, and other inputs.
- Intermediate results — LLM outputs, analysis results, and feature flags produced mid-run.
- Control state — retry counts, validation flags, and status changes.
For example, one node might write an LLM’s code changes to implementation, a later condition might read validationFailed to decide whether to retry, and a utility node records its scoring decisions to _utilitySelectorDecisions.
Status
Every node returns one of three statuses on each tick:
| Status | Meaning |
|---|---|
SUCCEEDED | The node finished successfully; the parent moves on to its next child. |
FAILED | The node failed; the parent reacts according to its own type. |
RUNNING | The node is still working; the tree stays on it for the next tick. |
How a parent reacts to a child’s status depends on the parent’s type — this is what gives a tree its shape:
- Sequence runs children left to right, failing immediately on the first failure and succeeding only when all children succeed.
- Selector runs children left to right, succeeding immediately on the first success and failing only when all children fail.
- Retry repeats a child up to N times on failure, succeeding if any attempt succeeds.
- Parallel runs children concurrently, with the final result determined by its variant.
Node types
Behavior tree nodes fall into four groups: control nodes that shape flow, leaf nodes that do work, LLM nodes that add reasoning, and utility/logic nodes for scoring and symbolic reasoning.
Control nodes
| Type | What it does |
|---|---|
root | Entry point. Holds a single child; every tree has exactly one. |
sequence | Run children left to right; fail on the first failure, succeed when all succeed. |
selector | Run children left to right; succeed on the first success, fail when all fail. |
parallel / race / all | Run children concurrently; result depends on the variant’s semantics. |
retry | Repeat a child up to N times if it fails. |
repeat | Repeat a child N times unconditionally. |
flip / inverter | Invert the child’s result (success becomes failure and vice versa). |
succeed | Always return success. |
fail | Always return failure. |
wait | Pause for a fixed duration. |
branch | Reference and splice in a named subtree. |
lotto | Pick a random child, optionally weighted. |
forEach | Iterate a child over a collection from the blackboard, updating the current item and index keys each pass. |
Leaf nodes
| Type | What it does |
|---|---|
action | Call a registered function (for example, commit changes or run validation). Returns success or failure based on the handler. |
condition | Evaluate a boolean predicate synchronously. Returns success if true, failure if false. |
LLM nodes
These nodes add reasoning. They are transformed into standard nodes when the tree is loaded, so they tick and propagate status like any other node.
| Type | What it does |
|---|---|
llm-action | Call the LLM to generate structured output or take an action. |
llm-condition | Ask the LLM a yes/no question and branch on the answer. |
llm-selector | Let the LLM route to one of several named branches. |
llm-sequence | Let the LLM filter or reorder a set of steps. |
Utility and logic nodes
| Type | What it does |
|---|---|
utility-selector | Pick a child by scoring children from blackboard state, using static or learned weights instead of fixed priority. |
logic | Run a deterministic query against the rules/policy engine — a rule schema and facts; succeeds when the query has at least one solution. |
LLM nodes are rewritten into plain action, condition, selector, or sequence nodes at load time. This keeps the running tree fully deterministic in shape — only the LLM’s answers introduce variation, and every call is recorded for replay.
Utility behavior trees
A utility-selector replaces a static-priority selector with scoring. Rather than always trying children in a fixed order, it scores each child from features in the blackboard and picks based on those scores.
It works in a few steps:
- Extract features from the blackboard using dot-notation paths, such as
task.complexityoragent.recentFailures. - Score each child, either from inline static weights (a deterministic argmax) or from a referenced model artifact applied to the features.
- Select a child by mode:
maxtakes the highest score,distributionsamples from the scores, andthreshold-then-randompicks randomly among children within a threshold of the top score. - Apply an inertia bonus to the currently-running child to reduce needless flipping between options.
- Record the decision to the blackboard for debugging and training.
Because the utility-selector remains a native composite node rather than an action wrapper, the BT visualizer still sees the correct tree topology and snapshots capture which child was selected.
{
"type": "utility-selector",
"name": "ChooseStrategy",
"mode": "max",
"featuresFrom": ["task.complexity", "agent.recentFailures"],
"weightsRef": "strategy-model@v1",
"fallbackChild": 2,
"children": [
{ "type": "sequence", "name": "DeepRefactor", "children": [] },
{ "type": "sequence", "name": "QuickPatch", "children": [] },
{ "type": "action", "name": "Escalate", "call": "EscalateToHuman" }
]
}Static weights give you a fully deterministic argmax, while a model reference and the distribution or threshold-then-random modes introduce sampling. Choose static weights when you need a node’s choice to be reproducible across runs.
Putting it together
A typical branch combines control, leaf, and LLM nodes — for example, retrying an LLM implementation step and committing only once it succeeds:
{
"type": "sequence",
"children": [
{ "type": "action", "call": "LoadContext" },
{
"type": "retry",
"attempts": 3,
"child": {
"type": "llm-action",
"name": "ImplementTask",
"prompt": "Implement the following task: {{taskDescription}}",
"contextKeys": ["taskDescription"],
"outputKey": "implementation"
}
},
{ "type": "action", "call": "CommitChanges" }
]
}To understand where behavior trees sit in AgentLoop’s broader design — how deterministic control flow, LLM reasoning, and symbolic logic combine — read The Neurosymbolic Loop.