Open-source project
telcy/modular-behavior-tree avatar
telcy/modular-behavior-tree

modular-behavior-tree: XML trees assembled at load time by injection

A JavaScript implementation of modular behavior trees

22 stars4 forksJavaScriptMIT

At a glance

What is it?
modular-behavior-tree is a JavaScript behavior tree library that reads trees from XML and resolves every tag name against a plain object you pass as the second argument to the parse call, so subtrees and custom nodes are injected at load time rather than declared in the file. It ships seven basic nodes, one runtime dependency, and a test script that runs the example instead of asserting anything.
Who is it for?
Reach for modular-behavior-tree if you want behavior trees authored as XML in a small JavaScript project, especially if you like the idea that a subtree is an ordinary value you can pass into a parse call. It is a small library at version 1.0.3, last pushed on 2026-08-23, with no GitHub releases and a default branch called master, so pin the npm version rather than tracking the repository.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • telcy/modular-behavior-tree22 ★ · +0
Star history
Repository2026-08-232026-10-22
telcy/modular-behavior-tree2222

0 Stars in the last 60 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

npm test runs the example file instead of asserting anything

The two scripts in the manifest are the whole build and verification story, and the second one is not a test suite.

json
"test": "babel-node --presets es2015 example/index.js",
"build": "babel src --presets babel-preset-es2015 --out-dir lib --no-comments"

Running the test command executes `example/index.js` through babel-node with the es2015 preset. There is no assertion framework in the dev dependencies at all, only babel-cli and babel-preset-es2015, both at version 6, so a passing run means the example did not throw, not that any behavior was checked. The build transpiles `src` into `lib` and strips comments on the way, and the package main points at `./lib/index.js`, which is why `src` is the only source directory in the checkout and `lib` is not among the top level entries. There is exactly one runtime dependency, `xml-parser` at `^1.2.1`, and it is doing all of the XML work. Everything above the parse call is written by the library itself.

MemSequence stores the index of the RUNNING child and resets it to null

The memory composites work by remembering which child was in flight, and the mechanism is a single field set to the loop index.

js
import { Composite, SUCCESS, RUNNING } from 'modular-behavior-tree'

class MemSequence extends Composite {

   constructor({children = []} = {}) {
      super({
         children
      })
      this._openNodeIndex = null
   }

