Skip to main content
A .unit.yaml file is the source of truth for a Basis unit. It defines what topics a unit subscribes to, how inputs are synchronized, and what topics it publishes. The code generator reads this file and produces the C++ boilerplate so you only write handler logic. The file is validated against unit/schema.yaml before code generation runs.

Top-level fields

threading_model
"single"
required
The concurrency model for all handlers in this unit.single is the only currently supported value. All handlers run mutually exclusive from each other — only one handler executes at a time.
threading_model: single
args
object
Named arguments that are passed to the unit at launch time. Each key is the argument name. Each argument is itself an object with a type field and optional metadata.Generated as --argument-name command-line flags. Accessible in topic name templates as {{args.argument_name}}.
args:
  camera_name:
    type: string
    help: "Name of the camera to use"
  confidence_threshold:
    type: float
    default: 0.5
  debug_mode:
    type: bool
    optional: true
args.<name>.type
string
required
A C++ type name. Use primitive names: bool, string, float, double, int, int32_t, etc. Avoid std:: prefixes. Vectors are not yet supported.
args.<name>.help
string
A help string shown on errors and with --help.
args.<name>.optional
boolean
When true, wraps the generated field in std::optional<T>. Mutually exclusive with default.
args.<name>.default
any
The default value when the argument is not supplied. Mutually exclusive with optional.
cpp_includes
string[]
A list of additional #include paths to inject into the generated unit_base.h. Use this when your handler types reference custom message headers or other C++ headers not pulled in by the serializer plugins.
cpp_includes:
  - my_msgs/my_type.pb.h
  - my_project/utils.h
handlers
object
required
A map of handler names to handler definitions. Each handler describes a single callback function that fires when its synchronization condition is met. Handler names become C++ method names on the generated unit class.
handlers:
  StereoMatch:
    # ...
  OnLidarSync:
    # ...

Handler fields

Each entry under handlers is an object with the following fields.
handlers.<name>.sync
object
required
Controls when this handler fires. See sync block below.
handlers.<name>.inputs
object
A map of topic names to input definitions. Topic names are the full ROS-style topic path, e.g. /camera_left. If omitted, the handler has no message inputs (useful with rate or external sync).
handlers.<name>.outputs
object
A map of topic names to output definitions. Outputs are optional; a handler that only sends data to an external system may have none.

Sync block

sync.type
"all" | "equal" | { approximate: number }
The synchronization strategy for message inputs.
ValueBehavior
allFires when every non-optional input has at least one buffered message.
equalFires when designated sync_field values are identical across all synced inputs.
{ approximate: <ms> }Fires when designated sync_field values are within the specified milliseconds of each other.
# all
sync:
  type: all

# equal
sync:
  type: equal
  buffer_size: 2

# approximate within 5ms
sync:
  type:
    approximate: 5ms
  buffer_size: 5
For external handlers, write sync: external (bare scalar, no type key).
sync.rate
number
Fire the handler at a fixed rate (in Hz) regardless of message arrival. The value is a number in Hz.
sync:
  rate: 10hz
When rate is set, inputs act as a cache — the most recently received message is forwarded to the handler each tick. Non-optional inputs must have been received at least once before the handler fires.
rate and type are independent. You can combine a rate-based timer with required or optional topic inputs.
sync.buffer_size
number
Maximum number of messages to buffer per input channel for equal and approximate synchronizers. When the buffer is full, the oldest message is dropped.
sync:
  type: equal
  buffer_size: 2

Input fields

Each entry under handlers.<name>.inputs is an object with the following fields.
type
string
required
The message type, in serializer:CppType format. The serializer prefix determines which serialization plugin is used. The C++ type name follows after the colon.
type: protobuf:StampedVectorData
type: rosmsg:sensor_msgs::Image
See Serialization for the full list of available serializer prefixes.
sync_field
string
The field or method to call on the message to extract the synchronization key. Used with equal and approximate sync types.
SyntaxGenerated code
header.stamp()msg->header.stamp() (method call chain)
::time&MyType::time (pointer-to-member; double-colon prefix)
complex.access.to_string()[](T* msg){ return msg->complex.access.to_string(); } (lambda)
/camera_left:
  type: rosmsg:sensor_msgs::Image
  sync_field: header.stamp()
/vector_data:
  type: protobuf:StampedVectorData
  sync_field: ::time
optional
boolean
default:"false"
When true, this input does not block the handler from firing. If the message has arrived by the time the handler is called, it is included; otherwise the field is nullptr.
/event_data:
  type: protobuf:Event
  optional: true
cached
boolean
default:"false"
When true, the most recently received message is not cleared after the handler fires. It is reused in every subsequent invocation until a newer message arrives. Useful for slow-changing configuration or calibration topics.
/camera_intrinsics:
  type: protobuf:CameraIntrinsics
  cached: true
accumulate
integer | true
Collect multiple messages between handler invocations. The generated input type becomes std::vector<std::shared_ptr<const T_MSG>> instead of a single std::shared_ptr<const T_MSG>. Set to a positive integer to cap the buffer, or true for unlimited accumulation.
/event_data:
  type: protobuf:Event
  optional: true
  accumulate: 10
