← Learn

OpenBrain: how our AI agents remember what happened yesterday

What OpenBrain is

We run a team of AI agents (computer programs that each do one job for us). Each one works on a single project, and each one starts every session (one stretch of work, from when it opens to when it closes) blank. When a session restarts, the agent doesn't know what it did yesterday. It doesn't know what the founder decided last week, or which page it published, or which email it sent.

A blank agent repeats work. It asks the founder questions he already answered. It can undo a decision because it never heard about it.

OpenBrain is how we fixed that. It's one shared table of short, structured rows. Agents write to it when something worth keeping happens, and read from it when a session starts. The table outlives every session, so the memory survives restarts.

This comes from Nate B. Jones's Open Brain. He laid out the guide in March: Why your AI starts from zero every time you open a new chat, with the code at github.com/NateBJones-Projects/OB1. This is our version.

We kept his core design: one Supabase table of notes with vector embeddings (a way to find notes by meaning), and an MCP server (the connector an AI app uses to reach it). His design locks it with a shared key; ours sits behind OAuth (a standard sign-in check). We split saving, searching and reading recent notes into their own functions. Search matches by meaning and by keyword, and the recent read pulls what happened lately, by date. Every agent writes a session summary before its nightly restart.

It isn't a chat log. Each row holds one thing: a decision, a fact, a quote, or a summary of one session.

Below is what OpenBrain is, how it works, the rules we keep, what you'll need, and how to build your own. We wrote it for people setting this up and for AI agents reading it to learn how. The names below are made up. Pick your own.

How it works

The shape of a row

Every memory is saved the same way: the memory itself, who wrote it, and a few labels. The middle column is for builders; the right column says it in plain words.

Field Type What goes in it
content string The thing itself: someone's exact words, or one plain sentence of fact.
source string Who wrote it and when, as <agent>-<YYYY-MM-DD>.
metadata.type string, required What kind of row this is.
metadata.decision_kind string For decisions only: what sort of decision.
metadata.topics array of strings Tags from a fixed list, so rows can be sorted later.
metadata.people array of strings Who the row is about, or who said it.
metadata.links array of objects Where it came from, each with a kind and an id.
metadata.supersedes string or null The id of an older row this one replaces.
metadata.raw_excerpt string The original words, when content is a summary of them.

Allowed values for type:

  • decision: something the founder or a lead agreed to.
  • fact: something true about the world or the system.
  • quote: a person's exact words.
  • session_summary: what one session did, what's in progress and what's waiting.

Allowed values for decision_kind: policy, scope, tactical, config, personnel.

Allowed values for topics: finance, deal, agent, spec, policy, customer, brand, infra, memory. We add a new topic only on purpose, never on the fly.

Three examples

In these examples, staging means a private copy of the site where changes are checked before anyone can see them.

A decision, in the founder's words

{
  "content": "Every new landing page goes to staging first. Nothing goes live without a phone-width check.",
  "source": "web-agent-2026-03-04",
  "metadata": {
    "type": "decision",
    "decision_kind": "policy",
    "topics": ["policy", "brand"],
    "people": ["the founder"],
    "links": [{"kind": "chat_msg", "id": "4471"}],
    "supersedes": null,
    "raw_excerpt": "Every new landing page goes to staging first. Nothing goes live without a phone-width check."
  }
}

A session summary

{
  "content": "Rebuilt the pricing page on staging and passed the phone-width check. The live push waits on the founder's sign-off. The FAQ section is still a draft.",
  "source": "web-agent-2026-03-05",
  "metadata": {
    "type": "session_summary",
    "topics": ["spec", "customer"],
    "people": ["the founder"],
    "links": [{"kind": "card", "id": "9182"}, {"kind": "commit", "id": "a1b2c3d"}],
    "supersedes": null,
    "raw_excerpt": "Founder: looks right on my phone, hold the live push until I read the FAQ."
  }
}

A row that replaces an older one

{
  "content": "Small copy fixes can go live without staging. Layout changes still go to staging first.",
  "source": "web-agent-2026-03-12",
  "metadata": {
    "type": "decision",
    "decision_kind": "policy",
    "topics": ["policy"],
    "people": ["the founder"],
    "links": [{"kind": "chat_msg", "id": "4630"}],
    "supersedes": "<id of the row from the first example>",
    "raw_excerpt": "Typo fixes don't need staging. Anything that moves the layout still does."
  }
}

The third row doesn't edit the first. It points at it. Anyone who finds the old rule also finds the newer one, and can see when and why it changed.

How our agents use it every day

At the end of a session, write

Before a session closes or restarts, the agent writes a session_summary row: what got done, what's half done, and what's waiting on someone. It links the task cards and commits (saved snapshots of code changes). A decision made in the session gets its own decision row too, in the speaker's words. Every agent writes at least one row per session, so no session ends without a trace.

At the start of a session, read

