---
title: Agent-native by design
description: "relate, the embedded Cypher graph database (relatedb.ai): 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."
type: explanation
surfaces:
  - cli
  - mcp
  - agents
status: stable
verified: 0.1.2
verified_by:
  - cypher
  - cli
canonical_url: https://relatedb.ai/docs/concepts/agent-native/
prerequisites: []
related:
  - reference/cli/agent-mode
  - guides/agents/drive-relate-from-an-agent
  - guides/agents/agent-safety
  - guides/agents/connect-over-mcp
  - reference/errors
last_updated: 2026-11-09
---

# 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.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

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](https://relatedb.ai/docs/reference/glossary/#agent-harness) is running it. The skill and the [MCP](https://relatedb.ai/docs/reference/glossary/#mcp) 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.

```mermaid
flowchart TB
  accTitle: The agent loop around relate
  accDescr {
    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.
  }
  K["Skill and agent-context"] -.-> A["Agent"]
  A -->|"statement"| R["relate: CLI or MCP tool"]
  R -->|"rows"| V["Envelope: columns, types,<br/>rows, truncated"]
  R -->|"failure"| X["Error object: code, kind,<br/>retryable, exit code"]
  V -->|"use the rows"| A
  X -->|"fix, or retry if retryable"| A
```

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](https://relatedb.ai/docs/reference/glossary/#envelope) 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](https://relatedb.ai/docs/reference/glossary/#exit-code) 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`:

```bash verify
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:

```text
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](https://relatedb.ai/docs/reference/glossary/#agent-mode) 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](https://relatedb.ai/docs/reference/cli/output-modes/#timings-and-the-plan-cache):

```bash verify
relate "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
CLAUDECODE=1 relate "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
```

```text
{"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:
  miss
```

Detection 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:

```cypher run name=names
UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name
```

```table
| 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`.

```json data=mcp-calls
{"calls": [
  {"tool": "read_cypher", "arguments": {"statement": "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"}},
  {"tool": "read_cypher", "arguments": {"statement": "CREATE (:Person {name: $name})", "parameters": {"name": "Ada"}}},
  {"tool": "write_cypher", "arguments": {"statement": "CREATE (:Person {name: $name})", "parameters": {"name": "Ada"}}},
  {"tool": "get_schema", "arguments": {}}
]}
```

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:

```bash
CLAUDECODE=1 relate --read-only --mode table "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
```

```text
+---------+
| name    |
+---------+
| "Ada"   |
| "Grace" |
| "Noor"  |
+---------+
```

`--mode envelope` prints one line on stdout:

```bash
CLAUDECODE=1 relate --read-only --mode envelope "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
```

```text
{"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:

```bash
CLAUDECODE=1 relate --read-only --mode jsonl "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
```

```text
{"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:

```bash
CLAUDECODE=1 relate --read-only --mode toon "UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"
```

```text
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:
  miss
```

Under 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.

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"UNWIND ['Ada', 'Grace', 'Noor'] AS name RETURN name"}}}
```

```json
{"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:

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_cypher","arguments":{"statement":"CREATE (:Person {name: $name})","parameters":{"name":"Ada"}}}}
```

```json
{"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.

```json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"write_cypher","arguments":{"statement":"CREATE (:Person {name: $name})","parameters":{"name":"Ada"}}}}
```

```json
{"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.

```json
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_schema","arguments":{}}}
```

```json
{"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:

```bash
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
```

```text
{"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](https://relatedb.ai/docs/reference/server/mcp/#errors-and-refusals) 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](https://relatedb.ai/docs/reference/cli/agent-mode/): the detection variables, the defaults, and the switches.
- [Drive relate from an agent](https://relatedb.ai/docs/guides/agents/drive-relate-from-an-agent/): the envelope, the error object and the row cap in a working loop.
- [Keep an agent safe](https://relatedb.ai/docs/guides/agents/agent-safety/): `--read-only`, `--safe`, timeouts, and when to retry.
- [Connect over MCP](https://relatedb.ai/docs/guides/agents/connect-over-mcp/): the config block for each agent host.
- [Install the agent skill](https://relatedb.ai/docs/guides/agents/install-the-skill/): the agents `relate skill install` supports and where the files land.
- [Errors](https://relatedb.ai/docs/reference/errors/): every error with its code, kind, exit code, and whether a retry helps.

## Terms used on this page

- **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.
- **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`.
- **envelope**: One compact JSON object per result set, printed by `--mode envelope`, that keeps the column list even when there are no rows.
- **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.
- **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.
