buildd
Features

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 scope field 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 — files links a memory to the paths it concerns. recall does 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=search is the only interface that filters on files directly.

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:

TypePurposeExample
gotchaNon-obvious bugs or traps"neon-http driver doesn't support interactive transactions"
patternRecurring code conventions"All API routes use Bearer token auth via the Authorization header"
decisionArchitectural choices with rationale"Chose Pusher over SSE for realtime because of Vercel cold starts"
discoveryLearned behaviours or undocumented APIs"SDK v2 session API ignores the settingSources option"
architectureSystem structure and data flow"Workers run externally; the API is coordination-only"

Choosing between them:

SituationType
"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}/memory forwards body.type straight to the store.
  • buildd_memory action=update forwards params.type straight through, unlike its sibling save.

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:

ParamDescription
queryNatural language query: task title, error text, or concept. Required unless id is given
idDirect fetch by memory ID — bypasses ranking; all other params are ignored
limitMax results (default 10, capped at 50)
scopeCorpus 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"]
ParamRequiredDescription
typeYesgotcha, pattern, decision, discovery, or architecture. The legacy summary is rejected on write
titleYesShort title
contentYesThe lesson — what the next agent would have wanted to know
filesNoRelated file paths, for searchability
tagsNoTags for categorisation
scopeNoProject / monorepo scope for this memory
supersedesNoMemory 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_task reply. recall only returns it if you pass includeCandidates: true, or fetch it directly by id.
  • 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 through learn at 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.

DecisionWhat it decidesStatus
Worth keepingDurable lesson, or just a task summary?Live
TypeWhich of the five types fits a writeLive
UpdateADD / UPDATE / SUPERSEDE / NOOP against similar existing memoriesLive
Use labelDid the agent actually act on a memory it saw?Live (measurement only)
Chat tierIs a chat message a standing rule, general knowledge, or neither?Live
Directive scopeShould a new standing rule default to everywhere or just this workspace?Live
Relevance gateShould this specific match actually be shown for this task?Shadow — logged, not yet acted on
PromoteShould 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.

ToolPurpose
recallRead team knowledge
learnWrite a durable lesson
buildd_memoryLegacy 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:

ActionDescription
consolidate_knowledgeSurface near-duplicate entries (op=find_duplicates), find never-retrieved decayed entries (op=find_decayed), or archive a batch (op=archive, recoverable)
memory_deletePermanently 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=10

Create 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.

On this page