Manifest Schema Reference

This document describes the complete YAML schema for SOVD system manifests.

Top-Level Structure

A manifest file has the following top-level structure:

manifest_version: "1.0"    # Required - manifest schema version

metadata:              # Optional - document metadata
  name: string
  version: string
  description: string

config:                # Optional - discovery behavior settings
  unmanifested_nodes: string
  inherit_runtime_resources: boolean
  allow_manifest_override: boolean

areas: []              # Optional - area definitions
components: []         # Optional - component definitions
assets: []             # Optional - manual asset inventory entries
apps: []               # Optional - app definitions
functions: []          # Optional - function definitions
scripts: []             # Optional - pre-defined script entries

manifest_version (Required)

The manifest schema version. Currently must be "1.0".

manifest_version: "1.0"

metadata (Optional)

Document metadata for identification and documentation.

Field

Type

Description

name

string

System/robot name (e.g., “turtlebot3-nav2”)

version

string

Manifest version (e.g., “1.0.0”)

description

string

Human-readable description

metadata:
  name: "my-robot"
  version: "2.0.0"
  description: "Mobile robot with Nav2 navigation stack"

config (Optional)

Discovery behavior configuration.

Field

Type

Description

unmanifested_nodes

string

Policy for ROS nodes not in manifest

inherit_runtime_resources

boolean

Copy topics/services from runtime nodes (default: true)

allow_manifest_override

boolean

Manifest values can override runtime (default: true)

unmanifested_nodes options:

  • ignore - Don’t expose unmanifested nodes

  • warn - Log warning, include as orphans (default)

  • error - Fail startup if orphan nodes detected

  • include_as_orphan - Include with source: "orphan"

config:
  unmanifested_nodes: warn
  inherit_runtime_resources: true
  allow_manifest_override: true

Areas

Areas represent logical or physical groupings (subsystems, locations, etc.). In runtime-only mode, areas are derived from ROS 2 namespaces.

Note

The areas: section is optional. For simple robots without subsystem hierarchy, you can omit areas entirely and use components as the top-level entities. See Flat Entity Tree below.

Schema

areas:
  - id: string              # Required - unique identifier
    name: string            # Required - human-readable name
    namespace: string       # Optional - ROS 2 namespace path
    category: string        # Optional - classification
    description: string     # Optional - detailed description
    tags: [string]          # Optional - tags for filtering
    translation_id: string  # Optional - i18n key
    parent_area_id: string  # Optional - parent area reference
    subareas: []            # Optional - nested area definitions

Fields

Field

Type

Required

Description

id

string

Yes

Unique identifier (alphanumeric, hyphens allowed)

name

string

Yes

Human-readable name

namespace

string

No

ROS 2 namespace path (e.g., “/perception”)

category

string

No

Classification for filtering

description

string

No

Detailed description

tags

[string]

No

Tags for filtering and grouping

translation_id

string

No

Internationalization key

parent_area_id

string

No

Parent area ID (for flat hierarchy definition)

subareas

[Area]

No

Nested area definitions

Example

areas:
  - id: perception
    name: "Perception Subsystem"
    category: "sensor-processing"
    description: "Sensor data acquisition and processing"
    tags:
      - sensors
      - realtime
    subareas:
      - id: lidar-processing
        name: "LiDAR Processing"
        description: "Point cloud processing pipeline"

      - id: camera-processing
        name: "Camera Processing"
        description: "Image processing pipeline"

  - id: navigation
    name: "Navigation Subsystem"
    category: "motion-planning"

Components

Components represent hardware or virtual entities (ECUs, sensors, controllers). In runtime-only mode, synthetic components are created per namespace to group Apps (nodes). In manifest mode, components are explicitly defined and Apps are linked to them.

Schema

components:
  - id: string              # Required - unique identifier
    name: string            # Required - human-readable name
    type: string            # Optional - component type
    category: string        # Optional - classification
    area: string            # Optional - reference to area.id
    namespace: string       # Optional - ROS 2 namespace
    fqn: string             # Optional - fully qualified name
    variant: string         # Optional - hardware variant
    description: string     # Optional - detailed description
    tags: [string]          # Optional - tags for filtering
    translation_id: string  # Optional - i18n key
    parent_component_id: string  # Optional - parent component
    depends_on: [string]    # Optional - component IDs this depends on
    subcomponents: []       # Optional - nested definitions
    external: boolean       # Optional - non-ROS external asset (default: false)

    identity:               # Optional - asset-identity nameplate
      manufacturer: string        # Vendor / manufacturer name
      model: string               # Product designation / order code
      serial_number: string       # Unit serial number
      hardware_revision: string   # Hardware revision
      firmware_version: string    # Firmware version
      software_version: string    # Software/application version
      network_endpoint: string    # e.g. "opc.tcp://plc.local:4840"
      role: string                # Functional role (e.g. "plc", "drive")
      extra:                      # Optional - vendor-specific extras
        <key>: string             # Free-form string map (rack/slot, MAC, asset tag, ...)

    lock:                   # Optional - per-entity lock configuration
      required_scopes: [string]  # Collections requiring a lock before mutation
      breakable: boolean         # Whether locks can be broken (default: true)
      max_expiration: integer    # Max lock TTL in seconds (0 = global default)

