Robot Fleet Orchestration with Open-RMF: A Tutorial
Robot fleet orchestration is the problem of getting many autonomous machines — possibly from different vendors, running different navigation stacks — to share one physical space without colliding, deadlocking, or fighting over the same charger. A single autonomous mobile robot (AMR) running Nav2 is a solved problem in 2026. Ten of them in a warehouse aisle, plus two cleaning robots and a delivery bot that all need the same lift, is not. Open-RMF (the Open Robotics Middleware Framework) is the open-source layer that sits above ROS 2 and turns a pile of independent robots into a coordinated fleet. This tutorial walks through the architecture, the traffic and task-allocation machinery, and a worked example wiring a fleet adapter and dispatching tasks across multiple AMRs.
What this covers: the multi-robot coordination problem, Open-RMF’s architecture, how traffic negotiation and task dispatch actually work, a hands-on fleet-adapter and dispatch walkthrough, the trade-offs that bite in production, and practical recommendations.
Context: why multi-robot orchestration is hard
The naive mental model is that robot fleet orchestration is “single-robot navigation, times N.” It is not. The hard parts are emergent.
Shared space is a contended resource. Two robots planning independently will both happily claim the same corridor at the same time. Each one’s plan is locally optimal and globally wrong. You need a referee that reasons about space and time together — robot A occupies cell (12, 4) from t=10s to t=14s — so you can detect that robot B’s plan intersects it before either moves.
Deadlock is the default failure mode. Put two robots nose-to-nose in a one-lane aisle and a purely reactive system freezes: each waits for the other to yield. Real fleets hit this constantly at intersections, doors, and lift lobbies. Avoiding it requires either central reservation or a negotiation protocol with a tie-breaking rule.
Heterogeneity is the norm, not the exception. A real facility runs robots from multiple vendors. One speaks a proprietary cloud API, another exposes a raw ROS 2 topic, a third only accepts goal poses over a REST endpoint. Orchestration cannot assume a single control interface.
Shared infrastructure has state. Doors, lifts (elevators), and chargers are singletons with their own constraints. A lift carries one robot at a time; a door must open before a robot arrives and stay open until it clears. These are coordination problems in their own right, coupled to the traffic problem.
Task allocation is an optimization, not a queue. Given fifteen pending pickups and eight idle robots, which robot does which task — accounting for current position, battery, and the routes everyone else has already reserved — is a combinatorial assignment problem. A round-robin queue leaves robots driving across the building for no reason.
There is one more dimension that makes this genuinely hard: the system is open-loop with respect to humans. A warehouse is not a sterile testbed. People walk through aisles, forklifts cut across lanes, a pallet gets dropped where the map says there’s clear floor. Single-robot navigation handles this locally — Nav2’s costmap inflates around the obstacle and the local planner swerves. But a swerve changes the robot’s spacetime footprint, which can invalidate a reservation that the fleet layer believed was safe. Coordination and reaction therefore have to be reconciled continuously, not computed once.
Finally, throughput is the metric nobody mentions in the demo video but everybody fights over in production. A fleet that never collides but only achieves forty percent of single-robot throughput per machine is a commercial failure. Every coordination decision — who yields, how wide the safety buffers are, how aggressively tasks are reassigned — is implicitly a throughput trade-off. The orchestration layer’s real job is not just safety; it is extracting the most useful work from the fleet per hour, subject to never crashing.
This is the gap Open-RMF fills. It does not replace your per-robot autonomy; it assumes you already have working navigation (typically Nav2 on each AMR) and adds the fleet-level layer on top. Crucially, it is an open framework — a community-maintained, vendor-neutral standard rather than a single company’s closed platform — which is why it has become the lingua franca for multi-vendor deployments in logistics, hospitals, and commercial buildings.
Open-RMF architecture
Open-RMF is the reference open-source stack for robot fleet orchestration: a collection of ROS 2 packages — rmf_core is the heart — that together provide traffic management, task dispatch, and standardized interfaces to fleets and building infrastructure. The key design decision is that Open-RMF is cooperative and decentralized in spirit but centralized in arbitration: each fleet keeps its own planner and control, but they all submit to a shared traffic schedule and a shared task dispatcher.

