Real Franka Pick & Place#
Pick up every Jello gelatin box on the table and drop it in the white bucket,
looping perceive_target β grasp β transport until the perception subgraph
reports the table clean. Runs on a Franka Panda with a Robotiq gripper and a
ZED camera, driven through the vendored
robots_realtime bridge. Source:
examples/real_franka_pick_place.
Note
Requirements
A Franka Panda with a Robotiq gripper, a ZED camera, the
third_party/robots_realtime submodule (own process and environment β GaP
never imports it), a GPU with weights for the grounding-dino and sam3 tool
bundles, and a VLM provider credential (OPENROUTER_API_KEY or a Vertex
setup). The graph validates with no hardware at all.
Warning
This example moves a real robot arm. Read Safety in full before running it. In particular:
Keep the E-stop within reach at all times, and test it before the first run.
Clear the workspace: nobody inside the armβs reach envelope; no fragile objects besides the task items; cables and the camera tripod outside the sweep volume.
Approach heights and drop clearances in
graph/workflow.jsonwere tuned for a specific table height. Re-check them against your setup before the first descent.
What is in the example#
graph/workflow.json v3 graph (hand-migrated from the dev tree's v2 schema)
graph/scripts/ perception + approach/align/drop scripts
task.yaml task metadata (prompt, suite, cameras)
Hardware setup (rr-session)#
The robots_realtime stack is vendored as a pinned submodule and runs in its own process and environment β GaP talks to it over a TCP socket and never imports it:
git submodule update --init third_party/robots_realtime
cd third_party/robots_realtime && uv sync # one-time
The session config is
third_party/robots_realtime/configs/franka/franka_robotiq_client.yaml: a
RobotNode (Franka + Robotiq), a ZED CameraNode, and the
FrankaOscClientCartesianAgent client that connects back to GaPβs msgpack
server on 127.0.0.1:9000. Adjust the camera device_id, extrinsics file,
and robot config for your cell.
Both directions of the wire are framed as a 4-byte big-endian length plus a
msgpack payload with numpy support: the client sends observations
{left: {joint_pos[8]}, <camera>: {images, depth_data, intrinsics, pose_mat}}
and receives actions {timestamp, left: {joint_pos[7], gripper}}, which GaP
republishes at 50 Hz. The details live in
Real-Robot Connectors.
Run#
# Validate the graph first (no hardware needed):
uv run gap run examples/real_franka_pick_place/graph --validate-only
# Full run β spawns rr-session automatically, waits for the first camera
# frame, then executes the graph:
uv run gap run examples/real_franka_pick_place/graph --real franka
Two-terminal debug flow, when you want to watch the realtime clientβs own logs:
# terminal 1
uv run --directory third_party/robots_realtime \
rr-session configs/franka/franka_robotiq_client.yaml
# terminal 2
uv run gap run examples/real_franka_pick_place/graph --real franka --no-rr-autostart
Programmatic:
import gap
conn = gap.connector.real("franka") # rr_autostart=True, port 9000
result = gap.execute("examples/real_franka_pick_place/graph", conn)
conn.close()
If startup hangs in wait_ready, the timeout error names the silent wire
stage (no client connected / camera node not publishing / RGB key missing) and
points at the rr-session log.
How the graph works#
The top level chains four subgraphs through conditional edges:
START β perceive_container β found βββ perceive_target β found βββ grasp β grasped βββ transport
β β β β
not_found table_clean collision placed β perceive_target
β β β blocked
abort done abort β
abort
perceive_containerandperceive_targetuse theperceiving-objectsskill:robot.get_observationβ a DINO+VLM perception script βgeometry.filter_and_compute_obb.grasp(grasping-direct-ik, no motion planner): open gripper βgeometry.top_down_grasp_candidates(z_offset: -0.04) β approach above the target (approach_height: 0.35) β compute an align pose β tworobot.go_to_posemoves (rotate at height, then straight descent β never rotate during the descent) βrobot.close_gripper(settle_steps: 60).transportre-observes the container, computes the drop pose (approach_height: 0.2,drop_clearance: 0.15β note that GaP OBBextentvalues are half-extents), approaches, descends, releases, and retracts.
Two structural points are worth copying into your own real-robot graphs:
on_errorencodes semantic outcomes. v3 failures are exceptions routed to a single exit symbol per subgraph. Hereperceive_target_sgsetson_error: "table_clean": when the VLM answers that no Jello box remains (the curatedobject_descriptionprompt instructs it to βanswer with no letterβ), the perception failure path is the loopβs success exit.The
abortend node declares explicit recovery tools (robot.open_gripper, thenrobot.go_home). On real robotsrobot.go_homeis disabled β it logs a warning and does nothing β so the recovery effectively only opens the gripper. Nothing implicit moves the arm after an abort.
There is no automatic success check: scripts/verify/check_placement.py
exists (it raises if the target-container XY distance exceeds
max_distance=0.06) but is deliberately not wired into the graph. Real
connectors have no scripted success check, so you judge placement yourself or
wire the verify node in.
About task.yaml#
gap run never reads task.yaml β you point it directly at the graph
directory. The fileβs task: / suite: / run: keys are documentation
metadata for this example. Two of its details still matter:
The camera name
robot0_robotviewunderenvironment.camerasmust match theCameraNodepublished by your rr-session config. The env auto-discovers the wire-level camera key; this is just GaPβs name for it.When a task config of this shape is consumed by the codegen/benchmark pipeline (
gap generate,gap benchmark), itspolicies:block is read by the PolicyManager to boot or locate learned-policy servers β see Collect & Train. This exampleβs graph uses no policies, so the file carries none.
v2 β v3 migration notes#
The dev-tree source graph used schema v2 (states/transitions, per-state
on_success/on_failure). The mapping applied here, useful if you are
porting your own v2 graphs:
each v2 subgraph state β a v3 node; the linear
on_successchain βedges;all
on_failuretargets inside a subgraph collapsed into the singleon_errorexit symbol (v3 failures are exceptions, not edges);v2 terminal states β a
noopnode listed inexit.success_values;v2
transitionsβ the top-levelconditional_edgesmapping (including thetransport β perceive_targetloop);service/skill states β
type: toolnodes (gripper.Openβrobot.open_gripper,RobotControl.GoToPoseβrobot.go_to_pose,top_down_grasp_poses_from_obbβgeometry.top_down_grasp_candidates,filter_and_obbβgeometry.filter_and_compute_obb).
See the workflow schema reference for the full v3 format.
Next steps#
Safety β the full pre-session checklist and what GaP does and does not enforce.
Real-Robot Connectors β startup ordering, heartbeat diagnostics, and the msgpack bridge in detail.
Cable UR β the perception-only counterpart of this example.