
One Postgres table runs the whole company
I run a niche vertical-data product with a fleet of autonomous AI agents. They share one Postgres table. That table (not a vector store, not a doc wiki, not a prompt) is the company's memory and its operating manual.
Every agent in my system starts the same way. Before it scrapes, scores, writes, or deploys anything, it runs a SELECT. It reads its own job description out of a database table I call the brain, finds its task queue, reads the current state of the world, and only then begins. There is no onboarding doc in a Google folder, no README that drifts, no "ask the senior engineer." There is one table, and the first rule of the system is printed on the inside of it:
If it is not in the brain, assume it does not happen.
That sentence sounds like a slogan until you have been burned by the alternative. For about seven weeks, a set of scoring jobs in my pipeline failed silently. No alert fired because nothing in the system asserted that anyone was supposed to be watching them. Nobody knew, because nothing said anyone should know. The brain doctrine exists to make "nobody knew" structurally impossible.
One table, many roles
The brain is a single Postgres table. Its shape is deliberately boring:
-- the columns that matter
file_key text -- the namespaced address of a row
title text
content text -- markdown / prose / SQL / whatever
summary text
kind text -- the row's type in the taxonomy
category text
archived boolean -- active vs. retired
updated_at timestamptz
last_read_at timestamptz
search_vector tsvector -- full-text recall
content_embedding vector -- semantic recall
Two things do all the heavy lifting: file_key, which is the addressing scheme, and kind, which is the type system. Everything else is plumbing. That exact shape, one knowledge table plus one task queue, ships as a copy-pasteable starter schema on the downloads page.
file_key: a namespace, not a filename
The file_key is a flat string, but it reads like a path. The prefix before the first underscore tells you the domain of the row. A glance at the distribution of mine:
ORCHESTRATOR_*,AGENT_*: run logs and per-agent operating notesSKILL_*,ROLE_*: the operating manuals each agent loads at bootDOC_*: the documentation bible (more on this below)INBOX_*: message channels between agents and to meSPEC_*,PROTOCOL_*: specifications and rules of procedure<DOMAIN>_*(data, scrape, content, research...): per-mode state and logsPROJECT_STATUS,PLATFORM_*: the live state of the world
This convention is the whole magic trick. An agent doesn't need a service-discovery layer to find its instructions; it needs to know its mode and run SELECT content FROM brain WHERE file_key = 'SKILL_DATA'. A reader API doesn't need a separate documents table; it does WHERE file_key LIKE 'DOC_%'. Routing a message to me is an INSERT into an INBOX_* row. The namespace is the API.
kind: a small taxonomy that explains intent
Where file_key says where a row lives, kind says what it is for. The live distribution across my ~5,400 rows:
| kind | what it is |
|---|---|
log |
append-only history; what happened, when, by whom (by far the largest) |
spec |
a description of how something should work |
reference |
durable facts, glossaries, lookups |
skill |
an agent's operating manual for a mode |
state |
the current value of something that changes |
protocol |
a rule of procedure |
artifact |
a produced output worth keeping |
map / research |
indices and investigation notes |
The split between log, state, and spec matters more than it looks. Logs are immutable and accumulate: they're the audit trail, the reason I can reconstruct why an agent did something three weeks ago. State rows are mutable and few: they're the present tense. Specs and protocols are the contract. Conflating these is how knowledge bases rot. When "what happened" and "what's true now" live in the same undifferentiated blob, every read is a guess. Typing the rows keeps the present tense small and the history complete.
Docs as rows: instant content, deployed nav
The most load-bearing design choice is how the documentation lives. My entire operating manual (call it the bible) is a set of DOC_* rows. A reader route pulls them straight out of the table and renders them. Each row begins with a header comment the route parses:
<!--doc slug=meta-methodology nav="Meta > Methodology"
v2=1 desc="..." owns="how the bible is built" order=380-->
The consequence is the point: editing a doc is one UPSERT, and it's live instantly. No build, no deploy, no PR. When an agent changes a scraper, it edits the owning doc row in the same unit of work, and the manual is correct the moment the change lands.
But there's a deliberate seam. The content of every page lives in the brain (instant). The navigation tree (which pages appear in the sidebar and where) lives in code, in a hardcoded array, and changing it requires a deploy. This isn't an oversight; it's the right boundary. Content changes constantly and should be frictionless. Information architecture changes rarely and benefits from review and version control. Rows render instantly; structure ships through the pipeline. Knowing which side of that line a change falls on is half of operating the system correctly.
Each doc also declares what it owns in its header. That single attribute is the anti-duplication contract: when two pages mention the same fact, the one that owns the topic is canonical and must be edited when reality changes. A nightly read-only auditor agent does nothing but diff the docs against the live system, looking for a cron that changed but whose owning page didn't, two pages that contradict each other, a stale "last updated" date. It files flags. It never edits. Authoring is the author's job; the auditor only makes drift visible.
Recall: full-text and semantic, side by side
With thousands of rows, an agent can't just know the right file_key. So the table carries two retrieval mechanisms as columns. A tsvector search_vector gives fast, exact full-text search: great when you know the term ("find every protocol mentioning the offer gate"). A content_embedding vector column gives semantic search: great when you know the idea but not the words ("how do we keep scrapers from getting blocked?"). An agent that doesn't know where something lives can fall back to either. Lexical for precision, vector for fuzzy recall. Both are just columns on the same row, indexed in the same database, queried in the same SELECT. There is no second system to keep in sync.
Active vs. archived, and the discipline around terminal states
Knowledge doesn't just grow; it expires. The brain marks that with a plain archived boolean and a brain_active view that excludes retired rows. Broad queries go through the view so an agent never resurfaces a decision that's been reversed. It's unglamorous and it works.
The same discipline shows up in the task queue, which lives in its own table but follows the same philosophy. Tasks carry eight status values, and exactly four of them are terminal. An early version of the "open tasks" query excluded only two terminal states and left several hundred already-superseded tasks looking workable. Agents kept "discovering" dead work. The fix was not a smarter agent; it was getting the status semantics exactly right in one canonical query that everyone copies. In a shared-memory system, the query is the contract.
Why a boring SQL table beats the fancy options
I've watched people reach for purpose-built agent-memory stores, vector databases, and orchestration frameworks for exactly this job. For a real operating system that has to be trusted, a single Postgres table keeps winning, for reasons that are all about operations, not features:
- It's transactional. An agent can change a doc, append a log, and update state in one atomic transaction. Memory and history never disagree because they commit together.
- It's inspectable by a human with SQL. When something breaks at 2 a.m., I don't reverse-engineer an embedding index. I run a SELECT. Every agent decision is one query away.
- One query language for memory, search, and state. Full-text, vector similarity, exact lookup, and the live business data all live in the same database and join in the same SELECT. No glue, no sync lag, no "which store is authoritative?"
- It degrades gracefully. Lose the embeddings? Full-text still works. Lose both? You still have
file_keyandkind, which is a perfectly good filesystem. There's no single fragile component whose failure blinds the fleet. - Schema is documentation.
file_key,kind,archived: the structure teaches a new agent how the system thinks before it reads a single row.
The fancy stores optimize for retrieval quality on a pile of unstructured text. That's the wrong objective. My agents don't need the best recall over a blob; they need a single source of truth they can read, write, audit, and trust transactionally. A boring table is exactly the right amount of structure: enough to enforce a doctrine, little enough to never get in the way.
Takeaways
- Give your agents one source of truth, and make reading it the first thing they do. "If it's not in the brain, it doesn't happen" only works if the brain is the literal first SELECT of every session.
- Namespace your keys and type your rows. A prefix convention (
SKILL_,DOC_,INBOX_,LOG_) plus a smallkindtaxonomy turns one flat table into a navigable filesystem and a type system. - Separate fast content from slow structure. Let prose change with a single UPSERT and render instantly; reserve the deploy pipeline for the things that genuinely deserve review.
- Keep "what happened" and "what's true now" in different row types. Append-only logs plus a small set of mutable state rows beats one ever-edited blob.
- Prefer a transactional, queryable, human-inspectable store over a specialized one until you can prove the boring table is actually the bottleneck. It usually isn't.
Get the next one
New pieces on building autonomous systems, every few days.