Blog

How to build a company knowledge base for AI agents

A step-by-step guide to building searchable, agent-ready knowledge across Slack, docs, code, and internal tools.


A step-by-step guide to building searchable, agent-ready knowledge across Slack, docs, code, and internal tools.


Every company past a certain size has the same problem: useful information is scattered across dozens of tools and the people who know where things are become bottlenecks. The fix is not consolidating everything into one platform, because people will always generate information wherever it is convenient. The fix is building an infrastructure layer that indexes what already exists and makes it queryable by both humans and agents.

This guide walks through building one with Exabase, from choosing what to index through to maintaining it in production.



Step 1: Decide what to index

Not everything is worth indexing. Start with the sources that generate the most repeated questions, and expand from there.

For most companies, the high-value sources in rough order of priority are: Slack or Teams (where the most current discussions happen), internal documentation and wikis (where processes and decisions are recorded), code repositories (where institutional knowledge lives in comments, PRs, and README files), and project management tools like Jira or Linear (where decisions and status are tracked).

A useful heuristic: if a new employee would need to ask a person to find something, it should be indexed. If they could find it through the tool's own search, it probably does not need to be in the knowledge base unless the tool's search is poor.

Start with two or three sources. You can add more later without restructuring anything.



Step 2: Create your Bases

A Base is an isolated cloud filesystem instance. Think of it as a scoped workspace for a team, project, or domain.

If you index everything into a single global namespace, search results quickly become noisy. The compiler team does not need sales playbooks in their results. Creating one Base per team or domain means search is scoped by default and new employees get relevant results immediately.


javascript

import { Exabase } from "@exabase/sdk";
const exabase = new Exabase({ apiKey: process.env.EXABASE_API_KEY });

// Create a base for the engineering team
const engineeringBase = await exabase.bases.create({
  name: "Engineering",
});

// Create a base for the product team
const productBase = await exabase.bases.create({
  name: "Product",
});
import { Exabase } from "@exabase/sdk";
const exabase = new Exabase({ apiKey: process.env.EXABASE_API_KEY });

// Create a base for the engineering team
const engineeringBase = await exabase.bases.create({
  name: "Engineering",
});

// Create a base for the product team
const productBase = await exabase.bases.create({
  name: "Product",
});
import { Exabase } from "@exabase/sdk";
const exabase = new Exabase({ apiKey: process.env.EXABASE_API_KEY });

// Create a base for the engineering team
const engineeringBase = await exabase.bases.create({
  name: "Engineering",
});

// Create a base for the product team
const productBase = await exabase.bases.create({
  name: "Product",
});

The same resource can exist in multiple Bases without duplication, so shared channels or company-wide docs can appear wherever they are relevant.

Start with broad team-level Bases. You can subdivide later if search results are still too noisy within a team.



Step 3: Build your connectors

Exabase indexes whatever you send it. Your job is writing the code that pulls data from your sources and sends it in. The connector reads from the source. Exabase handles extraction, chunking, embedding, and indexing automatically.


Documents and files

Upload files directly through the Resources API. PDFs, Word docs, spreadsheets, images, audio, and video are processed by Extract into searchable chunks with location references.


javascript

// Upload a document to a base
await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'file',
  file: documentBuffer,
  name: "Architecture Decision Record - Auth Service.pdf",
});
// Exabase extracts, chunks, embeds, and indexes automatically
// Upload a document to a base
await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'file',
  file: documentBuffer,
  name: "Architecture Decision Record - Auth Service.pdf",
});
// Exabase extracts, chunks, embeds, and indexes automatically
// Upload a document to a base
await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'file',
  file: documentBuffer,
  name: "Architecture Decision Record - Auth Service.pdf",
});
// Exabase extracts, chunks, embeds, and indexes automatically


Web pages and wiki content

For internal wikis, Confluence pages, or Notion docs, pull the URL and let Extract handle the content:


javascript

await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'url',
  url: "https://wiki.internal.company.com/auth-service-overview",
});
await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'url',
  url: "https://wiki.internal.company.com/auth-service-overview",
});
await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'url',
  url: "https://wiki.internal.company.com/auth-service-overview",
});


Slack conversations

Slack is usually the most valuable and most challenging source. Raw messages are noisy: greetings, reactions, tangents, and one-word replies are all mixed in with genuine technical discussions.

Rather than indexing raw messages, feed conversation threads through the Memory API. It extracts the structured facts and stores them as queryable memories, discarding the noise.


javascript

// Pull a Slack thread via the Slack API
const thread = await slack.conversations.replies({
  channel: channelId,
  ts: threadTs,
});

