Skip to content

Task Orchestrator

Repo: rmf2_task_orchestrator · Image: rmf2_task_orchestrator:latest · Container: task_orchestrator

A Rust workflow-execution engine built on open-rmf crossflow. It receives task schedules over AMQP, executes workflow diagrams, and coordinates robots via AMQP / MQTT.

Task Orchestrator coordinating multi-robot execution in the warehouse simulation

What problem it solves

A real warehouse job is rarely a single move: it's "pick at A, then drop at B", "send three robots in parallel and wait for all of them", with pauses, retries and ordering constraints. The orchestrator turns a declarative workflow diagram into coordinated, monitored, multi-robot execution — fanning work out across robots, calling MAPF/robot services in the right order, and reporting status back. It's the layer between "high-level task" and "individual robot actions".

Internal crates

CratePurpose
rmf2_task_orchestratorMain binary — HTTP server on port 2727
workflow_executorCrossflow-based workflow engine
amqpAMQP client for RabbitMQ communication

Tasks

A few related concepts show up across the system — worth pinning down:

TermWhat it is
TaskRequestA single requested action on an asset (e.g. liftrack, depalletize). See the Simulation TaskRequest format.
ScheduleA wrapper carrying a workflow diagram ({ id, type: "Schedule", payload: <diagram> }) published to the AMQP @RECEIVE@ exchange.
Workflow diagramA Crossflow graph of operations (fork / nodes / join). Samples in workflow_executor/diagrams/.
TaskStatusProgress/completion messages emitted as the workflow runs.

In short: a Schedule carries a workflow diagram that the orchestrator executes, which in turn issues TaskRequests to robots/services and reports TaskStatus back.

Role in the system

  1. A scheduler publishes a Schedule (with a workflow diagram) onto the AMQP @RECEIVE@ exchange.
  2. The orchestrator executes the diagram, emitting TaskRequest messages and tracking TaskStatus.
  3. It coordinates devices and robots with MAPF over MQTT/AMQP, publishing status back onto the @RECEIVE@ amqp exchange

HTTP API

