Tutorial 2: High-Level Actions — Pick, Drop, Move, Wait
Source file: examples/basics/action_system_demo.py
This tutorial introduces PyBulletFleet’s action queue system: a way to give an agent an ordered list of tasks — pick up a pallet, carry it to a drop-off, drive to a charging station, wait — and let the simulation execute them automatically.
What you’ll learn:
The difference between
set_goal_poseand the action queueSpawning pickable objects with
SimObjectSpawnParamsThe four built-in action types:
MoveAction,PickAction,DropAction,WaitActionQueueing a task sequence with
add_action_sequenceMonitoring progress via
get_current_action
If you haven’t read Tutorial 1 — Spawning Objects yet,
start there to understand SimulationParams, Agent, and register_callback.
1. Why Use Actions Instead of set_goal_pose?
set_goal_pose is great for a single navigation goal, but it has no concept of sequencing.
If you set a new goal before the previous one is reached, the old goal is discarded.
The action queue solves this:
|
Action queue |
|
|---|---|---|
Multiple steps |
❌ — one goal at a time |
✅ — ordered task list |
Mixed task types |
❌ — navigation only |
✅ — move, pick, drop, wait |
State tracking |
❌ |
✅ — each action has |
Re-use / replay |
❌ |
✅ — actions are reset-able |
2. Setup
from pybullet_fleet.agent import Agent, AgentSpawnParams, MotionMode
from pybullet_fleet.core_simulation import MultiRobotSimulationCore, SimulationParams
from pybullet_fleet.geometry import Pose, Path
from pybullet_fleet.sim_object import SimObject, SimObjectSpawnParams, ShapeParams
from pybullet_fleet.action import MoveAction, WaitAction, PickAction, DropAction
params = SimulationParams(gui=True, timestep=0.1, target_rtf=3, physics=False)
sim = MultiRobotSimulationCore(params)
target_rtf=3 — the simulation runs at most 3× real time, making the GUI easy to follow.
For maximum speed (benchmarking / headless) use target_rtf=0.
import os
import numpy as np
urdf_path = os.path.join(os.path.dirname(__file__), "../robots/mobile_robot.urdf")
agent = Agent.from_params(
AgentSpawnParams(
urdf_path=urdf_path,
initial_pose=Pose.from_xyz(0, 0, 0.3),
motion_mode=MotionMode.DIFFERENTIAL,
max_linear_vel=2.0,
max_linear_accel=5.0,
max_angular_vel=2.0,
max_angular_accel=5.0,
mass=0.0, # kinematic — no physics forces
),
sim_core=sim,
)
# Optional: visualise the planned path as a coloured line in the GUI
agent.path_visualize = True
agent.path_visualize_width = 3.0
AgentSpawnParamsvsAgent.from_urdf:from_paramstakes a dataclass, which is more convenient when you have many parameters or want to pass spawn configurations around. Internally, both routes go through the same code.
3. Spawn a Pickable Object
For PickAction to work, the target object must be created with pickable=True.
Use SimObjectSpawnParams + SimObject.from_params for this:
pallet_params = SimObjectSpawnParams(
visual_shape=ShapeParams(
shape_type="mesh",
mesh_path=os.path.join(os.path.dirname(__file__), "../mesh/11pallet.obj"),
mesh_scale=[0.5, 0.5, 0.5],
rgba_color=[0.8, 0.6, 0.4, 1.0],
),
collision_shape=ShapeParams(shape_type="box", half_extents=[0.5, 0.4, 0.1]),
initial_pose=Pose.from_euler(5, 0, 0.1, roll=np.pi / 2, pitch=0, yaw=0),
mass=0.0,
pickable=True, # ← required for PickAction
)
pallet = SimObject.from_params(pallet_params, sim_core=sim)
Pose.from_euler with roll/pitch/yaw — the pallet is rotated 90° around the X axis
(roll=np.pi/2) so it lies flat (horizontal) rather than standing upright. Adding a rotation
at spawn time is often easier than editing the mesh or URDF.
5. PickAction — Approach and Attach
PickAction moves the agent to a pickup approach pose, then attaches the target object
to the robot as if it were physically holding it:
pick = PickAction(
target_object_id=pallet.body_id, # which object to pick
use_approach=True, # move to an approach waypoint first
approach_offset=1.5, # approach from 1.5 m away
pick_offset=1.0, # final pick position: 1.0 m from object centre
attach_relative_pose=Pose.from_euler(
0.6, 0, -0.2, # attach point: 0.6 m forward, 0.2 m below robot centre
roll=np.pi / 2, pitch=0, yaw=0
),
)
What happens step by step:
Agent navigates to the approach waypoint (auto-calculated from target position +
approach_offset).Agent navigates to the pick pose (
pick_offsetfrom object).The target object is kinematically attached to the agent at
attach_relative_pose. It moves with the agent for all subsequent steps.
use_approach=Trueis recommended when the agent may be coming from any direction — it inserts an intermediate waypoint that prevents the agent from colliding with the object during approach.
6. DropAction — Approach and Release
DropAction moves the agent to a drop-off area and detaches the currently held object:
import pybullet as p
pallet_quat = p.getQuaternionFromEuler([np.pi / 2, 0, 0]) # horizontal orientation
drop = DropAction(
drop_pose=Pose(position=[5, 5, 0.1], orientation=list(pallet_quat)),
place_gently=True, # set object velocity to 0 on release
use_approach=True,
approach_offset=1.5,
drop_offset=1.0,
)
The drop_pose defines where the object is placed in the world after release —
not the agent’s final position. The orientation in drop_pose sets the object’s
orientation when placed, which is why we pass the same pallet_quat used at spawn time.
Tip
For EE-attached objects (arm robots, mobile manipulators), drop_relative_pose
lets you drop relative to the object’s current position instead of an absolute
world coordinate. See Tutorial 5 §6 — drop_relative_pose for details.
7. WaitAction — Pause for a Fixed Duration
WaitAction keeps the agent stationary for a given number of simulated seconds.
Use it to simulate charging, loading time, or synchronisation delays:
charge = WaitAction(
duration=3.0, # simulated seconds to wait
action_type="charge", # label (used in logs); options: "idle", "charge", "loading", "custom"
)
Time here is simulated time, not wall-clock time. With
target_rtf=3, a 3-secondWaitActioncompletes in ~1 second of real time.
8. Queue the Task Sequence
Pass all actions as a list to add_action_sequence. They execute one after another,
each starting only when the previous one reports completion:
agent.add_action_sequence([pick, drop, move_to_charger, charge])
print(f"Queued: {agent.get_action_queue_size()} actions") # → 4
Repeated tasks: After all actions complete, the queue is empty and the agent becomes idle. To repeat the sequence, call
add_action_sequenceagain (e.g., from a callback that detects the queue is empty).
9. Monitor Action Progress
Use a callback to display or react to the current action state:
last_print = [0.0] # list trick: allows mutation inside a nested function
def status_callback(sim_core, dt):
if sim_core.sim_time - last_print[0] < 2.0:
return
last_print[0] = sim_core.sim_time
current = agent.get_current_action()
if current:
print(f"[t={sim_core.sim_time:.1f}s] "
f"{current.__class__.__name__} — {current.status.value} "
f"(queue: {agent.get_action_queue_size()})")
else:
print(f"[t={sim_core.sim_time:.1f}s] All tasks completed")
sim.register_callback(status_callback, frequency=None)
Useful methods:
Method |
Returns |
|---|---|
|
The currently executing |
|
Number of actions remaining (including current) |
|
|
10. Run
sim.run_simulation()
The expected sequence you’ll see in the GUI:
Agent approaches pallet → picks it up (pallet moves with agent)
Agent carries pallet to drop-off zone → releases it
Agent navigates to charging station
Agent waits in place for 3 simulated seconds
Console prints “All tasks completed”
Full Example
python examples/basics/action_system_demo.py
See Also
Tutorial 1 — Spawning Objects: SimObject, Agent, Pose, callbacks
Tutorial 3 — Managing a Fleet: apply action queues to 100+ robots via
add_action_sequence_allAPI Reference: full Action, Agent, and SimObject class documentation