Skip to content

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 compile run

Background: how verbs work

A verb is a named, reusable robot behavior. It has three parts:

  1. YAML definition — name, capabilities required, parameters, dependencies
  2. Jinja2 template — the BT XML fragment that gets rendered with the step's parameters
  3. Task referenceverb: wait_at in 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:

(built-in) defined/wait
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:

wait.xml.j2
<Action ID="Wait" name="wait_{{ duration }}s" duration="{{ duration }}" />

Reference: go_to verb

(built-in) defined/go_to
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:

go_to.xml.j2
<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:

  1. Navigate to a target position (x, y)
  2. Wait for duration seconds
  3. 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:

verbs/wait_at.yaml
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 need differential_drive and lidar_2d because we call GoTo.
  • template — the Jinja2 file to render (relative to the verb's directory).
  • parameters — each parameter has a type, optional flag, and optional default. Parameters with required: false and a default are safe to omit in the task file.

Step 3: Write the Jinja2 BT XML template

Create verbs/wait_at.xml.j2:

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:

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

defined compile tasks/survey_and_wait.task.yaml

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

defined run tasks/survey_and_wait.task.yaml

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.yaml in the built-in verb library for an example of a verb with rgb_camera as 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).