Fields

Field

Type

Required

Description

id

string

Yes

Unique identifier

name

string

Yes

Human-readable name

type

string

No

Component type (sensor, actuator, controller, etc.)

category

string

No

Classification for filtering

area

string

No

Parent area ID

namespace

string

No

ROS 2 namespace path

fqn

string

No

Fully qualified name (namespace + id)

variant

string

No

Hardware variant identifier

description

string

No

Detailed description

tags

[string]

No

Tags for filtering

translation_id

string

No

Internationalization key

parent_component_id

string

No

Parent component ID

depends_on

[string]

No

List of component IDs this component depends on

subcomponents

[Component]

No

Nested component definitions

external

boolean

No

True if the component is a non-ROS external asset (PLC, fieldbus device, any asset a protocol plugin bridges into SOVD). Tri-state: omitting external: leaves it unset, so in hybrid mode it does not clear an external classification contributed by another discovery layer (e.g. a protocol plugin). An explicit value is authoritative and resolves by normal layer priority. An external component with no bound child apps owns its fault_manager faults under its own entity id, so a Function or Area hosting it rolls up its faults without a synthetic child app (#516).

identity

object

No

Asset-identity nameplate of the asset behind the component. All keys are optional strings (manufacturer, model, serial_number, hardware_revision, firmware_version, software_version, network_endpoint, role) plus an extensible extra string map for vendor-specific keys not modeled up front. Each populated field is recorded with provenance manifest; protocol plugins (e.g. OPC UA device-info) fill in or override fields per the identity merge precedence. A live protocol read outranks the manifest only over an authenticated session (e.g. an OPC UA secured channel with certificate validation); an unauthenticated read only fills fields the manifest left empty. Exposed over REST as x-medkit.identity.

Common Component Types

  • sensor - Sensors (LiDAR, camera, IMU)

  • actuator - Actuators (motors, grippers)

  • controller - Controllers (main computer, ECU)

  • accelerator - Compute accelerators (GPU, TPU)

  • communication - Communication interfaces

  • power - Power management

Example

components:
  - id: main-computer
    name: "Main Computer"
    type: "controller"
    area: control
    description: "Raspberry Pi 4 running ROS 2"
    variant: "rpi4-8gb"
    subcomponents:
      - id: gpu-unit
        name: "GPU Processing Unit"
        type: "accelerator"

  - id: lidar-sensor
    name: "LiDAR Sensor"
    type: "sensor"
    area: perception
    description: "360° laser range finder"
    tags:
      - safety-critical
      - realtime

  - id: imu-sensor
    name: "IMU Sensor"
    type: "sensor"
    area: perception

  # External device component (not a ROS node); owns its faults by entity id
  - id: line-plc
    name: "Line PLC"
    external: true

Assets

Assets declare manually inventoried equipment that no protocol layer can describe (or fully describe): unnetworked devices, third-party hardware, spare nameplate data. Each asset becomes a Component with source: "inventory" and a structured asset identity carrying per-field provenance "inventory", and merges into the entity tree by id alongside protocol-discovered structure. In the identity merge, inventory ranks below manifest and live protocol reads but above runtime guesses.

Schema

assets:
  - id: string                 # Required - stable asset id (merge key)
    manufacturer: string       # Optional - vendor / OEM
    model: string              # Optional - model / order code
    serial: string             # Optional - serial number
    hardware_rev: string       # Optional - hardware revision
    firmware: string           # Optional - firmware / software version
    endpoint: string           # Optional - network endpoint (URL / host:port)
    role: string               # Optional - functional role
    area: string               # Optional - Area id placing the asset in the tree
    namespace: string          # Optional - operator-declared placement (sets fqn)
    name: string               # Optional - display name (default: "<manufacturer> <model>")
    description: string        # Optional - detailed description
    variant: string            # Optional - hardware variant identifier
    type: string               # Optional - component type
    translation_id: string     # Optional - internationalization key
    parent_component_id: string # Optional - parent component ID
    depends_on: [string]       # Optional - component IDs this asset depends on
    tags: [string]             # Optional - tags for filtering
    external: boolean          # Optional - non-ROS external asset (default: false)
    any_other_key: string      # Kept verbatim as an identity extra

Fields

The identity keys accept the same aliases as the CSV import: serial_number for serial, hardware_revision / hw_rev for hardware_rev, and firmware_version / fw for firmware. Aliased keys land on the typed identity fields, not in the extras. Any scalar key not listed above is preserved as an identity extra.

external classifies the asset as a non-ROS device, the same tri-state field as on a components: entry. An external asset with no bound child apps owns its fault scope by its own entity id. It is a recognized key, so it classifies the Component instead of being kept as an identity extra.

Placement is optional: without area (and namespace) the asset is reachable at /components/{id} and in the flat component list, but does not appear under any Area. An area must reference an area defined in the manifest, otherwise validation fails (rule R006).

Example

areas:
  - id: cell-3
    name: "Cell 3"

assets:
  - id: hyd-pump-2
    manufacturer: Grundfos
    model: CR-5
    serial_number: "GP-2214-0087"
    area: cell-3
    role: pump
    rack: R2          # kept as identity extra "rack"

CSV Inventory Import

The same asset entries can be bulk-imported from a CSV file via the discovery.inventory.csv_path gateway parameter (requires a manifest-backed discovery mode: manifest_only, or hybrid with discovery.manifest_path set; empty = disabled). The CSV is re-read on every manifest load / reload and appended to the merged manifest before validation.

  • Columns: the header row is matched case-insensitively after trimming. Canonical columns are id (required), manufacturer, model, serial, hardware_rev, firmware, endpoint, role and area, with the same aliases as the assets: list. Any other column is kept as an identity extra keyed by its original header.

  • Quoting: RFC-4180-style; double-quoted fields may contain commas, newlines and escaped quotes (""). Unquoted fields are whitespace-trimmed; a UTF-8 BOM (Excel “CSV UTF-8” export) is stripped.

  • Size cap: the file is rejected before reading if it exceeds 1 MiB.

  • Row policy: rows without an id are skipped with a warning; for duplicate ids within the CSV the first row wins. A row whose id is already a manifest component keeps the manifest definition and folds the row’s identity in as gap-fill; a row whose id collides with any other manifest entity is skipped, and an unknown area value is dropped (asset kept, placement-less). None of these fail the load. A missing file is skipped with a warning; an unreadable or malformed file (e.g. no id column) fails the load.

Apps

Apps represent software applications, typically mapping 1:1 to ROS 2 nodes. Apps exist only in manifest and hybrid modes.

Schema

apps:
  - id: string              # Required - unique identifier
    name: string            # Required - human-readable name
    category: string        # Optional - classification
    is_located_on: string   # Optional - component ID
    depends_on: [string]    # Optional - app IDs this app depends on
    description: string     # Optional - detailed description
    tags: [string]          # Optional - tags for filtering
    translation_id: string  # Optional - i18n key
    external: boolean       # Optional - not a ROS node (default: false)

    ros_binding:            # Required for hybrid mode linking
      node_name: string     # Required - ROS node name
      namespace: string     # Optional - namespace (default: /)
      topic_namespace: string  # Optional - match by topic prefix

    lock:                   # Optional - per-entity lock configuration
      required_scopes: [string]  # Collections requiring a lock before mutation
      breakable: boolean         # Whether locks can be broken (default: true)
      max_expiration: integer    # Max lock TTL in seconds (0 = global default)

Fields

Field

Type

Required

Description

id

string

Yes

Unique identifier

name

string

Yes

Human-readable name

category

string

No

Classification for filtering

is_located_on

string

No

Component ID where app runs

depends_on

[string]

No

List of app IDs this app depends on

description

string

No

Detailed description

tags

[string]

No

Tags for filtering

translation_id

string

No

Internationalization key

external

boolean

No

True if not a ROS node (treated as not external when omitted). The classification is tri-state internally: omitting external: leaves it unset, so in hybrid mode it does not clear an external classification contributed by another discovery layer (e.g. a protocol plugin) - the app keeps its bare-id fault scope. An explicit value is authoritative and resolves by normal layer priority, so an authoritative manifest external: false overrides a plugin’s external: true. Combining external: true with a ros_binding is contradictory and raises validation warning R013 (the binding is ignored for linking and fault scoping - the app is scoped by its entity id).

ros_binding Fields

Field

Type

Required

Description

node_name

string

Yes*

ROS 2 node name to bind to

namespace

string

No

Namespace (“*” for wildcard)

topic_namespace

string

Yes*

Alternative: match by topic prefix

* Either node_name or topic_namespace is required.

Matching behavior:

  1. Name and namespace match (default): node_name must match exactly. namespace uses path-segment-boundary matching: /nav matches /nav and /nav/sub but NOT /navigation.

  2. Wildcard namespace: Set namespace: "*" to match node in any namespace

  3. Topic namespace: Match nodes by their published topic prefix

Example

apps:
  # Match by exact node name and namespace
  - id: lidar-driver
    name: "LiDAR Driver"
    is_located_on: lidar-sensor
    ros_binding:
      node_name: velodyne_driver
      namespace: /sensors

  # Match node in any namespace
  - id: camera-driver
    name: "Camera Driver"
    ros_binding:
      node_name: usb_cam
      namespace: "*"

  # Match by topic namespace
  - id: perception-pipeline
    name: "Perception Pipeline"
    ros_binding:
      topic_namespace: /perception

  # App with dependencies
  - id: slam-node
    name: "SLAM Node"
    category: "localization"
    is_located_on: main-computer
    depends_on:
      - lidar-driver
      - imu-driver
    ros_binding:
      node_name: slam_toolbox
      namespace: /mapping

  # External app (not a ROS node)
  - id: cloud-connector
    name: "Cloud Connector"
    external: true
    description: "External cloud service integration"

Functions

Functions represent high-level capabilities spanning multiple apps. Functions are always manifest-defined and aggregate data from their host apps.

Schema

functions:
  - id: string              # Required - unique identifier
    name: string            # Required - human-readable name
    category: string        # Optional - classification
    hosted_by: [string]     # Required - list of app IDs
    depends_on: [string]    # Optional - function IDs
    description: string     # Optional - detailed description
    tags: [string]          # Optional - tags for filtering
    translation_id: string  # Optional - i18n key

Fields

Field

Type

Required

Description

id

string

Yes

Unique identifier

name

string

Yes

Human-readable name

hosted_by

[string]

Yes

List of app IDs that implement this function

category

string

No

Classification for filtering

depends_on

[string]

No

List of function IDs this function depends on

description

string

No

Detailed description

tags

[string]

No

Tags for filtering

translation_id

string

No

Internationalization key

Function Capabilities

Functions aggregate capabilities from their host apps:

  • Data: Combined topics from all host apps

  • Operations: Combined services/actions from all host apps

  • Faults: Faults from all host apps

Example

functions:
  - id: autonomous-navigation
    name: "Autonomous Navigation"
    category: "mobility"
    description: "Complete autonomous navigation capability"
    tags:
      - safety-critical
      - autonomous
    hosted_by:
      - amcl-node
      - planner-server
      - controller-server
      - bt-navigator

  - id: localization
    name: "Localization"
    category: "state-estimation"
    hosted_by:
      - amcl-node
      - map-server

  - id: perception
    name: "Environment Perception"
    category: "sensing"
    hosted_by:
      - lidar-driver
      - camera-driver
      - point-cloud-processor

Scripts

Scripts define pre-deployed diagnostic scripts that are available on entities. Scripts defined in the manifest are managed - they cannot be deleted via the REST API.

Note

Manifest scripts are only loaded when scripts.scripts_dir is configured in the gateway parameters. Without it, the scripts: block is parsed but scripts are not exposed via the REST API.

Schema

scripts:
  - id: string              # Required - unique identifier
    name: string            # Optional - human-readable name (defaults to id)
    description: string     # Optional - detailed description
    path: string            # Required - filesystem path to script file
    format: string          # Required - execution format (bash, python, sh)
    timeout_sec: integer    # Optional - execution timeout (default: 300)
    entity_filter: [string] # Optional - glob patterns for entity matching
    env:                    # Optional - environment variables
      KEY: "value"
    args:                   # Optional - argument definitions
      - name: string
        type: string
        flag: string
    parameters_schema:      # Optional - JSON Schema for parameters

Fields

Field

Type

Required

Description

id

string

Yes

Unique script identifier

name

string

No

Human-readable name (defaults to id)

description

string

No

Detailed description

path

string

Yes

Filesystem path to the script file

format

string

Yes

Execution format: bash, python, sh

timeout_sec

integer

No

Max execution time in seconds (default: 300)

entity_filter

[string]

No

Glob patterns for entity matching (e.g., components/*, apps/*). Empty means all entities.

env

map

No

Environment variables passed to the script

args

[object]

No

Argument definitions with name, type, flag fields

parameters_schema

object

No

JSON Schema for execution parameters validation. Nested objects and arrays are fully supported.

Example

scripts:
  - id: run-diagnostics
    name: "Run Diagnostics"
    description: "Check health of all sensors"
    path: "/opt/scripts/run-diagnostics.sh"
    format: "bash"
    timeout_sec: 30
    entity_filter:
      - "components/*"
    env:
      GATEWAY_URL: "http://localhost:8080"

  - id: calibrate-sensor
    name: "Calibrate Sensor"
    path: "/opt/scripts/calibrate.py"
    format: "python"
    timeout_sec: 60
    args:
      - name: threshold
        type: float
        flag: "--threshold"

See also

See the Scripts section in REST API Reference for API endpoints.

Flat Entity Tree

For simple robots where the entire system is a single unit, you can omit the areas: section and use a flat component tree instead. The top-level component represents the robot itself, with subcomponents for hardware modules:

manifest_version: "1.0"

metadata:
  name: "flat-turtlebot"
  version: "1.0.0"
  description: "TurtleBot3 without area hierarchy"

# No areas section - components are top-level entities

components:
  - id: turtlebot3
    name: "TurtleBot3 Burger"
    type: "mobile-robot"

  - id: raspberry-pi
    name: "Raspberry Pi 4"
    type: "controller"
    parent_component_id: turtlebot3

  - id: lds-sensor
    name: "LDS-02 LiDAR"
    type: "sensor"
    parent_component_id: turtlebot3

apps:
  - id: lidar-driver
    name: "LiDAR Driver"
    is_located_on: lds-sensor
    ros_binding:
      node_name: ld08_driver
      namespace: /

For manifest-based discovery (manifest_only or hybrid), simply omit the areas: section as shown above - no additional configuration is needed. In runtime-only discovery, Areas are never created - they come from manifest only. A complete example is available at config/examples/flat_robot_manifest.yaml in the gateway package.

Complete Example

Here’s a complete manifest for a TurtleBot3 robot:

manifest_version: "1.0"

metadata:
  name: "turtlebot3-nav2"
  version: "2.0.0"
  description: "TurtleBot3 with Nav2 navigation stack"

config:
  unmanifested_nodes: warn
  inherit_runtime_resources: true

areas:
  - id: perception
    name: "Perception"
    category: "sensor-processing"

  - id: navigation
    name: "Navigation"
    category: "motion-planning"

  - id: control
    name: "Control"
    category: "motion-control"

components:
  - id: lidar-sensor
    name: "LiDAR Sensor"
    type: "sensor"
    area: perception

  - id: main-computer
    name: "Main Computer"
    type: "controller"
    area: control

apps:
  - id: lidar-driver
    name: "LiDAR Driver"
    is_located_on: lidar-sensor
    ros_binding:
      node_name: ld08_driver

  - id: amcl-node
    name: "AMCL Localization"
    category: "localization"
    is_located_on: main-computer
    ros_binding:
      node_name: amcl

  - id: planner-server
    name: "Planner Server"
    category: "navigation"
    is_located_on: main-computer
    depends_on:
      - amcl-node
    ros_binding:
      node_name: planner_server

functions:
  - id: autonomous-navigation
    name: "Autonomous Navigation"
    category: "mobility"
    hosted_by:
      - amcl-node
      - planner-server

scripts:
  - id: run-diagnostics
    name: "Run Diagnostics"
    path: "/opt/scripts/diagnostics.sh"
    format: "bash"
    timeout_sec: 30
    entity_filter:
      - "components/*"

Validation

Manifests are validated during loading. The validator checks:

Required fields:

  • manifest_version must be present and equal to “1.0”

  • All entities must have id and name

  • Apps with ros_binding must have node_name or topic_namespace

  • Functions must have at least one entry in hosted_by

  • Scripts must have id, path, and format

  • format must be one of: bash, python, sh

References:

  • area references must point to valid area IDs

  • is_located_on must point to valid component IDs

  • depends_on must point to valid app/function IDs

  • hosted_by must point to valid app IDs

Uniqueness:

  • All entity IDs must be unique within their type

  • IDs must be unique across all entity types (areas, components, apps, functions, scripts)

Format:

  • IDs should contain only alphanumeric characters and hyphens

  • IDs should not start with numbers

Validation errors are reported with the path to the invalid field:

Validation error at apps[2].ros_binding: 'node_name' or 'topic_namespace' required
Validation error at functions[0].hosted_by[1]: App 'unknown-app' not found

See also