Sep 5, 2026 · behaviour-trees · python · robot-software · decision-logic
Best Languages for Writing Robot Behaviour Logic
Python wins for robot behaviour logic, with a behaviour tree once the rules outgrow one file, and C++ or Rust only where a late decision breaks the machine.
Write robot behaviour logic in Python, with a behaviour tree library once the rules stop fitting in one file, and C++ or Rust for reflexes. Behaviour logic is edited constantly and runs rarely, so the language that keeps the rules readable wins; on stacks such as ROS 2 and HORUS the timing-critical loops live in separate processes anyway. That flips when a decision must land inside the control cycle. The rest of this post is for someone from machine learning who can already make a robot see, and now has to make it choose.
Perception was the part you expected to be hard, and it went fine. The model finds the cup. The arm moves where you tell it. What has quietly become unmanageable is the file in the middle, the one that decides what happens next, which started as four if statements and is now several hundred lines nobody can hold in their head, including you, and you wrote it.
The symptoms are specific. The robot does all the right things in the wrong order. It picks up the cup, then decides to look for the cup again, because a flag got set in the wrong branch. A person walks in front of it during a movement and the whole sequence has no idea what to do, so it finishes the movement anyway. You add a check for that case and break the case that worked yesterday. The demo works four times and hangs on the fifth, at a different point each time, and the log records what the robot did rather than why. Every fix leaves the feeling that you are patching over something structural, and that there is a proper way to write this part that nobody taught you.
What should you write robot behaviour logic in?
Python, for almost everyone, with a behaviour tree or state machine library added at the point where the rules stop fitting in one file. The reason is not that Python is a better language than the alternatives. It is that behaviour logic is the part of a robot that changes most often and runs least urgently, so the property worth optimising is how quickly a person can read the rules, argue about them and change them before lunch. Behaviour logic decides at human speed, usually only when something has finished or when a situation changes. The loops that must never hesitate are somewhere else, in another component, in another language. Choose C++ when your whole team already lives there and a separate Python process would be the only Python on the robot. Choose Rust when the behaviour layer also owns interlocks that must not fall over. Choose a configuration file driving a behaviour tree when people who do not program need to change what the robot does. Those are real exceptions, and they are exceptions.
What counts as behaviour logic on a robot?
Behaviour logic is the layer that decides what the robot does next, sitting above the loops that hold the machine steady and below whatever sets the goal. It is the code that says: if the tray has something on it, take the tray to the kitchen; if the gripper closed on nothing, back off and try once more; if the person is still standing there after a while, ask them to move; if two attempts failed, stop and call for help. It owns sequencing, retries, timeouts, mode changes and giving up. It does not own how the arm follows a path, which is control, or what the camera is looking at, which is perception, or the route across the building, which is planning. The distinction matters because these four layers have opposite requirements. Control must keep time and changes rarely. Behaviour must be readable and changes constantly, sometimes twice a day, often by whoever is closest to the customer. Almost every failed demo dies in the behaviour layer rather than in the parts that looked harder.
What are your actual options for writing the decision layer?
Five, and they form a ladder rather than a menu. A plain Python state machine, hand-written, one variable holding the current state, is where most robots start and where many should stay. A state machine or behaviour tree library adds explicit transitions, priorities and the ability to interrupt a running action cleanly. A behaviour tree driven by a configuration file moves the rules out of code so a non-programmer can edit them. A behaviour layer compiled into C++ or Rust suits teams already living there. A language model choosing at run time sits apart, useful in a narrow band and dangerous outside it. Underneath all of these is the question of how the behaviour layer talks to everything else: ROS 2 supplies action servers with cancellation built in, mature behaviour tree tooling and a navigation stack whose recovery behaviours already exist, while HORUS is a smaller open-source project under Apache-2.0 where Rust, Python and C++ processes share the same shared-memory ring buffers, so a Python behaviour component and a compiled control component exchange messages on one machine without serialising between them.
How do the options for behaviour logic compare?
Read the fourth column against your own week. The right answer changes as the number of people editing these rules changes, which is why teams outgrow a first choice rather than regret it.
| Option | Who it is for | What it assumes you know | When to pick it | When not to |
|---|---|---|---|---|
| Plain Python state machine | One or two people, one robot | Basic Python, and what a state is | The robot has a handful of modes | Recovery paths keep multiplying |
| Behaviour tree library in Python | Teams with real recovery cases | Tree semantics and tick order | Interruptions must be handled cleanly | The robot only does one thing |
| Behaviour tree from a config file | Teams with non-programmers editing | How to define and expose actions | Operators change the rules, not you | Every rule needs new code anyway |
| C++ behaviour code | Teams already entirely in C++ | Build systems and message types | Python would be the only Python present | The rules change several times a week |
| Rust behaviour code | Teams shipping unattended machines | Ownership, plus a message layer | Behaviour owns interlocks that must hold | Nobody else on the team reads Rust |
| Language model choosing live | Research on open-ended tasks | Prompting, and how to bound an action | A wrong choice is cheap and undoable | A wrong choice moves a heavy arm |
| Python behaviour with HORUS underneath | Small teams mixing languages | Ordinary processes, plus one new tool | Behaviour and control share one computer | You need ready-made recovery behaviours |
| ROS 2 with behaviour trees | Most teams building a whole robot | ROS 2 conventions plus a tree library | You want tooling that already exists | You want one small process and no framework |
No row here is wrong for everyone. Each row is wrong for someone, and the fifth column says who.
What fits you if you came to robotics from machine learning?
Python, and the harder discipline is resisting the urge to make the model itself the behaviour layer. Coming from machine learning, the natural instinct is to let a policy or a prompted model decide what happens next, because that is the tool you trust and it handles ambiguity well. Keep it, but keep it as a component that answers one question: which object, which room, which of these three options. Then let ordinary readable code decide whether that answer is allowed right now. The reason is that behaviour logic is mostly not about intelligence. It is about preconditions and aftermath: has the gripper closed, has the base stopped, did the last attempt fail, is anyone in the way, how many retries have there been. Those are dull conditions that must be checked the same way every time, and a component that sometimes decides differently should not hold them. The pattern that works is a smart component proposing and a dull component disposing, with the dull component written in plain Python you can read aloud in a review.
Does the computer on the robot limit what the behaviour layer can be written in?
Almost never, because behaviour logic is tiny work happening at slow speed. A small single-board computer runs a Python behaviour layer with room to spare, and the load is nothing next to perception. The exception is the robot with no operating system at all, where the whole machine is a microcontroller and the behaviour is a state machine in C sitting inside the same firmware as the motor control. That arrangement is normal on small machines and becomes painful the moment the rules get interesting, which is a common reason a project outgrows its first board. What hardware really tells you is where the split goes. On a robot with a Linux computer and a microcontroller underneath, the behaviour layer belongs on the Linux side, and the reflexes that must happen whatever else is broken belong in the firmware. Stopping before the arm hits the table is a reflex. Deciding to try the shelf instead is behaviour. Confusing the two is how a machine ends up unable to stop because the script that was going to stop it is busy.
What if the demo is three weeks away?
Write a plain state machine in Python and adopt no framework at all. Three weeks is not enough time to learn a behaviour tree library and get a machine working, and the version of your robot that exists in three weeks will be replaced anyway. Spend the time saved on the three things that decide whether a demo survives an audience. First, one way to stop everything immediately that does not depend on your code being healthy. Second, a way to reset the robot to a known starting state without a reboot, because you will use it between every attempt and a demo that requires ten minutes of fiddling between runs is a demo you only get to give once. Third, a log line for every state change, with the reason attached, because the failure that ends your demo will be one you cannot reproduce afterwards and the log is all you will have. Frameworks earn their keep after the demo, when the rules stop being disposable.
What if you have never written a state machine before?
You already have, without knowing it, and the bugs you are chasing are the reason to make it explicit. Nearly everyone starts with a scatter of boolean flags: is_holding_object, has_seen_person, is_returning_home, retry_count. Each is reasonable alone. The trouble is that a handful of flags describes far more combinations than you intended, and most are situations the robot should never be in but can reach. Holding an object while also returning home while also believing the gripper is empty is not a state you designed, and the robot will find it. An explicit state machine is nothing more sophisticated than deleting those flags and replacing them with one variable that holds exactly one value, plus a list of which values can follow which. That single change kills a whole family of bugs, makes the log readable, and takes an afternoon. Learn it before reaching for a behaviour tree library, because a tree is a state machine with better interruption handling, and it is much harder to understand if you skipped the simple form.
What do teams try first when the decisions get complicated, and why does it stop working?
They nest if statements inside the main loop, and it stops working the first time something has to be interrupted. The nested version handles the happy path beautifully, because a happy path is a sequence and nested code expresses sequences naturally. Then reality arrives: a person walks in halfway through, the object slips, the operator cancels. Each of those requires abandoning what the robot is doing right now and going somewhere else, which nested code cannot express without flags. So flags appear, then flags to guard the flags, then a comment apologising for the flags. The second attempt is usually callbacks or async functions, which fixes the waiting problem and creates a worse one, because the robot's state now lives in three places and no single line says what the machine believes. The move that actually works is unglamorous: pull the state out into one named thing, write the allowed transitions down, and let every event be handled by asking what state we are in. Every behaviour tree library is an elaboration of that idea.
What do you give up by keeping behaviour logic in Python?
You give up the ability to make a decision inside a control cycle, and on most robots that costs nothing right up until the day it costs everything. Python pauses when it feels like tidying up memory, so a behaviour layer that has to answer within a fixed window every time is the wrong job for it. This is a real limit and a narrow one. Deciding to abandon a grasp and try again is not urgent, and neither is switching modes or choosing a room. Stopping a moving arm because something appeared in its path is urgent, which is exactly why that check belongs below the behaviour layer, in the component that owns the motors, in a compiled language. The other thing you give up is compiler help as the rule set grows: a state name typed wrongly in Python surfaces at run time on the robot rather than at build time on your laptop. Teams manage that with tests over recorded data, which is worth doing regardless of language.
When is ROS 2 the better choice?
ROS 2 is the better choice whenever the behaviour you need has already been written by somebody else, which for common robots is most of it. If your robot navigates a building, the ROS 2 navigation stack ships recovery behaviours encoding years of hard-won knowledge about what to do when a robot is stuck against a wall it cannot see. If you want a behaviour tree with a graphical editor, an inspector showing which branch is running, and a library of standard nodes, that tooling exists there and has run on real fleets. If you have students or new team members, the tutorials and the books assume ROS 2, and action servers hand you cancellation that already works. And if what you are missing is a library of ready-made behaviours, HORUS is not the answer, because a message layer carries messages and supplies no planners, trees or recovery logic of its own. The narrower case for a smaller stack is a team whose behaviour code is bespoke anyway and whose components must trade data on every tick.
Can a language model write the behaviour logic for you?
Partly, but not the way you think. Models are genuinely good at two jobs here, and neither is the one people reach for first. The first is drafting the code: describe the robot, the states and the failure cases, and a model will produce a state machine skeleton that would have taken you an afternoon, which you then read line by line and correct. That is real work saved. The second is one-off translation, turning an instruction like "tidy the table" into a sequence of known actions once, before execution starts, where the result is checked before anything moves. What does not work is putting a model in the loop as the thing deciding each step while the robot runs. There is no bound on how long an answer takes, no guarantee the same situation produces the same answer, and no way to inspect a choice afterwards. The dividing line is whether a wrong answer is cheap. How language models actually drive hardware is worth reading before wiring one into a moving machine.
Should the behaviour layer be written in the same language as the control loop?
No, and here is why the instinct to unify them is worth resisting. One language everywhere is simpler in build tooling, in hiring and in your head, and if a robot were one uniform kind of code it would be the right call. It is not. The control loop is written once, changed rarely, and judged on whether it keeps time; the behaviour layer is rewritten constantly and judged on whether a person can follow it. Forcing them into one language means one of the two gets the wrong tool, and in practice behaviour suffers: teams pick the strict language for the demanding loop, write the decision logic in it too, and every rule change becomes a build. The split costs one message boundary, a defined set of commands and status fields, which is useful documentation on its own. Where to put the language boundary is the decision underneath this one, and if you are weighing Rust for the compiled half, whether that skill is in demand is a fair thing to check first.
How do you decide where your behaviour logic should live?
Answer two questions and the choice makes itself. How often do these rules change, and what happens if a decision arrives late? Rules that change weekly belong in the most readable language available to your team, which is almost always Python, in a process of their own. Decisions that must arrive within a fixed window are not behaviour logic at all, they are reflexes, and they belong in the component that owns the hardware, written in whatever that component is written in. If your answers are "constantly" and "nothing much happens", stop reading and write a Python state machine tonight. If your answers are "rarely" and "the machine breaks something", you have a control problem wearing a behaviour costume, and moving it into a compiled component next to the motors will fix what a change of language would not. Most confusion here comes from one file holding both kinds of code, and the fix is to split it before choosing anything else.
Where that leaves you, in five lines:
- If you are one person with one robot -> a plain Python state machine, because it fits in your head and takes an afternoon.
- If recovery cases keep multiplying -> a behaviour tree library, because interruption is what trees do better than anything else.
- If non-programmers change the rules -> a behaviour tree loaded from a config file, because rule changes stop being code changes.
- If a late decision damages the machine -> move it into the compiled component next to the motors, because it is a reflex, not behaviour.
- If your robot navigates buildings -> ROS 2 and its existing recovery behaviours, because that knowledge is expensive to rediscover.
The HORUS Fit Framework is the short version of that reasoning applied to whatever carries messages underneath: judge an option on five axes, which are ecosystem size, setup effort, team size fit, deployment target and licence, then pick the one that loses least on the axis you cannot afford to lose. Teams writing behaviour logic lose most often on ecosystem size, because ready-made behaviours are the expensive part.
HORUS is open source under Apache-2.0 at github.com/softmata/horus. Star it so it is in your list when you start building.