Skip to main content
Version: 10

Motion Planning Best Practices

Introduction

Motion planning reliability — the rate at which planners return natural, collision-free paths — depends heavily on how the robot configuration package is set up. A robot whose joint limits allow large wrap-arounds or whose saved waypoints live in different elbow configurations will produce inconsistent, unpredictable, and sometimes scary-looking motions, even when the planner technically succeeds.

This guide collects practical tips for raising planning success rate and producing consistent, human-predictable motions. These recommendations apply to most 6- and 7-DoF serial arms.

Constrain Joint Limits

Prefer a [-π, π] range per joint on revolute joints

In your robot configuration package's joint_limits.yaml, limit every revolute joint to the range [-π, π] (approximately [-3.14159, 3.14159]). This prevents the planner from finding solutions that wrap around multiple times, which otherwise produce trajectories that slowly "unwind" a fully-rotated joint.

See moveit_params.joint_limits for how to declare these overrides.

tip

After tightening the limits, re-open your waypoints.xml (or waypoints.yaml) and update any saved joint values that fall outside the new range. Waypoints whose joint values are outside the new limits will fail to load or will plan poorly.

tip

If the joint doesn't have position limits, i.e. it can rotate continuously, set its type to 'continuous' in the URDF. Planners handle these joints differently to avoid wrap-around.

Further constrain the elbow

If your application allows it, limit the elbow joint to a single, narrow range — for example [-0.05, 1.57] (approximately 0 to π/2). This forces solvers to work entirely on one side of the kinematic singularity, preventing the elbow from flipping between positive and negative during a plan.

Why this matters: an elbow flip requires the arm to pass through a fully-stretched configuration at some point during the motion. This normally produces weird-looking, fast, or potentially unsafe motions even when the planner is successfully finding a 'short' path.

If the application requires the full elbow range (both positive and negative), at minimum ensure that all pre-recorded waypoints share the same elbow sign, or you minimize switching between the two elbow 'hemispheres' as much as possible.

Tighten the joint range for a single motion

The joint_limits.yaml limits above apply globally to every plan. When a single motion needs a tighter range — without changing the robot's global limits — set the joint_range_constraint input port on the ProRRT planning Behaviors (PlanToJointGoal, SetupMTCPlanToRobotState, SetupMTCConnectWithProRRT, and SetupMTCPlanToPose). List only the joints you want to restrict; joints you omit keep their full URDF range. For example, you can keep the base joint from swinging toward a nearby obstacle on one approach motion while leaving it unconstrained everywhere else.

Required Version
This feature requires MoveIt Pro version 9.4 or newer.

Save Waypoints in Natural Configurations

Stay away from limits and self-collision

Prefer "natural" configurations — joints as close to their zero as possible (while not being at a singularity) and links well away from each other.

The further the joints are from their position limits or from link-to-link collision, the easier it is for the planner to find a smooth path in and out of that waypoint.

Keep waypoints consistent with each other

The planner finds a path between two waypoints by effectively interpolating in joint space and then correcting for collisions and constraints. If the saved waypoints already live in the same kinematic "branch" (same elbow sign, same wrist flip, similar J0 orientation), the planner usually finds a short, direct path. If they live in different branches, the planner has to re-solve for a kinematic change in the middle of the motion — which is slow, unreliable, and often produces surprising motions.

When saving a new waypoint, consciously choose a configuration that is consistent with your existing waypoints.

Teleoperation Technique for Saving Waypoints

Avoid using the interactive marker (IMarker) to save new waypoints except for small, local displacements of an existing waypoint. The IMarker drives an IK solver which is free to choose any branch it likes — so two waypoints saved via IMarker from similar-looking end-effector poses can easily end up in very different joint configurations.

The following teleoperation sequence produces more consistent waypoints. See the Teleoperation view pane for how to access the Joint Jog and Pose Jog controls. Note that this assumes a typical 6R shoulder-elbow-wrist robot manipulator:

  1. Orient the arm with J0 (base). Use the joint slider for the base joint to roughly aim the arm at the working area.
  2. Rough tip position with J1 / J2 (shoulder and elbow). Use the sliders to bring the tool tip into the right region of the workspace.
  3. Orient the end-effector with J3–J6 (wrist). Use the remaining joint sliders to set the tool orientation.
  4. Local adjustments with IMarker or Pose Jog. Only now, once the kinematic branch is fixed by the joint-slider pose, use IMarker or Pose Jog for the final local refinement. Because the starting configuration already constrains the IK solver, it will stay on the same branch instead of flipping.