const transcript = thread.messages
  .map(m => `${m.user}: ${m.text}`)
  .join('\n');

// Extract structured memories from the conversation
await exabase.memories.create({
  baseId: engineeringBase.id,
  source: 'text',
  content: transcript,
});
// Pull a Slack thread via the Slack API
const thread = await slack.conversations.replies({
  channel: channelId,
  ts: threadTs,
});

const transcript = thread.messages
  .map(m => `${m.user}: ${m.text}`)
  .join('\n');

// Extract structured memories from the conversation
await exabase.memories.create({
  baseId: engineeringBase.id,
  source: 'text',
  content: transcript,
});
// Pull a Slack thread via the Slack API
const thread = await slack.conversations.replies({
  channel: channelId,
  ts: threadTs,
});

const transcript = thread.messages
  .map(m => `${m.user}: ${m.text}`)
  .join('\n');

// Extract structured memories from the conversation
await exabase.memories.create({
  baseId: engineeringBase.id,
  source: 'text',
  content: transcript,
});

The Memory API extracts facts, preferences, and decisions from the conversation. If someone mentioned a deadline in one thread and it changed in a later one, the contradiction is resolved automatically. Your search results reflect the current state, not a historical snapshot.

For real-time ingestion, set up a Slack bot that listens for new messages and feeds completed threads into the Memory API as they resolve.


Code repositories

For code, upload the files you want searchable. README files, architecture docs, and heavily-commented modules are usually more valuable than raw source code for a knowledge base.


javascript

// Index key documentation files from a repo
const readmeContent = await fetchFromGitHub('org/repo', 'README.md');

await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'text',
  content: readmeContent,
  name: "auth-service/README.md",
});
// Index key documentation files from a repo
const readmeContent = await fetchFromGitHub('org/repo', 'README.md');

await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'text',
  content: readmeContent,
  name: "auth-service/README.md",
});
// Index key documentation files from a repo
const readmeContent = await fetchFromGitHub('org/repo', 'README.md');

await exabase.resources.create({
  baseId: engineeringBase.id,
  source: 'text',
  content: readmeContent,
  name: "auth-service/README.md",
});

Custom internal sources

If a team has their own database, internal tool, or API that holds knowledge worth querying, the pattern is the same: pull the data out, send it to Exabase as a resource or a memory, and it becomes searchable alongside everything else.



Step 4: Configure search

Deep Search runs hybrid semantic and keyword search automatically, with temporal weighting so recent information surfaces appropriately. You do not need to configure the search strategy, but you do have control over precision.


javascript

// High precision for specific factual questions
const results = await exabase.search.query({
  baseId: engineeringBase.id,
  query: { text: "what is the rate limit on the auth service?" },
  precision: 0.8,
});

// Lower precision for broader research questions
const results = await exabase.search.query({
  baseId: engineeringBase.id,
  query: { text: "how does our deployment pipeline work?" },
  precision: 0.5,
});
// High precision for specific factual questions
const results = await exabase.search.query({
  baseId: engineeringBase.id,
  query: { text: "what is the rate limit on the auth service?" },
  precision: 0.8,
});

// Lower precision for broader research questions
const results = await exabase.search.query({
  baseId: engineeringBase.id,
  query: { text: "how does our deployment pipeline work?" },
  precision: 0.5,
});
// High precision for specific factual questions
const results = await exabase.search.query({
  baseId: engineeringBase.id,
  query: { text: "what is the rate limit on the auth service?" },
  precision: 0.8,
});

// Lower precision for broader research questions
const results = await exabase.search.query({
  baseId: engineeringBase.id,
  query: { text: "how does our deployment pipeline work?" },
  precision: 0.5,
});

The precision parameter trades recall for relevance. Set it high when the user is asking for a specific fact and you want fewer, more targeted results. Set it low for open-ended questions where coverage matters more.

Results come back with location references: page numbers for PDFs, timestamps for audio and video, and source attribution for all content types. Your agent can cite where an answer came from without injecting entire documents into the prompt.

For memory queries, use the Memory API directly:


javascript


const memories = await exabase.memories.search({
  baseId: engineeringBase.id,
  query: "who is the owner of the auth service?",
  rerankThreshold: 0.5,
});
const memories = await exabase.memories.search({
  baseId: engineeringBase.id,
  query: "who is the owner of the auth service?",
  rerankThreshold: 0.5,
});
const memories = await exabase.memories.search({
  baseId: engineeringBase.id,
  query: "who is the owner of the auth service?",
  rerankThreshold: 0.5,
});