A start hook calls the recent-rows function for that agent and pulls its last few session summaries from the past several days. It prints them at the top of the new session, so the agent already knows what it did before it asks anyone anything.

We read by date here, not by meaning. A search by meaning can skip a recent summary that happens to be about something else, and at the start of a session, recent is what matters.

During a session, search

When an agent needs the history of a topic, it calls the search function. If what it sees right now doesn't match a row, it says so. What you can see now beats what you remember.

Our rules

Keep the data clean

First, content is either the speaker's own words or one plain sentence. If you shorten something, put the original in raw_excerpt. Second, every session summary carries the original quotes in raw_excerpt. A summary with no source words drifts over time.

Never delete a row

When a decision changes, write a new row with supersedes set to the old row's id. The old row stays. You keep the full history, and no agent can quietly erase something another agent relies on. Anyone reading follows the chain and uses the newest row.

Keep some things out

Task tracking belongs on the task board. Code and its history live in the repo. Secrets never go in OpenBrain, ever.

What you'll need

  • A Supabase account. The free plan works, because it has the pgvector extension and edge functions. Free projects pause after about a week with no activity, so if you stop using it, you'll need to un-pause it. We run on a paid plan.
  • The pgvector extension turned on. It's one click in the Supabase dashboard, or create extension vector in the SQL editor.
  • An embeddings key, so search can find notes by meaning. We use OpenRouter with the model openai/text-embedding-3-small. Any OpenAI-compatible embeddings key works, as long as the vector size stays 1536.
  • A shared access key that you make up yourself. Your agents send it in a header on every call. Without it, anyone who finds the address can read or write your memory.
  • A place to run your agents: any machine with Claude Code, or any app that can connect to an MCP server, such as Claude Desktop.
  • An afternoon, if you've used Supabase before.

Set it up, step by step

1. Create a Supabase project

This gives you a place on the internet to keep the memory.

A free project is enough to start. Keep the service key (the master password Supabase gives you) on the server side only. Never put it in a browser or a repo (the folder where your code is stored).

2. Create the table

This makes the notebook where every memory goes, one line per memory.

Call it thoughts. This is the part you or your builder copies into the SQL editor (the box in Supabase where you paste setup commands). You don't need to read every line.

create table thoughts (
  id uuid primary key default gen_random_uuid(),
  content text not null,
  source text not null,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

alter table thoughts enable row level security;

Turn on row level security (a switch that blocks everyone by default) and add no public policies (no exceptions that let outsiders in). That way only your own server functions, using the service key, can read or write.

If you want to search by meaning, add a vector column for an embedding (a number code that lets the table find rows with similar meaning) and fill it in when a row is written. For a small team of agents, plain text search works fine.

3. Build the write function

This is the one door agents use to add a memory, and it turns away anything in the wrong shape.

Set up one server function (a Supabase edge function works) that saves a row. Call it something like save-memory. It takes a POST (a request that sends data) with a JSON body (a simple text format computers use to pass data) and checks three things: content is there, source is there, and metadata.type is one of the allowed values below. If all three pass, it inserts the row and returns the new id. If not, it refuses with an error that names the field it refused.

4. Build two read functions

These are the doors agents use to look things up: one for the latest notes, one to search.

  • A recent-rows function returns the newest rows for one agent, filtered by source and type, newest first. This is what a session reads when it starts.
  • A search function takes a question and returns the closest matching rows. Agents use it when they need the history of a topic.

Both return JSON, and both cap how many rows they send back.

5. Lock every function

This puts a lock on every door, so only your own agents can get in.

Each of the three functions refuses any call without a secret, and checks it first, before it reads the body. Use the sign-in check Supabase already has, or a secret header that only your agents hold.

Send the secret in a header (a hidden label sent along with each request), never in the web address. Addresses end up in logs, where other people can read them.

The read functions need the same secret as the write function. Anyone who can read the memory learns what every agent knows. Anyone who can write to it puts words in front of every agent when it starts. A memory anyone can write is a memory anyone can poison.

Test it: a call with no secret must get a 401.

6. Give each agent the address and the secret

This tells each agent where the notebook is and gives it the key.

Put the function addresses and the secret in each agent's own environment file, not in its code. Then write one short shared instruction that every agent loads. It tells the agent the shape of a row and when to write one.

Connecting it to Claude or another AI app through an MCP server is a separate setup, and this guide doesn't cover it.

7. Add a failure folder

This makes sure a memory isn't lost when the connection drops.

If a write fails, the agent tries once more. If it fails again, the agent saves the JSON to a local folder to send later. A memory write is never dropped without a trace.

Where to start

Build the table and the write function first, and lock it before anything else. Have one agent write a session summary at the end of its next session. Then add the start hook, so the next session reads it back. When that agent picks up where it stopped, add the rest.

Robb Lejuwaan

Every practical lesson, the wins and the flops, goes into Robb on AI, a free email newsletter.

Robb Lejuwaan, writer of Robb on AI
Read it free