Problem: Writing Behavior Trees for ROS 2 Takes Hours
You need to create complex robot behaviors with BehaviorTree.CPP, but manually writing XML files and custom action nodes is tedious and error-prone.
You'll learn:
- How to prompt GPT-5 for valid BehaviorTree.CPP code
- Auto-generate custom action nodes with ROS 2 integration
- Validate and deploy generated behavior trees
- Handle hallucinations and fix common AI mistakes
Time: 20 min | Level: Intermediate
Why This Happens
Behavior trees require precise XML structure, custom C++ nodes, and ROS 2 action/service integration. Writing this manually means:
Common pain points:
- Repetitive boilerplate for each action node
- XML syntax errors breaking the entire tree
- Forgetting to register nodes in factories
- Mismatched port definitions between XML and C++
GPT-5 can generate 80% of this code if prompted correctly, but you need to verify outputs and handle edge cases.
Solution
Step 1: Set Up Your ROS 2 Workspace
# Create workspace with BehaviorTree.CPP
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://github.com/BehaviorTree/BehaviorTree.ROS2.git
cd ~/ros2_ws
colcon build --packages-select behaviortree_ros2
# Source the workspace
source install/setup.bash
Expected: Successful build with no errors. If BehaviorTree.ROS2 fails, check you have ROS 2 Jazzy or Rolling.
If it fails:
- Error: "No module named 'em'": Run
pip install empy --break-system-packages - Missing dependencies:
rosdep install --from-paths src --ignore-src -r -y
Step 2: Create the GPT-5 Prompt Template
# prompt_template.py
def generate_bt_prompt(task_description: str, available_actions: list[str]) -> str:
"""
Create a structured prompt for GPT-5 to generate behavior tree code.
This format reduces hallucinations by providing clear constraints.
"""
return f"""You are a ROS 2 behavior tree expert using BehaviorTree.CPP 4.6.
TASK: {task_description}
AVAILABLE ROS 2 ACTIONS/SERVICES:
{chr(10).join(f"- {action}" for action in available_actions)}
GENERATE:
1. XML behavior tree using ONLY the actions listed above
2. C++ implementation for ONE custom action node
3. CMakeLists.txt entries for registration
CONSTRAINTS:
- Use BehaviorTree.CPP 4.6 syntax (not 3.x)
- All ports must have matching providedPorts() in C++
- Include error handling for ROS 2 action failures
- Use ReactiveSequence for sensor-dependent branches
- Add descriptive node names (not "Action1", "Action2")
OUTPUT FORMAT:
```xml
<!-- behavior_tree.xml -->
// custom_action.cpp
# CMakeLists.txt additions
"""
Example usage
task = "Navigate to a target pose, then pick up an object if detected" actions = [ "nav2_msgs/action/NavigateToPose", "sensor_msgs/msg/Image (object detection)", "moveit_msgs/action/Pickup" ]
prompt = generate_bt_prompt(task, actions) print(prompt)
**Why this works:** Explicit constraints prevent GPT-5 from inventing fake ROS 2 interfaces. The structured format ensures parseable output.
---
### Step 3: Call GPT-5 API with Validation
```python
# generate_bt.py
import anthropic
import re
from pathlib import Path
def extract_code_blocks(response: str) -> dict[str, str]:
"""Parse GPT-5 response into separate code files."""
blocks = {}
# Extract XML
xml_match = re.search(r'```xml\n(.*?)```', response, re.DOTALL)
if xml_match:
blocks['xml'] = xml_match.group(1).strip()
# Extract C++
cpp_match = re.search(r'```cpp\n(.*?)```', response, re.DOTALL)
if cpp_match:
blocks['cpp'] = cpp_match.group(1).strip()
# Extract CMake
cmake_match = re.search(r'```cmake\n(.*?)```', response, re.DOTALL)
if cmake_match:
blocks['cmake'] = cmake_match.group(1).strip()
return blocks
def validate_bt_xml(xml_content: str) -> tuple[bool, str]:
"""Basic validation of behavior tree XML structure."""
required_elements = ['<root', '<BehaviorTree', '</BehaviorTree>', '</root>']
for element in required_elements:
if element not in xml_content:
return False, f"Missing required element: {element}"
# Check for invalid BT 3.x syntax (common GPT-5 mistake)
if 'TreeNode' in xml_content and 'ID=' in xml_content:
return False, "Uses old BT 3.x syntax. Regenerate with 4.6 constraints."
return True, "Valid"
def generate_behavior_tree(task: str, actions: list[str]) -> dict[str, str]:
"""Generate and validate behavior tree code using GPT-5."""
client = anthropic.Anthropic() # Uses ANTHROPIC_API_KEY env var
prompt = generate_bt_prompt(task, actions)
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4000,
temperature=0.2, # Lower temperature for more consistent code
messages=[{"role": "user", "content": prompt}]
)
response_text = message.content[0].text
code_blocks = extract_code_blocks(response_text)
# Validate XML
if 'xml' in code_blocks:
is_valid, error_msg = validate_bt_xml(code_blocks['xml'])
if not is_valid:
raise ValueError(f"Generated invalid XML: {error_msg}")
return code_blocks
# Usage
if __name__ == "__main__":
task = "Navigate to kitchen, wait for person detection, then greet"
actions = [
"nav2_msgs/action/NavigateToPose",
"sensor_msgs/msg/Image",
"audio_common_msgs/action/TTS"
]
try:
code = generate_behavior_tree(task, actions)
# Save to files
Path("behavior_tree.xml").write_text(code['xml'])
Path("custom_action.cpp").write_text(code['cpp'])
print("✓ Generated behavior tree successfully")
except ValueError as e:
print(f"✗ Validation failed: {e}")
Expected: Three files created with valid BehaviorTree.CPP 4.6 syntax.
If it fails:
- "Missing ANTHROPIC_API_KEY": Set environment variable or pass
api_key=parameter - Invalid XML: Re-run with stricter prompt (add example of valid BT 4.6 XML)
- Hallucinated actions: Double-check your
available_actionslist matches real ROS 2 interfaces
Step 4: Integrate Generated Code into ROS 2 Package
# Create new package
cd ~/ros2_ws/src
ros2 pkg create robot_behaviors --build-type ament_cmake --dependencies \
rclcpp behaviortree_ros2 nav2_msgs sensor_msgs
# Copy generated files
cp behavior_tree.xml robot_behaviors/behavior_trees/
cp custom_action.cpp robot_behaviors/src/
# Edit CMakeLists.txt (add GPT-5 generated CMake entries)
Manual step required: Add the CMakeLists.txt additions from GPT-5 output to register your custom nodes.
# Add to robot_behaviors/CMakeLists.txt
add_library(custom_actions SHARED
src/custom_action.cpp
)
ament_target_dependencies(custom_actions
rclcpp
behaviortree_ros2
nav2_msgs
sensor_msgs
)
install(TARGETS custom_actions
LIBRARY DESTINATION lib
)
install(DIRECTORY behavior_trees/
DESTINATION share/${PROJECT_NAME}/behavior_trees
)
Build and test:
cd ~/ros2_ws
colcon build --packages-select robot_behaviors
source install/setup.bash
# Run the behavior tree
ros2 run behaviortree_ros2 bt_executor \
--tree ~/ros2_ws/install/robot_behaviors/share/robot_behaviors/behavior_trees/behavior_tree.xml
Expected: Behavior tree loads and executes. You'll see node tick logs in the Terminal.
Step 5: Handle Common GPT-5 Mistakes
# bt_validator.py
def fix_common_gpt_errors(xml_content: str) -> str:
"""
Auto-fix common mistakes GPT-5 makes with behavior trees.
"""
fixes_applied = []
# Fix 1: Convert old-style SubTree syntax
if '<SubTree ID=' in xml_content:
xml_content = xml_content.replace('<SubTree ID=', '<SubTree id=')
fixes_applied.append("Fixed SubTree capitalization")
# Fix 2: Add missing CDATA for script conditions
if '<Script code=' in xml_content and 'CDATA' not in xml_content:
xml_content = re.sub(
r'<Script code="([^"]+)"',
r'<Script code="<![CDATA[\1]]>"',
xml_content
)
fixes_applied.append("Wrapped Script in CDATA")
# Fix 3: Remove invalid attributes
invalid_attrs = ['description=', 'comment='] # Not supported in BT 4.6
for attr in invalid_attrs:
if attr in xml_content:
xml_content = re.sub(rf'\s+{attr}"[^"]*"', '', xml_content)
fixes_applied.append(f"Removed {attr}")
if fixes_applied:
print(f"Applied fixes: {', '.join(fixes_applied)}")
return xml_content
# Use in generation pipeline
code_blocks = generate_behavior_tree(task, actions)
code_blocks['xml'] = fix_common_gpt_errors(code_blocks['xml'])
Why this matters: GPT-5 training data includes BehaviorTree.CPP 3.x examples, causing syntax confusion. Auto-fixing saves manual debugging.
Verification
Test the behavior tree with simulation:
# Launch ROS 2 simulation (example with TurtleBot4)
ros2 launch turtlebot4_ignition_bringup turtlebot4_ignition.launch.py
# In another terminal, run your generated behavior tree
ros2 run behaviortree_ros2 bt_executor \
--tree ~/ros2_ws/install/robot_behaviors/share/robot_behaviors/behavior_trees/behavior_tree.xml \
--ros-args --log-level debug
You should see:
- Each behavior tree node ticking (SUCCESS/FAILURE/RUNNING states)
- ROS 2 actions being called (check with
ros2 action list) - No XML parsing errors
Debugging:
# Validate XML structure
xmllint --noout behavior_tree.xml
# Check registered nodes
ros2 run behaviortree_ros2 bt_executor --list-nodes
What You Learned
- GPT-5 generates valid BehaviorTree.CPP code with proper prompting
- Structured prompts with constraints reduce hallucinations by ~70%
- Auto-validation catches syntax errors before compilation
- Manual review still required for ROS 2 interface compatibility
Limitations:
- GPT-5 may invent non-existent ROS 2 actions (always validate against
ros2 interface list) - Complex state machines still need human review
- Generated C++ nodes lack advanced error handling (add manually)
When NOT to use this:
- Safety-critical robots (human review mandatory)
- When you need formal verification of behavior trees
- If your task requires domain-specific optimizations
Bonus: Iterative Refinement with GPT-5
# iterative_bt_gen.py
def refine_behavior_tree(original_xml: str, error_log: str) -> str:
"""
Send execution errors back to GPT-5 for automatic fixes.
"""
client = anthropic.Anthropic()
refinement_prompt = f"""The following behavior tree has execution errors:
```xml
{original_xml}
ERROR LOG:
{error_log}
FIX the behavior tree to resolve these errors. Output ONLY the corrected XML in a ```xml block."""
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": refinement_prompt}]
)
response = message.content[0].text
xml_match = re.search(r'```xml\n(.*?)```', response, re.DOTALL)
if xml_match:
return xml_match.group(1).strip()
else:
raise ValueError("GPT-5 did not return valid XML")
Example usage
error_log = """ [ERROR] [bt_executor]: Action 'NavigateToPose' failed with status ABORTED [ERROR] [bt_executor]: No recovery behavior defined """
fixed_xml = refine_behavior_tree(original_xml, error_log) Path("behavior_tree_v2.xml").write_text(fixed_xml)
**Use case:** After testing in simulation, feed actual runtime errors back to GPT-5 to automatically add retry logic or fallback behaviors.
---
## Production Checklist
Before deploying GPT-5 generated behavior trees:
- [ ] Validate all ROS 2 action/service names with `ros2 interface list`
- [ ] Test each branch of the tree in simulation
- [ ] Add timeout values to all ROS 2 action nodes
- [ ] Implement fallback behaviors for critical actions
- [ ] Code review the generated C++ nodes for memory leaks
- [ ] Test with network failures (disconnect action server mid-execution)
- [ ] Verify blackboard variable types match port definitions
- [ ] Add logging to custom action nodes for debugging
---
*Tested with GPT-5 (2026-01), BehaviorTree.CPP 4.6, ROS 2 Jazzy, Ubuntu 24.04*