AI agent guide — building graphs via the companion endpoint¶
At a glance
- Read this if you are an AI agent asked to build or modify a MiniGraph. It is the single context you need — you should not need to read the engine source.
- Generate from rules, not guesses. The command grammar and its machine-readable form
minigraph-commands.jsonare the source of truth. Validate every command against them before sending.- Two endpoints, two jobs — see below. Both are dev-only (
app.env=dev), no auth.
Which endpoint?¶
| Goal | Endpoint | Notes |
|---|---|---|
| Execute a deployed graph | POST /api/graph/{graph-id} |
Send the request body; get the response. No session. |
| Build/edit a graph — AI agents, preferred | POST /api/companion/{session-id}/sync |
Synchronous — returns the command outcome in-band {ok, output, error, result}; output is also teed to the human's WS console. |
| Build/edit a graph — fire-and-forget (legacy) | POST /api/companion/{session-id} |
Dispatches the command; outcome streams to the WS console only (HTTP returns just an ack). Kept for Java parity. |
| Read the live model | GET /api/graph/session/{session-id} |
Returns the current graph as JSON. |
This guide is about the companion flow — co-authoring a graph with a human watching the Playground.
The companion contract¶
An AI agent should use the synchronous /sync endpoint so it sees every outcome in-band and
can self-correct without a human relaying the console:
1. A human opens the Playground (ws://{host}/ws/graph); the first WebSocket frame carries the
session id (ws-<6 digits>-<counter>, e.g. ws-384729-17). Get this id from the human.
2. For each command: POST /api/companion/{session-id}/sync Content-Type: text/plain,
exactly ONE command in the body.
3. The HTTP response returns the outcome IN-BAND as JSON:
{ "ok": bool, "id": "ws-...", "command": "...", "output": ["...console lines..."],
"error": "...", "result": [ ... structured, e.g. a run's output.body ... ] }
- NULL FIELDS ARE OMITTED from the wire (serializer null-omission): a success carries no
"error" key and, unless the command yields data, no "result" key. Treat ABSENT as null —
do not require the keys.
- The "ok" flag is derived from the console lines with whole-output context (import's
normal "Graph model not found in /tmp/... Found deployed graph model" fallback is
correctly reported ok:true). When ok is false, the error field carries the first
failing line — still read the output for the full picture.
- A malformed command answered with a "Syntax: ..." usage hint is a FAILURE: ok:false
with the hint as the error (the command did nothing).
- Repeating an identical command back-to-back is safe: /sync commands are never
dedup-dropped (the engine's 1-second duplicate guard protects only the WS UI path).
- If ok is false, read error/output, fix it, and re-issue — self-correct; no human relay needed.
- Use result to verify a run/inspect (e.g. output.body).
4. The same output is ALSO teed to the human's WebSocket console, so a watcher — and any
`session subscribe`d session (e.g. a product owner) — sees it live: real-time human+AI collaboration.
5. Read the model shape any time with GET /api/graph/session/{session-id}.
Status codes: 200 executed (read ok/error in the body); 400 missing/empty/non-text body;
404 no active session for that id.
Legacy fire-and-forget (
POST /api/companion/{session-id}, no/sync): returns only{status:"accepted"}; the outcome streams to the WS console, not the HTTP response, so the caller is blind to errors. Prefer/sync.
Rules of engagement: one command per POST (multi-line commands are fine — see the grammar);
the session must already be open (you do not create it); single operator — don't POST while a
human is typing in the same instant; never expose this beyond a trusted dev host.
Session topology is off-limits: a companion is an assistant to the session in the URL, not a
WebSocket session of its own — both companion endpoints reject session subscribe /
session unsubscribe / session reset (the read-only session status query is allowed).
Subscriptions are managed from WebSocket-connected sessions only.
Generate deterministically¶
- Use the grammar as source of truth —
command-reference.mdfor the rules,minigraph-commands.jsonto look up a command's exact syntax, params, and allowed values. Do not infer syntax from a single example. - Validate before sending — check each command against this list (the engine's invariants):
Pre-send checklist - [ ] The root node is named
root; the end node is namedend. - [ ] Node names are lowercase letters, digits and hyphen (types: descriptive labels, conventionally Capitalized). - [ ] Each node has 0 or 1 skill (skill={route}); the skill's required properties are present (see the skill→property matrix). - [ ] Every node in the traversal path connects to ≥1 node (orexportfails). - [ ] No node is left unconnected. Config nodes (Dictionary/Provider) are referenced by name (dictionary[]=,provider=) and not traversed — wire them under agraph.island(root -[contains]-> island -[data]-> dictionary -[provider]-> provider): the island is the graph's entity-relationship knowledge layer (required convention). - [ ] Multi-line commands (create/update/instantiate) are sent as one block; multi-line values use'''…'''. - [ ]instantiate graphprecedesrun/execute/inspect. - [ ]{…}in a syntax line is a placeholder — substitute the value and do not type the braces (inspect output.body, notinspect {output.body};execute fetcher, notexecute {node}). - [ ] Exactly one command per POST.
Canonical build recipe¶
A reliable order for building a graph:
- Plan the nodes and the connections (root → … → end) before issuing commands. Composing
by delegation? Discover the valid targets first:
list graphs(deployed graph models, with each root's purpose) andlist flows(Event Script flows), thendescribe graph {graph-id}for the chosen model's contract view (itsinput.*/output.*data surface) — read-only commands, so no out-of-band brief and no trial execution are needed forextension=targets. - Create nodes:
create node root(typeRoot), the active/skill nodes, andcreate node end(typeEnd, usually withgraph.data.mapperto shapeoutput.body). - Connect them so traversal flows root → end, with no orphans.
- Wire the knowledge layer: whenever the graph has
Dictionary/Provideror data-entity nodes, anIsland(skill=graph.island) is required — connectroot -[contains]-> island -[data]-> dictionary -[provider]-> provider; no node is left unconnected. For a graph with none, an island with data-entity nodes documenting the domain is encouraged (convention). - Instantiate with mock input:
instantiate graph+{constant} -> input.body.{key}lines. - Run and inspect:
run(orexecute {node}), theninspect output.body; iterate. ({node}is a placeholder — you write e.g.execute fetcher,inspect output.body.) - Export & deploy:
export graph as {name}, deploy the JSON, then callPOST /api/graph/{name}.
Worked example¶
Building the hello-world graph via the synchronous /sync endpoint, one command per request.
Each call returns {ok, id, command, output} plus error/result when non-null — check ok and
self-correct on failure:
SID="ws-384729-17" # from the WebSocket welcome frame
curl -sS -X POST "http://{host}/api/companion/${SID}/sync" -H 'Content-Type: text/plain' \
--data-binary $'create node root\nwith type Root\nwith properties\npurpose=demo'
# → {"ok":true,"id":"ws-384729-17","command":"create node root...","output":["> create node root...","node root created"]}
# (no "error"/"result" keys on success — null fields are omitted)
curl -sS -X POST "http://{host}/api/companion/${SID}/sync" -H 'Content-Type: text/plain' \
--data-binary $'create node end\nwith type End\nwith properties\nskill=graph.data.mapper\nmapping[]=text(hello world) -> output.body'
curl -sS -X POST "http://{host}/api/companion/${SID}/sync" -H 'Content-Type: text/plain' \
--data-binary 'connect root to end with done'
curl -sS -X POST "http://{host}/api/companion/${SID}/sync" -H 'Content-Type: text/plain' \
--data-binary 'instantiate graph'
curl -sS -X POST "http://{host}/api/companion/${SID}/sync" -H 'Content-Type: text/plain' \
--data-binary 'run'
# → {"ok":true,...,"result":[{"output":{"body":"hello world"}}]} # the run outcome, in-band
Because each response carries ok/error/result, an agent verifies and corrects itself — no
need to relay the WebSocket console. The same lines are still teed to the human's console, so a
watcher (and any session subscribed session) follows along live.
See also¶
- MiniGraph command grammar +
minigraph-commands.json— the source of truth. - Built-in skills reference — per-skill properties and examples.