This workflow trades a few seconds of teleoperation for waypoints that are kinematically consistent with each other — which pays back many times over in planning reliability and motion predictability.

Debug Unnecessarily Long Paths and Joint Flips

Even with well-constrained joint limits and consistent waypoints, you will occasionally see a plan that takes a longer path than expected, or that includes a joint flip or "unwind" mid-motion. These symptoms tend to occur more often in MoveIt Task Constructor (MTC) pipelines, which explore multiple solution branches in search of a feasible task plan. A single unlucky branch can survive the search and produce a path that is feasible but ugly.

When this happens, the most effective approach is to introspect the MTC pipeline result in the Task Constructor Debugger to see which candidates each stage proposed and why the others were rejected. See Debug Task Constructor Planning Pipelines for how to open the Task Constructor Debugger and step through the stages.

Start with the generator stages

Generator stages (for example, Inverse Kinematics) are where the kinematic branch is chosen, so that is where most "weird" motions originate. For each generator stage, ask:

  • Is IK proposing reasonable solutions but they are being rejected? Check the failure reason for each rejected candidate. A common cause is that the IK target, or the robot configuration that would reach it, is in collision with the scene or with the robot itself.
  • Is IK not proposing the expected solution at all? The target may simply be unreachable given the current joint limits, or it may only be reachable in a configuration you did not anticipate (e.g., with the opposite elbow sign).

If the IK solution itself is unreasonable — far from the starting configuration, in the wrong kinematic branch, or near a limit — expect the downstream path planner to produce an equally unreasonable path to reach it. Fix the IK result first; the path will usually follow.

Tune the batch IK stage

When the pipeline uses SetupMTCBatchPoseIK to feed candidate IK solutions into MTC, tune its max_ik_solutions, ik_timeout_s, and spawn_top_k ports together with PlanMTCTask's max_solutions port. The Behavior generates IK solutions per target pose until it either collects max_ik_solutions valid solutions or ik_timeout_s elapses — whichever comes first ends the IK search for that pose. Out of the resulting set, only the spawn_top_k lowest-cost solutions (by joint-space distance to the current state) are inserted into the MTC pipeline; the rest are dropped before any downstream planning happens.

  • max_ik_solutions too high, spawn_top_k unset: MTC costs each IK solution by joint-space L1 (Manhattan) distance to the seed (current) state — the sum, over each active joint, of the per-joint distance — and spawns them in increasing-cost order, so the closest-to-seed candidates are propagated first. A very high cap without a spawn_top_k prune forces downstream stages to also chew through far-from-seed candidates that are unlikely to beat what was already seen. Planning time grows sharply.
  • max_ik_solutions too low: the best IK solution may never enter the pipeline at all, and downstream stages have to work from a suboptimal seed. This is a common cause of joint flips even after the generator stage otherwise looks correct.
  • spawn_top_k too low relative to path-planning failure risk: capping propagation aggressively speeds up the "happy path" but leaves fewer fallback candidates if the closest-to-seed IK cannot be reached by the downstream path planner (for example, because the direct path is blocked). Widen spawn_top_k — or leave it at 0 (propagate all) — when the reach pose has known collision risk.
  • PlanMTCTask's max_solutions port left at 0 (default): MTC does not cap the number of task-level solutions to return, so it keeps propagating candidates through the pipeline instead of stopping after the first few global solutions are found. This compounds the first problem.

A reasonable starting point for many applications:

  • SetupMTCBatchPoseIK: max_ik_solutions around 50, with ik_timeout_s large enough to actually generate that many (for example, 0.1 seconds). Start with spawn_top_k = 0 (propagate everything) and only lower it after profiling shows Connect spending time on far-from-seed candidates that are not helping. Tightening the prune before you have that evidence is a common way to starve cluttered cells: the closest-to-seed branches are often the most likely to be blocked by the same obstacle the seed is already near, and dropping the fallbacks turns a solvable pipeline into "no plan".
  • PlanMTCTask: max_solutions set to a small value like 2-5, so the pipeline exits as soon as a handful of global solutions are found.

The pattern is generate many (cheap IK) → prune to few (cheap sort) → plan few (expensive Connect). Broaden the IK search with max_ik_solutions and ik_timeout_s when you want to be sure the best branch is discovered; tighten spawn_top_k when you want the pipeline to only spend planning time on the closest-to-seed of those branches.

Continuous revolute joints

