Core Concepts#
This page defines the vocabulary the rest of the documentation uses. Skim the table, then read the sections for the concepts you will touch first. The normative specifications live in the reference section: workflow schema and executor semantics.
Concept |
One-line definition |
|---|---|
Workflow / graph |
The v3 JSON policy artifact: a top-level DAG of nodes plus named subgraphs. |
Node |
One unit in a graph: |
Subgraph |
A self-contained inner graph owned by one skill, with typed inputs/outputs, exit declarations, and optional |
Tool |
The unit of execution: one typed callable dispatched by flat name through the ToolRegistry. |
Skill |
The unit of packaging, discovery, and LLM context: an Agent Skills bundle ( |
Connector |
The embodiment: owns a simulator or robot link and registers the |
Checkpoint |
An authored postcondition predicate over a world snapshot, evaluated when its subgraph exits. |
Trial / trace |
One execution of a graph, and the on-disk record it produces. |
Registry |
A local directory of skill bundles; multiple registries layer by precedence. |
Workflow (graph)#
A workflow is the policy artifact: a directory containing
workflow.json # the v3 graph: nodes, edges, conditional_edges, subgraphs
scripts/ # Python bodies for script nodes (paths relative to the dir)
checkpoints/ # one optional sidecar module per subgraph (see below)
workflow.json has exactly these top-level keys: version (must be 3),
meta (name, description), nodes, edges, conditional_edges, and
subgraphs. Parsing is strict: any unknown key anywhere is a hard
error, and legacy v2 constructs are rejected with targeted migration
messages rather than silently misread.
Control flow is explicit. START and END are virtual node names —
["START", "x"] edges seed execution, an edge to END marks a scope’s
terminal node, and neither is ever declared. Multiple outgoing edges from
one node run their targets concurrently in the same scheduler super-step;
data flows per input via {"$ref": "node.field.subfield"} references
rather than shared channels.
The top level of a typical generated workflow is small — subgraph nodes, end nodes, and conditional edges that route on each subgraph’s exit value:
{
"version": 3,
"meta": {"name": "pick_and_place"},
"nodes": {
"grasp": {"type": "subgraph", "ref": "grasp_sg"},
"done": {"type": "end", "status": "success"},
"abort": {"type": "end", "status": "failure",
"recovery": [{"tool": "robot.open_gripper"},
{"tool": "robot.go_home"}]}
},
"edges": [["START", "grasp"]],
"conditional_edges": {
"grasp": {"router_field": "exit",
"mapping": {"grasped": "done", "failed": "abort"}}
},
"subgraphs": {"grasp_sg": {"...": "..."}}
}
Node types#
Type |
Requires |
What it does |
|---|---|---|
|
|
Calls one registered tool by flat name ( |
|
|
Runs a Python module from |
|
|
Like |
|
|
Enters a named subgraph from the top-level |
|
— |
Passthrough that returns |
|
|
Terminates the workflow with |
tool and script nodes may set streaming: true: the node is spawned
detached, never blocks readiness, must have no outgoing edges, and
publishes snapshots that consumers read transparently via $ref — that is
how a camera feed coexists with a step-by-step graph.
Subgraphs, exits, and on_error#
A subgraph is an inner graph owned by one skill. Its declaration carries:
skill— the owning skill bundle (e.g.perceiving-objects); this is what places the bundle’s SKILL.md in front of the generating LLM.inputs— typed declarations ({"target_name": "str"}); nodes inside reference them as{"$ref": "in.target_name"}. Type names resolve through thegap.schemaregistry (Observation,Se3Pose,OrientedBoundingBox, scalars, …).outputs— named$refbindings into inner-node results. Dataflow between subgraphs is by name matching: a subgraph input is satisfied by any upstream subgraph declaring a same-named output.exit— how the subgraph reports what happened, andon_error— what it reports when something raises.
The exit declaration has two modes:
"router_field": null— the exit value is the name of the terminal node reached;success_valuesmust name declarednoopnodes (the common case:add_exit("found")in the builder)."router_field": "some_field"— the exit value is read from that field of the terminal node’s output; success values must not be node names.
on_error is a symbol, never a node: any exception inside the subgraph is
caught and surfaced as that exit value, so the top-level graph can route
failures to a recovery end node. Without on_error, an exception crashes
the whole workflow. The resolved exit value must be in
success_values ∪ {on_error}. At the top level, conditional edges on a
subgraph node route on "router_field": "exit".
Subgraphs do not nest, and $refs cannot cross a subgraph boundary —
all coupling goes through declared inputs and outputs.
Tools#
A tool is the unit of execution: one typed callable dispatched by flat
name through the engine’s ToolRegistry, either as a type: tool node or
from inside a script via ctx.tool(name, **kwargs). Inputs and outputs are
introspected from type hints into a schema (gap tools show <name> prints
it live). Tools come from three sources sharing one namespace:
Connector tools —
robot.*andsim.*. These prefixes are reserved: only a connector can claim them, so a graph can never invent motion. They are the embodiment surface (robot.get_observation,robot.go_to_pose,sim.step, …) shipped by the engine, not by skill bundles. See the connector tool reference.Bundle tools —
<bundle>.<func>, e.g.sam3.segment_text,curobo.plan_to_pose,geometry.iou. Defined with the@tooldecorator in a tool bundle’stools.py; the bundle name is the model name.Plugin tools — your own
@toolfunctions, registered the same way.
Tools can carry tags (perception, planning, sim_step) that subject
them to call-count guards (GAP_MAX_PERCEPTION_CALLS and friends) — a
runaway graph hits a limit instead of burning API quota or sim time.
Graph script nodes are neither tools nor skills: they are per-graph
generated code that calls tools.
Skills#
A skill is the unit of packaging, discovery, and LLM context: a bundle
directory in the Agent Skills format. Its SKILL.md carries YAML
frontmatter (name must equal the directory name; description says when
to use it) plus GaP extensions under a single gap: key — allowed tools,
exit conditions, typed required_inputs/produces_outputs, canonical
scripts, prompts, and runtime requirements. The markdown body is guidance
the generating LLM reads verbatim.
The bundle’s kind is determined by its folder, never by frontmatter:
tools/<bundle>/— a tool bundle: model-backed@toolfunctions. Tool bundles never own subgraphs.skills/<bundle>/— a skill bundle: a strategy that owns subgraphs in generated graphs, bundling canonical scripts (emitted astype: scriptnodes) and LLM guidance; some are also callable as a single unit.
So “tool vs skill” is what the robot can compute vs what the robot can do. See Authoring bundles for the full format.
Connectors#
A connector is the embodiment object: it owns a simulator environment
or a real-robot link and registers the robot.*/sim.* tools that graphs
execute against. You create one and pass it to gap.execute:
conn = gap.connector.sim("libero", task="libero_object/0") # simulator
conn = gap.connector.real("franka") # real hardware
The same graph runs on either — but capabilities differ by design:
simulator connectors expose sim.* tools and a privileged world snapshot
(which is what checkpoints evaluate against); real connectors register no
sim.* tools, and a perception-only connector like ur_zed registers no
motion tools at all, making unintended motion structurally impossible. See
Connectors and
Safety.
Checkpoints#
A checkpoint is an authored postcondition: a Python predicate over a
privileged World snapshot (optionally also the subgraph’s outputs),
evaluated when its subgraph exits. Checkpoints live in sidecar modules —
checkpoints/<subgraph_name>.py exporting a CHECKPOINTS list — and are
never referenced from workflow.json; the executor auto-loads them by
path.
Two flavors:
Hard checkpoints (
validate=True, the default) are enforced according to the checkpoint mode:off,warn(default, log and continue), orraise(fail the run withVerificationFailed). Select the mode withgap run --checkpoints …orgap.execute(..., checkpoints=...).Probes (
validate=False) surface in feedback but never gate anything; the executor skips them entirely at enforcement time.
Checkpoints are never evaluated on an on_error exit (the subgraph already
declared failure), and when the connector exposes no world snapshot — real
robots — enforcement degrades to a single warning and the run proceeds.
See Checkpoints.
Trials and traces#
A trial is one execution of a graph; a trace is its on-disk record.
Tracing is on by default — gap run writes outputs/run_<timestamp>/:
dag_trace.json # enriched node metadata: timing, status, edges, events
workflow.json # copy of the executed workflow (plus scripts/)
node_data/<id>/ # per-node resolved inputs, outputs, ctx.tool sub-calls,
# stream reads, and extracted assets (PNG masks/images,
# NPY depth, NPZ point clouds)
This layout is a stability guarantee consumed by gap viz (the trial
browser) and gap trace-diff (structural comparison of two trials).
Benchmarks aggregate success over many seeded trials. See
Traces.
Registries#
A registry is a local directory containing tools/ and/or skills/
bundle roots — open-robot-skills is the canonical public
one. Several registries can be active at once, resolved by precedence:
--skills PATHflags (repeatable) — full override$GAP_SKILLS_PATH(an OS-pathsep-separated list) — full overridethe nearest
pyproject.tomlwith[tool.gap].registries~/.config/gap/registries.toml, managed bygap registry add/removean
open-robot-skillscheckout next to the engine (auto-discovered)
Layers 3–5 merge; the first two replace everything. When two registries ship a bundle with the same name, the higher-precedence one wins with a loud warning — so a lab registry can shadow a single public bundle instead of forking the repo. See Registries.
How the pieces fit#
One sentence end to end: gap generate asks an LLM to compose skills
into a workflow of subgraphs whose nodes call tools; you
execute it against a connector with gap run, the executor enforces
checkpoints at every subgraph exit, and the trial trace is what you
debug with gap viz. Try it in the 15-minute tour.