Open-RMF’s orchestration layer (dispatcher, traffic schedule, negotiation, building map) drives vendor-specific fleet and infrastructure adapters, which in turn talk to per-robot Nav2 stacks over the ROS 2 DDS or Zenoh transport.
Reading the stack bottom-up:
- ROS 2 middleware layer. Each robot runs its own navigation stack (Nav2 or a vendor equivalent) and publishes/subscribes over DDS — or increasingly Zenoh, which is becoming the default transport for distributed robotics. Open-RMF is transport-agnostic; it speaks ROS 2 messages and does not care what carries them.
- Adapters. A fleet adapter is the bridge between one vendor’s fleet and the RMF core. Door, lift, and charger adapters do the same for infrastructure. Adapters are the integration surface — almost all of your custom code lives here.
- Orchestration layer. The
rmf_traffic_schedulenode, the negotiation service, the building map server, and the task dispatcher. These are largely off-the-shelf; you configure them rather than rewrite them.
The building_map deserves special mention. It is a static description of the facility — the navigation graph (waypoints and lanes), the locations of doors and lifts, charger positions, and which lanes belong to which fleet. It is authored in the RMF Traffic Editor and exported to a YAML/.building.yaml artifact that every component reads. The traffic schedule reasons in terms of this graph, not raw occupancy grids. That is a deliberate and important abstraction. Reasoning about spacetime conflicts on a graph of a few hundred lanes is tractable in real time; doing the same reasoning on millions of occupancy-grid cells across a dozen robots would not be. The graph is a lossy but coordination-friendly view of the world. Your robots still navigate on full occupancy grids locally — the graph is purely the shared coordination substrate, the common language the fleets use to talk about where they intend to be.
A useful way to think about the whole stack is in terms of who owns what decision. The per-robot Nav2 layer owns reactive, local decisions — avoid that person, follow this lane edge precisely, recover from a stuck state. The fleet adapter owns translation and reporting. The orchestration layer owns the global, cross-robot decisions — which robot does which task, and whose plan wins when two conflict. Open-RMF’s architecture is essentially a clean separation of those three concerns, and most integration pain comes from blurring the boundary between them.
Traffic schedule and negotiation
The traffic schedule is the spacetime reservation system. Every fleet adapter, before it commits a robot to a path, submits the planned itinerary — a sequence of trajectory segments, each annotated with the time window it occupies — to the schedule node. The schedule is the single source of truth about who is going where, and when.
When two itineraries conflict (overlap in space and time), the schedule does not just reject one. It opens a negotiation: a structured, time-boxed protocol where the affected participants propose alternative plans, the system scores the combinations, and a conflict-free joint solution is committed. This is what prevents the nose-to-nose deadlock — the loser of the negotiation reroutes or waits, deterministically.

When fleet adapter B’s submitted route conflicts with A’s in spacetime, the schedule opens a negotiation room; both adapters submit alternative proposals, the negotiation service scores the combined cost and commits a resolved schedule.
It helps to be concrete about what an itinerary actually is. It is not a goal pose; it is a trajectory through spacetime — a sequence of waypoints with associated time intervals, plus the robot’s spatial profile (the vicinity_radius swept along the path). The schedule maintains all live itineraries in a spatiotemporal index so it can answer the only question that matters quickly: does this new itinerary intersect any existing one? When the answer is no, the route is simply accepted and the robot proceeds. The negotiation machinery only wakes up on conflict, which keeps the common, conflict-free case cheap.
The negotiation itself is a bounded search. Each participant proposes alternative itineraries — reroute via another lane, slow down, wait at a holding point — and the negotiation service scores combinations by total cost (roughly, summed delay across the participants). It commits the lowest-cost feasible combination. Because the search is over a small set of participants and a bounded set of proposals, it terminates fast in the normal case. The interesting failures are the abnormal ones.
Two properties matter in practice. First, negotiation is cooperative — every participant is assumed to follow the agreed itinerary. A rogue robot that ignores its committed plan breaks the model, which is why physical lane discipline and accurate localization matter. Second, the negotiation has a deadline; if no agreement is reached in time, RMF falls back to a conservative resolution (typically one party stops). You will see this as occasional “polite gridlock” where a robot pauses longer than you’d like — that is the safety fallback firing, not a bug. The art of tuning a dense fleet is keeping negotiations rare (by designing the map with enough passing places and one-way lanes) so the fallback almost never has to fire.
Fleet adapter and task dispatch
The fleet adapter is where the rubber meets the road. It implements two responsibilities. On the control side, it translates RMF commands into whatever your robots actually understand (goal poses to Nav2, a vendor REST call, etc.) and reports robot state back. On the coordination side, it participates in traffic negotiation and responds to the task dispatcher’s bid requests.
Task dispatch in RMF uses an auction. When a task arrives, the dispatcher broadcasts a bid request to all capable fleet adapters. Each adapter estimates the cost of having one of its robots do the task — factoring in travel time from the robot’s current position, battery, and the routes already reserved — and submits a bid. The dispatcher awards the task to the lowest-cost bidder. This is how RMF achieves sensible allocation across heterogeneous fleets without any fleet knowing the internals of the others.

