Docs
View as markdown

Drive relate from an agent

Run relate from an agent through the CLI, with JSON output, an error object and exit codes to branch on, a row cap, parameters, and a time bound per statement.

cliagents

Before you start: Install relate

This guide shows you how to drive relate from an agent through the CLI: read the briefing, ask for 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, pass values as parameters, branch on the error object and 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, read the row cap The limit on rows printed per statement, 100 by default under a detected agent harness; --max-rows sets it, and a cut is reported in-band. Read more in the glossary, pick an output mode, and put a time bound on each statement. Each relate command is one process that runs one statement or a ;-separated list and exits, so an agent calls it like any other shell tool.

Prerequisites

  • relate on PATH (Install relate).
  • A shell the agent can run commands in. No server and no configuration file are involved.

Read the briefing

relate agent-context prints the commands, query-run flags, and exit codes of the binary that is installed, so an agent works from the version it has rather than one it remembers. relate agent-context <command> prints one command's synopsis, flags, and examples.

The briefing ends with the exit codes, the part an agent branches on:

relate agent-context | tail -7
Exit codes:
  0  success: success, including an empty result
  1  fatal_error: runtime failure: store open or I/O error, capacity limit, query execution failure, internal error
  2  usage_error: unknown flag, bad or missing value, extra positional
  5  conflict: write-write conflict, or the store is locked by another opener
  6  validation_error: the statement is rejected: syntax, semantic or type error
  7  timeout: a statement exceeded --timeout

Before them it lists the twelve commands and every query-run flag; the agent quickstart shows the whole output.

Create the example graph

The steps use one small graph: three people and the two projects they work on. This writes a new single-file store, team.rl, from a seed script:

