Skip to main content
Version: 10

Force a Node's Result

Introduction​

While debugging an Objective you often need to reach the part after a node, not the node itself. Substituting a node makes it immediately report SUCCESS or FAILURE without doing any work, so the rest of the tree runs without the hardware, motion, or preconditions that node would require. It is the equivalent of the fake result you would otherwise get by commenting out a Behavior and wiring a constant in its place, without editing the Objective at all.

Required Version
This feature will not be released until MoveIt Pro version 10.3.

Allow Substitution in Simulation​

Node substitution is off until the robot configuration allows it. A forced result skips work the robot's safety may rely on, so a configuration that drives hardware must leave it off. In the config.yaml of a configuration for the MoveIt Pro Simulator, set objectives.allow_node_substitution:

objectives:
allow_node_substitution: true

Restart MoveIt Pro after you change it. The Runtime also reads the ros2_control hardware plugins in the robot description: unless there is at least one and every one is the MuJoCo plugin the MoveIt Pro Simulator uses (picknik_mujoco_ros/MujocoSystem) or mock hardware (mock_components/GenericSystem), substitution stays off and the Runtime logs which plugin kept it off. Third-party simulators are not recognized, so a Gazebo or Isaac Sim configuration stays off too; topic_based_ros2_control/TopicBasedSystem in particular can bridge to real hardware. Substitution also stays off when hardware.launch_robot_state_publisher or hardware.launch_control_node is false, because another process then loads the hardware the Runtime would check.

The check is a backstop, not proof that nothing real moves. It cannot see anything driven outside ros2_control: nodes started from runtime_launch_file, additional_driver_launch_file, or additional_agent_launch_file, a vendor action server that ExecuteTrajectory or MoveGripperAction is pointed at, a gripper driven over Modbus or I/O by its own node, or a mobile base driven by its own computer. Never allow substitution on a Runtime that shares a ROS domain with a real robot. The setting is not inherited: a configuration whose based_on_package allows substitution does not, unless it sets the key itself, so a hardware configuration built on a simulation configuration stays off.

While it is off, the Result section reads Result (disabled for this robot) and offers no choices except Run Normally to clear a substitution already set. The Runtime refuses any run that asks for a substitution, with the reason: the configuration key, the hardware.launch_* setting, or the hardware plugin that kept substitution off. The section reads the same when the Desktop App is connected to a Runtime version without node substitution.

Force a Result​

Substitution applies to a single Behavior, so Result appears in the menu only on an Action. On any other node the section is absent rather than greyed out:

  • A Sequence, Fallback, decorator, or SubTree runs other nodes, and forcing its result would skip all of them without saying which. To skip a whole branch, substitute the Behavior inside it, or comment the branch out.
  • A node shown inside an expanded Subtree belongs to that Subtree's own Objective, so the editor does not offer it from this Objective. Open the Subtree's Objective to substitute it there.
  • A Condition decides whether other Behaviors run. Forcing its result would skip the check itself rather than one Behavior's work, so it is refused — comment it out if that is what you want, which is visible in the Objective rather than only in your session.

A forced SUCCESS or FAILURE empties the node's output ports, so a Behavior that reads them fails instead of acting on stale values, and the rest of the tree notices that the work did not happen. Any Action whose skipped work this reveals can be substituted, a custom one included: planners, IK, perception, MTC stage setup, and motion Behaviors such as ExecuteTrajectory and MoveGripperAction. An Action refuses substitution when emptying its outputs would not reveal what it skipped:

  • A check or an operator gate (ValidateTrajectory, the Is… checks, ScanMatchResidual, the WaitFor…Approval Behaviors, IsUserAvailable, BreakpointSubscriber), where an Inverter or a Fallback above it can move the robot past the check whichever result is forced.
  • An effect outside its output ports: a service call or a published topic (CallTriggerService zeroing a force/torque sensor before admittance control, PublishString), a parameter (SetRos2Parameter), a broadcast TF frame (DetectAprilTags, PublishTF), a controller or its limits (SwitchController, SetAdmittanceParameters), the planning scene a planner plans against (AddCollisionBox, AttachObject, and the other Planning Scene Behaviors), a file, a saved waypoint, a recording, or the simulator's state.
  • A blackboard write that emptying its ports does not undo: BehaviorTree.CPP's Script, SetBlackboard, and UnsetBlackboard.

An Action with no output or in/out port is refused unless it is marked as safe to force, because a forced result empties nothing. Selected Behaviors that command motion, log, wait, or only draw something for the operator are explicitly marked as allowed. An unmarked Action named like a check or a gate (Is…, Validate…, Check…, WaitFor…, BlockUntil…, …Approval) is also refused, even with outputs, because a check's output does not reveal that the check was skipped. On a refused Behavior the section reads Result (this Behavior always runs) and offers no choices, except Run Normally to clear a substitution set before the Runtime reported it, and the Runtime refuses the request if a script sends it.

A custom Behavior marks itself in the list its metadata() returns, with MakeSubstitutionRefusedMetadata() for a check, a gate, or an effect outside its outputs, or MakeSubstitutionAllowedMetadata() for one that is safe to force but would otherwise be refused, because it has no outputs or is named like a check:

#include <moveit_pro_behavior_interface/metadata_fields.hpp>