A task request triggers the dispatcher to solicit cost bids from each fleet adapter; the winning adapter reserves a conflict-free path in the traffic schedule and drives its robot’s Nav2 stack, streaming state updates back to the dispatcher.
The auction model has a subtle but important consequence: the quality of allocation is only as good as the cost estimates the adapters return. If your adapter returns a naive straight-line distance instead of a real route estimate that accounts for one-way lanes and current congestion, the dispatcher will make confidently wrong choices — awarding a task to a robot that looks close on paper but has to take the long way around. Investing in an honest cost function inside the adapter pays back directly in fleet efficiency.
Open-RMF ships two common adapter flavors. The full control adapter is for fleets where RMF plans the path and the robot just executes goal poses — RMF owns navigation. The traffic light (or read-only) adapter is for fleets that plan their own paths internally; RMF only grants or withholds permission to proceed, acting as a traffic signal. Most teams start with full control for RMF-native AMRs and use traffic-light integration for closed vendor systems they can’t fully open up. There is a spectrum between them in practice: some vendors expose enough to let RMF influence routing but not fully own it, and the adapter is where you encode exactly how much authority RMF has over that particular fleet. Getting this boundary right per-vendor is one of the more strategic decisions in a real deployment, because it determines how much coordination leverage you actually have when the floor gets busy.
Door, lift, and charger integration
Shared infrastructure is coordinated through the same adapter pattern, but the semantics differ from fleets. A door adapter exposes a door’s state (open, closed, moving) and accepts open/close requests; a robot whose route crosses a door triggers a request through RMF, waits for confirmation that it’s open, passes, and the door closes behind it. A lift adapter is more involved — it must summon the lift to the robot’s level, hold it, admit one robot, move to the destination level, and release. Because a lift serializes everything that needs to change floors, RMF treats it as a scarce reservable resource much like a contended corridor.
Charger integration ties into the battery model in the fleet config. When a robot’s estimated state of charge drops below recharge_threshold, RMF stops bidding that robot into new tasks and routes it to its assigned charger. The charger location is just a flagged waypoint in the building map, so charging is, satisfyingly, expressed in the same vocabulary as everything else — it’s a task that sends a robot to a special waypoint and holds it there until charged. This uniformity is one of Open-RMF’s quiet strengths: doors, lifts, chargers, and navigation all reduce to reservations over a shared graph, which keeps the mental model small even as the facility grows complex.
Walkthrough: dispatching tasks across a fleet
Let’s wire a minimal two-AMR fleet and dispatch a task. The snippets below are illustrative and trimmed for clarity — they show the shape of real Open-RMF configuration, not a copy-paste-ready deployment. Always check the version-matched docs for your RMF release.
Step 1 — The building map
You author the map in the Traffic Editor GUI, but it exports to a building file the core reads. Conceptually it defines waypoints, the lanes between them, and named infrastructure.
# warehouse.building.yaml (illustrative excerpt — real files are editor-generated)
name: warehouse_l1
levels:
L1:
vertices:
- [10.0, 5.0, 0.0, {name: pickup_a, is_charger: false}]
- [22.0, 5.0, 0.0, {name: dropoff_a, is_charger: false}]
- [4.0, 2.0, 0.0, {name: charger_1, is_charger: true}]
lanes:
- [0, 1, {bidirectional: false, graph_idx: 0}]
- [2, 0, {bidirectional: true, graph_idx: 0}]
doors:
- {name: dock_door, motion_direction: 1, type: SingleSwing}
The graph_idx tags lanes to a navigation graph; a fleet only uses lanes on its assigned graph, which is how you keep a slow cleaning fleet out of the fast picking lanes.
Step 2 — The fleet adapter config
Each fleet gets a YAML config describing its robots’ kinematics, battery model, and which task types it accepts. This is the file you tune most.
# amr_fleet.config.yaml (illustrative)
rmf_fleet:
name: amr_alpha
limits:
linear: [0.7, 0.5] # max velocity, max accel (m/s, m/s^2)
angular: [0.6, 0.8] # max velocity, max accel (rad/s, rad/s^2)
profile:
footprint_radius: 0.35
vicinity_radius: 0.55
reversible: false
battery_system:
voltage: 24.0
capacity: 40.0 # Ah
charging_current: 8.0
recharge_threshold: 0.15
task_capabilities:
loop: true
delivery: true
clean: false
robots:
amr_01:
charger: charger_1
amr_02:
charger: charger_1
The vicinity_radius (slightly larger than the footprint) is the buffer RMF uses for spacetime conflict checks — too small and robots clip each other, too large and the schedule becomes needlessly conservative and throughput drops.
Step 3 — Launch the core and the adapter
Bring up the schedule, dispatcher, building map server, and your fleet adapter. Real deployments compose several launch files; the common entry points look like this:
# Terminal 1 — RMF core: schedule, dispatcher, building map, door/lift managers
ros2 launch rmf_demos warehouse.launch.xml \
config_file:=warehouse.building.yaml
# Terminal 2 — full-control fleet adapter bound to your robots
ros2 launch rmf_demos_fleet_adapter fleet_adapter.launch.xml \
config_file:=amr_fleet.config.yaml \
nav_graph_file:=warehouse.nav.yaml
At this point the adapter should register with the schedule. A quick way to confirm the dispatcher sees your fleet is to query its state topic:
ros2 topic echo /fleet_states --once
You should see amr_alpha with both robots reporting mode: IDLE and their charger assignments.
Step 4 — Dispatch a task
Open-RMF ships a rmf_demos_tasks CLI for submitting tasks. To dispatch a delivery — robot picks up at pickup_a, drops at dropoff_a:
ros2 run rmf_demos_tasks dispatch_delivery \
-p pickup_a -ph dispenser_a \
-d dropoff_a -dh ingestor_a \
--use_sim_time
For a simpler patrol/loop task that just moves a robot between named places N times:
ros2 run rmf_demos_tasks dispatch_patrol \
-p pickup_a dropoff_a -n 3 \
--use_sim_time
Now watch the auction play out end to end. The dispatcher requests bids, both AMRs estimate cost, the closer/cheaper one wins, reserves its itinerary, and drives — opening the dock door en route if the path crosses it.

