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.
Declare a collection
Addrecords 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:
src/agent.ts — add to the createAgent options
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 topeople 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 ascontext.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:
src/tools.ts
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 The question’s wording is what the model passed to
questions_for_me, which
returns:Tool result
ask_seller. As Maria, the same tool
returns []: she is not the seller of anything.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). 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:
src/agent.ts — buildAgent
Question from src/tools.ts and import it here. The agent answers routes through
agent.fetch(request). The nudge follows the usual delivery rules: its status says
whether it was sent, held for quiet hours, or refused, for example outside WhatsApp’s window.
What the records API does
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:
Read every waiting question, page by page
What a value may hold
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
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:
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:+15550100100or+15550100100. This holds also when yourresolveor a sign-in keys them askey:....
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:
Forget a seller who never wrote
Export records
From the agent’s folder, print every record as JSON Lines, one record per line:Terminal
records.jsonl
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:
Copy records to an agent on another store
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 getscontext.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
- Flow's hosting
In the process that runs the messaging agent, give the MCP server that agent’s records:Serve
src/server.ts — a fragment
mcp as in the MCP guide. 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.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:
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
Upgrading to 0.6.0
ToolContexthas a new field,records. Flow fills it for every tool call. Code that builds aToolContextitself, such as a unit test calling a tool’srun, addsrecords: agent.records.- A custom
storethat forwards each call to aStore, as Flow’s hosting does, forwards the new record methods too:putRecord,getRecord,queryRecords,deleteRecord,forgetRecords,exportRecordsandreindexRecords. - On Flow’s hosting, an agent on 0.6 deploys once the host runs 0.6; until then
flow deployrefuses 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.
Write a tool
Reach your own database and APIs for data other systems read.
Keep the store durable
Configure SQLite or Postgres and handle restarts.