Agent-native by design
Why the relate CLI prints envelopes, types its errors, caps rows under agent harnesses, ships a skill and serves MCP, and what each choice costs.
climcpagents
What does it mean for a database to be agent-native? An agent reads every byte a command prints, pays for each one in its context window, and decides its next step from the result alone. relate's CLI is built for that reader: it prints results in shapes a model can parse, reports failures as typed objects with stable exit codes, keeps large results from flooding the context, and notices when an agent harness The program that runs a model and executes the commands it chooses, such as Claude Code, Codex or Cursor; relate detects one from its environment variables. Read more in the glossary is running it. The skill and the MCP The Model Context Protocol, which agent hosts use to call tools; relate serve --mcp exposes a store as read_cypher, get_schema and write_cypher. Read more in the glossary surface carry the same contract to agents that never open a shell.
The loop
An agent runs a statement, reads what comes back, and decides: use the rows, fix the statement, or retry. Each piece of the design shortens one turn of that loop.
In words: An agent sends a statement to relate, through the CLI or an MCP tool. relate answers with a result envelope, or with an error object and an exit code. On rows, the agent uses them; on an error it fixes the statement, and it retries only when the error says retryable. The skill and agent-context give the agent the commands and flags before the first turn.
Shapes a model can parse. --mode envelope prints one JSON object per result set. The envelope One compact JSON object per result set, printed by --mode envelope, that keeps the column list even when there are no rows. Read more in the glossary keeps the column list even when there are no rows, so "no rows" never reads as "no output". columnTypes gives one type per column, taken from the first row, so an empty result reports ANY. The envelope also carries the query notifications. When stdout is piped, the default is one JSON object per row (jsonl).
Typed failures. A failed statement prints its message on stderr and exits with the exit code The number a relate run returns to the shell; each nonzero value names a class of failure, such as 2 for usage, 5 for conflict, 6 for a rejected statement. Read more in the glossary of its class: 6 for a statement the engine rejected, 5 for a conflict or a locked store, 7 for a timeout. In the JSON modes it also prints an error object on stdout whose code, kind and retryable fields an agent branches on without parsing prose. retryable is true for a write-write conflict and for a --concurrent-logical-workspace-limit refusal while other statements hold the workspace pool, so an agent never loops on an error that cannot succeed.
A row cap. One statement that returns every node of a large graph can fill a context window. Under a detected agent harness with stdout not a terminal, the CLI prints at most 100 rows per statement and says so in-band. Here the same cut is forced with --max-rows 2:
relate --mode envelope --max-rows 2 "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"The first line is the warning on stderr; the envelope on stdout names both counts:
warning: result truncated to 2 rows (the statement returned 3); pass --max-rows 0 for all rows
{"columnTypes":["STRING"],"columns":["name"],"notifications":[],"planCache":"miss","rowCap":{"kept":2,"total":3},"rows":[{"name":"Ada"},{"name":"Grace"}],"timings":{"elapsed_ms":0.678416,"execute_ms":0.5296660000000001,"lex_parse_ms":0.023083,"lower_ms":0.032458,"parse_ms":0.098,"plan_ms":0.041625,"result_generation_ms":0.00041600000000000003,"rewrite_ms":0.007083},"truncated":true}Harness detection. relate turns on agent mode CLI defaults for a detected agent harness on a non-terminal stdout, or forced on: at most 100 rows printed per statement, the cut reported, TOON in a pipe. Read more in the glossary when it sees an agent harness's environment variable, such as CLAUDECODE or CURSOR_AGENT, and stdout is not a terminal. Agent mode sets two defaults: the 100-row cap, and TOON as the piped output: one header line, then one line per row, then the statement's timings and plan-cache outcome. The same statement, run plainly and then as Claude Code would run it; both modes close with the timing record:
relate "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
CLAUDECODE=1 relate "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"{"name":"Ada"}
{"name":"Grace"}
{"name":"Noor"}
{"planCache":"miss","timings":{"elapsed_ms":0.404375,"execute_ms":0.249166,"lex_parse_ms":0.023,"lower_ms":0.031917,"parse_ms":0.102042,"plan_ms":0.043542,"result_generation_ms":0.000625,"rewrite_ms":0.007459}}
rows[3]{name}:
Ada
Grace
Noor
timings:
{"elapsed_ms":0.38104099999999996,"execute_ms":0.222542,"lex_parse_ms":0.024708,"lower_ms":0.034208999999999996,"parse_ms":0.10475,"plan_ms":0.043292,"result_generation_ms":0.0005,"rewrite_ms":0.007500000000000001}
planCache:
missDetection changes output only. It never gates writes and never turns on --safe; those stay explicit choices, because a heuristic that changed what a statement may do would make the same command mean different things in different terminals.
Facts from the binary. relate agent-context prints the commands, flags and exit codes of the binary that is installed, so the briefing cannot describe another version. The skill, relate's cheat sheet for agents, ships inside the binary, and relate skill install copies it into each detected agent's skills folder.
MCP. relate serve --database <path> --read-only --mcp stdio serves one store to an agent host as two tools, read_cypher and get_schema; without --read-only it also lists write_cypher. A result is one JSON object with the rows, columns, columnTypes and notifications the envelope has, plus stats, the plan and timings; it has no truncated or rowCap field. The server classifies each statement from its plan before running it, so read_cypher refuses a write with Only MATCH queries are allowed for read-query. Internal failures reach the host as Error: internal error, so engine detail stays out of tool-call logs.
The terminal and the tryout below run on an empty database in this page, opened by the engine the page loads. The terminal takes this statement, the one in the row-cap and harness examples:
UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name| name |
|---|
| "Ada" |
| "Grace" |
| "Noor" |
3 rows
| name | |---------| | "Ada" | | "Grace" | | "Noor" | 3 rows
It prints the statement as relate does in four output modes, table, envelope, jsonl and toon, with the exit code, and opens on TOON with "Run as an agent harness" on, as Claude Code runs it; its 250-row preset shows the cap. The tryout sends the MCP tools: read_cypher with the same statement, the same tool with a CREATE, which the plan check refuses before it runs, then write_cypher and get_schema. Each reply carries the plan, the notifications and the timings of its own run, and no truncated or rowCap.
The statement as relate 0.1.2 prints it in four of its output modes under an agent harness (CLAUDECODE=1), rendered from this page's recorded result by the terminal's renderers, which match the relate 0.1.2 binary byte for byte on the recorded runs, timing values aside. The recording keeps no timings, so each one shows as <ms>; relate prints the milliseconds of its own run, and the live terminal prints the engine's. Exit code 0, as relate 0.1.2 returns it.
--mode table prints seven lines on stdout:
CLAUDECODE=1 relate --read-only --mode table "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"+---------+
| name |
+---------+
| "Ada" |
| "Grace" |
| "Noor" |
+---------+--mode envelope prints one line on stdout:
CLAUDECODE=1 relate --read-only --mode envelope "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"{"columnTypes":["STRING"],"columns":["name"],"notifications":[],"planCache":"miss","rows":[{"name":"Ada"},{"name":"Grace"},{"name":"Noor"}],"timings":{"elapsed_ms":<ms>,"execute_ms":<ms>,"lex_parse_ms":<ms>,"lower_ms":<ms>,"parse_ms":<ms>,"plan_ms":<ms>,"result_generation_ms":<ms>,"rewrite_ms":<ms>},"truncated":false}--mode jsonl prints four lines on stdout:
CLAUDECODE=1 relate --read-only --mode jsonl "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"{"name":"Ada"}
{"name":"Grace"}
{"name":"Noor"}
{"planCache":"miss","timings":{"elapsed_ms":<ms>,"execute_ms":<ms>,"lex_parse_ms":<ms>,"lower_ms":<ms>,"parse_ms":<ms>,"plan_ms":<ms>,"result_generation_ms":<ms>,"rewrite_ms":<ms>}}--mode toon prints eight lines on stdout:
CLAUDECODE=1 relate --read-only --mode toon "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"rows[3]{name}:
Ada
Grace
Noor
timings:
{"elapsed_ms":<ms>,"execute_ms":<ms>,"lex_parse_ms":<ms>,"lower_ms":<ms>,"parse_ms":<ms>,"plan_ms":<ms>,"result_generation_ms":<ms>,"rewrite_ms":<ms>}
planCache:
missUnder a detected agent harness, relate prints at most 100 rows per statement; this statement returns three rows, so the cap leaves it whole.
Live: the statement runs in your browser on an empty read-only database, as relate --read-only opens one, and the pane shows what relate prints in the mode you pick, with the exit code and, under "Run as an agent harness", the row cap; the statement can be edited or swapped for a preset.
These calls ran when the page was recorded, on a database that holds what this page's cells write, through the same mapping the live tryout uses. Timings show as <ms>; live, each reply carries the timings of its own run. The server lists write_cypher, read_cypher, get_schema.
read_cypher: the request a host sends, then the reply.
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"}}}{"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"{\"columnTypes\":[\"STRING\"],\"columns\":[\"name\"],\"elapsed_ms\":<ms>,\"error\":null,\"execute_ms\":<ms>,\"lex_parse_ms\":<ms>,\"lower_ms\":<ms>,\"notifications\":[],\"parse_ms\":<ms>,\"plan\":{\"profiled\":false,\"root\":{\"actual\":null,\"children\":[{\"actual\":null,\"children\":[],\"details\":\"AS name\",\"estimated_rows\":null,\"identifiers\":[\"name\"],\"operator\":\"Unwind\",\"warnings\":[]}],\"details\":\"1 item(s)\",\"estimated_rows\":null,\"identifiers\":[],\"operator\":\"ProduceResults\",\"warnings\":[]},\"schema\":\"relate.plan/v1\",\"statement\":\"UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name\"},\"plan_cache\":\"hit-raw-fastpath\",\"plan_ms\":<ms>,\"result_generation_ms\":<ms>,\"rewrite_ms\":<ms>,\"rows\":[{\"name\":\"Ada\"},{\"name\":\"Grace\"},{\"name\":\"Noor\"}],\"stats\":{\"constraints_added\":0,\"constraints_removed\":0,\"indexes_added\":0,\"indexes_removed\":0,\"labels_added\":0,\"labels_removed\":0,\"nodes_created\":0,\"nodes_deleted\":0,\"properties_set\":0,\"relationships_created\":0,\"relationships_deleted\":0}}","type":"text"}],"isError":false}}| name |
|---|
| Ada |
| Grace |
| Noor |
read_cypher with a write: the plan's operators read as query type w, so the server refuses the call before it runs. The request a host sends, then the reply:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"CREATE (:Person {name: $name})","parameters":{"name":"Ada"}}}}{"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"Only MATCH queries are allowed for read-query","type":"text"}],"isError":true}}Tool error: Only MATCH queries are allowed for read-query.
write_cypher: the request a host sends, then the reply.
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_cypher","arguments":{"statement":"CREATE (:Person {name: $name})","parameters":{"name":"Ada"}}}}{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"{\"columnTypes\":[],\"columns\":[],\"elapsed_ms\":<ms>,\"error\":null,\"execute_ms\":<ms>,\"lex_parse_ms\":<ms>,\"lower_ms\":<ms>,\"notifications\":[],\"parse_ms\":<ms>,\"plan\":{\"profiled\":false,\"root\":{\"actual\":null,\"children\":[],\"details\":\"(:Person)\",\"estimated_rows\":null,\"identifiers\":[],\"operator\":\"Create\",\"warnings\":[]},\"schema\":\"relate.plan/v1\",\"statement\":\"CREATE (:Person {name: $name})\"},\"plan_cache\":\"miss\",\"plan_ms\":<ms>,\"result_generation_ms\":<ms>,\"rewrite_ms\":<ms>,\"rows\":[],\"stats\":{\"constraints_added\":0,\"constraints_removed\":0,\"indexes_added\":0,\"indexes_removed\":0,\"labels_added\":1,\"labels_removed\":0,\"nodes_created\":1,\"nodes_deleted\":0,\"properties_set\":1,\"relationships_created\":0,\"relationships_deleted\":0}}","type":"text"}],"isError":false}}Counters: nodes_created 1, properties_set 1, labels_added 1; rows is empty, as write_cypher returns counters.
get_schema: the request a host sends, then the reply.
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_schema","arguments":{}}}{"jsonrpc":"2.0","id":4,"result":{"content":[{"text":"{\"Person\":{\"count\":1,\"properties\":{\"name\":{\"type\":\"String\"}},\"type\":\"node\"}}","type":"text"}],"isError":false}}| Label | Nodes | Properties | Relationships out |
|---|---|---|---|
| Person | 1 | name (String) | - |
write_cypher on a server started --read-only: it lists only read_cypher and get_schema, and answers a write_cypher call with a tool error before the engine sees the statement. This run is recorded from relate 0.1.2 (errors.json mcp-unknown-tool, replayed against the binary by docs:cli-check); the page quotes it as the binary printed it:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"write_cypher","arguments":{"query":"CREATE (:B)"}}}' | relate serve --mcp stdio --database ./graph.rl --read-only{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"relate-mcp","version":"0.1.2"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"text":"unknown tool 'write_cypher'","type":"text"}],"isError":true}}Transport answers (HTTP 401, 403 and 415, the body cap, the header deadline) are not part of the tryout; the MCP server reference lists them.
Live: each call is a button that sends its tools/call request to the page's database in your browser and shows the request and the reply, built from the engine's result the way relate serve --mcp builds it, with the statement and parameters editable; "Serve read-only" drops write_cypher from the tool list, as --read-only does, and shows the engine's own refusal of the write on a read-only database beside the server's recorded answer.
What it costs
| Choice | What it buys | What it costs |
|---|---|---|
| Row cap under a harness | A runaway result cannot flood the context | The engine still computes the whole result; the cap bounds printing, not work. A Cypher LIMIT bounds the work |
| TOON as the piped default under agent mode | Fewer bytes per row than JSON | TOON is not JSON. A tool that expects one JSON object per row gets it with --mode jsonl (or --no-agent-mode), followed by one timing line per statement; --mode envelope gives one JSON object per result set |
| Detection from environment variables | No flag to remember inside a harness | A variable set for another reason turns agent mode on; --no-agent-mode or RELATE_AGENT_MODE=0 turns it off. Detection is silent, so the same command prints differently inside and outside a harness unless --mode pins the output |
| The skill as a copy in each agent's folder | The agent reads relate's rules before its first command | A copy can fall behind the binary after an upgrade; relate skill check exits 1 when an installed copy differs |
| MCP classification before execution | A read tool, annotated readOnlyHint, that cannot change the graph or its indexes | One extra planning pass per call |
The other path was to leave agents with the human table and free-text errors, and let each harness parse them. That puts the parsing in every agent's prompt and the failures in every agent's context. relate puts it in the binary once, where it can be tested.
Where this shows up
- Agent mode reference: the detection variables, the defaults, and the switches.
- Drive relate from an agent: the envelope, the error object and the row cap in a working loop.
- Keep an agent safe:
--read-only,--safe, timeouts, and when to retry. - Connect over MCP: the config block for each agent host.
- Install the agent skill: the agents
relate skill installsupports and where the files land. - Errors: every error with its code, kind, exit code, and whether a retry helps.