End-to-end task dispatch: the CLI submits a delivery, the dispatcher runs a bid auction, the winning fleet adapter reserves an itinerary in the traffic schedule, sends a goal pose to the AMR’s Nav2 stack, requests a door open mid-route, and reports completion.
Step 5 — Observe and verify
You can watch task state and traffic decisions through RMF’s topics and the web dashboard (rmf-web). For a terminal-only check, the task summary topic tells you what each task is doing:
ros2 topic echo /task_summaries
Look for the task transitioning through Queued → Underway → Completed. If a task sits in Queued forever, the usual culprits are: no fleet declared the capability in task_capabilities, the pickup/dropoff names don’t match the building map, or every robot is below recharge_threshold and headed to a charger.
Trade-offs and what goes wrong
Open-RMF is cooperative, not adversarial-safe. The whole model assumes every participant honors its committed itinerary. It is not a system for coordinating robots that might cheat or fail silently. If a robot loses localization and drifts off its reserved path, the schedule’s guarantees evaporate and you can get the very collisions RMF was meant to prevent. Localization quality is a hard prerequisite, not a nice-to-have.
The negotiation deadline is a throughput knob with teeth. Short deadlines mean RMF gives up and applies the conservative stop-one fallback more often — robots pause, throughput sags. Long deadlines mean a tricky conflict can stall multiple robots while the negotiation grinds. There is no universally right value; it depends on map topology and fleet density, and you tune it empirically.
The building map is load-bearing and brittle. Almost every “robot won’t move” or “task stuck in Queued” bug traces back to a map mismatch — a waypoint name typo, a lane assigned to the wrong graph index, a charger not flagged is_charger. The map is authored in a GUI and easy to get subtly wrong, and the errors surface far from their cause.
Fleet-adapter integration dominates the effort. The core is largely turnkey; the adapter is where you’ll spend weeks. Closed vendor systems that only expose a thin API force you into traffic-light integration, which gives RMF much less leverage — it can stop a robot but can’t reroute it. The more control the vendor hands you, the better RMF performs.
Infrastructure adapters are real systems. A lift adapter that talks to a building’s elevator controller via Modbus or a proprietary BMS protocol is a genuine integration project with its own failure modes. Doors and lifts also become throughput bottlenecks: a single lift serializes every robot that needs to change floors.
Simulation-to-reality gap is significant. The rmf_demos packages run beautifully in Gazebo. Real floors have people, dropped pallets, Wi-Fi dead zones, and robots that don’t perfectly track their plans. Budget time for the gap; a clean sim run is the start of integration, not the end.
Practical recommendations
- Start in
rmf_demos, then swap one piece at a time. Get the warehouse demo running in simulation first. Then replace the simulated fleet with your real fleet adapter while keeping everything else stock. Change one component per iteration so failures are attributable. - Treat the building map as version-controlled source. Keep the
.building.yamland nav graphs in git, review changes, and validate them in CI if you can. Map errors are your most common and most confusing failure class. - Begin with full-control adapters where you can. RMF earns its keep when it owns navigation. Reserve traffic-light integration for genuinely closed vendor systems, and push vendors to expose more control over time.
- Tune
vicinity_radiusand the negotiation deadline against real density. These two parameters trade safety margin against throughput. Profile them with your actual robot count and aisle widths, not defaults. - Invest early in localization and lane discipline. RMF’s guarantees are only as good as the robots’ adherence to their itineraries. Bad localization undermines everything above it.
- Instrument task lifecycle from day one. Pipe
/task_summariesand/fleet_statesinto your observability stack. When throughput drops at 2 a.m., you want the auction and negotiation history, not a screenshot of a frozen dashboard. - Treat robot fleet orchestration as a systems-integration discipline. The algorithms in
rmf_coreare mature; the project risk lives in maps, adapters, infrastructure protocols, and localization quality. Staff and schedule accordingly, and resist the temptation to rewrite the core when the real bug is a map typo. - Plan infrastructure adapters as separate projects. Don’t bolt lift integration onto the fleet-adapter sprint. It’s its own protocol, its own controller, its own safety review.
For teams building the broader autonomy stack around this, the same layering discipline applies to manipulation and legged platforms — see how a humanoid robot control stack is architected for a parallel treatment of separating policy, planning, and hardware abstraction.
FAQ
What is the difference between Open-RMF and ROS 2?
ROS 2 is the middleware and tooling for building a single robot’s software — transport, message types, navigation packages like Nav2. Open-RMF sits above ROS 2 and coordinates many robots and the shared infrastructure they use. ROS 2 gets one robot to navigate; Open-RMF gets a heterogeneous fleet to share space, allocate tasks, and use doors and lifts without colliding or deadlocking. You run Open-RMF on top of ROS 2, not instead of it.
Does Open-RMF replace my robot’s navigation stack?
No. Open-RMF assumes each robot already has working autonomy — typically Nav2 or a vendor stack. It does not do obstacle avoidance or local planning. With a full-control fleet adapter, RMF plans the route through the building graph and hands goal poses to your navigation stack to execute. Your stack still owns localization, local planning, and collision avoidance with dynamic obstacles like people.
How does Open-RMF prevent two robots from colliding?
Through a spacetime traffic schedule. Before a robot moves, its fleet adapter submits the planned itinerary — paths annotated with time windows — to a central schedule. If two itineraries overlap in space and time, RMF opens a negotiation where the participants propose alternatives and the system commits a conflict-free joint plan. It is a cooperative reservation system, so it depends on every robot actually following its committed itinerary.
Can Open-RMF coordinate robots from different vendors?
Yes — that is a core design goal. Each vendor’s fleet is integrated through its own fleet adapter, which translates RMF commands into that vendor’s control interface. The task dispatcher runs an auction across all fleets, so a task goes to whichever vendor’s robot can do it most cheaply, without any fleet knowing the others’ internals. Heterogeneous, multi-vendor fleets are exactly what RMF is built for.
Why is my task stuck in the Queued state?
The common causes are: no fleet declared the matching capability in task_capabilities; the pickup or dropoff names in the dispatch command don’t match waypoint names in the building map; or every eligible robot is below its recharge threshold and committed to charging. Check /fleet_states to see robot modes and /task_summaries for rejection reasons, and verify the location names against the map.
Further reading
- Open-RMF documentation — official site, concepts, and getting-started guides.
- Open-RMF on GitHub —
rmf_core, demos, fleet adapter templates, and the Traffic Editor. - ROS 2 documentation — the middleware
