Skip to content
NewA data check for swims the watch got wrong
Nov 4, 2026·5 MIN READ
One agent, three clients: moving our Slack bot's brain into the backend

One agent, three clients: moving our Slack bot's brain into the backend

Vimek, our team assistant, lived inside a Slack bot. When I wanted it in Gmail too, I moved the agent into the Django backend and turned Slack, Gmail and the terminal into thin clients. The production test found a dead API key that no unit test could.

Vimek is the assistant our Vimo Swim team talks to in Slack. It reads the CRM, prepares GitHub tasks, 4DX entries and Meta campaigns, and every change waits for a person to click Approve. Today I wanted the same Vimek in Gmail, next to the email I have open: who is this lead, what did they order, add a task to call them back.

The first plan was an HTTP endpoint on the Slack bot. I dropped it after one question: where does the agent belong? The bot held the model choice, the Claude to OpenAI fallback, the tools, the proposals and the cost footer. A second client would mean a second copy of all that. So the agent moved into the backend, and every client became a thin one.

The new shape

flowchart LR
    SL["Vimek Slack"] --> BE["vimoswim-backend<br/>agent, tools, proposals, costs"]
    GM["Vimek Gmail"] --> BE
    CL["Vimek CLI"] --> BE
    BE --> LLM["Claude or OpenAI"]
    BE --> DATA["CRM, 4DX, docs, ads<br/>called in-process"]
    BE --> GH["GitHub Projects"]
    BE --> CZ["cezar"]

Each client has its own API key and knows three calls: start a run, read it back, and approve, edit or cancel a proposal. A run is answered in a Celery task. Gmail add-on actions stop after 30 seconds, so the POST can wait up to 25 seconds and the client polls after that. Proposals moved from memory into a table, so a restart no longer drops a pending Approve, and a double click writes once.

The part I liked: the backend calls itself, without the network

Vimek already wrote to the backend through a small /api/v1/vimek/... API: lead changes, 4DX records, documents, ads. Those views carry the real rules: field whitelists, the audit entry on the lead's timeline, error shapes the agent knows how to explain. Rewriting them as ORM calls would have meant two copies of every rule.

Instead the integration clients got an injectable httpx transport. Inside the backend it builds a Django request and hands it straight to the view that would have answered over HTTP. Same serializers, same audit log, no socket, no key.

inprocess.py, the core of it
match = resolve(environ["PATH_INFO"])
response = match.func(WSGIRequest(environ), *match.args, **match.kwargs)

The permission class accepts a flag in request.META in place of a key. WSGI keys from the network always start with HTTP_, so an outside request cannot set it. There is a test that tries.

The read tools went the same way: the MCP server object is called in-process, and its audit log names the caller.

How it went

1. Move the engine
The agent package moved into a backend app as it was. 429 of its tests ran green after renames and a database store for cezar.
2. Thin the clients
Slack kept events, files and Block Kit. Gmail got cards with Approve, Edit and Cancel. No LLM key left in either.
3. Ship and test on production
Backend first, then the Slack deploy, which refuses to start without its API key. Then one real question end to end.

What the tests did not catch

Every suite was green: the backend, the ported engine, the Slack client. The first real request on production came back with Claude API 401: invalid x-api-key. The backend had its own Anthropic key, and it was dead. The old bot had used a different, working one. Nothing in CI could have seen that, because CI never talks to Anthropic with the production secret.

CheckCaught it?
Unit and API testsno
Local end-to-end run with my keyno
One real request on productionyes

One more lesson: a security review flagged that the [cez] shortcut, which starts a coding agent with no Approve, was now open to any key holder. In the bot only allowed Slack users could reach it. It is limited to the Slack key again.

What it took

About a day of focused work, most of it moving tests and deleting code from the bot. What I would do differently: run one real production request before calling anything done, and check every secret the new home inherits, not only the ones I copied over.

Share
Mariusz Smenżyk
Written by

Mariusz Smenżyk

Builder for sport. Make Things Live.

Related notes