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

# Slack bot

> Relay integration with namespaced composite subjects, so two Slack workspaces never share memory.

Relay suits Slack: your app already receives the event, already knows who sent it, and Patchwork never talks to Slack directly.

Needs an API key and a published agent. No connection required unless your agent calls your own endpoints.

## Subjects are the whole design decision

A Slack user id is unique within a team, not globally. And the same human in two Slack workspaces should not share one memory partition. So compose both:

```
slack:T04AB:U0APP
      ^team ^user
```

Prefixing by platform leaves room for `wa:` or `email:` later without collisions. See [Subjects](/guides/subjects).

## The handler

```js theme={null}
import { App } from "@slack/bolt";

const app = new App({ token: process.env.SLACK_BOT_TOKEN,
                      signingSecret: process.env.SLACK_SIGNING_SECRET });

const BASE = "https://api.usepatchwork.co/v1/loom";
const threads = new Map(); // slack thread_ts → patchwork thread id

function pw(subject) {
  return {
    "Authorization": `Bearer ${process.env.PATCHWORK_API_KEY}`,
    "Patchwork-Subject": subject,
    "Content-Type": "application/json",
  };
}

app.event("app_mention", async ({ event, say }) => {
  const subject = `slack:${event.team}:${event.user}`;
  const key = event.thread_ts ?? event.ts;
  const headers = pw(subject);

  // One Patchwork thread per Slack thread.
  let threadId = threads.get(key);
  if (!threadId) {
    const res = await fetch(`${BASE}/threads`, {
      method: "POST", headers,
      body: JSON.stringify({ agent: process.env.PATCHWORK_AGENT }),
    }).then((r) => r.json());
    threadId = res.data.id;
    threads.set(key, threadId);
  }

  const started = await fetch(`${BASE}/threads/${threadId}/messages`, {
    method: "POST", headers,
    body: JSON.stringify({ content: event.text }),
  }).then((r) => r.json());

  const reply = await settle(started.data.run_id, headers);
  await say({ text: reply, thread_ts: key });
});

async function settle(runId, headers, tries = 60) {
  for (let i = 0; i < tries; i++) {
    const { data } = await fetch(`${BASE}/runs/${runId}`, { headers })
      .then((r) => r.json());
    if (data.terminal) {
      return data.state === "failed"
        ? "Something went wrong on my side."
        : data.message?.content ?? "(no reply)";
    }
    await new Promise((r) => setTimeout(r, 1000));
  }
  return "Still working on that one.";
}
```

## Persist the thread map

The `Map` above is illustrative. In production put it in your database, keyed by Slack team and thread — an in-memory map loses every conversation on deploy.

## Questions and gates

If your agent can ask questions, `settle` will time out rather than return, because `awaiting_input` is not terminal. Handle it by polling `GET /v1/loom/threads` and reading `pending_elicitation`, then posting the user's next Slack message as the answer.

If any wired patch is **Always ask**, you also need an approval path — Slack Block Kit buttons that call [the confirm endpoint](/examples/approving-writes) are a natural fit, and `pending_confirmation.summary` is built to render as the prompt.

## Do not leak the key

`sk_…` lives in your bot's environment and never anywhere near a client. It can assert **any** subject in your workspace, which is exactly why the subject string above is computed from the verified Slack event and never from user input.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.