spawn_top_k's cost is joint-space L1 (Manhattan) distance to the seed — the sum, over each active joint, of MoveIt's per-joint distance metric. For revolute joints marked continuous (no lower/upper limit), that metric wraps modulo , so a candidate wound one full turn away from the seed can score near zero and hijack the top-K. If your planning group has any continuous joints, leave spawn_top_k = 0 until the metric is made wrap-aware.

Diagnostic surface

spawn_top_k narrows the diagnostic surface for successful candidates only. Failure candidates (collision, constraint violation, no IK found) always propagate as failure solutions regardless of spawn_top_k, so PlanMTCTask's "why did my Objective fail" reporting is not affected by tightening the prune.

Then inspect the path planner

If the IK solution looks reasonable but the plan to reach it is still long, inspect the path planning stage (typically a sampling-based planner such as RRT). Two causes are common:

  • Obstacles in the way. Stray voxels in the occupancy map, phantom objects in the planning scene, or an overly conservative collision padding can force the planner to route around a region that looks clear visually. Inspect the planning scene in the 3D Visualizer and clean up any spurious obstacles.
  • A joint limit between the start and the goal. Even when the start and goal configurations look close in Cartesian space, a joint may need to "unwind" all the way across its range because it cannot cross its limit to take the direct route. This often looks like an unexpectedly long motion on a single joint.
tip

Visual similarity of the robot pose is not a reliable signal of configuration similarity. Two configurations that look nearly identical in the 3D Visualizer can still be far apart in joint space if a joint is near a limit and has to unwind. Always check the joint values, not just the visualization.

Tune planner exploration and refinement

If the IK solution and joint limits both look correct but the path planner still returns a longer path than expected, the ProRRT Behaviors (PlanToJointGoal, SetupMTCPlanToRobotState, SetupMTCConnectWithProRRT, and SetupMTCPlanToPose) expose an optimization_params port that trades planning time for path quality:

  • planning_seed_attempts: runs RRT-Connect from several different seeds and keeps the cheapest result. More attempts explore different ways around obstacles, which helps when a single seed gets stuck routing the long way around.
  • optimization_iterations: runs Informed RRT* refinement on the chosen path. More iterations shorten the path further, at the cost of additional planning time.

Raise these values gradually — planning time grows roughly in proportion to them. Leave the port empty to keep the default: a single seed with no refinement.

Required Version
This feature requires MoveIt Pro version 9.4 or newer.

Understanding what each MTC stage is doing — and why it is accepting or rejecting each candidate — is the key to producing robust, predictable, and natural-looking motion.

Keep Memory Bounded in Large Objectives

When MoveIt Pro plans an MTC task, the task keeps every candidate it evaluated, including a snapshot of the planning scene for each one, so you can inspect the results in the Task Constructor Debugger. From MoveIt Pro 10.2, only the most recently planned MTC tasks keep that data (10 by default, configurable with mtc_introspection_retained_tasks in the objectives section of config.yaml); older tasks release theirs automatically, so memory no longer grows with the number of planning Subtrees in an Objective. On earlier versions, that data stays in memory for as long as the Objective is loaded, and every use of an MTC planning Subtree in the Objective holds its own task, so memory grows with the number of planning Subtrees, with the detail of the collision model, and with the number of candidates each task explores. On a machine with limited memory, a long enough Objective can exhaust it, and the operating system kills MoveIt Pro (see Performance Troubleshooting to recognize this). A few practices bound memory further:

  • Turn off introspection where you don't need it. From MoveIt Pro 9.4, the InitializeMTCTask Behavior has an enable_introspection input port. Setting it to false frees a task's planning data as soon as planning completes, at the cost of being able to inspect that task in the Task Constructor Debugger. This is the most direct way to bound memory. See Disabling introspection to reduce memory for the full trade-offs.
  • Cap max_solutions. Leaving PlanMTCTask's max_solutions port at 0 explores and keeps every valid solution, so each task accumulates more. Capping it stops the search early, so the solution MTC returns may not be the lowest-cost one. See Tune the batch IK stage for how to set it alongside the IK ports.
  • Run one Subtree in a loop rather than copying it. An Objective that references a planning Subtree ten times creates ten tasks that all stay in memory; one Subtree that runs ten times under a loop Behavior (for example, RepeatUnlessFailureEachTick) holds only one at a time, because each run re-initializes the task and releases the previous one. Where your workflow allows it, prefer the loop.
  • Keep collision geometry simple. Every planning scene snapshot carries the robot's collision model, so a heavily detailed collision mesh multiplies the cost of everything above. See Optimize Model Meshes.

Mitigate Collision Tunneling

