Skip to documentation
Documentation

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):

JSON
{
  "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 builtOn KeikiPage
Slack user → your user → role and scopesIdentity webhook; scopes enforced on every tool call and approvalIdentity
Per-organization instructions and account context in the promptContext providers on the agentbelow
Approval cards, approve/decline handlersGoverned actions: Slack card, conversational action_respond, and webapp routesActions
View in dashboard linksLiveActions
Loading pending approvals into the next promptThe platform-written Action state sectionActions
Preview ledger and "did the facts change?" checksLiveActions
Grouped or sequential writes and their UILive batch approvalsActions
Speaker name and mention state in the promptStructured speaker on every messageSlack agents
Current time in the promptInjected every turnSlack agents
Per-thread run queue, follow-up injectionSteering: debounce, queue / interruptSlack agents
"Am I being addressed?" heuristicsIngress classification plus a stay-quiet contractSlack agents
Stop keywords, a Stop buttonBuilt inSlack agents
Skills copied into the Slack prompt and the MCP serverOne skill library, served to Slack, the API, and any MCP clientbelow

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:

JSON
{
  "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:

JSON
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
nameStable key; repeating a deployment with the same name updates it.
titleThe heading the agent sees.
urlA public https:// URL returning text/markdown. Signed like the identity webhook.
ttl_secondsHow 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.md in a GitHub repository (acme/agent-docs#billing, or the path to the file), pulled the way npx skills add does;
  • 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:

SurfaceHow
Slack, web widget, API gatewayThe 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 serverKeep 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

CheckExpected
Ask a read-only questionAnswered; no card.
Ask for a write, as a user with the scopePreview 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 scopeRefused in the thread, naming the scope; no card.
Approve as a user without the scopeEphemeral refusal; card still pending.
Approve as a different user with the scopeRuns; record shows requester and approver.
Change the underlying data between preview and approvalCard marked superseded with the diff; nothing ran; agent proposes again on the next message.
Press Approve twice, fastOne execution.
Approve from the dashboard after the Slack buttons expireRuns; Slack card updated.
Group of writes, decline oneparallel: others run. sequential: later steps skipped.
Two people talking to each other in the agent's threadSilent turn in the trace, nothing posted.
Five messages in three secondsOne turn, all five in it.
"stop" mid-turnTurn aborted, reaction on the message, pending cards untouched.
Slack Connect user from another workspace asks a questionAnswered (or refused by name, if your webhook says so) — never ignored.
Ask "did that go through?" after an approvalAnswered from the Action state, no tool call.
Same question from Slack and from an IDE over /harness/mcpSame 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.