# Give it a record: the tables a worker runs against, the audit trail, and the MCP wire (/docs/agent/give-it-a-record)

---
type: Procedure
title: "Give it a record: the tables a worker runs against, the audit trail, and the MCP wire"
description: "Where the book continues after this room's basic worker, from The AI Agent Factory: every worker runs against a system of record; four tables every worker keeps; Postgres with pgvector over a separate vector store; the embedding pipeline; the audit trail written in the same transaction as the action; what MCP is and is not; a development plane that never reaches runtime; when a custom MCP server earns its keep over a plain function tool; reads run free, writes are gated, schema changes never reach production through the agent."
status: stable
order: 16
generated: { by: "process:claude-code", at: 2026-09-09T07:30:00Z }
verified:
  - { by: "human:rrizwan98", at: 2026-09-09T07:47:26Z }
sources:
  - id: af-fte
    title: "Building a Digital FTE: A Crash Course. The AI Agent Factory, Panaversity, 2026"
    resource: https://agentfactory.panaversity.org/docs/digital-fte-crash-course
ksor:
  audience: [public]
  owner: human:rrizwan98
  approval: { by: "human:rrizwan98", at: 2026-09-09T07:47:26Z }
trust_tier: human-reviewed
build_id: sha256:4d4bff09bd0a1dcff0a0a50f20f7ef514fc471e2b1f9e81e0e7b6150b93c0793
dirty: true
---

The worker this room builds keeps its conversation in a session file and its
knowledge in the tool bodies. The book's next invariant is the one that turns
an agent into a worker: **every worker runs against a system of record.** The
engine is what a worker runs on; the system of record is what it runs against.
Two open standards keep that portable: skills let capabilities travel between
tools, and MCP is the wire the agent uses to reach the record. [^af-fte]

This page is where the book continues after
[test end to end](test-end-to-end.md), and it is also the answer to the most
common way a text worker fails to feel natural. A worker with no way to list
the records a person owns cannot answer "where is my order"; the test build
on [design the experience](design-the-experience.md) failed exactly there.
The record is not an optional layer for later. It is what the tools read.

## Four kinds of data a worker cannot afford to forget

The store holds four things: [^af-fte]

| Kind | What it is | Where it lives |
| --- | --- | --- |
| Business records | Customers, tickets, orders: the truth the worker reads and updates | Ordinary relational tables the agent reaches through tools |
| Reference library | Policies, knowledge-base articles, summaries of past resolved cases: knowledge searched by meaning | `documents` and `embeddings` |
| State | The live conversation, its turns written by the SDK's session; one row beside it as the envelope: who, when, a closing summary | The SDK's own tables plus `conversations` |
| Trace | The replayable record of what the worker did | `audit_log` |

A failure in production traces to one of three places: a skill that never got
discovered, a system of record two workers disagree about, or a wire that
drops events. The map tells you which. [^af-fte]

## Why Postgres, and why not a separate vector store

Pick which Postgres, not Postgres versus a vector database. Three reasons that
hold in 2026: one database means one transaction and one auth boundary, with
the vectors next to the records they relate to so a join stays a join rather
than a network hop; Postgres already does the hard parts, transactions,
indexes, foreign keys, row-level security, point-in-time recovery; and MCP
servers exist for Postgres at every layer. A dedicated vector store wins only
when search by meaning is the product rather than a feature beside your
business data: more vectors than fit in one server's memory, search traffic
heavy enough to need an engine built only for vectors, or vectors used on
their own by many services. A worker with a tickets table and its embeddings
beside it is nowhere near that point. [^af-fte]

A managed Postgres that scales to zero costs nothing while the worker is idle,
which matters when many workers are each busy only in bursts; one that
branches lets the agent try a schema change on a copy and delete the branch if
it goes wrong; and one with an official MCP server lets your coding agent
create projects, manage branches and run migrations in plain language while
you build. Any Postgres with the vector extension works; the branching and
the MCP server are the conveniences, not the requirement. [^af-fte]

## The tables every worker keeps

Four tables are the shared spine, and one more you add when you want usage
analytics: [^af-fte]

- **`conversations`**: one row per conversation, who it was with, when, and a
  short summary at the end. The turn-by-turn messages are stored separately by
  the SDK's session, which creates and writes its own tables, so you never
  design those. Picture a transcript and a cover sheet: the transcript is the
  SDK's, the cover sheet is yours, linked by the same session id.
- **`documents`** and **`embeddings`**: the reference library. Documents hold
  the text; embeddings make it searchable by meaning. One embeddings table
  serves both documents and past conversations, with a check constraint that
  makes each row point at exactly one, so "have we answered this before?" uses
  one index, not two.
- **`audit_log`**: the trace, every meaningful action in order, including
  business events such as a refund being issued. It uses an auto-incrementing
  integer key rather than a UUID, because audit rows pile up fast and a plain
  integer keeps writes quick and the order obvious.