Sampling-based planners like ProRRT do not check a path continuously: they collision-check a finite set of states along it. An obstacle thin enough to fit between two consecutive checked states can go unnoticed, so a plan can validate successfully and still pass through geometry. This is called collision tunneling. Time parameterization adds a second gap: it rounds the corners of the validated path within a small tolerance, so the resulting trajectory can graze geometry slightly more often than the path itself. Controller tracking adds a further small deviation during execution. If the robot occasionally clips an obstacle that the plan appeared to clear, tunneling is a likely cause. A few mitigations go a long way:

  • Add link padding. The ProRRT planning Behaviors (PlanToJointGoal, SetupMTCPlanToRobotState, SetupMTCConnectWithProRRT, and SetupMTCPlanToPose) can inflate the robot's collision geometry by a given distance in meters. Padding does not make the planner check more states; it adds a buffer that a missed contact has to cross before it reaches the real geometry. A few millimeters already removes most tunneling at a small planning-time cost; pushing toward a centimeter buys a little more protection but can make planning a bit slower. From MoveIt Pro 10.2, set it in the collision_check_params input port with the override flag engaged (e.g. [{override_scene_link_padding: true, link_padding: 0.005}]) or through the port's field editor. link_padding applies only while override_scene_link_padding is true — an explicit per-Behavior override, including an override to 0; while false, the Behavior leaves in place the link padding of the planning scene it is handed (for MTC stages, the scene produced upstream in the task), which is 0 on a fresh MoveIt Pro instance. A nonzero link_padding without the flag is rejected rather than silently ignored. The standalone link_padding port is deprecated: while it is set, its value takes precedence over collision_check_params and the Behavior logs a deprecation warning — unset the port to migrate. The shipped motion Objectives now expose collision_check_params (defaulting to [{override_scene_link_padding: true, link_padding: 0.01}], the same padding as before) and keep their old link_padding parameter as a deprecated passthrough. The jog Behaviors (PoseJog, JointJog) take the same collision_check_params port and honor only its padding fields; the teleoperation Objective feeds its port to the planning Subtrees and to PoseJog alike, while its JointJog is pinned to no padding.
  • Tighten the trajectory blend deviation. Time parameterization rounds each corner of the validated path within a tolerance, max_deviation, set through the trajectory_timing input port of the same ProRRT planning Behaviors (and of PlanCartesianPath), in radians for revolute joints or meters for prismatic joints. The default is 0.05. Lowering it keeps the trajectory closer to the collision-checked path at the corners, at the cost of slower corners; a value of 0 applies the default. Wire the port with, for example, [{mode: time_optimal, max_deviation: 0.02}].
  • Don't overdo it. Heavy padding makes narrow clearances read as blocked, so the planner routes around gaps the robot could actually pass through. These Behaviors pad the robot for self-collision checking too, so past a robot-specific value, poses that bring links close together become self-colliding and every plan starting from them fails.
  • Padding covers the payload too. Objects attached to the robot inherit the padding of the link they are attached to, so the buffer also protects what the robot carries. Link padding and world padding add up: a padded payload approaching a padded obstacle keeps the sum of both margins. From MoveIt Pro 10.1, the planning scene's set_attached_body_padding Python method overrides the inherited value for one attached body, and a value of 0 strips it. The mesh caveat below applies here too, and a padded mesh payload is rebuilt on every collision check, so keep its collision geometry simple.
  • Subdivide segments by workspace distance. From MoveIt Pro 10.2, from the Python API, set workspace_step on pro_rrt.RRTParams (in meters), or call set_workspace_step on ProRRTPlanner. It is off by default; when positive, the planner validates each motion segment at samples spaced so that no point of the robot sweeps more than that distance between checks, instead of relying on the joint-space step alone. 0.1 is the benchmarked starting point — it roughly halves tunneling through thin obstacles, at more collision checks per segment and longer planning in obstacle-rich scenes. Unlike padding, this makes the planner check more states rather than adding a buffer. The ProRRT planning Behaviors expose it through the collision_check_params input port, e.g. [{workspace_step: 0.1}].
  • Re-validate the final trajectory as a last resort. From MoveIt Pro 10.2, the ProRRT planning Behaviors accept sanity_collision_check: true in their collision_check_params input port. The mitigations above lower the chance of tunneling but cannot eliminate it; this check catches what slips through. When enabled, the final trajectory — corner blends included — is densely collision-checked against the actual collision geometry, without padding or scaling, before the Behavior returns it; a contact that exists only inside a padding margin does not count, so padding does not cause replans. Note that the corner blends are validated only by this check: they carry no padding margin, only a finite-resolution collision check against the actual geometry at the trajectory sampling rate. If a collision is found, planning is repeated with a different seed within a configurable attempt budget (sanity_check_max_attempts, default 4 total attempts); if every attempt still collides, the Behavior fails and reports the colliding bodies instead of returning a trajectory that would hit them. Wire the port with, for example, [{sanity_collision_check: true, sanity_check_max_attempts: 4}], or use the edit icon next to the port in the Objective editor. It is off by default: the check costs roughly one collision check per trajectory sample, and each attempt is a full planning call — the worst case multiplies planning time (and any per-stage planning timeout) by the attempt budget — so enable it on motions where clipping an obstacle is worse than the occasional slower or failed plan. The check's resolution follows the output trajectory sampling rate, so lowering trajectory_sampling_rate also coarsens the check. The same option is available from the Python API as sanity_collision_check and sanity_check_max_replans on pro_rrt.RRTParams, or set_sanity_collision_check and set_sanity_check_max_replans on ProRRTPlanner.
  • Pad world objects too. From the MoveIt Pro Python API, pass world_padding to ProRRTPlanner or call its set_world_padding method. From MoveIt Pro 10.1 the planning scene has its own set_world_padding and per-object set_world_object_padding methods, honored by the direct planning functions and collision checks. From MoveIt Pro 10.2, ProRRTPlanner inherits the scene's world padding when world_padding is None (the default); on 10.1 it replaces that padding with zero. Passing a value or calling set_world_padding replaces scene-wide and per-object padding, 0.0 removes it, and use_scene_world_padding() puts the planner back on the scene's padding. Only collision checking sees the inflated shapes; visualization keeps the exact geometry. Mesh vertices are displaced away from the mesh centroid, so the margin on elongated or concave meshes comes out smaller than the requested value. Octree (occupancy map) and plane shapes cannot be padded; the plan result's message names the affected objects. Padded shapes are rebuilt at the start of each planning attempt, a few milliseconds per detailed mesh, so scenes with many padded meshes start plans slower. From MoveIt Pro 10.2, the MTC ProRRT planning Behaviors have no world-padding input port but keep the world padding configured in their input planning scene; on 10.1 they replace it with zero.
