Want to use the agent you deployed on Amazon Bedrock AgentCore from Slack? You're not alone — there are already quite a few samples and blog posts that connect AgentCore agents to Slack.
Reading through them, I noticed something: on the Slack side, everyone is solving the same problems. Receiving events, fetching thread history and converting it into the model's input format, rendering replies. The agents themselves all differ, but the Slack work is the same.
What if that common part could be factored out, so it doesn't have to be rebuilt for every agent? That's the thought behind Welt.
What Is Welt
Welt is a general-purpose Slack frontend for agents on AgentCore. The pieces line up like this:
Slack ⇄ Welt ⇄ AgentCore Runtime
└── your agent (exchanging JSON with Welt through an adapter)
The division of labor is clear:
- Welt owns: Slack tokens, event intake, thread-history fetch, streaming rendering of replies, file upload/download, and rendering approval buttons
- You own: the agent itself — model, tools, MCP, memory
Welt and the agent exchange plain JSON, so at the protocol level, nothing ties it to any particular agent framework. The vision is to support a wide range of frameworks.
The first step is welt-io, an adapter that translates that JSON for Strands Agents (Python). Right now, that's the combination Welt connects to.
The Agent-Side Changes Are Small
If you already have a Strands agent, connecting it to Slack takes nothing more than wiring in a few welt-io functions. This is Welt's biggest selling point.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from welt_io import decode_file_blocks, renderable_events
app = BedrockAgentCoreApp()
@app.entrypoint
async def invoke(payload: dict):
messages = payload["messages"]
decode_file_blocks(messages) # files uploaded to Slack -> the format Strands expects
agent = Agent(tools=[...]) # <- your agent, unchanged
async for event in renderable_events(agent.stream_async(messages)):
yield event # events Welt renders into the Slack thread
app.run()
It does exactly two things:
- On the way in,
decode_file_blocks()restores the Converse-shaped messages Welt sends (files from Slack uploads arrive base64-encoded) to the form Strands expects - On the way out,
renderable_events()reduces the rawstream_asyncevents to the JSON-serializable events Welt can render
Your agent's logic — model choice, tools, system prompt — stays untouched. And you never write a single line of Slack API code.
What You Get on the Slack Side
With just the code above, here's what works in the Slack thread:
- Thread history: on every turn, Welt fetches the whole thread history, converts it into Converse-shaped messages, and hands it to your agent. The agent needs no conversation store of its own (if you'd rather manage history on the agent side, there's a mode that sends only the new messages)
- Streaming: the agent's reply streams into the thread in real time, with an indicator while a tool is running
-
Files, both ways: files uploaded to Slack go to the agent, and images or documents the agent (or its tools) generate are uploaded back into the thread. strands-tools'
generate_imageworks with zero extra code
Human-in-the-Loop Becomes Slack Buttons
Welt also supports human-in-the-loop. Among the AgentCore-to-Slack integrations out there, I haven't seen many that go this far.
With Strands interrupts, a tool can pause the run and wait for human approval. Welt renders that pause as buttons and a text field right in the Slack thread.
You describe the buttons and the text field with welt-io's interrupt_reason():
answer = tool_context.interrupt(
"deploy-approval",
reason=interrupt_reason(
"Deploy to prod?",
[
{"value": "y", "label": "Deploy", "style": "primary"},
{"value": "n", "label": "Cancel"},
],
input={"label": "Or tell me what to do instead"},
),
)
Pressing a button (or typing into the field) resumes the run. Strands' stock HumanInTheLoop intervention also works over Welt as-is — the stdio approval prompt simply becomes Slack buttons.
The complete code, including the resume handling, is in welt-io's example agent.
Try It
The Quick Start is four steps:
- Deploy welt-io's example agent with the AgentCore CLI
- Create a Slack app from the
manifest.ymlbundled in the Welt repository - Clone Welt and put the two tokens and the agent runtime ARN in a
.envfile uv run --env-file .env main.py
By default, Welt connects to Slack over Socket Mode. There's no public endpoint to stand up, so the local uv run just works.
When you're ready to run it for real, put the container image (ghcr.io/iwamot/welt) on ECS or similar, and it works over Socket Mode the same way.
If you'd rather not pay for an always-on process, you can also run Welt on AWS Lambda. In that case it receives HTTP at a Function URL instead of using Socket Mode (Welt handles the signature verification), and the idle cost drops to zero.
What's Next
Feature-wise, I feel Welt and welt-io both cover what they need to now. So the next step is growing the adapter lineup, and the current front-runner is Mastra (Strands' TypeScript SDK is also a candidate).
Since Welt and the agent exchange plain JSON, an adapter can be built for any stack. If there's a stack you'd like an adapter for, please let me know in an issue.
Welt and welt-io are both MIT-licensed open source. If you've been wanting to use your AgentCore agent from Slack, give it a try.
- Welt: https://github.com/iwamot/welt
- welt-io: https://github.com/iwamot/welt-io

Top comments (0)