Create your first custom verb¶
This tutorial walks through creating a wait_at verb — a verb that navigates to a POI, waits for a configurable duration, then reports. It is a practical example that combines two existing primitives into a single reusable step.
By the end you will have:
- a verb YAML definition
- a Jinja2 BT XML template
- a task that uses the verb
- a passing
defined compilerun
Background: how verbs work¶
A verb is a named, reusable robot behavior. It has three parts:
- YAML definition — name, capabilities required, parameters, dependencies
- Jinja2 template — the BT XML fragment that gets rendered with the step's parameters
- Task reference —
verb: wait_atin a task file
The compiler reads the YAML, validates capabilities and parameters, renders the template, and stitches it into a full BT XML tree.
Reference: wait verb¶
To understand the format, look at the built-in wait verb:
name: wait
description: "Pause execution for a specified duration"
required_capabilities: []
template: wait.xml.j2
primitives:
- delay
parameters:
duration:
type: float
required: true
description: "Wait duration in seconds"
And its template:
Reference: go_to verb¶
name: go_to
required_capabilities:
- differential_drive
- lidar_2d
parameters:
x:
type: float
required: true
y:
type: float
required: true
theta:
type: float
required: false
default: 0.0
timeout:
type: float
required: false
default: 60.0
Template:
<RetryUntilSuccessful num_attempts="{{ retries | default('3') }}">
<Action ID="GoTo" name="go_to_{{ x }}_{{ y }}"
x="{{ x }}" y="{{ y }}"
theta="{{ theta | default('0.0') }}"
frame_id="{{ frame_id | default('map') }}"
server_name="{{ server_name | default('/navigate_to_pose') }}"
timeout="{{ timeout | default('60.0') }}" />
</RetryUntilSuccessful>
Step 1: Choose what the verb does¶
wait_at will:
- Navigate to a target position (
x,y) - Wait for
durationseconds - Publish a status report
This saves three steps in a task file every time you want the robot to dwell at a location.
Step 2: Create the verb YAML¶
Create verbs/wait_at.yaml in your project:
name: wait_at
description: "Navigate to a position, wait for a duration, then report"
required_capabilities:
- differential_drive
- lidar_2d
template: wait_at.xml.j2
parameters:
x:
type: float
required: true
description: "Target X coordinate (meters)"
y:
type: float
required: true
description: "Target Y coordinate (meters)"
duration:
type: float
required: false
default: 5.0
description: "How long to wait at the position (seconds)"
message:
type: string
required: false
default: "arrived"
description: "Status message to report after waiting"
timeout:
type: float
required: false
default: 60.0
description: "Navigation timeout (seconds)"
# Software dependencies — same as go_to since we use the GoTo BT node
dependencies:
apt:
- ros-jazzy-nav2-bringup
source:
- repo: https://github.com/Defined-Robotics/defined-platform
ref: trunk
packages: [defined_runtime]
runtime:
nodes:
- package: nav2_bringup
executable: navigation_launch.py
parameters:
use_sim_time: true
interface:
actions:
- name: /navigate_to_pose
topics:
publishes:
- /task_reports
Key fields:
required_capabilities— the compiler checks these against the robot RDF. We needdifferential_driveandlidar_2dbecause we callGoTo.template— the Jinja2 file to render (relative to the verb's directory).parameters— each parameter has a type, optional flag, and optional default. Parameters withrequired: falseand adefaultare safe to omit in the task file.
Step 3: Write the Jinja2 BT XML template¶
Create verbs/wait_at.xml.j2:
<Sequence name="wait_at_{{ x }}_{{ y }}">
<RetryUntilSuccessful num_attempts="3">
<Action ID="GoTo"
name="go_to_{{ x }}_{{ y }}"
x="{{ x }}"
y="{{ y }}"
theta="0.0"
frame_id="map"
server_name="/navigate_to_pose"
timeout="{{ timeout | default('60.0') }}" />
</RetryUntilSuccessful>
<Action ID="Wait"
name="wait_{{ duration | default('5.0') }}s"
duration="{{ duration | default('5.0') }}" />
<Action ID="Report"
name="report_{{ message | default('arrived') }}"
message="{{ message | default('arrived') }}"
topic="/task_reports"
level="info" />
</Sequence>
The template is plain BT XML with Jinja2 {{ variable }} substitutions. The | default(...) filter provides a fallback when an optional parameter is not supplied.
The outer <Sequence> node means: run all three children in order. If any child fails, the sequence fails.
Step 4: Use the verb in a task¶
Create tasks/survey_and_wait.task.yaml:
name: SurveyAndWait
description: "Visit each survey point, wait 10 seconds, then return to dock"
steps:
- verb: wait_at
params:
target: $world.pois.survey-1
duration: 10.0
message: "survey_1_complete"
- verb: wait_at
params:
target: $world.pois.survey-2
duration: 10.0
message: "survey_2_complete"
- verb: go_to
params:
target: $world.pois.dock
- verb: report
params:
message: "mission_complete"
The $world.pois.* references are resolved by the compiler before the template is rendered. The compiler substitutes x and y from the world file into the template.
Step 5: Compile and check output¶
A successful compile prints something like:
Compiling survey_and_wait.task.yaml
robot: robots/burger.rdf.yaml (turtlebot3_burger)
world: worlds/open_room.yaml (room_10x10)
verbs: verbs/ [go_to, wait, report, capture_image, wait_at]
Resolving POIs...
survey-1 → (3.0, 3.0)
survey-2 → (-3.0, -3.0)
dock → (0.0, 0.0)
Checking capabilities...
wait_at needs: [differential_drive, lidar_2d]
burger has: [differential_drive, wheel_encoders, lidar_2d] OK
Output: .build/survey_and_wait.bt.xml
If you reference a POI that does not exist in the world file, or use a parameter that is not defined in the verb YAML, the compiler will tell you before anything runs.
Step 6: Run in simulation¶
The robot will navigate to each survey point, wait 10 seconds, then return to dock. Each Report node publishes to /task_reports, visible in the TUI log panel.
Common mistakes¶
Missing template file — if wait_at.xml.j2 does not exist next to wait_at.yaml, the compiler fails with template not found. The template path is relative to the verb YAML file.
Wrong parameter type — if you pass duration: "ten" (a string) to a float parameter, the compiler rejects it at the parameter validation stage.
Capability not on robot — if you add required_capabilities: [rgb_camera] but your robot RDF does not have an rgb_camera module, the compiler prints the missing capability and exits.
$world.pois.* in a parameter that expects x/y separately — the POI reference is resolved to x and y before template rendering. Your template receives x and y as separate float variables, not a dict.
What's next¶
- Look at
capture_image.yamlin the built-in verb library for an example of a verb withrgb_cameraas a required capability. - Read Tour of the starter repo to understand how verbs, worlds, and robots connect.
- Add a second template variant and use Jinja2
{% if %}blocks to support optional behavior (e.g., skip the report if a flag is false).