> For the complete documentation index, see [llms.txt](/llms.txt).
> A full single-fetch corpus is available at [llms-full.txt](/llms-full.txt).
---
title: Slack
description: Start AGNT5 workflows automatically from Slack messages, mentions, and reactions using the Slack Events API.
last_verified: 2026-09-29
---



Connect Slack to AGNT5 and your workflows will start automatically whenever a Slack event fires. Every delivery is signature-verified before any workflow runs.

---

## Setup


> Studio offers Slack in two places. **Integrations → Webhooks → Slack Events** creates a webhook source whose deliveries are dispatched by trigger; the workflow examples on this page apply to it. **Integrations → Slack App** creates a chat-provider integration bound to one agent or workflow, which receives a different input; see [Chat dispatch](#chat-dispatch).


### 1. Create the integration in Studio

1. Go to **Settings → Integrations** and click **Add Integration**.
2. Pick **Slack**.
3. Studio walks you through creating the Slack app and choosing the target environment.



![Studio Add Slack App panel showing app name, OAuth scopes, and bot events configuration.](/docs/integrations/slack-integration-light.png)

_Enter an app name, select the OAuth scopes and bot events you need, then click Create Slack App._



### 2. Copy the app credentials from Slack

Go to **Basic Information → App Credentials** and copy the following values


> | Field | Used for |
> |---|---|
> | **App ID** | Identifying your Slack app |
> | **Client ID** | Identifies your app during OAuth authorization; included in the redirect URL when requesting user permissions |
> | **Client Secret** | Paired with Client ID to exchange an authorization code for an access token, keep this server-side and never expose it publicly |
> | **Signing Secret** | AGNT5 webhook verification (required) |


Paste each value into the required fields in Studio when prompted.


### 3. Complete the setup in Studio

Paste the credentials into the required fields and save. Studio registers the webhook URL automatically.

---

## Events

The event name your trigger matches is `slack.<event_type>`, where `event_type` is the value of the Slack `event.type` field inside the request body.

| Event | Full event name | Required scope |
|---|---|---|
| `app_mention` | `slack.app_mention` | `app_mentions:read` |
| `message` (channels) | `slack.message` | `channels:history` |
| `message` (groups) | `slack.message` | `groups:history` |
| `message` (DMs) | `slack.message` | `im:history` |
| `reaction_added` | `slack.reaction_added` | `reactions:read` |
| `reaction_removed` | `slack.reaction_removed` | `reactions:read` |
| `member_joined_channel` | `slack.member_joined_channel` | `channels:read` |

A single workflow can subscribe to multiple event types at once by passing multiple `event()` calls in `triggers`.

---

## Workflow examples

### Logging messages and mentions



**Python:**

1. Workflows receive the trigger envelope as keyword arguments: `event`, plus `deployment_id`, `target_kind`, and `target_ref`. Declare `event` and accept the rest with `**_`.
2. Subscribe to both `slack.message` and `slack.app_mention` to cover channels and mentions in one workflow.
3. The webhook delivery sits at `event["data"]`; its `body` is the raw verified request body, always a string, so parse it defensively.
4. Filter out bot messages (`bot_id`, `subtype`) to avoid reply loops.

```python
import json
from agnt5 import WorkflowContext, workflow
from agnt5.types import event


def _parse_body(event: dict) -> dict:
    try:
        body = json.loads(event["data"]["body"])
    except (KeyError, TypeError, json.JSONDecodeError):
        return {}
    return body if isinstance(body, dict) else {}


@workflow(
    name="slack_message_logger",
    triggers=[event("slack.message"), event("slack.app_mention")],
)
async def slack_message_logger(ctx: WorkflowContext, event: dict, **_) -> dict:
    body = _parse_body(event)
    slack_event = body.get("event", {})

    # skip bot messages to avoid reply loops
    if slack_event.get("bot_id") or slack_event.get("subtype"):
        ctx.logger.info("Slack message skipped: bot or unsupported subtype")
        return {"status": "skipped"}

    user = slack_event.get("user", "unknown")
    text = slack_event.get("text", "")
    channel = slack_event.get("channel", "")

    ctx.logger.info("Slack message from %s in %s: %s" % (user, channel, text[:200]))

    # add your processing logic here, e.g. call an agent, look up data
    return {"status": "logged", "user": user, "channel": channel, "text": text}
```





**TypeScript:**

1. Workflows receive the trigger envelope as the second handler argument; the webhook delivery sits at `input.event.data`.
2. Subscribe to both `slack.message` and `slack.app_mention` to cover channels and mentions in one workflow.
3. `body` is always a string, so parse it defensively.
4. Filter out bot messages (`bot_id`, `subtype`) to avoid reply loops.

```typescript
import { workflow, event } from '@agnt5/sdk';
import type { Context } from '@agnt5/sdk';

interface WebhookEnvelope {
  event_type: string;
  body: string;
}

interface TriggerInput {
  event: { name: string; data: WebhookEnvelope };
}

function parseBody(envelope: WebhookEnvelope): Record<string, any> {
  try {
    return JSON.parse(envelope.body ?? '{}');
  } catch {
    return {};
  }
}

export const slackMessageLogger = workflow(
  'slack_message_logger',
  async (ctx: Context, input: TriggerInput) => {
    const body = parseBody(input.event.data);
    const slackEvent = body.event ?? {};

    // skip bot messages to avoid reply loops
    if (slackEvent.bot_id || slackEvent.subtype) {
      ctx.logger.info('Slack message skipped: bot or unsupported subtype');
      return { status: 'skipped' };
    }

    const user = slackEvent.user ?? 'unknown';
    const text = slackEvent.text ?? '';
    const channel = slackEvent.channel ?? '';

    ctx.logger.info(`Slack message from ${user} in ${channel}: ${text.slice(0, 200)}`);

    // add your processing logic here, e.g. call an agent, look up data
    return { status: 'logged', user, channel, text };
  },
  { triggers: [event('slack.message'), event('slack.app_mention')] },
);
```





**Go:**

1. Workflows receive the trigger envelope. The Go SDK has no built-in type for it, so declare a struct that mirrors the parts you need.
2. Pass multiple `agnt5.EventTrigger` calls to `WithTriggers` to cover channels and mentions in one workflow.
3. `Event.Data.Body` is always a string, so parse it defensively.
4. Filter out bot messages (`bot_id`, `subtype`) to avoid reply loops.

```go
import (
    "encoding/json"

    "github.com/agnt5dev/sdk-go/agnt5"
)

type TriggerInput struct {
    Event struct {
        Name string `json:"name"`
        Data struct {
            EventType string `json:"event_type"`
            Body      string `json:"body"`
        } `json:"data"`
    } `json:"event"`
}

func parseBody(body string) map[string]any {
    var parsed map[string]any
    if err := json.Unmarshal([]byte(body), &parsed); err != nil {
        return map[string]any{}
    }
    return parsed
}

err := agnt5.RegisterWorkflow(worker, "slack_message_logger",
    func(ctx *agnt5.Context, in TriggerInput) (SlackLog, error) {
        body := parseBody(in.Event.Data.Body)
        slackEvent, _ := body["event"].(map[string]any)

        // skip bot messages to avoid reply loops
        if slackEvent["bot_id"] != nil || slackEvent["subtype"] != nil {
            ctx.Logger().Info("Slack message skipped: bot or unsupported subtype")
            return SlackLog{Status: "skipped"}, nil
        }

        user, _ := slackEvent["user"].(string)
        text, _ := slackEvent["text"].(string)
        channel, _ := slackEvent["channel"].(string)

        ctx.Logger().Info("Slack message", "user", user, "channel", channel, "text", truncate(text, 200))

        // add your processing logic here, e.g. call an agent, look up data
        return SlackLog{Status: "logged", User: user, Channel: channel, Text: text}, nil
    },
    agnt5.WithTriggers(agnt5.EventTrigger("slack.message"), agnt5.EventTrigger("slack.app_mention")),
)
```



### Viewing triggered runs in Studio

After a Slack event fires, the run appears in **Studio → Runs** within seconds. Open it to inspect the envelope, extracted fields, and any reply output.

_[screenshot: Studio Runs list showing a completed slack_message_logger run triggered by a slack.app_mention event, with the run detail open showing the event body and workflow output]_

---

## Envelope structure

Every run started by a **Slack Events** webhook source receives the gateway's trigger envelope as its input (in Python, as keyword arguments). The webhook delivery sits at `event.data`:

```json
{
  "event": {
    "id": "0192b5d4-…",
    "name": "slack.app_mention",
    "data": {
      "_webhook": true,
      "source": "slack",
      "integration_id": "int_abc123",
      "event_type": "slack.app_mention",
      "idempotency_key": null,
      "timestamp": 1733337600,
      "headers": { "x-slack-signature": "v0=…", "x-slack-request-timestamp": "…" },
      "body": "{\"type\":\"event_callback\",\"event\":{\"type\":\"app_mention\",\"user\":\"U123\",\"text\":\"<@U456> hello\",\"channel\":\"C789\",\"ts\":\"…\"}}"
    },
    "source": "slack",
    "timestamp_ns": 1733337600000000000
  },
  "deployment_id": "dep_abc123",
  "target_kind": "deployment_id",
  "target_ref": "dep_abc123"
}
```

`event.data.body` is the raw verified request body. Parse it with `json.loads` (or `JSON.parse` / `json.Unmarshal`). AGNT5 doesn't read Slack's event id for deduplication, so `event.data.idempotency_key` is `null` and `event.id` is a generated UUID (see [Delivery semantics](#delivery-semantics)). See [Webhooks → What your workflow receives](/docs/build/webhooks.md#what-your-workflow-receives) for every field.

---

## Chat dispatch

A **Slack App** integration is bound to one agent or workflow rather than matched against triggers, so `event()` triggers do not fire for its deliveries. The bound component receives this input directly, with no `event` wrapper:

```json
{
  "source": "slack",
  "event_type": "slack.app_mention",
  "headers": { "x-slack-signature": "v0=…", "x-slack-request-timestamp": "…" },
  "body": "{\"type\":\"event_callback\",\"event\":{\"type\":\"app_mention\",\"user\":\"U123\",\"text\":\"<@U456> hello\",\"channel\":\"C789\",\"ts\":\"…\"}}",
  "bot_token": "xoxb-…"
}
```

There is no `_webhook`, `integration_id`, `idempotency_key`, or `timestamp` field. `body` is still the raw verified request body. `bot_token` is the app's bot token, included so the component can reply through the Slack Web API; treat it as a secret and keep it out of logs and run outputs.



**Python:**

```python
@workflow(name="slack_reply")
async def slack_reply(ctx: WorkflowContext, body: str, bot_token: str, **_) -> dict:
    slack_event = json.loads(body).get("event", {})
    if slack_event.get("bot_id") or slack_event.get("subtype"):
        return {"status": "skipped"}

    # reply with bot_token via chat.postMessage, e.g. inside a ctx.step(...)
    return {"status": "replied", "channel": slack_event.get("channel")}
```





**TypeScript:**

```typescript
interface ChatInput {
  event_type: string;
  body: string;
  bot_token: string;
}

export const slackReply = workflow('slack_reply', async (ctx: Context, input: ChatInput) => {
  const slackEvent = JSON.parse(input.body).event ?? {};
  if (slackEvent.bot_id || slackEvent.subtype) {
    return { status: 'skipped' };
  }

  // reply with input.bot_token via chat.postMessage, e.g. inside a step
  return { status: 'replied', channel: slackEvent.channel };
});
```





**Go:**

```go
type ChatInput struct {
    EventType string `json:"event_type"`
    Body      string `json:"body"`
    BotToken  string `json:"bot_token"`
}

err := agnt5.RegisterWorkflow(worker, "slack_reply",
    func(ctx *agnt5.Context, in ChatInput) (SlackLog, error) {
        slackEvent, _ := parseBody(in.Body)["event"].(map[string]any)
        if slackEvent["bot_id"] != nil || slackEvent["subtype"] != nil {
            return SlackLog{Status: "skipped"}, nil
        }

        // reply with in.BotToken via chat.postMessage, e.g. inside a step
        channel, _ := slackEvent["channel"].(string)
        return SlackLog{Status: "replied", Channel: channel}, nil
    },
)
```



---

## Signature verification

- Uses `X-Slack-Signature` (`v0=<hex>`) with `X-Slack-Request-Timestamp`.
- 5-minute replay window enforced.
- Requests with an invalid or expired signature are rejected with `401`.

---

## Delivery semantics

- Slack retries up to 3 times on non-2xx responses.
- **Retries are not deduplicated.** Slack sends the same top-level `event_id` on every delivery of an event, but AGNT5 doesn't use it to collapse retries.
- Make your workflow idempotent using the top-level `event_id` from the parsed request body. It's present on every Events API callback, unlike `event.client_msg_id`, which only user-typed messages carry.

---

## Related

- [Event sources overview](/docs/integrations/event-sources/overview.md)
- [Webhooks](/docs/build/webhooks.md): trigger mechanism, signature verification, and delivery semantics
- [Workflows](/docs/build/workflows.md): how to write and run workflows