   run(blackboard, tick) {

      for (let i=0; i<this.children.length; i++) {
         if(!this._openNodeIndex || (this._openNodeIndex && this._openNodeIndex === i) ) {
            let status = this.children[i]._execute(blackboard, tick);
            this._openNodeIndex = (status === RUNNING) ? i : null
            if (status !== SUCCESS) return status;
         }

The guard skips every index other than the stored one, which is what stops the earlier children from running again, and the field is written back to null the moment a child returns something other than RUNNING. MemSelector is the same loop with FAILURE instead of SUCCESS. Worth knowing before you copy either: the listing above is cut off mid-function in the documentation, with no closing braces, so the tail of the return path is not shown. The field is per node instance, so a MemSequence nested inside another node keeps its own cursor.

The manual tree example passes nodes where the documented Sequence reads children

There is a mismatch in the documentation worth catching early, because it is the first thing you hit if you build a tree in code rather than from XML.

js
let subtree = BehaviorTree.parseFileXML('./subtree.xml')

let tree = new Sequence({
    nodes: [
        new Action({
            run: function() {
                console.log("another test")
                return FAILURE
            }
        }),
        subtree
    ]
})

That example hands the constructor a `nodes` array. Every composite implementation shown later, including Sequence, Selector, MemSequence, and MemSelector, takes `children` and calls `super({children})`, then iterates `this.children[i]._execute(blackboard, tick)`. So the two spellings in the same document do not agree, and the code as printed would iterate an undefined `children`. The mixing idea itself is sound and is the point of the section: you can build part of a tree in JavaScript and splice a parsed subtree in as a value. Just check which key your constructor actually reads before you rely on the printed example.

The second argument to the parse call is the only registration point

Nothing in the XML declares a node type, which is what makes the trees modular. Tag names are resolved against a plain object supplied as the second parameter, and the same mechanism handles both custom nodes and whole subtrees.

js
// custom node
class MyCustomAction extends Action {
   // ...
}

// subtree
let subtree = BehaviorTree.parseFileXML('./subtree.xml')

// main tree with passed custom node and subtree
let maintree = BehaviorTree.parseFileXML('./maintree.xml', { MyCustomAction, Subtree: subtree })

The keys of that object are the tag names the loader will look for, so `MyCustomAction` makes `<MyCustomAction />` resolvable and `Subtree` is what lets a main file reference a parsed tree under a chosen name. The shipped example follows exactly the same pattern, with a `LogMessage` action registered so that `<LogMessage text="Waited 3 seconds" />` can appear in a file that has never heard of the class, and with `<Subtree1 />` and `<Subtree2 />` composed into `maintree.xml`. The cost of the approach is that a missing registration fails at load time rather than at author time, since the file itself carries no way to validate the names it uses.

Three hooks, and start is deliberately skipped when a node resumes

A node implements up to three methods, and the contract around them is where the tick model is expressed. `start` is called before `run`, but not if the node is resuming after ending with RUNNING, so it fires once per activation rather than once per tick. `run` is the main computation function and returns one of the three constants. `end` is called after `run` finishes with SUCCESS or FAILURE, so a node still returning RUNNING has not ended yet. The constants are imported from the package itself: SUCCESS for computation succeeded, FAILURE for computation failed, and RUNNING for computation still running. Note that the documentation writes the SUCCESS constant name as SUCCES in the `end` description, which is a typo in the prose rather than in the export. Taken together, the three hooks plus RUNNING are the entire concurrency model, and there is no separate cancel or abort hook to find.

Condition is offered as a base class with no built-in node beside it

There are four base classes to inherit from, and the page names all four: Action, Composite, Decorator, Condition. The advice that follows is that you will mostly be fine inheriting from Action. The list of nodes that actually ship is shorter than the list of base classes, and the import line makes the gap visible: `Sequence`, `Selector`, `MemSequence`, `MemSelector`, `Inverter`, `MaxTime`, and `Wait` is everything you get out of the box. Condition is not in that list, so a condition is a base class you extend yourself with nothing supplied to build on. For the other three bases the page does show implementations, and the composite and decorator examples are simple enough to read as reference. The four shipped decorators and conditions are described individually: the memory composites remember the last child that returned RUNNING, and the composites are described generically as nodes with children.

Inverter states two transitions and is silent about RUNNING

Three of the shipped nodes exist to manage the RUNNING status, and each does it differently. `Wait` is the simplest, a wait action that returns RUNNING for as long as the time is not over yet, and it takes its duration as a `ms` attribute. `MaxTime` returns FAILURE if the child takes too long, and it also makes sure the child keeps returning RUNNING while its computation is still going, so the timeout is enforced across ticks rather than inside one call. `Inverter` is the odd one out: the only behavior stated is `SUCCESS -> FAILURE, FAILURE -> SUCCESS`. Nothing is said about what RUNNING becomes, which is the one transition a decorator wrapping a long running child actually needs. If your inverted child can return RUNNING, that case is unspecified in the documentation, which is a reason to read the source before depending on the decorator rather than to assume the flip is total.

Babel 6, a master branch, and a lineage stated in the opening line

The provenance is declared rather than inferred. The library is a JavaScript implementation of modular behavior trees, described as similar to the MBTs in the Lumberyard engine, and it says it has been highly inspired by two existing packages, BehaviorTree.js and BEHAVIOR3JS on npm. That lineage matters for anyone porting trees between systems, since the Memory variants and the decorators are where implementations tend to diverge. The rest of the metadata is small: version 1.0.3, MIT licensed, authored by Jascha Burmeister, issues and repository both under the telcy organization, and the default branch named master rather than main. There are no GitHub releases, so npm is the only place a version number appears. The last push to that branch is dated 2026-08-23, and the toolchain in the dev dependencies is Babel 6 era while the examples are written with ES module import syntax, which is worth reconciling if you mix require and import in the same project.

Editorial conclusion

Reach for modular-behavior-tree if you want behavior trees authored as XML in a small JavaScript project, especially if you like the idea that a subtree is an ordinary value you can pass into a parse call. It is a small library at version 1.0.3, last pushed on 2026-08-23, with no GitHub releases and a default branch called master, so pin the npm version rather than tracking the repository. Before you build on it, read the two gaps first: the run method for a node must be able to return RUNNING for the memory composites to mean anything, and Condition is offered as a base class with no built-in node shipped next to it.

Frequently asked questions

How does a behavior tree work in modular-behavior-tree?

A node returns one of three constants, SUCCESS, FAILURE, or RUNNING. A Sequence runs its children until one returns something other than SUCCESS, a Selector until one returns something other than FAILURE, and RUNNING is how a node says its computation is still going across ticks.

What is the concept of a behavior tree in this library?

A tree of nodes built either by parsing an XML file or by constructing nodes in JavaScript, with both forms mixable. Modular means a parsed subtree is passed in as a value and registered under a tag name through the second argument to the parse call.

Are behaviour trees AI, according to modular-behavior-tree?

The documentation does not classify them either way. It describes the library as a JavaScript implementation of modular behavior trees, inspired by the Lumberyard engine's MBTs, BehaviorTree.js, and BEHAVIOR3JS, and it ships a blackboard for accessing global data.

What are the key differences between a decision tree and a behavior tree in modular-behavior-tree?

No comparison is given. What is documented is the tick model, with start, run, and end hooks, and RUNNING meaning the computation is still running, with start skipped when a node resumes.

Official sources

  1. Official README
  2. Project repository
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/telcy-modular-behavior-tree.svg)](https://hysenlabs.com/projects/telcy-modular-behavior-tree)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/telcy-modular-behavior-tree.svg)](https://hysenlabs.com/projects/telcy-modular-behavior-tree)