Get to know Cadenya
We’re developers who love to build. We set out to create a yes-code platform that makes building agents feel like the best parts of building software.
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 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
waitUntiloption “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
needsApprovaland returns interruptions. You saveresult.state.toString()and later callRunState.fromString(agent, serializedState). The docs warn thatfromString()“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,” andapproverslimits 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:
- 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.
- 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
unwrapcall. - 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.
- 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 atool_calledwebhook and waits for the reply you set withsetContent. - 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.
- 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.
- 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.
- 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.
- 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.
- 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 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 explains. To create an account, sign up at app.cadenya.com/signup, then copy your API key from the API keys page at app.cadenya.com/account/apiKeys.
Grow wherever AI goes next.
Start shipping agents that are equipped to evolve.