EndpointMethodDescription
/GETCrossflow diagram editor UI
/health_checkGETHealth check (used by the launcher's gate)
/api/executor/runPOSTExecute a workflow diagram (Crossflow's built-in executor)
/workflow/get_workflowsGETList the task_ids of workflows currently executing

AMQP integration

  • Subscribes to the @RECEIVE@ RabbitMQ (localhost:5672) exchange for Schedule and TaskStatus messages.
  • Publishes TaskRequest and TaskStatus messages to the same exchange.

Try it directly (send to test)

bash
cd ~/ros_industrial_ws/rmf_industrial/test_scripts/taskorchestrator

# default: 3 robots to default goals
python3 send_parallel_workflow_3_robots.py

# choose robots and goals
python3 send_parallel_workflow_3_robots.py \
  --robots Manufacturer_2,Manufacturer_3,Manufacturer_4 \
  --goals  P300,P301,P302

Watch it run:

bash
docker logs task_orchestrator --tail 50 -f

Full 25-robot scenario (send_workflow.py)

send_workflow.py runs the whole fleet. It loads two prebuilt diagrams from the same folder and publishes each as its own independent Schedule to @RECEIVE@:

bash
cd ~/ros_industrial_ws/rmf_industrial/test_scripts/taskorchestrator

# publishes both rack workflows (no args needed)
python3 send_workflow.py

# optional: target a remote broker
python3 send_workflow.py --host <amqp-host> --port 5672

The 25 AMRs are split across the two workflows, which the orchestrator executes concurrently:

DiagramSchedule idRobotsCount
rack_workflow_1.jsonurn:ngsild:Task:rack_wf1Manufacturer_1Manufacturer_1212
rack_workflow_2.jsonurn:ngsild:Task:rack_wf2Manufacturer_13Manufacturer_2513

Each diagram is a fork_clone into one sequential branch per robot — drive to a rack (MAPFGoToNode), then liftrack → move to a manipulator → depalletizedroprack (MQTTTaskRequestNode) — all rejoined at the end. Both schedules show up together in GET /workflow/get_workflows while they run:

bash
curl -s http://localhost:2727/workflow/get_workflows | jq
# ["urn:ngsild:Task:rack_wf1","urn:ngsild:Task:rack_wf2"]

Publish a Schedule yourself (AMQP)

Any AMQP client works — publish a Schedule to the @RECEIVE@ fanout exchange. The payload is a Crossflow workflow diagram. Here's a minimal one-node example that sends a single robot to a waypoint via the MAPFGoToNode (the AMQP/MAPF node), then terminates:

json
{
  "id": "urn:ngsi-ld:Schedule:my-test-01",
  "type": "Schedule",
  "payload": {
    "version": "0.1.0",
    "start": "move_to_goal",
    "ops": {
      "move_to_goal": {
        "type": "node",
        "builder": "MAPFGoToNode",
        "display_text": "Manufacturer_2 -> P300",
        "config": {
          "asset_name": "Manufacturer_2",
          "coordinates": "P300",
          "task_type": "amr_mapf",
          "task_id": "demo_move_1"
        },
        "next": { "builtin": "terminate" }
      }
    }
  }
}

Notes:

  • builder must be a node the orchestrator registers — see Workflow nodes below for the full list and each node's config params.
  • coordinates must be a real waypoint in the map graph (e.g. P300) — an unknown goal makes the downstream MAPF solver fail. For multi-robot fan-out, add a fork_clone start node, one node per robot, and a join before terminate (see send_parallel_workflow_3_robots.py).
  • payload may be the diagram object (as above) or a JSON-encoded string of it; the orchestrator accepts either.

HTTP execution and query endpoints

The AMQP path above is the one exercised by the demo, and it's the recommended way in. Under the hood the orchestrator forwards each Schedule to Crossflow's built-in executor at POST /api/executor/run (port 2727) — you can call that endpoint directly with a { "diagram": <diagram>, "request": <context> } body if you need to bypass AMQP.

The /workflow namespace only serves queries, not submissions — currently GET /workflow/get_workflows, which lists the task_ids of workflows in flight. The interactive diagram editor is served at the root path /.

Building workflows

There are three ways to author a diagram:

  • By hand — write the ops JSON directly (see the example above). Fine for small graphs.
  • Programmatically — generate the fork_clone / buffer / join ops in a script, like send_parallel_workflow_3_robots.py. Best for N-robot fan-out.
  • Visually, in the Crossflow editor — the orchestrator serves Crossflow's diagram-building frontend at the root path, e.g. http://localhost:2727/. There you can lay out nodes, wire the next edges, fill in each node's config, and run the diagram through the built-in executor (POST /api/executor/run) — the same execution path AMQP Schedules use. To run it later over AMQP, wrap the exported diagram in a Schedule and publish it to @RECEIVE@ as shown above.

For a step-by-step workflow building guide, see Create a workflow.

Workflow nodes

These are the builder types the orchestrator registers (in workflow_executor/src/nodes.rs). Each diagram op of "type": "node" names one of them and passes a config object.

Registration is client-gated: the three AMQP nodes (DefaultNode, MAPFGoToNode, WaitAMRTaskNode) register only when an AMQP client is configured; MQTTTaskRequestNode registers only when an MQTT client is configured. Unknown/unavailable builders fail the diagram.

Structural ops are Crossflow built-ins, not custom nodes: fork_clone (split into parallel branches), join (wait for buffers), buffer (a wait/hand-off point), and the terminal { "builtin": "terminate" }.

MAPFGoToNode — AMR move via MAPF (AMQP)

Publishes a TaskRequest to AMQP and blocks until the matching TaskStatus returns. The node's task id is formatted as urn:ngsi-ld:Task:{task_id}, and the emitted request's taskType is always amr_mapf (the task_type config below is only a label).

ParamTypeDefaultDescription
asset_namestring""Robot id, e.g. Manufacturer_2
coordinatesstring""Goal waypoint — must exist in the map graph (e.g. P300)
task_typestring""Label only; the published taskType is hardcoded to amr_mapf
task_idstring""Unique per node; wrapped as urn:ngsi-ld:Task:{task_id}
publish_exchangestring@RECEIVE@Exchange the TaskRequest is published to
publish_routing_keystring""Routing key (ignored by the fanout exchange)
response_exchangestring@RECEIVE@Exchange the response listener binds to
response_queue_prefixstring@RECEIVE@-task-Queue-name prefix for the response listener

MQTTTaskRequestNode — devices/handover task (MQTT)/manipulation

Used for liftrack / depalletize / droprack / dropoff. Publishes a TaskRequest to asset/{asset_id}/task_request, subscribes to asset/{asset_id}/task_status, and waits for a terminal status: COMPLETED → node done, FAILED → node fails, REJECTED → re-publish (up to max_retries). The outgoing request id is {id}:TaskRequest.

ParamTypeDefaultDescription
idstring""Base task id, e.g. urn:ngsild:Task:task_Depalletize001
task_typestring""liftrack / depalletize / droprack
task_commandstringSTARTtaskCommand field on the request
asset_idstring""Target asset — sets the MQTT topic asset/{asset_id}/task_request
task_paramsobject{}Arbitrary params forwarded in the request
task_expected_startstring""Passed through to the request
task_expected_endstring""Passed through to the request
max_retriesint3Re-publish attempts on REJECTED

WaitAMRTaskNode — timed wait (AMQP)

Sleeps for wait_duration_secs, then publishes a TaskStatus: COMPLETED (id {task_id}:TaskStatus) to @RECEIVE@. Handy for awaiting/simulating an AMR step without a robot.

ParamTypeDefaultDescription
asset_namestring""Robot id (logging)
task_idstring""Used to build the emitted TaskStatus id
task_typestring""taskType on the emitted status
wait_duration_secsint14Seconds to wait before completing

DefaultNode — pass-through (AMQP)

Logs and forwards the workflow context unchanged. Useful as a placeholder.

ParamTypeDefaultDescription
task_idstring""Identifier used in logs

Run

Via the demo launcher (recommended):

bash
./task_orchestrator_control.sh start   # or: start_environment_tmux.sh (step 5)

Standalone:

bash
docker run -d \
  --name task_orchestrator \
  --network rmf2_broker_rmf-network \
  -p 2727:2727 \
  -e TASK_ORCHESTRATOR__AMQP__HOST=rmf2_broker-rabbitmq-1 \
  -e TASK_ORCHESTRATOR__MQTT__HOST=mosquitto \
  rmf2_task_orchestrator:latest

Configuration

Environment variables (override config.toml):

VariableDefaultDescription
RUST_LOGinfoLog level (debug/info/warn/error)
TASK_ORCHESTRATOR__HTTP__HOST0.0.0.0HTTP bind address
TASK_ORCHESTRATOR__HTTP__PORT2727HTTP port
TASK_ORCHESTRATOR__AMQP__HOSTlocalhostRabbitMQ host
TASK_ORCHESTRATOR__AMQP__PORT5672RabbitMQ port
TASK_ORCHESTRATOR__MQTT__HOSTlocalhostMQTT host
TASK_ORCHESTRATOR__MQTT__PORT1883MQTT port

Limitations

Known constraints in this version:

  • AMQP exchange is fanout-only. The exchange kind is hardcoded to fanout (both the consumer and publisher in amqp/src/amqp.rs) — it is not configurable, so direct / topic / headers routing is not supported. routing_key is therefore ignored; every bound consumer receives every message.
  • Task IDs must be unique. The orchestrator correlates AMQP TaskStatus responses back to in-flight requests by task_id (and tracks active workflows by Schedule id). Two tasks sharing an id collide. A response can be delivered to the wrong waiter and duplicate Schedule ids clash in get_workflows. Give every node and schedule a distinct id.
    • Workaround: omit task_id from a node's config entirely rather than reusing one.

Local development

bash
# Ubuntu 22.04 + ROS 2 Humble
sudo apt install clang ros-humble-example-interfaces ros-humble-geometry-msgs ros-humble-std-msgs
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

source /opt/ros/humble/setup.bash
cargo build --release
RUST_LOG=debug cargo run

For FastDDS discovery, also source ~/ros_industrial_ws/ros_industrial_demo/launch/fastdds_setup.sh before running.

Troubleshooting

bash
docker logs task_orchestrator --tail 50
curl -sf http://localhost:2727/health_check && echo "orchestrator up"

Released under the Apache-2.0 License.