Skip to main content
External channels let people talk to agents in apps like Slack, Microsoft Teams and WhatsApp. There is one Luno app for Slack and one for Teams. You install it once in your Slack workspace or Microsoft 365 tenant and connect it to your Luno organization; then you choose, per channel, which agent answers. Another company that installs the same app gets its own agents — workspaces and tenants never see each other’s.

How messages are routed

Binding a single channel needs the project Admin role (project:manage). The organization-wide defaults — Slack Direct messages, a custom app’s Everywhere the app is, Teams Direct messages and unassigned chats — and creating Slack channels from Luno need an organization owner or admin (like connecting a WhatsApp number), because they route the whole organization’s chat. Each thread is its own conversation (agent session), so follow-ups in the thread keep context. Moving a channel to another agent starts fresh conversations with the new agent. Replies of the shared app carry the name of the agent that answered (Slack shows it as the sender name). While the agent works you see it: Slack shows a 👀 reaction on your message, the “is typing…” status in the thread, and the answer appearing as it is written; Teams shows the typing indicator. Approval buttons appear in the thread when an action needs a human (see Approvals in chat). A message Slack or Teams delivers twice is answered once.

Several agents in one workspace

One company can run several agents in the same Slack workspace or Teams tenant — for example a contracts agent people DM with documents, a hot-leads agent that posts in #ventas and chats there, and an HR assistant that only works in DMs.

Channels

  • Bind each agent where it should answer. #ventas → leads (All messages if it should follow every message there), #contracts → contracts.
  • Restrict an agent to DMs with channels in its definition; mentioned anywhere else it replies with a short notice instead of running. The rule belongs to the agent, so it holds however the agent was reached — a channel binding, a DM pick, or a handoff:
  • Let an agent speak first with notify: the thread it starts keeps answering with that agent, whatever the channel is bound to.

Direct messages: one bot, several agents

Direct messages to the one Luno bot need a choice. There are three ways, and they combine:

Agents people can switch to

On the workspace’s Direct messages binding (Teams: Direct messages and unassigned chats), open Can switch to and tick the agents of the same project people may talk to in DMs. From the CLI:
With that list:
  • Slack: /luno lists the agents (and who answers you now), /luno hr switches your DMs to HR, /luno reset goes back to the default. The app’s Home tab shows the same list with a button per agent.
  • Teams (1:1 chats): agents shows a card with a button per agent, use hr switches, use default goes back.
  • Handoffs: whoever answers a DM gets a handoff_to_<slug> tool for each listed agent (see handoffs), so “can I talk to contracts?” just works and an agent can pass the conversation on when the question is not its own.
Each person’s choice is their own (per workspace). Switching starts a new conversation with the new agent; a handoff keeps the same conversation, and the agent that took it answers from then on. Whichever agent answers sees who is speaking the same way, keeps its memory per person, as in any DM, and follows its own channels rules.

Receptionist

Make the DM agent a front desk whose only job is to route. Start from the template, deploy, bind it to Direct messages and list the specialists under Can switch to:
“I need vacation days” then goes to HR, which keeps the conversation; the person can still say “talk to contracts” or use /luno. The receptionist is a normal agent in your project: edit its prompt, pick a cheaper model, or add handoffs in code so it also routes in the web chat. Any agent can be the DM default; listing other agents is what makes it able to route.

Dedicated bot per agent (Slack)

A separate Slack app named after the agent (@HR), created for you with Slack’s App Manifest API:
  1. A workspace admin creates an app configuration token at api.slack.com/apps → Your App Configuration Tokens → Generate Token (valid 12 hours).
  2. Run luno channels create-slack-app hr --config-token xoxe.xoxp-… (or the custom app wizard in the dashboard). Luno creates the app (--name / --handle default to the agent’s) and prints an install URL.
  3. Open the URL and Allow. The agent is bound to the app everywhere it is: its DMs and mentions go to HR.
Upload the bot’s avatar in the Slack app settings (Basic Information → Display Information); the manifest API cannot set icons. Platform operators can set SLACK_APP_CONFIGURATION_TOKEN so the token is optional — the apps are then owned by the operator’s Slack account and install in the operator’s workspace unless distribution is enabled for them.
Teams: each Teams bot is its own Azure Bot registration, and Teams lets one bot id belong to only one app in a tenant (two app packages sharing the shared bot would also share the same 1:1 chat). Dedicated Teams bots per agent are not supported yet; in Teams use the picker or a receptionist with the shared Luno bot.

Slack connection kinds

