Exporting a run
graphmind record <runId> # NDJSON, for machinesgraphmind record <runId> --out bug-4471.ndjsongraphmind record <runId> --html # one page, for peopleTwo formats, same run. NDJSON is one wire envelope per line — the shape WS /ingest accepts,
so it round-trips. --html is the viewer itself, inlined. Default output is
graphmind-run-<runId>.ndjson / .html. Pass a run id you do not have and the command lists
recent run ids instead of failing blankly.
--html: the run as one page
Section titled “--html: the run as one page”graphmind record run_01H8… --html# exported run run_01H8… (214 events) → /Users/you/graphmind-run-run_01H8….html (1204 KB)# Open it in any browser, or send it to someone — it needs no server.The viewer’s own JS and CSS are inlined and the run’s envelopes are embedded in the page, so the file is the debugger UI reading a frozen run: no server, no network, no account, no install. Open it on a plane. Attach it to a GitHub issue. Drop it in Slack and the person who opens it sees the graph, every node’s input and output, the errors and the timings — exactly what you were looking at.
What is different from a live run: controls that would steer execution are hidden, because there is nothing to steer. Everything else — the canvas, the inspector, the timeline, filtering — works.
It needs the built viewer. The published package ships it; from a monorepo checkout run
pnpm build:viewer first, or the command tells you where it looked.
NDJSON: the format
Section titled “NDJSON: the format”Each line is a complete envelope, exactly as it went over the socket:
{"gm":1,"seq":0,"ts":1756254187512,"runId":"run_01H8…","type":"run.started","payload":{"app":"support-agent","sdk":{"name":"ai","version":"7.0.79"},"meta":{"name":"handle-ticket"}}}{"gm":1,"seq":1,"ts":1756254187533,"runId":"run_01H8…","type":"node.started","payload":{"nodeId":"tool:searchOrders","kind":"tool","name":"searchOrders","instanceId":"call_a1","input":{"email":"alex@example.com"}}}{"gm":1,"seq":2,"ts":1756254187904,"runId":"run_01H8…","type":"node.finished","payload":{"nodeId":"tool:searchOrders","instanceId":"call_a1","output":[…],"durationMs":371,"status":"ok"}}This is the same shape graphmind demo replays and WS /ingest accepts, which makes an export
a portable run: it can be re-ingested, diffed, scripted over, or attached to an issue.
What it is good for
Section titled “What it is good for”Bug reports. A trace narrative plus “it broke somewhere in the middle” is not reproducible. An NDJSON file is the actual run.
Diffing two runs. Export both and use ordinary tools:
jq -r 'select(.type=="node.started") | "\(.payload.nodeId)\t\(.payload.input|tojson)"' bad.ndjsonSee comparing runs.
Fixtures. A recorded run replays deterministically with no API key and no cost — which is
exactly how graphmind demo works. If you are building on GraphMind, record a run once and use
it as a test fixture.
Analysis. Every line is JSON, so jq, DuckDB, pandas and friends all apply:
# total token usage for the runjq -s '[.[] | select(.type=="node.finished") | .payload.usage // empty] | { input: map(.inputTokens) | add, output: map(.outputTokens) | add }' run.ndjson
# every error, in orderjq -r 'select(.type=="node.error") | "\(.payload.nodeId)\t\(.payload.error.name): \(.payload.error.message)"' run.ndjsonFinding the run id
Section titled “Finding the run id”From the viewer’s run picker, or from the API:
curl -s http://127.0.0.1:4747/api/runs | jq -r '.runs[] | "\(.id)\t\(.app)\t\(.status)"'Or from MCP — ask your coding agent for the last failing run and it will hand you the id and a deep link. See the MCP reference.
Reading a run without the CLI
Section titled “Reading a run without the CLI”Runs are also available over HTTP while the server runs:
curl -s 'http://127.0.0.1:4747/api/runs/<runId>/events?afterSeq=0&limit=1000'Returns { runId, total, events, nextAfterSeq } — events are full wire envelopes in ascending
seq order, nextAfterSeq is the cursor for the next page (or null on the last one). limit
defaults to 1000, max 5000.
Round-tripping
Section titled “Round-tripping”Because the format is the wire format, an exported run can be pushed back into a server over
WS /ingest — envelopes keep their original seq, and storage inserts are keyed (run_id, seq)
with INSERT OR IGNORE, so re-ingesting the same run is idempotent rather than duplicating it.