cat > team.cypher <<'EOF'
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
EOF
relate query --database ./team.rl --mode envelope < team.cypher
{"columnTypes":[],"columns":[],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":3.2087079999999997,"execute_ms":2.911125,"lex_parse_ms":0.01425,"lower_ms":0.021417,"parse_ms":0.279333,"plan_ms":0.012667,"result_generation_ms":0.000125,"rewrite_ms":0.0005},"truncated":false}

Every later command on this page opens team.rl with --read-only, which opens the store read-only An open mode (--read-only) that never writes, checkpoints or migrates the store; writes fail, and a directory store can be read beside its writer. Read more in the glossary: relate never writes it, and a write fails with exit code 6.

Ask for the envelope

--mode envelope prints one compact JSON object per result set, on one line:

relate --database ./team.rl --read-only --mode envelope 'MATCH (p:Person)-[w:WORKS_ON]->(j:Project) RETURN p.name AS person, j.name AS project, w.since AS since ORDER BY since'
{"columnTypes":["STRING","STRING","INTEGER"],"columns":["person","project","since"],"notifications":[],"planCache":"miss","rows":[{"person":"Grace","project":"opencypher-tck","since":2022},{"person":"Grace","project":"relate","since":2023},{"person":"Ada","project":"relate","since":2024}],"timings":{"elapsed_ms":0.454708,"execute_ms":0.2725,"lex_parse_ms":0.034041999999999996,"lower_ms":0.042833,"parse_ms":0.113917,"plan_ms":0.059333000000000004,"result_generation_ms":0.00025,"rewrite_ms":0.008042},"truncated":false}
KeyHolds
columnsThe column names in query order; present even when there are no rows
columnTypesOne type name per column, taken from the first row; ANY when that value is null or there are no rows
rowsOne object per row, with keys sorted alphabetically; columns gives the query's order
truncatedtrue when the row cap cut the rows, otherwise false
rowCapPresent only when the row cap cut the rows: {"kept": N, "total": M}
notificationsThe query notifications; always present, [] when there are none
planPresent only on an EXPLAIN or PROFILE result
planCacheWhether a cached plan served the statement, such as miss or hit-template
timingsThe statement's phase timings in milliseconds, such as elapsed_ms and execute_ms; the values change from run to run

The output modes reference defines every timing key and planCache value.

An empty result keeps its columns, so "no rows" is distinguishable from "no output":

relate --database ./team.rl --read-only --mode envelope "MATCH (p:Person {name: 'Zed'}) RETURN p.role AS role"
{"columnTypes":["ANY"],"columns":["role"],"notifications":[],"planCache":"miss","rows":[],"timings":{"elapsed_ms":0.42174999999999996,"execute_ms":0.222083,"lex_parse_ms":0.031291,"lower_ms":0.047541,"parse_ms":0.127041,"plan_ms":0.063667,"result_generation_ms":0.00033299999999999996,"rewrite_ms":0.009458},"truncated":false}

A run of several statements prints one envelope per statement, one per line, so the output is JSON Lines. New keys may be added to the envelope; existing ones keep their names.

Pass values as parameters

--param N=V binds the parameter $N, and repeats for each parameter. V is parsed as JSON, so it can be a number, true, a quoted string, an array, or a map; a bare word is a string. Single quotes around the statement keep the shell from expanding $names before relate sees it:

relate --database ./team.rl --read-only --mode envelope --param 'names=["Ada","Noor"]' --param since=2023 'MATCH (p:Person)-[w:WORKS_ON]->(j:Project) WHERE p.name IN $names AND w.since >= $since RETURN p.name AS person, j.name AS project'
{"columnTypes":["STRING","STRING"],"columns":["person","project"],"notifications":[],"planCache":"miss","rows":[{"person":"Ada","project":"relate"}],"timings":{"elapsed_ms":0.440333,"execute_ms":0.248584,"lex_parse_ms":0.033417,"lower_ms":0.049166,"parse_ms":0.12237500000000001,"plan_ms":0.060208,"result_generation_ms":0.000292,"rewrite_ms":0.008542000000000001},"truncated":false}

--fetch-params N=SRC reads a JSON file, or fetches an http or https URL, and binds the whole document as $N. In this example the document is a map, read from a file the first command writes:

printf '%s\n' '{"names": ["Grace"], "since": 2022}' > filter.json
relate --database ./team.rl --read-only --mode envelope --fetch-params filter=./filter.json 'MATCH (p:Person)-[w:WORKS_ON]->(j:Project) WHERE p.name IN $filter.names AND w.since >= $filter.since RETURN p.name AS person, j.name AS project ORDER BY project'
{"columnTypes":["STRING","STRING"],"columns":["person","project"],"notifications":[],"planCache":"miss","rows":[{"person":"Grace","project":"opencypher-tck"},{"person":"Grace","project":"relate"}],"timings":{"elapsed_ms":0.475125,"execute_ms":0.269167,"lex_parse_ms":0.036791,"lower_ms":0.050665999999999996,"parse_ms":0.131708,"plan_ms":0.064667,"result_generation_ms":0.000334,"rewrite_ms":0.008792},"truncated":false}

A fetch is capped at 64 MiB and 60 seconds and has no URL allowlist. Keep an agent safe lists it with the other flags that reach the network.

Branch on the exit code and the error object

A failed statement prints its message on stderr and exits with the code of its class. In --mode envelope, --json, and --jsonl it also prints an error object on stdout. This statement uses $name without binding it:

relate --database ./team.rl --read-only --mode envelope 'MATCH (p:Person {name: $name}) RETURN p.role AS role'
echo "exit=$?"

The first line is stderr, the second is stdout:

semantic error: ParameterMissing: Expected parameter(s): name
{"error":{"code":"validation_error","exit_code":6,"kind":"semantic","message":"semantic error: ParameterMissing: Expected parameter(s): name","retryable":false}}
exit=6

The object's code names the class and always comes with the same exit code. retryable is true for a write-write conflict, which a fresh transaction can fix, and for a --concurrent-logical-workspace-limit refusal while other statements hold the workspace pool:

ExitcodeRemedy
0successRead the rows; an empty result is still a success
1fatal_errorRead message: a missing store, an I/O error, a capacity limit, a LOAD CSV path outside its directory. The same command fails again, except a workspace pool refusal with retryable: true (kind resource), which can run again once other statements finish
2usage_errorFix the command line: an unknown flag or a bad value. No error object is printed
5conflictWith retryable: true (kind conflict), run the statement again in a fresh transaction. With retryable: false (kind locked), another process holds the store: wait for it to exit. A directory store can also be opened --read-only beside its writer; a .rl store cannot
6validation_errorFix the statement: syntax, a missing parameter, a type error, a safe mode refusal, or a write on a read-only store
7timeoutBound the work with LIMIT or a narrower pattern, or raise --timeout

A syntax error adds column and source, which point at the failing token. A run of several statements that stops partway adds statement, statements, and committed, so the agent knows whether earlier statements already changed the store:

relate --mode envelope 'RETURN 1 AS a; RETRN 2; RETURN 3 AS c'
echo "exit=$?"
{"columnTypes":["INTEGER"],"columns":["a"],"notifications":[],"planCache":"miss","rows":[{"a":1}],"timings":{"elapsed_ms":0.315959,"execute_ms":0.17925,"lex_parse_ms":0.019666,"lower_ms":0.028791,"parse_ms":0.088292,"plan_ms":0.039541,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.006834},"truncated":false}
syntax error: parse error: unexpected token "RETRN"
  RETRN 2
  ^
statement 2 of 3 failed; 1 completed and committed; 1 not run
{"error":{"code":"validation_error","column":1,"committed":1,"exit_code":6,"kind":"syntax","message":"syntax error: parse error: unexpected token \"RETRN\"   RETRN 2   ^","retryable":false,"source":"RETRN 2","statement":2,"statements":3}}
exit=6

The first statement printed its envelope and committed, the second failed, and the third never ran. The exit codes and errors reference lists every field.

Read the row cap

Under a detected agent harness, with stdout not a terminal, relate prints at most 100 rows per statement. A cut result says so three ways: a warning on stderr, "truncated": true with a rowCap object in the envelope, and a {"rowCap": …} line after the rows in --mode jsonl, just before the statement's timing line. The exit code stays 0. In this command CLAUDECODE=1 stands in for a harness, and grep keeps the rowCap line:

CLAUDECODE=1 relate --mode jsonl 'UNWIND range(1, 250) AS x RETURN x' 2>/dev/null | grep '"rowCap"'
{"rowCap":{"kept":100,"total":250}}

--max-rows N sets the cap everywhere, a terminal included, and --max-rows 0 lifts it. With a cap of 2, the warning comes first on stderr, then the envelope:

CLAUDECODE=1 relate --mode envelope --max-rows 2 'UNWIND range(1, 250) AS x RETURN x'
warning: result truncated to 2 rows (the statement returned 250); pass --max-rows 0 for all rows
{"columnTypes":["INTEGER"],"columns":["x"],"notifications":[],"planCache":"miss","rowCap":{"kept":2,"total":250},"rows":[{"x":1},{"x":2}],"timings":{"elapsed_ms":0.494834,"execute_ms":0.344042,"lex_parse_ms":0.023459,"lower_ms":0.038375,"parse_ms":0.095,"plan_ms":0.047416,"result_generation_ms":0.001209,"rewrite_ms":0.007292},"truncated":true}

The cap limits what is printed, not what the engine computes: the engine builds the whole result first. A LIMIT or an aggregate in the statement bounds the work itself.

Pick an output mode

Without --mode, the output depends on where stdout goes: the human table on a terminal, JSON Lines in a pipe, and TOON in a pipe under 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. TOON is a compact table: one rows[N]{columns}: header line, then one line per row, with nested values as inline JSON, closed by the statement's timings: and planCache: entries.

CLAUDECODE=1 relate --database ./team.rl --read-only 'MATCH (p:Person) RETURN p.name AS name, p.role AS role ORDER BY name'
rows[3]{name,role}:
  Ada,engineer
  Grace,engineer
  Noor,maintainer
timings:
  {"elapsed_ms":0.43470899999999996,"execute_ms":0.252333,"lex_parse_ms":0.032792,"lower_ms":0.044083,"parse_ms":0.11637499999999999,"plan_ms":0.056542,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.008208}
planCache:
  miss

To compare the modes on one result, run a statement in the terminal on this page. It runs in your browser on an empty database opened read-only, as relate --read-only opens one, and prints the bytes relate prints in table, envelope, jsonl and toon, with the exit code; the timings it prints are the engine's in the page, so their numbers differ from a run of the binary. The row column holds a map, which TOON and JSON Lines print as inline JSON and the table prints in Cypher's notation:

Cypher
UNWIND [{name: 'Ada', since: 2024}, {name: 'Grace', since: 2023}] AS p RETURN p.name AS person, p.since AS since, p AS row
Recorded result
personsincerow
"Ada"2024{name: "Ada", since: 2024}
"Grace"2023{name: "Grace", since: 2023}

2 rows

The statement as relate 0.1.2 prints it in four of its output modes, 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 six lines on stdout:

relate --read-only --mode table "UNWIND [{name: 'Ada', since: 2024}, {name: 'Grace', since: 2023}] AS p RETURN p.name AS person, p.since AS since, p AS row"
+---------+-------+------------------------------+
| person  | since | row                          |
+---------+-------+------------------------------+
| "Ada"   | 2024  | {name: "Ada", since: 2024}   |
| "Grace" | 2023  | {name: "Grace", since: 2023} |
+---------+-------+------------------------------+

--mode envelope prints one line on stdout:

relate --read-only --mode envelope "UNWIND [{name: 'Ada', since: 2024}, {name: 'Grace', since: 2023}] AS p RETURN p.name AS person, p.since AS since, p AS row"
{"columnTypes":["STRING","INTEGER","MAP"],"columns":["person","since","row"],"notifications":[],"planCache":"miss","rows":[{"person":"Ada","row":{"name":"Ada","since":2024},"since":2024},{"person":"Grace","row":{"name":"Grace","since":2023},"since":2023}],"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 three lines on stdout:

relate --read-only --mode jsonl "UNWIND [{name: 'Ada', since: 2024}, {name: 'Grace', since: 2023}] AS p RETURN p.name AS person, p.since AS since, p AS row"
{"person":"Ada","row":{"name":"Ada","since":2024},"since":2024}
{"person":"Grace","row":{"name":"Grace","since":2023},"since":2023}
{"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 seven lines on stdout:

relate --read-only --mode toon "UNWIND [{name: 'Ada', since: 2024}, {name: 'Grace', since: 2023}] AS p RETURN p.name AS person, p.since AS since, p AS row"
rows[2]{person,since,row}:
  Ada,2024,{"name":"Ada","since":2024}
  Grace,2023,{"name":"Grace","since":2023}
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:
  miss

Under a detected agent harness, relate prints at most 100 rows per statement; this statement returns two 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.

Turn on "Run as an agent harness", pick the 250-row preset and run it to see the cap cut the rows and the warning on stderr; the exit code stays 0.

An explicit --mode always wins, so a pipeline that parses JSON pins it:

ModePrintsA row-cap cut shows as
envelopeOne JSON object per result set, with column types"truncated": true and rowCap
jsonlOne JSON object per row, then one timing line per statementA {"rowCap": …} line before the timing line
jsonOne JSON object per result set, the rows in data; one compact line when pipedA rowCap key beside data
toonA header line, one line per row, then timings: and planCache: entriesA rowCap: entry before timings:
markdownA GitHub-flavored Markdown tableA closing > truncated: N of M rows shown line
csvCSV; a value starting with =, +, - or @ gets a leading 'The stderr warning only
tableThe human tableThe stderr warning only

Agent mode turns on the 100-row cap and the TOON default. It follows one order: --agent-mode or --no-agent-mode, then RELATE_AGENT_MODE=1 or 0, then harness detection. The flag and the variable print a one-line notice on stderr; detection is silent. The agent mode reference lists the variables that count as a harness.

Bound each statement

--timeout cancels a statement that runs longer than the duration given (500ms, 10s, 2m; a bare number is seconds) and exits 7. Without it, a statement has no time bound:

relate --mode envelope --timeout 100ms 'UNWIND range(1, 20000) AS a UNWIND range(1, 20000) AS b RETURN count(*) AS pairs'
echo "exit=$?"
Error: query timed out after 100ms
{"error":{"code":"timeout","exit_code":7,"kind":"execution","message":"query timed out after 100ms","retryable":false}}
exit=7

--read-only keeps a reading agent from writing. Keep an agent safe adds safe mode, backups before a write, and the permission matrix.

Verify

One read-only statement shows the whole contract: machine output on stdout, and an exit code the agent can test.

relate --database ./team.rl --read-only --mode envelope 'MATCH (p:Person) RETURN count(p) AS people'
echo "exit=$?"
{"columnTypes":["INTEGER"],"columns":["people"],"notifications":[],"planCache":"miss","rows":[{"people":3}],"timings":{"elapsed_ms":0.348958,"execute_ms":0.177792,"lex_parse_ms":0.031208999999999997,"lower_ms":0.044917,"parse_ms":0.109583,"plan_ms":0.053583,"result_generation_ms":0.000292,"rewrite_ms":0.008167},"truncated":false}
exit=0

If it goes wrong

  • semantic error: ParameterMissing: Expected parameter(s): <name> (exit code 6): the statement uses $<name> and no --param or --fetch-params binds it. Pass --param <name>=<value> (errors reference).
  • A syntax error whose source has a gap where $<name> was (exit code 6): the statement was in double quotes, so the shell replaced $<name> with an empty string before relate saw it. With the parameter inside a map, the message starts syntax error: parse error: unexpected token "}" (RightBrace). Single-quote the statement.
  • warning: result truncated to 100 rows (the statement returned <n>); pass --max-rows 0 for all rows (exit code 0): the row cap cut the output. Pass --max-rows 0, or bound the statement with LIMIT or an aggregate (errors reference).
  • Failed to open Relate: storage locked: <path> on stderr, with the error object message storage locked: <path> (exit code 5, retryable: false): another process has the store open. Wait for it to exit; a directory store can also be opened --read-only beside its writer (errors reference).
  • Agent mode: the row cap, the TOON default, and the harness variables.
  • Exit codes and errors: every exit code and every field of the error object.
  • Errors: every message with its cause and its remedy.
  • CLI flags: --param, --fetch-params, --max-rows, --timeout, and the rest.
  • Script the CLI: multi-statement scripts, transactions, and stdin.