In the web app, Integrations → Connect Slack offers three options. The CLI mirrors them with luno connect slack --kind <kind> (no --kind opens the same chooser):
The Luno OS Slack App and the slackbot catalog slug are the same thing. The Integrations page and chooser say “Luno OS Slack App”; luno credentials list shows the underlying catalog slug slackbot. luno connect slackbot still works and opens the same dialog as connect slack --kind luno.
Custom Slack Apps use the same credential key. Luno OS and custom installs both materialize slackbot credentials; the OAuth connection behind the instance (gateway slack vs slack-app:<handle>) is what changes the bot identity. Existing custom apps may need a reinstall to pick up scopes such as chat:write.public or users:read.email.
For channel-specific bindings as @Luno OS, connect the Luno OS Slack App (--kind luno). For a dedicated agent identity, use the Custom Slack App flow. A Personal Slack Account connection does not register a gateway workspace (luno channels accounts --platform slack stays empty).

Example requests

Ask your coding agent which agent to put in Slack and how it should respond.
“Put our support agent in the #support channel and have it reply only when mentioned.”
“Connect the data analyst agent to #data so the team can ask questions there.”
“Give Delbert its own Slack handle so it can be mentioned in any channel.”

Connect agents to Slack (web)

Each agent’s detail page has an External Channels panel (right side).
1

Open the agent

Go to Agents and select your agent. Open the External Channels panel (configure rail).
2

Choose Luno OS app or custom app

Click Connect. Pick Use the Luno OS Slack App (channel bindings as @Luno OS) or Create your own Slack App (one agent ↔ one custom bot identity).
3

Luno OS path — add a channel

If the Luno OS Slack App isn’t connected yet, complete OAuth first (Add to Slack; a workspace admin may need to approve). Then Add channel to pick an existing channel, or Direct messages to make this agent the one that answers DMs to the Luno app in that workspace. Invite the app to private channels (/invite @Luno) so they show up.
4

Custom path — wizard

Follow the custom app wizard (name + handle → Slack manifest → paste Client ID / Secret / Signing Secret → install). The installed app then shows up as its own workspace (@handle · Workspace) in External Channels: pick Everywhere the app is to answer every mention and DM of that bot with this agent, or bind single channels.
5

Message the agent

Message the bound channel (Luno OS) or mention the custom bot. Review runs in Runs by filtering Type to Agent.
Coding agents can deep-link into these flows with ?rail=configure&slack=luno or slack=custom on the agent URL (or luno channels setup <slug> --custom).

Channel access and credentials

Once an agent is bound to a Slack channel (or custom app), anyone who can message it uses the agent’s tools and credentials — not their own. Only bind agents to channels whose members you trust with everything the agent can do.

Speaker credentials

A personal assistant should read your inbox when you ask, and a teammate’s when they do. Opt the agent in with credentials: "speaker":
When a Slack or Teams message comes from an active member of your organization (matched by the chat account’s email, see who is speaking) who may use the agent, the run acts as that member, exactly like a prompt they send from the web app:
  • Tools whose credentials are .scope("user") use the speaker’s own connection. A speaker who has not connected it gets a reply with a link to connect it, then asks again.
  • Project and organization credentials work as before.
  • Owner tables (defineTable({ owner: true })) show the speaker’s own rows. The conversation is then stored encrypted for that speaker, so a shared thread that already holds one speaker’s rows answers other speakers with the agent’s credentials only.
When the author maps to no member (a guest, an external Slack Connect user, a bot) or may not use the agent, the agent still answers, but user-scoped tools fail — they never fall back to anyone else’s credentials. WhatsApp customers are not organization members, so WhatsApp prompts never act as a speaker. The default, credentials: "agent", never uses anyone’s personal credentials.
With credentials: "speaker" the agent’s code acts with the personal connections of every member who writes to it. Only enable it on agents whose tools you would let act as each of those people.

Who is speaking

For every Slack or Teams message Luno looks the author up in the platform’s directory — Slack: name, display name, email, title, timezone, bot or not, workspace; Teams: name, email (or UPN), Entra object id, tenant — and, when the email belongs to an active member of your Luno organization, that member. The agent reads each message as [Ana Pérez · ana@acme.com · HR Manager] text, so in a channel it knows who said what. Tools get the same data as trusted context: it comes from Slack / Teams, never from what the message says, so a tool can rely on it even when someone writes “I am the CEO”. An action reads it from its third argument:
Outside Slack and Teams (web chat, API, workflows) ctx.speaker and ctx.conversation are unset. Slack needs the users:read.email scope for emails (the Luno app has it; reinstall older custom apps). Profiles are cached for five minutes.

DMs vs channels