In practice, a query to your knowledge base will often combine both: memories for facts about people, preferences, and decisions, and Deep Search for information inside documents and files. The two APIs return different types of context that complement each other.



Step 5: Build the interface

The retrieval layer is handled. What remains is how your users interact with it. The common options:

Slack bot

The most natural interface for most teams. A user asks a question in a channel or DM, the bot queries Exabase, passes the context to an LLM, and posts the answer with citations.


javascript

app.event('message', async ({ event }) => {
  // Search memories and documents
  const memories = await exabase.memories.search({
    baseId: userBase,
    query: event.text,
  });

  const docs = await exabase.search.query({
    baseId: userBase,
    query: { text: event.text },
    precision: 0.7,
  });

  // Combine context and generate answer
  const context = [
    ...memories.hits.map(m => m.content),
    ...docs.hits.map(d => d.chunks.map(c => c.text).join('\n')),
  ].join('\n\n');

  const answer = await llm.generate({
    system: "Answer based on the following context. Cite sources.",
    context: context,
    question: event.text,
  });

  await say(answer);
});
app.event('message', async ({ event }) => {
  // Search memories and documents
  const memories = await exabase.memories.search({
    baseId: userBase,
    query: event.text,
  });

  const docs = await exabase.search.query({
    baseId: userBase,
    query: { text: event.text },
    precision: 0.7,
  });

  // Combine context and generate answer
  const context = [
    ...memories.hits.map(m => m.content),
    ...docs.hits.map(d => d.chunks.map(c => c.text).join('\n')),
  ].join('\n\n');

  const answer = await llm.generate({
    system: "Answer based on the following context. Cite sources.",
    context: context,
    question: event.text,
  });

  await say(answer);
});
app.event('message', async ({ event }) => {
  // Search memories and documents
  const memories = await exabase.memories.search({
    baseId: userBase,
    query: event.text,
  });

  const docs = await exabase.search.query({
    baseId: userBase,
    query: { text: event.text },
    precision: 0.7,
  });

  // Combine context and generate answer
  const context = [
    ...memories.hits.map(m => m.content),
    ...docs.hits.map(d => d.chunks.map(c => c.text).join('\n')),
  ].join('\n\n');

  const answer = await llm.generate({
    system: "Answer based on the following context. Cite sources.",
    context: context,
    question: event.text,
  });

  await say(answer);
});

MCP integration

Exabase supports MCP for Claude, Cursor, and Windsurf. This means developers using these tools can query the knowledge base directly from their editor or terminal without switching context. The retrieval tools are exposed as MCP primitives that the agent calls as needed.

Web UI

A standalone interface where users type a question and get an answer. More control over the experience but more to build. The backend pattern is the same: query Exabase, assemble context, generate answer.

API for other internal tools

If you have existing internal tools that would benefit from knowledge base access, expose a thin API that wraps Exabase queries and LLM generation. Other tools call your API rather than integrating with Exabase directly.


Step 6: Keep it current

A knowledge base that is not maintained becomes a source of stale answers, which is worse than having no knowledge base at all.

Automate ingestion. Connectors should run on a schedule or in response to events. When a new document is added to the wiki, it should be indexed. When a Slack thread resolves, it should be processed into memories. Workers can handle scheduled enrichment and maintenance tasks.

Let Memory handle contradictions. If someone's role changes, a deadline moves, or a decision is reversed, the Memory API resolves the contradiction when the new information is ingested. The old version is superseded rather than left to coexist with the new one. This is one of the most important properties of the system because memory drift is otherwise invisible and compounding.

Monitor search quality. Periodically review what users are asking and what the system is returning. If a category of questions consistently produces poor results, the fix is usually adding a missing data source rather than tuning the search. The connectors you started with in Step 3 are a starting point. Add sources as you discover gaps.

Prune stale content. Documentation from deprecated systems, conversations about projects that no longer exist, and outdated process docs all add noise. Periodically reviewing what is indexed and removing content that is no longer relevant keeps retrieval quality high.


What this looks like at scale

A well-built company knowledge base becomes one of the most-used internal tools. It reduces the time new employees spend finding information, frees experienced employees from answering repeated questions, and gives agents access to the same institutional knowledge that previously lived only in people's heads.

The infrastructure to support this, hybrid search, memory extraction, scoped storage, reranking, and temporal reasoning, is substantial to build from scratch. With Exabase it is the starting point rather than the destination. Your engineering time goes into the connectors for your specific data sources and the interface for your users, which is the part that is actually specific to your company.


Links:

Cut your token spend and give your agent precise context.

Get started in minutes.

Cut your token spend and give your agent precise context.

Get started in minutes.