> ## Documentation Index
> Fetch the complete documentation index at: https://docs.guild.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Biblos: a grounded Slack librarian

> Answer process questions in Slack from approved playbooks, and refuse when the library doesn't hold enough evidence.

Biblos is a one-shot TypeScript agent for Slack knowledge questions. A mention in an allowlisted channel starts a Guild session. Biblos reads only that thread, retrieves a bounded set of snippets from a Pinecone Assistant, and writes one reply in the same thread. When retrieval comes back weak or empty, Biblos sends a fixed refusal instead of inventing a process.

```text theme={null}
Slack mention -> channel guard -> bounded thread read -> Pinecone retrieve
                                                      -> grounded reply
                                                      -> fixed refusal
```

<Card title="dkountanis~biblos" icon="robot" href="https://app.guild.ai/hub/agents/dkountanis~biblos">
  View the agent and its source on the Agent Hub.
</Card>

## At a glance

* **Agent type:** auto-managed [TypeScript agent](/guide/coded-agents)
* **Integrations:** [Slack](/integrations/slack) (`guildai~slack`) for reactions, thread reads, posts, and updates, and the `dkountanis~pinecone-assistant` integration (`1.0.0`) for ranked retrieval
* **Runs from:** an [event trigger](/platform/event-triggers) on Slack `app_mention`
* **LLM calls:** at most one per mention, with no tools

**What it does:**

* Reacts with `:eyes:` and posts a `Writing…` placeholder right away
* Reads at most 20 Slack messages and retrieves at most 8 snippets per run
* Calls the LLM only when the evidence clears a configured floor
* Replies to the exact channel and thread from the validated webhook payload
* Neutralizes mass mentions and caps the length of what it posts
* Adds a delivery marker to each reply to guard against processing the same event twice
* Returns the reply instead of posting it when called with `delivery: "return"`

The Pinecone integration is required. You still upload your playbooks in Pinecone, because the integration exposes retrieval, not file upload.

## How it works

<Steps>
  <Step title="Validate before calling any tools">
    The input is a bounded Slack `event_callback` object. Biblos rejects an event from a channel that isn't allowlisted, or one a bot wrote, before it reads Slack, Pinecone, or the LLM.
  </Step>

  <Step title="Read one thread">
    `conversations.replies` reads at most 20 messages from the thread the mention came from. Biblos treats Slack text as untrusted content, never as authority to change its tools, recipients, or policy.
  </Step>

  <Step title="Retrieve, then decide">
    The mention and the thread context become one query. Pinecone returns at most eight snippets. Deterministic code applies the minimum score and the minimum number of snippets before the LLM is allowed to write anything.
  </Step>

  <Step title="Reply or refuse">
    With enough evidence, Biblos writes one short answer grounded only in the selected snippets. Without it, Biblos sends its fixed "above my head" reply. Where it can, Biblos updates the placeholder in place.
  </Step>
</Steps>

## Build the agent

### Define the webhook contract

Keep the variants inside an object-root schema. Unknown fields in the Slack envelope are stripped before orchestration or prompt construction:

```ts theme={null}
import { z } from "zod";

const slackTimestampSchema = z
  .string()
  .min(12)
  .max(32)
  .regex(/^\d{10,16}\.\d{1,6}$/);

const slackEventSchema = z
  .object({
    type: z.literal("app_mention"),
    channel: z.string().min(9).max(32).regex(/^[CDG][A-Z0-9]+$/),
    user: z.string().max(32).optional(),
    text: z.string().min(1).max(4_000),
    ts: slackTimestampSchema,
    thread_ts: slackTimestampSchema.optional(),
    bot_id: z.string().max(32).optional(),
    subtype: z.string().max(64).optional(),
  })
  .strip();

export const inputSchema = z
  .object({
    type: z.literal("event_callback").optional(),
    event_id: z.string().min(2).max(64).regex(/^Ev[A-Za-z0-9]+$/),
    event: slackEventSchema,
    delivery: z.enum(["slack", "return"]).optional(),
  })
  .strip();
```

The channel and thread always come from this validated envelope. Don't add a separate destination the caller controls.

### Register only the operations you need

Register each Slack operation the agent uses with `guildServiceTool("slack", ...)` rather than importing a Slack tool package. The published source includes the complete endpoint metadata and response schemas, and pins each operation to a specific integration version:

```ts theme={null}
import { guildServiceTool } from "@guildai/agents-sdk";

const tools = {
  slack_conversations_replies: guildServiceTool("slack", {
    description: "Read up to 20 messages from one exact Slack thread.",
    inputSchema: z.object({
      channel: z.string(),
      ts: z.number().positive(),
      limit: z.literal(20),
    }),
    outputSchema: z.object({
      ok: z.literal(true),
      messages: z.array(z.object({ text: z.string().optional() })).max(20),
    }),
  }),
  slack_chat_post_message: guildServiceTool("slack", {
    description: "Post one reply to one exact Slack channel and thread.",
    inputSchema: z.object({
      channel: z.string(),
      thread_ts: slackTimestampSchema,
      text: z.string().min(1).max(2_800),
      mrkdwn: z.literal(true),
      reply_broadcast: z.literal(false),
      unfurl_links: z.literal(false),
      unfurl_media: z.literal(false),
    }),
  }),
  pinecone_assistant_context_assistant: guildServiceTool(
    "pinecone-assistant",
    {
      owner: "dkountanis",
      versionNumber: "1.0.0",
      description: "Retrieve bounded snippets from one Pinecone Assistant.",
      inputSchema: z.object({
        assistant_name: z.string().min(1).max(64),
        query: z.string().min(1).max(4_000),
        top_k: z.number().int().min(1).max(8),
        multimodal: z.literal(false),
      }),
    },
  ),
};
```

For readability, this excerpt shows only the thread-read, final-post, and retrieve contracts. The same file also registers `reactions.add` and `chat.update`, which the orchestration below uses. Copy the complete definitions from `agent.ts` when you build a fork.

Before you adapt these definitions, confirm the current schemas:

```bash theme={null}
guild integration operation list <integration> --json
```

### Gate the evidence deterministically

Evidence selection is synchronous, can be tested on its own, and runs before the LLM:

```ts theme={null}
export function selectEvidence(
  snippets: readonly LibrarySnippet[],
  minCount = 1,
  minScore = 0.15,
): EvidenceDecision {
  const selected = snippets
    .filter((snippet) => snippet.content.trim().length > 0)
    .filter((snippet) => snippet.score >= minScore)
    .slice(0, 8)
    .map((snippet) => ({
      content: snippet.content.slice(0, 2_000),
      score: snippet.score,
      fileName: snippet.fileName,
    }));

  return {
    sufficient: selected.length >= minCount,
    snippets: selected,
  };
}
```

### Acknowledge, retrieve, and reply

