---
title: "Slack AI agents in 2026: Slack Bolt, assistant threads, human in the loop approvals, socket mode vs Events API — Cadenya"
url: "https://cadenya.com/other-content/slack-ai-agents"
description: "Build a Slack AI agent with Slack Bolt and assistant threads: acknowledge events within 3 seconds, run long tasks after the ack, human in the loop approval buttons, thread persistence and the conversations.history rate limit, user token vs bot token, compared across LangGraph, OpenAI Agents SDK, Chat SDK, Temporal, Inngest, Cursor SDK and a hosted agent loop."
---

A Slack AI agent has two parts. A Slack Bolt app receives events, acknowledges them within 3 seconds, and posts to assistant threads. Behind it, an agent loop does the slow work after the ack: it calls the model, runs tools, waits for human in the loop approval buttons, and keeps each thread’s conversation state.

Bolt is the first part in almost every build. So the real choice is the second part. Pick Vercel’s Chat SDK when you write TypeScript on Vercel and want approval cards built in. Pick LangGraph or the OpenAI Agents SDK when you want the loop in your own code and you’re fine running the database that holds paused runs. Pick Temporal or Inngest when you already run a durable engine. The Cursor SDK fits a Slack bot that writes code in a repository. And if you’d rather host none of it, Cadenya is a hosted agent loop: your Bolt app acks the event and makes one API call. A coding agent can set it up with no account, in a temporary workspace you claim later. [What it adds](#what-does-cadenya-add-to-a-slack-bolt-app) is at the end.

Every price and limit below comes from each vendor’s own pages, checked on October 8, 2026.

## Slack agents and AI apps: the options side by side

|  | What it gives you | Human in the loop approval | Where thread state lives | What you run |
| --- | --- | --- | --- | --- |
| Slack Bolt alone | Events, button actions, ack(), agent sessions, and streaming. Bolt ships for JavaScript, Python, and Java | You build it: Block Kit buttons plus your own wait | Your database, or a re-read with conversations.replies | The Bolt app, plus the queue Slack tells you to add |
| Chat SDK with Workflow SDK | One TypeScript bot for Slack, Teams, Google Chat, Discord and more | requestApproval posts Approve and Deny buttons and suspends a workflow | A state adapter: Redis or PostgreSQL in production | Your app, Redis or Postgres, and Workflow SDK |
| LangGraph | An agent graph in Python or JavaScript | interrupt() saves state. You post the buttons and resume | A checkpointer such as Postgres or MongoDB, keyed by thread_id | Your app and the checkpointer database |
| OpenAI Agents SDK | An agent loop in Python or TypeScript | needsApproval returns interruptions. You store the RunState and resume | Sessions, or a serialized RunState you store | Your app and storage for run state |
| Temporal or Inngest | Durable execution for code you write | A Signal (Temporal) or step.waitForEvent() (Inngest) after your button handler fires | Workflow history | Your workers, plus the Temporal or Inngest service |
| Cursor SDK | Cursor’s coding agent, callable from TypeScript | The headless local agent runs tools “without asking for approval”. You add hooks | An agent ID you pass to Agent.resume() | Your app. Cloud agents run on Cursor’s VMs with your repo cloned |
| Cadenya | A hosted agent loop behind your Bolt app, with SDKs for TypeScript, Python, Ruby, and Go | The objective parks on toolApprovalRequested. Your buttons call approve or deny | On Cadenya: a durable event log per objective | Your Bolt app, which acks and makes one API call |

Two more facts shape the choice. Slack says “Developing and using some AI features require a paid plan.” And on the Cursor SDK, “All inference goes through Cursor’s hosted models,” so you can’t bring another provider.

## Socket mode vs Events API: acknowledge events within 3 seconds

Both transports give you the same three seconds. The Events API says: “Your app should respond to the event request with an HTTP 2xx within three seconds.” If it doesn’t, Slack retries three times: nearly at once, after 1 minute, and after 5 minutes. Each retry carries an `x-slack-retry-num` header, so a slow handler can see the same `app_mention` up to four times.

Slack’s own advice is blunt: “Avoid actually processing and reacting to events within the same process” and “Implement a queue to handle inbound events after they are received.” Button clicks work the same way. An interaction payload must be acknowledged “within 3 seconds,” and Bolt makes `ack()` required in every action listener.

So where does the long task run after the ack? It depends on where Bolt runs:

-   **A long-lived server.** Ack, then hand the work to a queue and a worker you run.
-   **Serverless with Bolt for Python.** Lazy listeners let a function ack first and keep working. Slack says they’re “only available in Bolt for Python,” with no plans for other Bolt frameworks.
-   **Chat SDK on Vercel.** A `waitUntil` option “keeps the function alive until they finish, so a serverless platform doesn’t stop them early.”

Socket mode changes the transport, not the clock. It lets your app use the Events API and interactive features with no public Request URL, over a WebSocket URL that “refreshes regularly.” The catch for a product you sell: “Apps using Socket Mode are not currently allowed in the public Slack Marketplace.”

**Whatever runs the agent, the Slack handler’s job is to ack fast and hand off.** An agent turn that calls the model three times won’t fit in three seconds.

## Slack bolt agent with human in the loop: approval buttons

In Slack, an approval is a Block Kit message with two buttons. The agent pauses, your app posts the buttons in the thread, a person clicks, and Bolt’s action listener receives the click. Slack’s agent sessions even have a status for it: `suspended` means “the agent cannot make progress until the user intervenes, for example when the agent needs clarification, a decision, or a tool approval.”

The hard part is the pause. Here’s how each option holds it:

-   **LangGraph** saves state at `interrupt()` and pauses until you resume execution. On resume, it restarts the whole node from the beginning, so code before the interrupt runs again. It needs a persistent checkpointer: “In production, use a persistent checkpointer like AsyncPostgresSaver or MongoDBSaver.”
-   **The OpenAI Agents SDK** pauses at a tool with `needsApproval` and returns interruptions. You save `result.state.toString()` and later call `RunState.fromString(agent, serializedState)`. The docs warn that `fromString()` “does not authenticate the snapshot or the person submitting it,” so the snapshot belongs in your own server storage.
-   **Chat SDK** wraps all of it in `requestApproval`. The wait “survives deploys and restarts,” and `approvers` limits who can click.
-   **Temporal** waits for a Signal that your button handler sends. **Inngest** waits on `step.waitForEvent()` for the event that your button handler sends.

One Slack limit matters for a long wait. A button’s `response_url` accepts replies “up to 5 times within 30 minutes,” so a decision that lands later should post with `chat.postMessage` instead.

With a hosted loop, the click handler is the whole approval. Cadenya pauses the objective on `toolApprovalRequested` and sends your webhook. Your handler posts the buttons, and the Bolt listeners pass the decision back:

```
import { App } from '@slack/bolt';
import Cadenya from '@cadenya/cadenya';

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const cadenya = new Cadenya(); // reads CADENYA_API_KEY

// Your Cadenya webhook handler posted these buttons on toolApprovalRequested,
// with "objectiveId:toolCallId" as each button's value.
app.action('approve_tool', async ({ ack, action }) => {
  await ack();
  const [objectiveId, toolCallId] = action.value.split(':');
  await cadenya.objectives.approveToolCall(objectiveId, toolCallId);
});

app.action('deny_tool', async ({ ack, action }) => {
  await ack();
  const [objectiveId, toolCallId] = action.value.split(':');
  await cadenya.objectives.denyToolCall(objectiveId, toolCallId);
});
```

## Assistant threads and thread persistence: the conversations.history rate limit

A Slack thread is the natural unit of conversation for an agent. Slack’s agent guide says: “When your app receives the `thread_ts` parameter, you can retrieve the conversation by using `thread_ts` as the unique identifier.” So the thread key maps to whatever holds your agent’s state: a LangGraph `thread_id`, an OpenAI Agents SDK session, a Chat SDK thread, or a workflow ID.

Why not just re-read the thread from Slack on every message? Because of the rate limit for non-Marketplace apps. Since May 29, 2025, for new apps “commercially distributed outside of the Marketplace,” both `conversations.history` and `conversations.replies` are “rate limited to 1 request per minute,” and the `limit` parameter tops out at 15 messages. Marketplace and internal apps keep Tier 3 limits (50 or more a minute), and existing installs of older apps keep the old limits.

**For a commercial Slack agent outside the Marketplace, the thread’s state has to live on your side.** One read a minute can’t keep up with a conversation.

Two newer details matter if you’re building on assistant threads today. Slack is moving apps from `assistant_view` to `agent_view`, and its migration guide says existing apps can keep `assistant_view` only “for now.” And the `agents.sessions.setStatus` method needs an explicit `active` when your agent finishes, “or the session stays in `processing` until it times out after one hour.”

## Act on behalf of user: user token vs bot token, and the Slack MCP server

Slack apps get two kinds of identity. “Bot tokens represent a bot associated with an app installed in a workspace,” and they’re “not tied to a user’s identity.” “User tokens allow you to work directly on behalf of users when necessary.” An agent that searches what one person can see, or posts as that person, needs that person’s user token from OAuth.

The same split shows up in Slack’s MCP server at `https://mcp.slack.com/mcp`. It lets an agent “search channels, send messages, and perform other Slack actions,” authorized with Slack user tokens. It has a distribution rule: “Only apps published in the Slack Marketplace and internal apps can use MCP at this time; unlisted apps are prohibited from using MCP.” And “Like all Slack Web API methods, the Slack MCP server is subject to rate limits.”

Identity also applies to your own tools. When the agent calls your API for a Slack user, map the Slack user ID to your account and send that user’s token on the tool call, not one shared service key.

## What does Cadenya add to a Slack Bolt app?

Bolt receives the event and the click, and every other option leaves the agent loop, the paused runs, and the thread state for you to host. **Cadenya runs the agent loop, the approval waits, and the conversation state, so your Bolt app only acks and calls an API.** Here’s what that covers:

1.  **Nothing else to run.** No queue, no worker, no checkpointer database, no workflow cluster, and no server license to check. Your Bolt handler acks, then creates an objective or continues one, and Cadenya runs the loop after the ack.
2.  **SDKs for the language your Slack app is in.** TypeScript, Python, Ruby, and Go, all at 1.9.0 and Apache-2.0, plus a CLI and a plain HTTP API for Java. Webhooks are signed, and each SDK verifies them in one `unwrap` call.
3.  **Any model, on your own keys.** The model is a setting on a variation. Use any model on OpenRouter or your own endpoint that speaks the OpenAI chat format. The Cursor SDK sends all inference through Cursor’s hosted models.
4.  **Approvals answered from Slack.** Mark a tool as needing approval and the objective parks on `toolApprovalRequested`. Your webhook handler posts Block Kit buttons, and the click calls approve or deny. A denial carries a memo that steers the agent (“Ask the requester which account to use”). For a free-text answer, a Bare tool set sends a `tool_called` webhook and waits for the reply you set with `setContent`.
5.  **Workspaces and people kept apart.** Tag objectives with the workspace as a tenant and the person as a subject, so you can filter them, and purge a workspace’s data when it uninstalls.
6.  **Your Slack app stays yours.** It’s still a plain Bolt app, and Cadenya sits behind it as an HTTP API. Tools come from your own OpenAPI spec, MCP server, or HTTP endpoints, so the integrations you build work outside Cadenya too.
7.  **A price in one line.** Free for 1,000 loops a month, $49 a month for 50,000, then $0.0015 a loop. A loop is one LLM request, so a Slack reply that calls the model 4 times is 4 loops.
8.  **The wait isn’t in your code.** No process stays open while a manager decides, and no timer of yours races the click. Your action listener acks in milliseconds and makes one API call whenever the click arrives.
9.  **Edit the agent while approvals wait.** Each objective keeps its variation’s configuration snapshot, so a new prompt or model applies to new threads, and waiting ones finish on the setup they started with. The OpenAI Agents SDK docs suggest installing two SDK versions side by side with package aliases to get the same result.
10.  **A conversation per Slack thread.** Start an objective for a thread, and when it’s waiting, send the next reply in that thread as a Continue message. The agent keeps the thread’s history, and every objective keeps a durable event log of its loop.

[How objectives work](/docs/guides/the-basics/objectives) covers Continue and approvals, and the free plan needs no card.

No account yet? A coding agent can create a temporary workspace with `cadenya workspaces create --temporary`, run its first objectives there, and hand you a link to claim it, as [auth.md](https://cadenya.com/auth.md) explains. To create an account, sign up at [app.cadenya.com/signup](https://app.cadenya.com/signup?utm_source=cadenya.com&utm_medium=article&utm_campaign=slack-ai-agents), then copy your API key from the API keys page at `app.cadenya.com/account/apiKeys`.