- **`capability_invocations`**, optional: one row each time the worker runs a
  skill or calls a tool, with how long it took, whether it succeeded, and a
  rough cost. Both share the one table with a column that says which.

Two more tables live outside the spine: your business tables (customers,
tickets, orders, refunds), and `run_states`, which stores a paused approval
when a human signs off later or in another process rather than right away.
Every table carries a JSONB metadata column as an escape hatch; use it
sparingly, and promote anything you query often to its own column. [^af-fte]

Among the tables you build, the audit log grows fastest, because one
interaction can write several action rows while it adds one conversations row
and no new documents. Plan its retention and indexing first. [^af-fte]

## Search by meaning: the embedding pipeline

An embedding turns a piece of text into a point in space; text about refunds
lands near other text about refunds, so "find similar tickets" becomes "find
the nearest points". The rest is plumbing, four steps with one decision each:
[^af-fte]

1. **Chunk** long text at natural breaks, headings and paragraphs, into pieces
   of a few hundred words that each carry one idea, with a little overlap so
   an idea that straddles a boundary is still found. Do not chunk what is
   already short.
2. **Embed** each chunk with the model, in batches.
3. **Store** the text, its point and a little metadata in the embeddings
   table.
4. **Query** by turning the question into a point too and asking for the
   nearest stored points, cosine distance, nearest first.

The one rule that never bends: embed your stored text and your search queries
with the same model, or the matches come back as noise. The vector column's
size is decided by the model: 1536 for OpenAI's small text embedding model,
3072 for the large one. Start with an HNSW index, the fast-search default;
reach for IVFFlat only if build speed matters more than search speed. Re-embed
when the source text changed, when you switched embedding models (every old
point now lives on a different map), or when you changed the chunk size. When
results look wrong, the cause is almost always one of three: the query and
the stored text went through different models, you forgot to filter by
source type, or your chunks are too small to carry meaning. [^af-fte]

Filtering by source matters more than it sounds. If documents and past
conversations share one table and a search returns both unfiltered, the agent
can treat a snippet of an old chat as if it were policy. Ask for documents
only, or run two searches and weigh them. [^af-fte]

## The audit trail is a discipline, not a log

Two things sit close together and get confused. **The truth itself** is what
is currently the case, a customer's tier, a ticket's status, a policy's text;
it lives in the business records and the reference library. **The audit
trail** is the replayable record of what the worker did to that truth: which
tool it called, what it changed, what it returned, who approved it. It answers
a different question, not "what is true?" but "what did the worker do, and can
you prove it?" [^af-fte]

What goes in: every tool or skill call with its name, inputs, result and
duration; every change to the record; every guardrail decision; each model
call with its token cost. What stays out: the full conversation text, which
the session already holds; raw sensitive data in a row a person can read; the
model's private reasoning. [^af-fte]

The writing discipline is one sentence: **whatever code issues a refund writes
the refund and its audit row in one transaction; both land or neither does.**
Give every action a name from a small, agreed set and do not let those names
drift; enforce the vocabulary with a check constraint, so introducing a new
verb is a deliberate one-line migration. The test of an audit trail: given a
conversation and a time, you can reconstruct what the worker did and why
without re-running the model. And the business reason the book gives: workers
only become governable when a ledger makes them legible, and you cannot charge
for an outcome you cannot prove happened. [^af-fte]

Write the audit rows over their own connection, not the pool the business
tools use. Audit must succeed even when the data pool is saturated, and audit
writes must not compete with business writes. An audit subsystem that can be
starved by the system it is auditing is not an audit subsystem. [^af-fte]

## What MCP is, and is not

The Model Context Protocol is an open client-server protocol for how an agent
connects to external tools, data and prompts: one protocol, many
implementations, swap any side without breaking the other. A server exposes
three primitives: tools the model can invoke, by far the most used; resources,
read-only data the agent can fetch; and prompts, reusable templates a team can
publish. Three transports: a local subprocess for agent and server on one
machine; streamable HTTP for remote servers and production, one endpoint over
plain HTTPS; and the older server-sent-events transport that many servers
still expose and new ones increasingly leave behind. Streamable HTTP comes in
two flavours: stateless, where each call is an independent request so many
copies can sit behind a load balancer, and stateful, which keeps a session open
for streaming partial results but pins each client to one server instance.
Use stateless unless you have a specific reason. [^af-fte]

Four things MCP is not: [^af-fte]

- **Not a framework.** Your agent's MCP client speaks the protocol to an MCP
  server; the SDK includes the client.
- **Not a service.** There is no MCP cloud; servers are programs you run or
  that vendors run for you.
- **Not a security boundary.** MCP defines transport and protocol; what tools
  a server exposes and what they can do is the server's responsibility. A
  malicious server can do anything its code does. The trust boundary is still
  the agent loop deciding which tools to call and the sandbox the tools run in.