allow_transports
string[]
Restricts this subscription to only the listed transport names. Mutually exclusive with deny_transports.
/topic_a:
  type: protobuf:A
  allow_transports:
    - inproc
deny_transports
string[]
Excludes the listed transport names from this subscription. Mutually exclusive with allow_transports.
/topic_b:
  type: protobuf:B
  deny_transports:
    - inproc
qos
object
Quality-of-service settings for this subscription.
qos.depth
integer
default:"10"
The subscriber queue depth — how many unprocessed messages are buffered before older ones are dropped.
inproc_type
string
An alternative C++ type to use when the message is delivered over the inproc transport. When set, the generated input type becomes std::variant<std::monostate, std::shared_ptr<const NetworkType>, std::shared_ptr<const InprocType>>.

Output fields

Each entry under handlers.<name>.outputs supports the same type, inproc_type, allow_transports, deny_transports, and qos fields as inputs.
type
string
required
The published message type in serializer:CppType format.
/camera_stereo:
  type: rosmsg:example_msgs::StereoImage
optional
boolean
default:"false"
When true, the handler is allowed to return nullptr for this output without triggering an error diagnostic. Use this when a handler conditionally produces output.
/topic_c:
  type: protobuf:C
  optional: true
allow_transports
string[]
Restricts this publisher to only the listed transports.
deny_transports
string[]
Excludes the listed transports from this publisher.
qos
object
qos.depth
integer
default:"10"
The publisher queue depth.

Complete examples

The following examples are drawn directly from unit/example/example.unit.yaml.

StereoMatch — exact timestamp sync

example.unit.yaml
threading_model: single

handlers:
  StereoMatch:
    sync:
      type: equal
      buffer_size: 2
    inputs:
      /camera_left:
        type: rosmsg:sensor_msgs::Image
        sync_field: header.stamp()
      /camera_right:
        type: rosmsg:sensor_msgs::Image
        sync_field: header.stamp()
    outputs:
      /camera_stereo:
        type: rosmsg:example_msgs::StereoImage
Fires when a left and right frame share the same header.stamp. Keeps at most two frames per channel in the buffer.

OnLidarSync — approximate sync with accumulation

example.unit.yaml
handlers:
  OnLidarSync:
    sync:
      type:
        approximate: 5ms
      buffer_size: 5
    inputs:
      /lidar_data:
        type: rosmsg:sensor_msgs::PointCloud2
        sync_field: header.stamp.toSecs()
      /vector_data:
        type: protobuf:StampedVectorData
        sync_field: ::time
      /event_data:
        type: protobuf:Event
        optional: true
        accumulate: 10
    outputs:
      /lidar_sync_event:
        type: protobuf:LidarSyncEvent
Syncs a ROS PointCloud2 (timestamp via method call) with a Protobuf message (timestamp via direct field access) within 5 ms. Up to 10 optional event messages accumulate between firings.

TenHertzWithTopic — rate-driven with transport restrictions

example.unit.yaml
handlers:
  TenHertzWithTopic:
    sync:
      rate: 10hz
    inputs:
      /topic_a:
        type: protobuf:A
        allow_transports:
          - inproc
      /topic_b:
        type: protobuf:B
        optional: true
        deny_transports:
          - inproc
    outputs:
      /topic_c:
        type: protobuf:C
        optional: true
Fires 10 times per second once /topic_a has been received at least once. /topic_a is restricted to inproc delivery; /topic_b forbids inproc. The output is optional — the handler can return nullptr for /topic_c.

HandleAll — fire on any input combination

example.unit.yaml
handlers:
  HandleAll:
    sync:
      type: all
    inputs:
      /topic_c:
        type: protobuf:C
      /topic_d:
        type: protobuf:D
Fires when both /topic_c and /topic_d have been received. No outputs — this handler sends data to an external system.

External — externally triggered output

example.unit.yaml
handlers:
  External:
    sync:
      external
    outputs:
      /some_image:
        type: rosmsg:sensor_msgs::Image
The handler has no inputs and no automatic trigger. Your C++ code calls the generated RunHandlerAndPublish() directly, for example from a camera SDK callback.

Topic name templating

Topic names support Jinja2 {{ }} expressions that are evaluated against the unit’s args. This lets you parameterize topic names without recompiling.
args:
  camera_name:
    type: string

handlers:
  OnImage:
    sync:
      type: all
    inputs:
      "{{args.camera_name}}/rgb":
        type: rosmsg:sensor_msgs::Image
    outputs:
      "{{args.camera_name}}/processed":
        type: protobuf:ProcessedImage
At runtime, --camera_name front resolves the topics to /front/rgb and /front/processed.

Next steps

Code generation

How to run the generator and what files it produces

Serialization

How the serializer prefix in the type field maps to a plugin

Synchronizers

Deep dive into how each sync type works

Launch files

How to wire units together into processes

Build docs developers (and LLMs) love