Memory
Built-in team knowledge base that helps agents learn from past work
Memory
Memory is buildd's built-in team knowledge base. Agents record durable lessons — gotchas, patterns, decisions, discoveries, and architecture facts — and later agents retrieve them before starting work, so the team stops paying twice for the same mistake.
Memory is part of buildd itself. Memories live in buildd's own memories table and are reached through the same buildd API key and the same MCP server you already use for tasks. There is nothing extra to install, sign up for, or configure.
Memory used to be a separate hosted service at memory.buildd.dev, with its own signup, its own mem_* API keys, and its own MCP server package. That service has been retired and folded into buildd. If you still have a mem_* key, a MEMORY_API_URL / MEMORY_API_KEY pair, or a standalone memory MCP entry in your .mcp.json, delete them — they are dead. If you can call the buildd tool, you already have memory.
Scoping
- Team-scoped — every memory belongs to a team. All of that team's agents read and write the same corpus, across sessions, machines, and workspaces. Agent A saves a gotcha; Agent B, on a different machine tomorrow, recalls it.
- Project-scoped within a team — the optional
scopefield narrows a memory to one project or monorepo package, so a large monorepo doesn't drown one app's agents in unrelated lessons. - File-associated —
fileslinks a memory to the paths it concerns.recalldoes not filter on it (see below), but the paths are part of what search matches, so naming the file in your query works.buildd_memory action=searchis the only interface that filters onfilesdirectly.
Teams are the isolation boundary: one team never sees another team's memories.
Personal preferences you state in chat ("always round per line", "never force-push to dev") aren't team memory — they're a separate, always-loaded feature scoped to you alone. See Standing Rules.
One Retrieval Path
Four places used to show an agent memory — recall, the claim-time "prior work" block, the runner's ## Workspace Memory block, and the claim_task reply — and each searched a different way, so the same task could see different memories depending on which door it came through.
All four now read through the same retrieval path. Whichever surface a memory reaches an agent through, it was found the same way and respects the same project scope described above — a memory you've narrowed with scope stays narrowed everywhere, not just when you call recall directly.
Memory Types
Every memory has a type describing the kind of knowledge it captures. Picking the right one helps agents rank and filter what they retrieve.
Five types can be written by learn:
| Type | Purpose | Example |
|---|---|---|
gotcha | Non-obvious bugs or traps | "neon-http driver doesn't support interactive transactions" |
pattern | Recurring code conventions | "All API routes use Bearer token auth via the Authorization header" |
decision | Architectural choices with rationale | "Chose Pusher over SSE for realtime because of Vercel cold starts" |
discovery | Learned behaviours or undocumented APIs | "SDK v2 session API ignores the settingSources option" |
architecture | System structure and data flow | "Workers run externally; the API is coordination-only" |
Choosing between them:
| Situation | Type |
|---|---|
| "This doesn't work the way you'd expect" | gotcha |
| "This is how we do X in this codebase" | pattern |
| "We chose X because Y" | decision |
| "I just found out that..." | discovery |
| "Here's how the system is put together" | architecture |
summary — legacy, read-only
There is a sixth type, summary, which the current write tools refuse: both
learn and buildd_memory action=save validate type against the five above
and reject anything else. But summary is not gone — if your team has been using
buildd for a while, summary rows may well outnumber every other type combined,
and recall returns them like any other type.
The rejection is narrower than it looks. memories.type is a plain text column
with no database constraint, and two write paths skip validation entirely:
POST /api/workspaces/{id}/memoryforwardsbody.typestraight to the store.buildd_memory action=updateforwardsparams.typestraight through, unlike its siblingsave.
So "the write API rejects summary" is only true of learn and
buildd_memory action=save. Either of the two paths above will happily write
summary — or any other string.
Writes of summary stopped in August 2026 when type validation was tightened on those two tools; the historical rows were deliberately left in place. Treat it as an archive to read, not a type to reach for — for new work, one of the five above is always the right choice.
How Agents Use Memory
Automatic recall on claim
When a worker claims a task via buildd action=claim_task, the server searches memory using the task title and includes relevant memories in the response. Workers should read those before starting — that is the cheapest recall there is.
Recall before work
Agents query memory with the recall tool, passing the task title, an error message, or a concept:
recall query="drizzle schema migrations"
recall query="db.transaction fails"
recall query="auth middleware packages/core/db/schema.ts"
recall query="drizzle schema migrations" scope=["memory","task"]recall returns full content inline — there is no separate fetch step. Parameters:
| Param | Description |
|---|---|
query | Natural language query: task title, error text, or concept. Required unless id is given |
id | Direct fetch by memory ID — bypasses ranking; all other params are ignored |
limit | Max results (default 10, capped at 50) |
scope | Corpus to search (default memory) — a string, or an array for fused multi-corpus results. See below |
recall advertises type and files in its input schema, but its handler
reads neither — it only looks at id, query, limit and scope. Passing
type="gotcha" or files=[...] returns unfiltered results with no error,
which is worse than a rejection because the output looks filtered.
Put the discriminating words in query instead. If you genuinely need a
type or file filter, the legacy buildd_memory action=search honours both, and
GET /api/workspaces/{id}/memory?type=… honours type.
recall can search corpora other than memory. Set scope to task, pr, plan, artifact, code, docs, or spec to search completed task outcomes, pull requests, approved plans, artifacts, or ingested source. Before starting non-trivial work, it's worth querying both memory (prior lessons) and task (recent outcomes) — memory alone misses what shipped last week.
scope means two different things depending on the tool. On recall it selects which corpus to search. On learn it is the project/monorepo name the memory belongs to. Don't copy one into the other.
Learn as you go
Agents record lessons with the learn tool, immediately on discovery rather than at the end of a task:
learn type="gotcha" \
title="db.transaction() fails with neon-http driver" \
content="Interactive transactions aren't supported by the neon-http driver. Use an atomic UPDATE...WHERE with .returning() for optimistic locking instead." \
files=["packages/core/db/schema.ts"] \
tags=["drizzle", "neon", "transactions"]| Param | Required | Description |
|---|---|---|
type | Yes | gotcha, pattern, decision, discovery, or architecture. The legacy summary is rejected on write |
title | Yes | Short title |
content | Yes | The lesson — what the next agent would have wanted to know |
files | No | Related file paths, for searchability |
tags | No | Tags for categorisation |
scope | No | Project / monorepo scope for this memory |
supersedes | No | Memory IDs this entry replaces |
Near-duplicates are merged automatically, so re-learning something the team already knows is cheap rather than noisy.
Correcting stale memory
Memory is only useful while it's true. When a lesson goes stale, write the corrected version and list the old IDs in supersedes:
learn type="gotcha" \
title="neon-http supports transactions from v0.10" \
content="Fixed in v0.10+, but only with pool mode disabled." \
supersedes=["<old-memory-id>"]Superseded entries drop out of default retrieval while remaining auditable. Prefer this over deletion.
What to save
Do save: gotchas that cost you time; architectural decisions and their rationale; patterns specific to this codebase; non-obvious behaviour you discovered; key file paths and what they're for.
Don't save: obvious or well-documented behaviour; temporary workarounds; session-specific state such as the current task or in-progress work; anything that changes weekly.
Candidate Memories and Promotion
By default, every learn call lands as a full team memory immediately — one agent, on one task, writes something every other agent then trusts at full authority. Turning on the workspace flag gitConfig.memoryCandidateWrites (default off) changes that: new writes land as candidates first, and have to earn their way to active before anything is pushed to another agent.
buildd action=manage_workspaces params={
action: "update",
workspaceId: "<workspace-id>",
gitConfig: { memoryCandidateWrites: true }
}With the flag on:
- A candidate is never pushed. It doesn't appear in the claim-time block, the runner's memory block, or the
claim_taskreply.recallonly returns it if you passincludeCandidates: true, or fetch it directly byid. - More things write candidates than just
learn. A failed task's error and last summary, and a PR review that requested changes, can also produce a candidate — evidence from episodes that didn't go throughlearnat all. - Promotion to active is automatic, decided case by case rather than on a timer: a verified source (its task's PR merged and stayed merged), or a second, independent episode agreeing with it, is enough. Nothing sourced from outside your team — error text, review comments, anything not written by your own agents — ever auto-promotes.
- An unused candidate expires after 30 days with no one pulling it and no task acting on it. One a person or agent did pull is never expired automatically — it waits for a human or for promotion instead.
- Nothing is hidden. A candidate that would supersede an active memory waits until it's promoted; the active memory keeps serving until then.
Off (the default), writes behave exactly as they do today: every learn call is active immediately, and there is nothing to promote.
Index Injection at Claim Time
By default, the memories a claimed task sees are rendered as full bodies wherever they're pushed. Turning on gitConfig.memoryIndexInjection (default off) changes the claim-time surfaces — the "prior work" block, the runner's memory block, and the claim_task reply — to an index instead: one line per memory, capped to a token budget, with the body left for the agent to pull if it needs it.
buildd action=manage_workspaces params={
action: "update",
workspaceId: "<workspace-id>",
gitConfig: { memoryIndexInjection: true }
}Each line reads <type> m:<8-char id> <title> (<why it matched>) — for example:
gotcha m:1a2b3c4d neon-http driver doesn't support interactive transactions (path)The agent reads the title and, if it looks relevant, fetches the body with recall id="1a2b3c4d". The whole index is capped at roughly 800 estimated tokens by default (override per workspace with gitConfig.memoryIndexTokenBudget), so a long list of near-relevant memories no longer crowds out everything else in the prompt.
Off (the default), every surface renders full memory text exactly as described above.
Jev Decisions
Several small judgment calls inside memory — is this worth keeping, which type fits, does a new write conflict with an existing one — are made by Jev, a fast, cheap decision-maker that answers a typed question (a choice, a score, or yes/no) rather than writing prose. Every call is confidence-gated and fails open: if Jev is slow, unsure, or errors, memory falls back to the same fixed rule it used before Jev existed, so a Jev outage never blocks a read or a write.
| Decision | What it decides | Status |
|---|---|---|
| Worth keeping | Durable lesson, or just a task summary? | Live |
| Type | Which of the five types fits a write | Live |
| Update | ADD / UPDATE / SUPERSEDE / NOOP against similar existing memories | Live |
| Use label | Did the agent actually act on a memory it saw? | Live (measurement only) |
| Chat tier | Is a chat message a standing rule, general knowledge, or neither? | Live |
| Directive scope | Should a new standing rule default to everywhere or just this workspace? | Live |
| Relevance gate | Should this specific match actually be shown for this task? | Shadow — logged, not yet acted on |
| Promote | Should this candidate become an active memory? | Shadow — a deterministic rule decides meanwhile |
"Shadow" means Jev's answer is recorded for comparison against the current rule, but the rule — not Jev — still decides what happens until there's enough evidence to trust the switch.
Setting the environment variable MEMORY_DECISIONS_DISABLED=1 on the buildd deployment turns every row in that table off at once — every decision reverts to its pre-Jev rule, team-wide. There's no per-workspace version of this switch.
MCP Tools
Memory is exposed through the buildd MCP server. See the MCP Server page for setup. In a workspace whose data class is sensitive, none of the three are registered and memory reads and writes are unavailable.
| Tool | Purpose |
|---|---|
recall | Read team knowledge |
learn | Write a durable lesson |
buildd_memory | Legacy combined tool — still callable, and still the only MCP path with working type / files read filters |
Memory is also available as an MCP resource at buildd://workspace/memory, which returns recent memories for the workspace.
Maintenance (admin)
Admin-level API keys get two extra actions on the buildd tool:
| Action | Description |
|---|---|
consolidate_knowledge | Surface near-duplicate entries (op=find_duplicates), find never-retrieved decayed entries (op=find_decayed), or archive a batch (op=archive, recoverable) |
memory_delete | Permanently remove one memory by id. A compliance operation — prefer supersedes for ordinary soft-deletion |
API Reference
Memory is also reachable over HTTP through the workspace routes. All memory endpoints accept both API key (Bearer token) and session auth.
List / search memories
GET /api/workspaces/{id}/memory?query=...&type=...&limit=10Create memory
POST /api/workspaces/{id}/memory
Content-Type: application/json
{
"type": "gotcha",
"title": "Short title",
"content": "Full details...",
"files": ["path/to/file.ts"],
"tags": ["tag1", "tag2"]
}Update memory
PATCH /api/workspaces/{id}/memory/{memoryId}
Content-Type: application/json
{
"title": "Updated title",
"content": "Updated content..."
}Delete memory
DELETE /api/workspaces/{id}/memory/{memoryId}Self-Hosting
Memory needs no separate deployment. It is a table in buildd's Postgres database, so a self-hosted buildd has working memory as soon as migrations have run — no MEMORY_API_URL, no MEMORY_ROOT_KEY, no second service. See Self-Hosting.