- **Not a replacement for a function tool.** Both have a place; the decision
  is below.

To the model, an MCP tool and a function tool look the same: a callable with a
schema. The difference is where the implementation lives. An agent can connect
to several servers at once and sees the union of their tools. [^af-fte]

## The development plane never reaches runtime

The single most important line in the book's MCP part: the database vendor's
own MCP server, which exposes projects, branches, schema, migrations and
ad-hoc SQL as plain-language tools, is a development tool, not a production
one. Use it to design the schema, test a migration on a throwaway branch and
merge it, explore your own data, and look things up. A live worker should
never hold a run-arbitrary-SQL tool, because that is a door you cannot fully
lock. It needs a few narrow abilities, not the power to run any query.
[^af-fte]

The two production patterns are a custom MCP server that exposes only the
specific operations the worker needs, or a direct database connection wrapping
them. The book's worked build uses both: a scoped server with three tools for
the business operations, and a direct connection only for the audit
subsystem. The rule of thumb for the running worker's tools: **reads run free,
writes are gated behind human approval, and structural changes never reach
production through the agent at all.** Schema changes happen at development
time, on a branch, before they touch main. [^af-fte]

Four habits when the SDK's built-in MCP client connects to a server: open and
close the connection as a managed block so a subprocess or session never
leaks; cache the tool list in production so the agent asks "what tools do you
have?" once rather than on every run, and refresh it when the tools change;
stack servers, the model sees the combined tool set; and gate the dangerous
tools behind approval, which is also the practical knob for using the vendor
server by hand with its destructive tools behind a prompt. [^af-fte]

## A function tool, your own server, or a vendor's

The value of MCP rises with the value of the boundary it creates. A boundary
you do not need is overhead. [^af-fte]

| You want to expose | Use | Why |
| --- | --- | --- |
| One function with one input, used by one agent | A function tool | No protocol overhead; a local call is fine |
| Capabilities reused across agents, sensitive data, or a scope you want enforced | A custom MCP server | Scope: only the tools you define, no escape hatch. Isolation: its own process and pool. Reusability: a second worker talks to the same server |
| A third party's API with a vendor server that exists | The vendor's MCP server | Do not wrap a third-party API in your own server unless you need to add policy on top |

The shape of a custom server is simpler than it sounds: a small program that
declares a handful of named tools, each with a plain-language description that
tells the model when to reach for it and a short list of typed inputs. No
general SQL, no escape hatch. The trade-off is real: another process to
deploy, another set of logs, another hop, another version. Do not write one
for a single function used by a single agent; write one when the capability
will be reused, when scoping matters, or when isolation buys safety. And read
every tool's description in the built server the way you read a skill's
description: it is what the model reads to decide when to call it, and a vague
one fires at the wrong time. A tool whose input is "query: string" is a
run-SQL tool in disguise; a lookup should take an identifier. [^af-fte]

Under real traffic three pressures appear, each with a plain fix: move from a
local subprocess to the remote transport the moment more than one agent shares
the server; connect once at boot and keep the connection, cache the tool list,
and keep a pool of database connections inside the server; and put a ceiling
on everything, a step cap per request, a couple of retries on a failed tool
call, a rate limit on the server, with the trace following the call across the
boundary so a slow query inside the server is visible from outside. [^af-fte]

## The gate on the one action that moves money

The book adds the approval gate last, on purpose. A gate added before the
worker runs is untestable theatre: you cannot tell a working gate from a
broken one when nothing flows through it. Added on a worker you have watched
search, draft and audit, you can prove both halves: approve lets the refund
through, with the refund row, the order status and the audit row landing in
one transaction; reject stops it, with no refund written and the trace showing
the action was declined. The model proposes and a human disposes. When the
approver is not at the terminal, the paused run is serialised into
`run_states` and resumed from another process, the refund is made idempotent
on the order plus a request id so a retry cannot pay twice, and one active
turn per conversation is enforced with a lock. [^af-fte]

That gate is `needs_approval` from [build the text agent](build-the-text-agent.md),
now writing to a real record. The eval for it is on
[evals before shipping](evals-before-shipping.md): both halves, every time.

## What this page asks of the tools in this room

Before the record exists, the room's rules already point here. Every record a
person owns gets a tool that lists it, scoped by the identity the harness
supplies; every lookup takes an identifier, never free text the tool turns
into a query; every write is gated; every meaningful action leaves a row. When
the worker outgrows its session file, the tables above are where those rules
land, and the wire between them is MCP.

Continue to the [worked example](worked-example.md), which runs the room's
basic worker through all of it and says exactly where this page takes over.

[^af-fte]: Building a Digital FTE: A Crash Course. The AI Agent Factory, Panaversity, 2026.