Required Version
This feature requires MoveIt Pro version 10.1 or newer.

Capture Planning Scenarios for Offline Debug

When a planning failure or a "weird" plan is hard to reproduce live — it happens intermittently, or only in the field — capture the planning inputs to disk so they can be replayed later or sent to PickNik for analysis. A captured scenario records the planning scene and the full request to the planner. The robot model it was planned against (URDF, SRDF, kinematics and joint-limit overrides, and meshes) is stored separately and shared by every capture of the same robot.

Scenario capture is currently supported by the PlanToJointGoal Behavior (which uses MoveIt Pro's ProRRT planner).

Enable capture globally

In your robot configuration package's config.yaml, set:

moveit_params:
scenario_capture:
enabled: true
output_dir: "~/.local/share/moveit_pro/scenarios" # default
max_dir_bytes: 1073741824 # 1 GiB; set to 0 to disable cleanup

Every PlanToJointGoal call then writes a scenario directory under output_dir. When the directory exceeds max_dir_bytes, the oldest captures are deleted automatically.

tip

The default output_dir lives under ~/.local/share/moveit_pro, which is mounted from the host and persists across container restarts and MoveIt Pro version updates. If you change output_dir to a path that is not a mounted host directory, captures will be lost when the container is recreated.

Enable capture for a specific Behavior call

The PlanToJointGoal Behavior also exposes an optional capture_scenario input port that overrides the global default for that instance. This is useful when you want to capture only one Behavior in a tree, or to skip capture in a hot loop while leaving it on globally.

  • Leave capture_scenario unset — the Behavior uses the global scenario_capture.enabled setting.
  • Set capture_scenario to true — capture this call regardless of the global setting.
  • Set capture_scenario to false — skip capture for this call even when capture is globally enabled.

Share or replay a capture

A capture is split across two directories under output_dir: the capture itself in scenarios/, and the robot model it plans against in robots/<robot_hash>/, shared by every capture of the same robot. Replay needs both, so archive both, keeping their paths relative to output_dir. Read robot_hash from the capture's scenario.json:

cd <output_dir>
tar czf my_scenario.tgz scenarios/<capture_dir> robots/<robot_hash>

Send the archive to PickNik when filing a planning-failure report.