When the agent answers in a channel: by default only when mentioned, and then in threads it already takes part in (Mentions + follow-ups). With All messages it answers every message posted in the channel. Change it in the External Channels panel (Respond to) or with luno channels bind|update-binding … --listen all|mentions.
Teams and All messages. Teams only sends a bot the messages that mention it, unless the app has resource-specific consent. Once a Teams binding is set to All messages, Download app package includes the permissions (ChannelMessage.Read.Group, ChatMessage.Read.Chat): upload it again as an update in the Teams admin center. Until then, people mention the bot.
Channel conversations are governed by the channel’s settings, not by the agent’s member access list. Binding or unbinding channels needs the project Admin role (project:manage).

Agents that post first

notify posts a message as an agent — into a channel or to one person by email — from a workflow, trigger or tool. It uses the agent’s bindings on that platform to pick the workspace (or tenant) and the bot, starts a thread, and binds the thread to the agent’s session, with the posted text in its history. Replies in that thread continue the conversation with the agent.
It returns { platform, channelId, threadId, sessionId } (email has no thread or session). WhatsApp only allows replies inside its 24-hour customer-service window and is billed like any reply; email goes out from Luno’s sending address with the agent as sender name. Email is off until an organization admin turns it on (Governance → Organization details → Report email, or PUT /api/organizations/:organizationId/report-email with { "reportEmailEnabled": true, "allowExternalRecipients": false }). By default it only reaches members of your organization; with allowExternalRecipients it can reach anyone, and those emails carry an unsubscribe link (and the List-Unsubscribe header mail apps show). Someone who unsubscribes is never emailed by your organization’s agents again. Each organization can send at most 20 emails a minute and 200 a day, and sending stops when the organization is out of credits. notify throws with the reason (403 off / not a member / unsubscribed, 429 rate limit, 402 credits).

Reports

report is one document — title, optional summary (markdown), sections and actions (buttons), optional footer — rendered as Slack Block Kit, a Teams Adaptive Card, WhatsApp text with reply buttons, or an HTML email. Sections are text (markdown), fields (label / value), table (columns + rows), list (items with detail, url and their own buttons) and divider. Buttons only carry an opaque id; Luno keeps the target and accepts the click for 14 days, in the thread where it was posted. Slack and Teams show every button, WhatsApp up to 3 hook buttons (its customers aren’t members of your organization, so workflow buttons are left out), and email only links. notify runs inside deployed Luno runs (workflows, triggers, agent tools), not from the CLI. On Slack the app must be in the channel (/invite @Luno); a person by email needs a Slack account in the workspace. On Teams a person is found by email among the members of the teams and group chats the bot was added to, and a 1:1 notification continues in that person’s chat with the bot. To show a report inside a project app (src/apps/), render it with ReportView from @meetluno/app-client/react: workflow buttons run as the signed-in user (the workflow must be in the app’s uses.workflows) and hook buttons resume their hook with by: <user email>.

Manage Slack from the CLI

Prefer the happy path for the Luno OS Slack App:
(On a local machine that has never authenticated, run luno auth login first. Hosted sessions are already authenticated and must use --print-url, since they cannot open a browser.) Verify the gateway workspace registered before binding:
channels setup creates the Slack channel when needed (or binds an existing one with the same name), then binds the agent with Mentions + follow-ups (same default as the web UI). For a custom @handle, open the agent wizard (user finishes in the browser):

Advanced primitives

Reply behavior matches the External Channels UI: --listen all answers every message in the channel (UI Respond to → All messages); --listen mentions goes back to mentions.
The directory starts with the workspace default: dm:<team-id> (Slack DMs), app:<slack-app-slug> (a custom app everywhere it is) or tenant:<tenant-id> (Teams). Teams uses the same commands with --platform teams --account <tenant-id>. The agent positional takes the slug from defineAgent({ slug }) after deploy (or the platform agent id). See the CLI reference. Connecting the Luno OS Slack App registers the gateway for your organization; you don’t need to add Slack to luno.config.ts.

Approvals in chat

When the agent calls an action or tool marked needsApproval: true, it posts the pending call (tool name and input) in the thread with Approve and Deny buttons and waits. Approving runs the call and the agent finishes its reply in the same thread; denying tells the agent the call was refused. The card then shows who decided. When the tool has approvers, the card lists them and only those organization members (matched by the chat account’s email) can decide; anyone else gets a notice and the buttons stay. Without approvers, only a chat user whose email matches an active organization member with run access to the project can decide — being in the channel is not enough. WhatsApp never shows approval buttons (see WhatsApp). For tools marked editable, Slack adds Edit and approve (a form with the input as JSON; errors show in the form) and Teams adds an editable JSON field with Approve with edits; the same approver checks apply, and the card then says “Approved with edits by …”. The same pending calls can be decided from the web app chat or luno agents approvals. See human approval.

