> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow.engineer/llms.txt
> Use this file to discover all available pages before exploring further.

# The agent's own records

> Keep small JSON documents the agent needs across days and people, in the same store as its conversations.

Some things an agent must remember outside any one conversation: a buyer's question it passed to
a seller, waiting for the answer, or a note it keeps about a person for next week. Records hold
that state as small JSON documents in collections you declare on `createAgent`. They live in the
same store as the agent's conversations, so they survive restarts and deploys, and they behave the
same in the playground, self-hosted on SQLite or Postgres, and on Flow's hosting.

Records need `@flow-engineer/flow` 0.6.0 or later.

> [!WARNING]
>
> **If anything other than the agent reads the data, it belongs in your own database.** Records are
> the agent's own small working state. Orders, bookings, leads and anything your team, your app or
> a report reads go in your database, reached through a [tool](https://docs.flow.engineer/build-agent.md); keep only an id here
> when the agent needs one.

## Declare a collection

Add `records` to the `createAgent` options in your `buildAgent` (`src/agent.ts` in the starter).
Each collection names the top-level fields it can be queried by:

```ts src/agent.ts — add to the createAgent options theme={null}
records: {
  questions: { people: ["buyer", "seller"], fields: ["listing", "status"] },
},
```

| Key | Purpose |
| - | - |
| `people` | Fields that hold a person, as their `context.person` string. Queryable, and a record naming a person here is deleted when that person is forgotten |
| `fields` | Other fields to query by, such as a status or an id from your system |

Only declared fields can be queried. Everything else in a record is stored and returned, but not
matched on. A collection may declare no fields at all: it is then read by id, or listed whole.
`agent.records.collections` lists the declared names, in the order declared.

### Change a declaration

Add a field to `people` or `fields` and restart the agent. Before anything runs, the start indexes
the records already kept for the new field, so `query` and forgetting a person find them. A value
the new declaration refuses, such as a number in a `people` field, stays in the record but is left
out of the index.

The store keeps every field ever declared for a collection and keeps indexing it, so an older
declaration on the same store takes nothing away: a deploy's previous revision still running, or
an old checkout running `flow records export`. Records it puts are indexed for the newer fields too.

## Use records in a tool

A tool reaches the records as `context.records`. This pair of tools lets a buyer ask about a
listing and lets the seller see the questions waiting for them. Add them **inside the existing
`tools` array** in `src/tools.ts`, and the `SELLERS` map and `Question` type above it:

```ts src/tools.ts theme={null}
// Your listings' sellers, by listing id. Sam is one of the starter's test people.
const SELLERS: Record<string, string> = { "bike-42": "phone:+15555550102" };

interface Question {
  listing: string;
  question: string;
  buyer: string;
  seller: string;
  status: "waiting" | "answered";
  replyTo: { channel: string; address: string } | null;
  askedAt: number;
  answer?: string;
}

// Inside the tools array:
tool({
  name: "ask_seller",
  description: "Pass a buyer's question about a listing to its seller. The answer comes later.",
  input: z.object({ listing: z.string(), question: z.string() }),
  run: async ({ listing, question }, context) => {
    const seller = SELLERS[listing];
    if (!seller) return { error: `There is no listing ${listing}.` };
    const id = `${listing}:${context.idempotencyKey}`;
    await context.records.put("questions", id, {
      listing,
      question,
      buyer: context.person,
      seller,
      status: "waiting",
      replyTo: context.address ? { channel: context.channel, address: context.address } : null,
      askedAt: Date.now(),
    });
    return { id, status: "waiting" };
  },
}),
tool({
  name: "questions_for_me",
  description: "List buyers' questions waiting for this seller's answer, oldest first.",
  input: z.object({}),
  run: async (_input, context) => {
    const waiting = await context.records.query<Question>("questions", { seller: context.person, status: "waiting" });
    return waiting.map(({ id, value }) => ({ id, listing: value.listing, question: value.question }));
  },
}),
```

The id uses `context.idempotencyKey`, which is the same when a cut-off turn runs again, so a retry
replaces the record instead of adding a second one. `buyer` is `context.person`, the verified
identity, never a name the model supplied.

### Try it

**1. Ask as the buyer**

Run `npm start`, open the playground, choose **Maria** and send “Does the bike-42 come with
lights? Please ask the seller.”

**2. See the record**

Open the **Records** tab, shown when the agent declares collections. It lists the agent's
records by collection, every person's, newest first and read only, and refreshes after each
turn.

**Check it worked:** `questions` shows one record whose value includes
`"buyer": "phone:+15555550101"`, `"seller": "phone:+15555550102"` and `"status": "waiting"`.

**3. Read it as the seller**

Choose **Sam** and send “Any questions for me?” The agent calls `questions_for_me`, which
returns:

```json Tool result theme={null}
[
  {
    "id": "bike-42:<idempotency key>",
    "listing": "bike-42",
    "question": "Does the bike-42 come with lights?"
  }
]
```

The question's wording is what the model passed to `ask_seller`. As Maria, the same tool
returns `[]`: she is not the seller of anything.

> [!NOTE]
>
> The starter keeps its store in memory, so records go when `npm start` stops. To keep them
> locally, give `createAgent` a durable database such as `database: sqlite("data/agent.db")`
> ([self-hosting](https://docs.flow.engineer/self-hosting.md#persist-conversations)). **Clear history** in the playground
> forgets the person, which deletes the records that name them in a `people` field.

## Use records outside a turn

Everywhere else (your routes, `beforeSend`, `intercept`, a script, a job in your backend) use
`agent.records`. It is the same object a tool gets. This route takes the seller's answer from your
own app, keeps it on the record and has the agent tell the buyer:

```ts src/agent.ts — buildAgent theme={null}
const agent = createAgent({
  // ...the starter's options, and records as above
  routes: {
    "/answers": async (request) => {
      if (request.headers.get("authorization") !== `Bearer ${process.env.ANSWERS_TOKEN}`) {
        return new Response(null, { status: 401 });
      }
      const { id, answer } = (await request.json()) as { id: string; answer: string };
      const question = await agent.records.get<Question>("questions", id);
      if (!question || question.status !== "waiting") {
        return Response.json({ error: "no waiting question" }, { status: 404 });
      }
      await agent.records.put("questions", id, { ...question, status: "answered", answer });
      if (!question.replyTo) return Response.json({ status: "answered" });
      const sent = await agent.nudge({
        to: question.replyTo,
        prompt: `The seller answered the question about ${question.listing}: ${answer}`,
      });
      return Response.json(sent);
    },
  },
});
return agent;
```

Export `Question` from `src/tools.ts` and import it here. The agent answers routes through
`agent.fetch(request)`. The nudge follows the usual [delivery rules](https://docs.flow.engineer/nudges.md): its `status` says
whether it was sent, held for quiet hours, or refused, for example outside WhatsApp's window.

## What the records API does

| Call | Result |
| - | - |
| `put(collection, id, value)` | Keeps `value` under `id`, replacing the whole value. `createdAt` stays from the first put |
| `get(collection, id)` | The value, or `null` |
| `query(collection, where?, { limit, after }?)` | Entries `{ id, value, createdAt, updatedAt }` whose declared fields equal every value in `where` |
| `delete(collection, id)` | `true`, or `false` when there was no record |
| `forgetPerson(person)` | Deletes every record naming `person` in a `people` field, in any collection; resolves to how many |
| `collections` | The declared collections' names |
| `export()` | Every record of the agent, oldest first, as an async iterable of `{ collection, id, value, createdAt, updatedAt }` |

A query matches by equality only and returns the oldest first (by when each record was first put,
then by id). Without `where` it returns the whole collection. It returns 50 entries unless `limit`
says otherwise, at most 100. For the next page, pass the last entry you got as `after`:

```ts Read every waiting question, page by page theme={null}
import type { RecordEntry } from "@flow-engineer/flow";

let after: RecordEntry | undefined;
for (;;) {
  const page = await agent.records.query("questions", { status: "waiting" }, { limit: 100, after });
  for (const { id, value } of page) console.log(id, value.listing);
  if (page.length < 100) break;
  after = page.at(-1);
}
```

There are no ranges, sorting, joins, text search or transactions across records. Two puts to the
same id are not merged: the later value replaces the earlier one.

> **What a value may hold**
>
> A value is a plain JSON object. A `Date` is refused: keep a time as milliseconds
> (`Date.now()`) or ISO text. `NaN`, `Infinity`, a `Map`, a class instance or `undefined` inside a
> list are refused too, with an error naming where it is. Keys whose value is `undefined` are left
> out.
>
> A declared field holds a string, a number, a boolean or `null`. A `people` field holds a string.
> Queries compare JSON values, so `1` and `"1"` are different. A query cannot match a missing
> field or `null`.

> **Errors**
>
> Every refusal is an error whose message starts with `flow: records:` and says what to change,
> for example `no collection named orders: declare it in createAgent(...)` or
> `questions cannot be queried by askedAt: declare it in ...`. A declaration that breaks a limit
> throws when `createAgent` runs.

## Limits

`RECORD_LIMITS`, exported from `@flow-engineer/flow`:

| Limit | Value |
| - | - |
| One record's value, as JSON | 64 KB (65,536 bytes) |
| Records per agent, per environment, across collections | 10,000 |
| Collections per agent | 20 |
| Declared fields per collection (`people` and `fields` together) | 8 |
| A record id | 1 to 128 characters, no control characters |
| A declared field's string value | 256 characters |
| Entries per query | 50 by default, at most 100 |

A collection name starts with a letter and has up to 64 letters, digits, `-` or `_`. A declared
field is a top-level name of letters, digits and `_`. A put of a new record past 10,000 is refused,
and nothing is written; replacing an existing record always works. The count takes no lock, so new
records put at the same moment may pass 10,000 by up to as many as are put at once. Delete records the agent no
longer needs, or keep that data in your own database.

## Forgetting a person

`agent.forgetPerson(conversationId)` deletes everything kept about a person, and with it every
record that names them in a `people` field. A record matches when that field holds any of their
names:

* their person id (`phone:+15550100100`, `key:<your id>` or `<channel>:<address>`),
* their conversation id,
* any channel identity they wrote from, as `<channel>:<address>`,
* the number of each identity they have on one of the agent's phone channels, as
  `phone:+15550100100` or `+15550100100`. This holds also when your `resolve` or a sign-in keys
  them as `key:...`.

A record that mentions the person only in a field not listed in `people` is kept. So is a record
holding just a bare chat address, such as a Telegram id, since another channel may use the same
value.

A tool of theirs that is still running when they are forgotten cannot keep a record afterwards.
Forgetting waits for puts already under way and deletes what they wrote. A later put from that
tool is refused with `flow: records: not kept: the person was forgotten while the tool ran`.

Someone named in records who never wrote to the agent has no conversation to forget, such as a
seller who only answers through your app. `agent.records.forgetPerson(person)` deletes the records
holding exactly that value in a `people` field and resolves to how many it deleted:

```ts Forget a seller who never wrote theme={null}
const deleted = await agent.records.forgetPerson("phone:+15555550102");
```

## Export records

From the agent's folder, print every record as JSON Lines, one record per line:

```sh Terminal theme={null}
npx @flow-engineer/flow records export > records.jsonl
```

```json records.jsonl theme={null}
{"collection":"questions","id":"bike-42:<key>","value":{"listing":"bike-42","question":"Does the bike-42 come with lights?","buyer":"phone:+15555550101","seller":"phone:+15555550102","status":"waiting","replyTo":{"channel":"whatsapp","address":"+15555550101"},"askedAt":1792836642480},"createdAt":1792836642482,"updatedAt":1792836642482}
```

It ends with `Exported 1 record.` on stderr and exit status 0 (1 for a usage error or an agent that
could not be built). The command builds the agent from `src/agent.ts` (loading `.env` first) with a
model that is never called, so it needs no model key, starts no turn and sends nothing. It reads
the store your `buildAgent` opens on this machine, never a hosted agent's: with the starter's
in-memory store it exports nothing.

In code, `agent.records.export()` gives the same records, oldest first. To move them to another
store, put each one, in that order, into an agent built on the new store:

```ts Copy records to an agent on another store theme={null}
for await (const r of oldAgent.records.export()) {
  await newAgent.records.put(r.collection, r.id, r.value);
}
```

Each copy gets new `createdAt` and `updatedAt` times, the time of its put. Putting them oldest
first, as export gives them, keeps their order for `query`.

## Records in MCP tools

A tool served over MCP gets `context.records` only when the server is given them. Without them,
a tool that uses records fails with `not available here: give mcpServer({ records: agent.records })`.

**Self-hosted**

In the process that runs the messaging agent, give the MCP server that agent's records:

```ts src/server.ts — a fragment theme={null}
import { mcpServer } from "@flow-engineer/flow/mcp";
import { buildAgent } from "./agent.ts";
import { tools } from "./tools.ts";

const agent = buildAgent({ channels }); // your channels, as in your messaging entry point
const mcp = mcpServer({ name: "my-product", version: "1.0.0", tools, records: agent.records });
```

Serve `mcp` as in the [MCP guide](https://docs.flow.engineer/framework/mcp.md). Its tools then read and write the same
records as the messaging agent, in the agent's store. Building the agent needs its model key,
as `npm start` does.

**Flow's hosting**

On Flow's hosting, `src/mcp.ts` may export a factory instead of a server. The host calls it
with the hosted agent, whose records are in the host's store:

```ts src/mcp.ts theme={null}
import { fileURLToPath } from "node:url";
import type { Agent } from "@flow-engineer/flow";
import { mcpServer } from "@flow-engineer/flow/mcp";
import { name } from "./name.ts";
import { tools } from "./tools.ts";

export function mcp(agent: Agent) {
  return mcpServer({
    name,
    version: "1.0.0",
    tools,
    dir: fileURLToPath(new URL("../agent", import.meta.url)),
    records: agent.records,
  });
}
```

The starter's `npm run mcp` serves an exported server object, not a factory, so keep a
separate local entry point if you switch to one.

A standalone MCP call has no channel address, so `context.address` is absent there.

## Where records are kept

Records are rows in the agent's store, beside its conversations, keyed by the agent's id:

| Where the agent runs | Where its records are |
| - | - |
| `npm start` and the playground, with the starter | In memory, gone when it stops |
| Self-hosted | The agent's `database`, SQLite or Postgres, or your own `store` |
| Flow's hosting | The host's store, separately for each environment; promoting staging does not copy its records |
| [Scenario tests](https://docs.flow.engineer/testing.md) in the test cloud | A store of the test's own, starting empty |

The first start on 0.6.0 adds three tables to the agent's database: `records`, `record_index`
and `record_collections` (the fields declared for each collection). It changes no existing table. Back records up with the rest of the database.

> **Upgrading to 0.6.0**
>
> * `ToolContext` has a new field, `records`. Flow fills it for every tool call. Code that builds a
>   `ToolContext` itself, such as a unit test calling a tool's `run`, adds `records: agent.records`.
> * A custom `store` that forwards each call to a `Store`, as Flow's hosting does, forwards the new
>   record methods too: `putRecord`, `getRecord`, `queryRecords`, `deleteRecord`, `forgetRecords`,
>   `exportRecords` and `reindexRecords`.
> * On Flow's hosting, an agent on 0.6 deploys once the host runs 0.6; until then `flow deploy`
>   refuses it, as for any newer release line.

## Records or a nudge

A follow-up at a given time is not a record. `agent.nudge({ to, prompt, at })` gives the agent a
turn then, and that turn's tools can read the records. See [reminders and nudges](https://docs.flow.engineer/nudges.md).

> **[Write a tool](https://docs.flow.engineer/build-agent.md)**
>
> Reach your own database and APIs for data other systems read.

> **[Keep the store durable](https://docs.flow.engineer/self-hosting.md#persist-conversations)**
>
> Configure SQLite or Postgres and handle restarts.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.