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

# Build a Slack assistant

> An agent that reads and posts in Slack through Engini tools - no Slack SDK, no bot tokens in your code.

<Info>**Uses:** Python · TypeScript · CLI</Info>

Slack is usually the first place an agent needs to reach: it answers questions in a channel, posts summaries, or files what it found. With Engini the model calls Slack tools directly and never sees a bot token.

## 1. Connect Slack

Steps 1-2 are one-time setup, shown in the CLI because its guided flow is the fastest path - the connection it creates is immediately usable from both SDKs (and step 3 does the same discovery in SDK code):

```bash theme={null}
engini connect slack
```

That walks you through the credential or OAuth flow, verifies the connection, and leaves you with a connection id. Doing it from your own product instead? See [Connect a customer's app](/examples/oauth-onboarding).

## 2. Find the tools you actually have

Tool slugs vary by connector version, so **discover rather than assume**:

```bash theme={null}
engini tools list --application slack | jq -r '.[] | "\(.tool_slug)\t\(.description)"'
```

Pick the ones your assistant needs - typically a "post message" and a "list/read messages" tool - and note their slugs. Then read one contract in full before you write any code:

```bash theme={null}
engini tools get <the-post-message-slug> | jq '.input_schema'
```

## 3. Scope a toolset

Give the agent only what it needs. An assistant that should never delete anything simply isn't handed a delete tool:

<CodeGroup>
  ```python Python theme={null}
  from engini import Engini
  from engini.providers.anthropic import AnthropicProvider

  client = Engini(provider=AnthropicProvider())     # reads ENGINI_API_KEY
  conn_id = next(c.connection_id for c in client.connections.list(application="slack"))

  # discover slugs instead of hardcoding them
  slack_tools = [t.tool_slug for t in client.tools.get(applications=["slack"])]
  allowed = [s for s in slack_tools if any(k in s for k in ("post", "send", "list", "history"))]

  toolset = client.toolset(connections={"slack": conn_id}, tools=allowed)
  tools = toolset.tools()
  ```

  ```typescript TypeScript theme={null}
  import { Engini, AnthropicProvider } from "@engini/sdk";

  const client = new Engini({ provider: new AnthropicProvider() });   // reads ENGINI_API_KEY
  const [{ connectionId }] = await client.connections.list("slack");

  const slackTools = await client.tools.get({ applications: ["slack"] });
  const allowed = slackTools
    .map((t) => t.toolSlug)
    .filter((s) => ["post", "send", "list", "history"].some((k) => s.includes(k)));

  const toolset = client.toolset({ connections: { slack: connectionId }, tools: allowed });
  const tools = await toolset.tools();
  ```
</CodeGroup>

## 4. The agent loop

Identical to the [monday assistant](/examples/monday-agent) - the loop is app-agnostic:

<CodeGroup>
  ```python Python theme={null}
  import anthropic

  SYSTEM = (
      "You are a helpful assistant with access to the user's Slack workspace. "
      "Summarize clearly and keep posts short. Always confirm the channel before posting."
  )

  llm = anthropic.Anthropic()
  messages = [{"role": "user", "content": "Summarize today's #support channel and post it to #standup"}]

  while True:
      reply = llm.messages.create(
          model="claude-sonnet-5", max_tokens=1024,
          system=SYSTEM, tools=tools, messages=messages,
      )
      if reply.stop_reason != "tool_use":
          break
      messages.append({"role": "assistant", "content": reply.content})
      results = toolset.handle_tool_calls(reply)
      messages.append({"role": "user", "content": [b for m in results for b in m["content"]]})
  ```

  ```typescript TypeScript theme={null}
  import Anthropic from "@anthropic-ai/sdk";

  const SYSTEM =
    "You are a helpful assistant with access to the user's Slack workspace. " +
    "Summarize clearly and keep posts short. Always confirm the channel before posting.";

  const llm = new Anthropic();
  const messages: any[] = [
    { role: "user", content: "Summarize today's #support channel and post it to #standup" },
  ];

  for (;;) {
    const reply = await llm.messages.create({
      model: "claude-sonnet-5", max_tokens: 1024,
      system: SYSTEM, tools: tools as any, messages,
    });
    if (reply.stop_reason !== "tool_use") break;
    messages.push({ role: "assistant", content: reply.content });
    const results = await toolset.handleToolCalls(reply);
    messages.push({ role: "user", content: results.flatMap((m: any) => m.content) });
  }
  ```
</CodeGroup>

<Tip>
  Anthropic needs every tool result for one assistant turn in a **single** user message - that's the flattening in the last line. See the [monday recipe](/examples/monday-agent#2-one-helper-anthropic-needs) for why.
</Tip>

## Posting without an LLM

Plenty of "Slack agents" don't need a model at all - a scheduled job that posts a digest is just a tool call:

```bash theme={null}
engini tools call <post-message-slug> \
  --args '{"channel":"#standup","text":"Nightly sync complete."}'
```

Wire that into [CI or cron](/examples/ci-automation) and you have notifications without writing a Slack integration.

## Where to take it next

* **Scope it harder** - [toolsets](/examples/scoped-agent-toolset) make "read-only in #support" an API-enforced guarantee rather than a prompt instruction
* **Add a second app** - bind two connections in one toolset and the agent can read Slack and file a ticket in the same turn
* **Handle failures** - a Slack rate limit surfaces as `isSuccess: false` with the downstream status in `executionInfo`; see [debugging](/examples/debug-failed-execution)