Other external channels

Slack, Microsoft Teams and WhatsApp (below) are available today. The channels listed here will be added over the coming weeks. You can request specific channels by contacting us at team@meetluno.ai and we will add them promptly.
  • Telegram
  • Linear
  • Discord
  • Google Chat

Next steps

Run agents

Prompt the same agent directly while testing channel behavior.

Agent runs

Inspect Slack-started agent sessions in Runs.

Credentials

Connect OAuth credentials used by tools and integrations.

Build agents

Tune the agent prompt and tools for channel use.

Microsoft Teams

People can message the Luno bot 1:1 in Teams or @mention it in a channel or group chat; the agent answers in the same thread, and each thread keeps its own session.
1

Install the app

Open an agent → External Channels → Microsoft Teams → Connect → Download app package. A Teams admin uploads it: Teams admin center → Teams apps → Manage apps → Upload new app.
2

Link your tenant

In the same dialog (org owner or admin), Create link code, then send link XXXX-XXXX to the Luno bot in Teams. The code works once and expires after 15 minutes. The link then waits for an org owner or admin to approve it in the same dialog, which shows the tenant id and the Teams name and email of whoever sent the code (Reject it if you don’t recognize them). Approving sends you to Microsoft: sign in as an administrator of that Microsoft 365 tenant (Global, Privileged Role, Cloud Application or Application Administrator) and grant consent to the Luno app; Luno verifies the consent with Microsoft before it confirms the link. Until then the bot answers nobody in that tenant. One Luno organization answers per Microsoft 365 tenant: a tenant another organization already confirmed is never moved — the bot replies with the tenant id to raise with Luno support, and the attempt is recorded in both organizations’ audit logs. To unlink a tenant, disconnect it under Integrations → Microsoft.
3

Choose where each agent answers

Add the bot to the teams it should work in (team → Apps → add Luno). Back in External Channels, open the tenant and Add channel: pick a channel the bot was added to, or Direct messages and unassigned chats for 1:1 chats and every conversation without its own agent. Bind other channels from other agents’ pages.
Until the tenant is linked, the bot replies with these instructions; send it help anytime.
Alternatively, an admin can link the tenant from Integrations → Microsoft: connect with an account holding an active Global, Privileged Role, Cloud Application, Application or Teams Administrator role, then pick the agent under Teams bot (that sets the tenant default). Disconnect there unlinks the tenant and removes all its channel bindings.

WhatsApp

Customers message your own WhatsApp Business number; the agent you pick answers. Each customer chatting with a number is one agent session, so the agent remembers the conversation.
1

Connect a number

Open the agent, then External Channels → WhatsApp → Connect (org owners and admins). Luno opens Meta’s WhatsApp Business setup (hosted by our WhatsApp partner, Kapso): sign in with Facebook, pick or create the business portfolio and WhatsApp Business account, and verify the phone number. You can keep using the WhatsApp Business app on the same number (coexistence) or dedicate it to the API.
2

Come back to Luno

When the setup finishes you land back on the agent page and the number shows under WhatsApp. If the setup failed, start again from Connect.
3

Choose the agent

Click Answer next to the number. One agent answers per number; choosing Answer on another agent moves the number to it, and Stop disconnects it (customers then get a short “not connected yet” reply).
What customers get:
  • Text, voice notes and captions. Voice notes are transcribed and media captions are read; stickers, locations and other types get a short “I can only read text” reply.
  • Read receipts and typing. The message is marked read and WhatsApp shows typing… while the agent works (for up to about 25 seconds).
  • Formatting. Bold, italics, strikethrough and code from the agent’s answer use WhatsApp formatting; links stay as plain URLs; long answers are split into several messages.
  • Approvals. When the agent pauses on a tool marked needsApproval: true, the customer gets a short message saying the request is waiting for approval in Luno — never Approve / Deny buttons. Your team decides it from the web app chat, luno agents approvals, the API, or (when the tool has approvers) a Slack or Teams card; the customer then gets the answer in WhatsApp.
Everyone who can message the number uses the agent’s tools and credentials (even with credentials: "speaker": customers are not organization members). Bind numbers only to agents whose tools are safe for the public.
WhatsApp only allows free-form replies within 24 hours of the customer’s last message, which covers normal back-and-forth. Agents cannot start a conversation on WhatsApp yet (that needs Meta-approved message templates).

For platform operators

Running your own Luno OS platform? Create the shared apps once per environment with the setup guides that ship with the platform source: deploy/slack/README.md (Slack app), deploy/teams/README.md (Teams bot) and deploy/whatsapp/README.md (WhatsApp through Kapso).