Generate a Graph#

One natural-language instruction goes through the LLM agent pipeline (coordinator β†’ per-subgraph agents β†’ checkpoint agent β†’ validation + script-fix loop) and comes out as a typed, verified workflow directory β€” the same artifact gap.builder authors by hand and gap run executes.

Tip

End-to-end gap generate "<task>" + gap run --sim works end-to-end on the public quickstart task. The coordinator picks perceiving-objects + grasping-with-planner + transporting-objects from the registry without any prompt hinting; the runtime then runs the generated workflow in sim against LLM-authored postcondition checkpoints. gemini-3.1-flash-lite-preview on the default openrouter provider is the fastest path; Vertex works too β€” see Providers.

Note

Requirements An LLM credential (OPENROUTER_API_KEY by default β€” see Providers) plus the engine install (uv sync) and the open-robot-skills checkout. Generation runs without the simulator β€” only an LLM credential and the open-robot-skills checkout are required; running the result in sim has the usual quickstart requirements.

Source: examples/generate_a_graph.

CLI#

export OPENROUTER_API_KEY=...   # default provider; see "Providers" below

uv run gap generate "pick up the alphabet soup can and place it in the basket" --out my_graph

The open-robot-skills checkout is auto-discovered ($GAP_SKILLS_PATH or the checkout next to the graph-as-policy checkout); pass --skills /path/to/open-robot-skills to override (repeatable, precedence-ordered). Other flags:

Flag

Default

Meaning

--skills PATH

resolved registry set

Skill registry root(s); repeatable

--provider

openrouter

openrouter | vertex

--model

provider default

LLM model override

--out DIR

outputs/generated_<timestamp>

Output directory

--config YAML

β€”

Pipeline config (llm/composition/skills knobs)

-v, --verbose

off

Debug logging

Warning

The generated workflow lands in <out>/task_00/, not in --out itself β€” point gap run at the task subdirectory (my_graph/task_00).

Python#

import gap

graph = gap.agent.generate_sync(
    "pick up the alphabet soup can and place it in the basket",
    out_dir="my_graph",          # skills= omitted -> auto-discovered
)
print(graph)                     # the graph, as terminal text (below)
print(graph.path)                # the written workflow folder (my_graph/task_00)
print(sorted(graph.code))        # every generated source file

print(graph) renders the workflow as box-drawing text β€” the same output gap generate prints. On the checked-in sample_generated_graph:

task_00
Pick the blue and yellow alphabet soup can and place it in the basket.

START
  β”‚
  β–Ό
β”Œβ”€ target_sg ───────────────────────────────────────── perceiving-objects ─┐
β”‚ observe ─▢ perceive ─▢ filter_obb                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚ found                                                    abort β–Ά βœ— abort
  β–Ό
β”Œβ”€ container_sg ────────────────────────────────────── perceiving-objects ─┐
β”‚ observe ─▢ perceive ─▢ filter_obb                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚ found                                                    abort β–Ά βœ— abort
  β–Ό
β”Œβ”€ grasp_sg ─────────────────────────────────────── grasping-with-planner ─┐
β”‚ open ─▢ compute_grasp ─▢ approach ─▢ observe ─▢ build_world ─▢ plan      β”‚
β”‚   ─▢ execute ─▢ close                                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚ grasped                                                  abort β–Ά βœ— abort
  β–Ό
β”Œβ”€ transport_sg ──────────────────────────────────── transporting-objects ─┐
β”‚ compute_drop ─▢ move_above ─▢ release                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚ placed β–Ά βœ“ done                                          abort β–Ά βœ— abort

βœ“ done (success)   βœ— abort (failure, recovery: open_gripper, go_home)

gap.agent.generate is the async variant with the same signature; gap.viz.to_text(workflow) renders any workflow dict or directory the same way.

generate.py wraps this in a small CLI:

uv run python examples/generate_a_graph/generate.py "pick up the milk and put it in the basket"

What the output directory contains#

my_graph/task_00/           # = graph.path
β”œβ”€β”€ workflow.json           # the v3 graph: top-level DAG + one entry per subgraph
β”œβ”€β”€ scripts/                # agent-authored scripts referenced by type:script nodes
β”‚   β”œβ”€β”€ perceive_dino_vlm.py
β”‚   β”œβ”€β”€ plan_grasp.py
β”‚   └── ...
β”œβ”€β”€ checkpoints/            # LLM-authored postcondition sidecars per subgraph
β”‚   β”œβ”€β”€ grasp_sg.py         #   (validate=True predicates, enforced against
β”‚   └── transport_sg.py     #    sim ground truth at subgraph exit)
β”œβ”€β”€ agent_traces/           # per-agent LLM transcripts for debugging
└── multi_agent_meta.json   # pipeline provenance (models, retries, timings)

grocery_fulfillment/sample_generated_graph is a checked-in, unedited example of one (its workflow.json, scripts/, and checkpoints/ β€” the agent_traces/ and multi_agent_meta.json debug artifacts are not committed).

Validate and run the result#

uv run gap run my_graph/task_00 --validate-only           # structural + skill checks
MUJOCO_GL=egl uv run gap run my_graph/task_00 \
  --sim libero_object_all_variance/0                      # execute on LIBERO
uv run gap viz                                            # browse the trace

Or in Python: gap.execute(graph.path, gap.connector.sim("libero", task=...)). See Execution and Traces.

Providers#

Provider

Setup

openrouter (default)

export OPENROUTER_API_KEY=...; the default model is gemini-3.1-flash-lite-preview (OpenRouter accepts Gemini slugs with or without the google/ prefix). Reach any other OpenAI-compatible server (e.g. local vLLM) via a --config YAML with llm: {provider: openrouter, endpoint: ...}

vertex

gcloud auth application-default login, the vertex extra, and a --config YAML setting llm: {provider: vertex, project_id: ..., region: ...}; GEMINI models only

Both providers default to gemini-3.1-flash-lite-preview; override per call with --model gemini-3.1-pro-preview (or llm: {model: ...} in the config). OpenRouter accepts Gemini slugs with or without the google/ prefix.

Pick per call with --provider/--model, or pin everything (endpoint, temperature, max tokens, concurrency, per-agent models) in a config YAML passed via --config:

llm:
  provider: openrouter
  model: my-model
  endpoint: http://localhost:8000/v1

See LLM providers for the full config reference, and Generation for how the multi-agent pipeline works.

Benchmark-scale generation#

Generating one graph is the unit; the benchmark harness drives the same pipeline over task Γ— seed grids. See grocery_fulfillment for the flagship recipe β€” gap generate on grocery instructions under pose / permutation / basket-swap variations, gated by the config’s gate_threshold β€” and Benchmarking for the harness. A single green run is not a success-rate claim: gate it with gap benchmark <yaml> --gate.