BT::KeyValueVector MyGripperCheck::metadata()
{
return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Grasping" },
{ moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionMyGripperCheck },
moveit_pro::behaviors::MakeSubstitutionRefusedMetadata() };
}

An Action with an authored _successIf, _failureIf, _skipIf, or _while precondition cannot be substituted: BehaviorTree.CPP evaluates that condition before the forced-result hook. The Runtime refuses the substitution and names the precondition rather than silently reporting another result.

A Behavior derived from BT::ThreadedAction or BT::CoroActionNode cannot be substituted. A ThreadedAction ticks itself on its own thread, out of the Runtime's reach, and the Runtime cannot safely stop a run that re-ticks a forced CoroActionNode.

A parent that ticks a finished child again without resetting it, such as a WhileDoElse ticking its condition, gets the forced result again from a Behavior derived from AsyncBehaviorBase or BT::StatefulActionNode, which includes every Behavior that moves the robot. A synchronous one, such as TransformPose or GenerateCuboidGraspPoses, would do its work on that second tick, so the Runtime stops the run with an error naming the node instead.

On an Action with unsaved edits the section reads Result (save the Objective first) and offers no choices: the Runtime names a node by a path the save re-stamps, so until then the editor cannot say which node a forced result would reach.

Select a node in the Behavior Tree editor and open More Actions in its node actions. Under Result, pick:

  • Run Normally — the default; the node does its own work.
  • Always Succeed — the node reports SUCCESS without running.
  • Always Fail — the node reports FAILURE without running.

A substituted node gets a thick green or red border and a Forced success · not run or Forced fail · not run label under its title. The node is marked in the editor and while the Objective runs, and the run view shows a Forced results banner above the tree. Look for these marks to tell a forced result from a real one: the status a forced node reports is otherwise the same as a real result. A node you set while viewing a run that did not force it shows a muted Forced next run label instead, because the substitution applies from the next run.

The Runtime reports which nodes a run forces, so the run view marks them for everyone watching the run: another operator, and a run started by a script calling /do_objective or /execute_objective with substitutions of its own.

A script sets the node_substitutions field of the DoObjectiveSequence goal or the ExecuteObjective request. Each entry names a node by the path and registration_name of one of the occurrences that stamp_tree_uids reports for the saved Objective, the same address a breakpoint uses; NodeSubstitution.msg describes the fields.

danger

A forced node does no work, and the Behaviors after it act as though it did. Never allow substitution in a configuration that drives a real robot.

  • For either forced result, the Runtime empties the blackboard entries the Behavior's output and in/out ports write, so a downstream Behavior that reads them — a trajectory, a grasp pose, a detected object — fails on a missing input instead of acting on a value an earlier step left there. This also applies when a ForceSuccess or recovery branch continues after a forced FAILURE.
  • The Behaviors after it still change the world model to match. Forcing MoveGripperAction to succeed and then running AttachObject attaches an object the gripper never closed on: the planner then ignores that object where it still stands, and plans the carried geometry the gripper does not hold.
  • A trajectory planned before a forced ExecuteTrajectory still starts where that execution would have ended. The admittance controller refuses one whose start is far from the robot; a controller that interpolates from the current state, such as the upstream joint_trajectory_controller, moves there at the speed the first waypoint's timing implies, with no joint velocity limit on that segment.
  • After a forced NavigateToPoseAction or FollowPathAction, the base is not at the goal, so arm motions to base-relative waypoints and perception that expects the new viewpoint run from where the base still is.
  • The node's _onSuccess, _onFailure, and _post scripts still run for the result it reports, so a flag such as _onSuccess="grasped := true" is set for work that never happened.

Substitutions Last for One Session​

A substitution is a debugging aid, so it is deliberately never written to the Objective:

  • It is not saved. Reloading the MoveIt Pro Desktop App clears it, and it can never be committed into an Objective by accident.
  • It applies to the Objective you set it on. Opening a different Objective discards it, because the node it named only exists in the tree it was stamped from.
  • It is local to your session. Another operator connected to the same Runtime does not inherit it, and an Objective started by a script calling /do_objective or /execute_objective runs unsubstituted unless that script asks for substitutions itself. A run that does force results is still marked for every viewer, as described above.

Editing the Objective's structure clears every substitution on it: adding, deleting, or reordering nodes renumbers the nodes below the edit, and renaming a node clears them too. So does a save of the Objective made elsewhere, such as from another Desktop App, that changes its structure, once this Desktop App reads that save back, for example when you reopen the Objective. Until then the editor cannot see it, and the Runtime only refuses a substitution whose path now holds a different Behavior. Adding or removing a breakpoint does not, because a breakpoint does not change the Objective. Set your substitutions after the tree is the shape you want to debug.

Substitute or Comment Out?​

Both keep a Behavior from doing its work, and they answer different questions:

UseWhen you want
SubstitutionThe node's parent to see a specific result. A Fallback takes its second branch only if the first reports FAILURE, so forcing FAILURE on the Action in its first branch is how you exercise that branch.
Comment outThe node skipped entirely, as though it were not in the tree. Its result does not participate in the parent's logic at all.

Commenting out is also saved with the Objective, so reach for it when the change should outlive your session.