Migrate a bespoke Slack agent
Move a Slack assistant you built yourself onto Keiki — one agent per customer organization, your tools and rules, Keiki's runtime.
Many teams have built a Slack assistant for their own product: a bot that answers questions about a customer's account and, with a human's say-so, changes things in it. The bot works, and it has also grown a runtime — permission checks, approval cards, a preview ledger, a queue for messages that arrive mid-turn, a stop command, a dashboard of pending approvals, skills duplicated between Slack and an MCP server.
This guide is for taking that runtime out. At the end you run one Keiki agent per customer organization, provisioned from your backend, that calls your API through your MCP server, follows that organization's rules, asks the right people before it writes, and behaves the same in Slack, in the dashboard, and from a developer's IDE. What stays yours is what only you know: the tools, the identities, the rules, and the skills.
For the whole thing as one deployment request — tools, policy, preview, identity, dashboard — see the worked billing example.
Some of what this guide relies on — identity, governed actions, context providers — is rolling out in phases. Build against the contracts on those pages.
Start from the Leaf template
If your Slack agent is a billing operations teammate, start with the complete leaf template
and supply the organization-specific wiring in one
partner deployment (POST /api/v1/developer/deployments,
create-or-update by external_ref):
{
"external_ref": "org_acme",
"agent": { "template": "leaf", "name": "Acme billing" },
"channels": [
{ "type": "slack", "bot_token": "xoxb-…", "signing_secret": "…" }
],
"mcp_connections": [
{
"name": "Acme billing",
"endpoint": "https://billing.acme.com/mcp",
"auth_mode": "public",
"default_permission": "allow"
}
],
"identity": {
"webhook_url": "https://billing.acme.com/keiki/identity",
"secret": "identity-secret-16-chars"
},
"actions": {
"view_url": "https://billing.acme.com/keiki/actions/{actionId}",
"webhook_url": "https://billing.acme.com/keiki/action-events",
"secret": "actions-secret-16-chars"
}
}Expose only read and preview tools over that MCP connection, so allow is safe; if the
server also serves writes, set each of them to deny in the connection's tool
permissions, or the model can run them directly. The approval cards,
preview freshness, grouping and # Actions history described on
governed actions apply to tools the platform dispatches itself: register
each billing write as a developer tool (or webhook tool) with
policy: { approval: "ask", requiredScopes, previewTool, freshnessFields } rather than
exposing it over MCP, whose ask permission is a plain approve/deny pause without a
persisted action. The identity webhook supplies the speaker and scopes used for approval
decisions; the action settings provide the dashboard link and lifecycle webhook. See
identity and governed actions.
What moves where
| You built | On Keiki | Page |
|---|---|---|
| Slack user → your user → role and scopes | Identity webhook; scopes enforced on every tool call and approval | Identity |
| Per-organization instructions and account context in the prompt | Context providers on the agent | below |
| Approval cards, approve/decline handlers | Governed actions: Slack card, conversational action_respond, and webapp routes | Actions |
| View in dashboard links | Live | Actions |
| Loading pending approvals into the next prompt | The platform-written Action state section | Actions |
| Preview ledger and "did the facts change?" checks | Live | Actions |
| Grouped or sequential writes and their UI | Live batch approvals | Actions |
| Speaker name and mention state in the prompt | Structured speaker on every message | Slack agents |
| Current time in the prompt | Injected every turn | Slack agents |
| Per-thread run queue, follow-up injection | Steering: debounce, queue / interrupt | Slack agents |
| "Am I being addressed?" heuristics | Ingress classification plus a stay-quiet contract | Slack agents |
| Stop keywords, a Stop button | Built in | Slack agents |
| Skills copied into the Slack prompt and the MCP server | One skill library, served to Slack, the API, and any MCP client | below |
Three things do not move, because they are your product: the MCP server with your tools,
the identity endpoint that says who a Slack user is in your system, and the rules and
skills you write for the agent. MCP ask tools become persisted governed Actions;
URL and structured-form elicitations remain daemon-owned and separate from persisted
agent actions.
1. Annotate your tools
For dispatcher-owned developer tools, include them in the deployment request with a policy:
{
"external_ref": "acme",
"agent": { "template": "leaf" },
"channels": [{ "type": "slack", "bot_token": "…", "signing_secret": "…" }],
"tools": [{
"name": "update_customer_plan",
"description": "Change a customer's plan.",
"executionType": "webhook",
"webhookUrl": "https://billing.example/tools/update_customer_plan",
"policy": { "approval": "ask", "requiredScopes": ["billing:write"] }
}]
}Read-only tools need nothing. SDK defineTool and POST /developer/tools are the
single-agent alternatives. Preview pairing and freshness checks for persisted actions are
live. MCP ask tools go through governed Actions too, but they cannot carry
requiredScopes or preview policy; URL and structured-form elicitations keep their
daemon-owned mcp_resume flow.
2. Stand up the identity webhook
One endpoint: given a Slack team and user (and email when the app has it), return your user
id, a role label, and the scopes that user holds in that organization — see the
contract. If your MCP server authenticates each user
separately, return mcpHeaders in the same response and skip the separate per-sender
credential calls.
Return { "userId": null } for people you do not know. They can still read, and nothing
gated will run for them.
3. Deploy one agent per organization
From your backend, when an organization installs your Slack app, create its agent with a
partner deployment. The external_ref is your organization id,
so re-running the request updates rather than duplicates:
POST /api/v1/developer/deployments
{
"external_ref": "org_acme",
"agent": {
"name": "Acme billing assistant",
"model": "<model-id>",
"system_prompt": "You are Acme's billing assistant. …",
"features": { "mcp": true, "sandbox": true, "steering": true },
"harness": "flue"
},
"channels": [
{ "type": "slack", "bot_token": "xoxb-…", "signing_secret": "…" }
],
"mcp_connections": [
{
"name": "Acme API",
"endpoint": "https://mcp.acme.com/mcp",
"auth_mode": "public",
"default_permission": "allow"
}
],
"identity": {
"webhook_url": "https://api.acme.com/keiki/identity",
"secret": "whsec_…"
},
"context_providers": [
{ "name": "org_rules", "title": "Organization rules", "url": "https://api.acme.com/keiki/context/org_acme/rules", "ttl_seconds": 300 },
{ "name": "account", "title": "Account summary", "url": "https://api.acme.com/keiki/context/org_acme/account", "ttl_seconds": 900 }
],
"skills": ["acme-billing", "acme-support-tone"]
}auth_mode: "public" on the connection means every call carries a sender's credential —
the mcpHeaders your identity webhook returned — and no agent-wide one. The agent needs
mcp and sandbox on; steering gathers bursts and queues mid-turn messages with the
platform defaults, which you can tune on the agent's settings page or with
steering_config on PATCH /developer/config.
harness: "flue" is the Keiki-hosted loop and the default.
The deployment API rejects a request with a top-level field it does not know, so a
misspelled field is a 400, never silently ignored. identity is accepted today;
context_providers and skills are the next fields to land — until then, leave them out
of the request and attach skills from the dashboard.
Deleting the deployment when an organization uninstalls removes the agent, its channels and its records.
4. Per-organization rules and context
A context provider is a URL Keiki fetches and renders as a system-prompt section, titled as
you name it. It is how per-organization instructions ("always offer the annual plan first",
"never discuss pricing with anyone but the account owner") and slow-changing account facts
(plans, features, limits) reach the agent without being baked into system_prompt.
| Field | |
|---|---|
name | Stable key; repeating a deployment with the same name updates it. |
title | The heading the agent sees. |
url | A public https:// URL returning text/markdown. Signed like the identity webhook. |
ttl_seconds | How long Keiki reuses the response. Default 5 minutes, maximum 24 hours. |
Responses are cached per agent and refreshed in the background once stale, so a turn never waits on your endpoint after the first one; a fetch that fails keeps the last good body and marks the section as possibly out of date. Keep each body under 32 KB — a provider is for rules and facts, not for documents, which belong in skills.
The request carries the agent and organization the same way the identity webhook does, so one endpoint can serve every organization.
5. Skills everywhere
A skill is a Markdown document with a name and a one-line description. Attached to an agent,
its name and description sit in every prompt, and the agent loads the body with read_skill
when it needs it — so a long billing playbook costs nothing until it is relevant.
Import skills once, into your Keiki organization, from the dashboard's Skills page or from where they already live:
- a
SKILL.mdin a GitHub repository (acme/agent-docs#billing, or the path to the file), pulled the waynpx skills adddoes; - a URL to a Markdown file your docs pipeline publishes;
- an uploaded file.
Then name them in skills on the deployment, and every agent you provision for every
organization gets the same library. Update the skill and every agent has the new version on
its next turn.
The same skills reach every surface:
| Surface | How |
|---|---|
| Slack, web widget, API gateway | The agent's prompt and read_skill, automatically. |
| A developer's IDE or CLI (Claude Code, Cursor, any MCP client) | Add https://<your-keiki-host>/api/v1/harness/mcp with the agent's API key. The agent's tools and skills are served over MCP, identically to what the agent itself sees. |
| Your own MCP server | Keep serving them as MCP resources if you already do; Keiki does not require it. |
You stop maintaining one copy of a skill for the prompt and another for the MCP server.
6. Cut over
Run the Keiki agent beside your bot in one organization, in a channel of its own, until the checks below pass. Then point that organization's Slack app at Keiki, and repeat per organization from your backend. Pending approvals in your old system can drain on their own; Keiki's action records start at cut-over.
What to verify
| Check | Expected |
|---|---|
| Ask a read-only question | Answered; no card. |
| Ask for a write, as a user with the scope | Preview shown, one card, pending; nothing changed yet. Approve → applied, card updated, change visible in your system. |
| Ask for a write, as a user without the scope | Refused in the thread, naming the scope; no card. |
| Approve as a user without the scope | Ephemeral refusal; card still pending. |
| Approve as a different user with the scope | Runs; record shows requester and approver. |
| Change the underlying data between preview and approval | Card marked superseded with the diff; nothing ran; agent proposes again on the next message. |
| Press Approve twice, fast | One execution. |
| Approve from the dashboard after the Slack buttons expire | Runs; Slack card updated. |
| Group of writes, decline one | parallel: others run. sequential: later steps skipped. |
| Two people talking to each other in the agent's thread | Silent turn in the trace, nothing posted. |
| Five messages in three seconds | One turn, all five in it. |
| "stop" mid-turn | Turn aborted, reaction on the message, pending cards untouched. |
| Slack Connect user from another workspace asks a question | Answered (or refused by name, if your webhook says so) — never ignored. |
| Ask "did that go through?" after an approval | Answered from the Action state, no tool call. |
Same question from Slack and from an IDE over /harness/mcp | Same tools, same skills, same answer. |
What you delete
Your Slack event handlers, approval tables and card renderers, preview ledger, run queue, stop-command parsing, speaker and date-time prompt assembly, and the copy of your skills that lived in the bot. What remains is a provisioning call on install, the identity endpoint, the context endpoints, your MCP server, and your skills.
Slack agents
How an agent behaves in Slack — when it answers, who it thinks is talking, how it handles bursts of messages, and how you stop it.
Example: a billing agent per organization
A worked example of a Slack billing assistant on Keiki — one deployment request per organization, two webhook tools, an identity webhook, and your own dashboard for approvals.