The two Slack acknowledgements are independent, so the agent sends them together with [`task.gatherSettled`](/sdk/task-object#task-gathersettled-—-concurrent-calls-with-individual-outcomes). The tool calls are written inline in the array, so no unresolved call is held in a variable across a suspend:

```ts theme={null}
async function runBiblos(input: Input, task: Task<Tools>): Promise<Output> {
  const origin = resolveOrigin(input);
  const config = parseRuntimeConfig(task.env);

  if (!config.allowedChannelIds.includes(origin.channelId)) {
    return {
      status: "rejected",
      channelId: origin.channelId,
      threadTs: origin.threadTs,
      error: "channel_not_allowed",
    };
  }

  let draftTs: string | undefined;
  if (input.delivery !== "return") {
    const acknowledgements = await task.gatherSettled([
      task.tools.slack_reactions_add({
        channel: origin.channelId,
        name: "eyes",
        timestamp: origin.messageTs,
      }),
      task.tools.slack_chat_post_message({
        channel: origin.channelId,
        thread_ts: origin.threadTs,
        text: "Writing…",
        mrkdwn: true,
        reply_broadcast: false,
        unfurl_links: false,
        unfurl_media: false,
      }),
    ]);
    draftTs =
      acknowledgements[1].status === "fulfilled"
        ? acknowledgements[1].value.ts
        : undefined;
  }

  const history = await task.tools.slack_conversations_replies({
    channel: origin.channelId,
    ts: Number(origin.threadTs),
    limit: 20,
  });

  const context = await task.tools.pinecone_assistant_context_assistant({
    assistant_name: config.assistantName,
    query: buildRetrieveQuery(
      input.event.text,
      history.messages.map((message) => message.text ?? ""),
    ),
    top_k: 8,
    multimodal: false,
  });

  const evidence = selectEvidence(snippetsFromRetrieve(context));
  const replyText = evidence.sufficient
    ? sanitizeSlackReply(
        (
          await task.llm.generateText({
            system: systemPrompt,
            prompt: buildSynthesisPrompt(input, history.messages, evidence.snippets),
            stream: false,
          })
        ).text,
      )
    : BIBLOS_REFUSAL_TEXT;

  if (input.delivery === "return") {
    return {
      status: "replied",
      channelId: origin.channelId,
      threadTs: origin.threadTs,
      text: replyText,
      usedLibrary: evidence.sufficient,
      snippetCount: evidence.snippets.length,
    };
  }

  const finalText = `${replyText}\n\n[guild-delivery:${input.event_id}]`;
  if (draftTs) {
    const updated = await task.tools.slack_chat_update({
      channel: origin.channelId,
      ts: draftTs,
      text: finalText,
    });
    return {
      status: "replied",
      channelId: origin.channelId,
      threadTs: origin.threadTs,
      replyTs: updated.ts,
      text: replyText,
      usedLibrary: evidence.sufficient,
      snippetCount: evidence.snippets.length,
    };
  }

  const posted = await task.tools.slack_chat_post_message({
    channel: origin.channelId,
    thread_ts: origin.threadTs,
    text: finalText,
    mrkdwn: true,
    reply_broadcast: false,
    unfurl_links: false,
    unfurl_media: false,
  });
  return {
    status: "replied",
    channelId: origin.channelId,
    threadTs: origin.threadTs,
    replyTs: posted.ts,
    text: replyText,
    usedLibrary: evidence.sufficient,
    snippetCount: evidence.snippets.length,
  };
}
```

The published implementation wraps each stage in bounded failure handling, so credential, history, retrieval, synthesis, and posting failures stay distinguishable without exposing raw provider errors.

### Export the agent

```ts theme={null}
export default agent({
  description:
    "Answers one allowlisted Slack mention from a Pinecone Assistant knowledge base.",
  inputSchema,
  outputSchema,
  tools,
  run: runBiblos,
});
```

## Set it up

<Steps>
  <Step title="Add the agent">
    Open [`dkountanis~biblos`](https://app.guild.ai/hub/agents/dkountanis~biblos) on the Agent Hub and add it to a workspace.
  </Step>

  <Step title="Connect Slack">
    Connect a [Slack](/integrations/slack) credential, and use a [credential policy](/platform/credential-policies) to limit it to `conversations.replies`, `reactions.add`, `chat.postMessage`, and `chat.update`.
  </Step>

  <Step title="Build the library">
    Create a Pinecone Assistant and upload your approved playbooks to it.
  </Step>

  <Step title="Connect Pinecone">
    Connect a credential for `dkountanis~pinecone-assistant` with your Pinecone API key.
  </Step>

  <Step title="Set workspace variables">
    Add these [workspace variables](/platform/workspace-variables), using your own values:

    ```text theme={null}
    SLACK_ALLOWED_CHANNEL_IDS=C0123456789
    BIBLOS_ASSISTANT_NAME=biblos-kb
    BIBLOS_MIN_EVIDENCE=1
    BIBLOS_MIN_SCORE=0.15
    ```
  </Step>

  <Step title="Create the trigger">
    Create an [event trigger](/platform/event-triggers) for `app_mention`, limited to the same channels:

    ```bash theme={null}
    guild trigger create \
      --workspace "<owner>~<workspace>" \
      --type webhook \
      --agent dkountanis~biblos \
      --integration slack \
      --event app_mention \
      --service-config '{"channel_ids": ["C0123456789"]}' \
      --input '{}'
    ```

    The empty `--input` [passes the webhook payload unaltered](/platform/event-triggers#pass-the-webhook-payload-unaltered), which is the input Biblos expects.
  </Step>

  <Step title="Ask a question">
    Check that the trigger is active, then mention Biblos in an allowlisted channel:

    > @Biblos How should I run LinkedIn outbound for this ICP?
  </Step>
</Steps>

The source includes `examples/linkedin-outbound-playbook.md`, a safe starter document for your first retrieval check.

## Expected result

A question the library covers returns:

```json theme={null}
{
  "status": "replied",
  "channelId": "C0123456789",
  "threadTs": "1789011000.000100",
  "replyTs": "1789011001.000300",
  "usedLibrary": true,
  "snippetCount": 2
}
```

A question the library doesn't cover also returns `status: "replied"`, with `usedLibrary: false` and the fixed refusal text. A delivery or credential failure returns `failed`, and never claims a successful post.

## Run it locally

From the agent's directory:

```bash theme={null}
pnpm install
pnpm test
pnpm run typecheck
pnpm run build
pnpm run bundle
guild agent test
```

## Relevant code

| File | What it holds |
| - | - |
| `agent.ts` | Guards, tool registrations, retrieval, synthesis, and delivery |
| `slack-event.ts` | Webhook and runtime configuration validation |
| `evidence.ts` | Deterministic evidence selection |
| `system-prompt.md` | The grounded reply policy |
| `examples/linkedin-outbound-playbook.md` | Starter library content |

## Boundaries

Biblos doesn't upload documents, administer Slack, send direct messages, or invent steps the library doesn't contain. Each follow-up question has to mention the app, unless you deliberately configure a separate message trigger.

## Related

<CardGroup cols={2}>
  <Card title="Event triggers" icon="bolt" href="/platform/event-triggers">
    Run an agent when something happens in a connected service.
  </Card>

  <Card title="Slack" icon="slack" href="/integrations/slack">
    Connect Slack and the operations agents can call.
  </Card>

  <Card title="Coded agents" icon="code" href="/guide/coded-agents">
    How TypeScript agents are structured and built.
  </Card>

  <Card title="Credential policies" icon="shield" href="/platform/credential-policies">
    Limit a credential to the operations an agent needs.
  </Card>
</CardGroup>
