> ## Documentation Index
> Fetch the complete documentation index at: https://docs.os.meetluno.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# External channels

> Put agents in Slack and Microsoft Teams, with a different agent per channel.

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

| Where someone writes | Who answers |
| - | - |
| A channel (or Teams group chat) bound to an agent | That agent, when mentioned, or every message with **All messages** |
| A thread an agent started or already answers | That agent, even in a channel bound to another one |
| A Slack DM to the Luno app | The agent that person picked for their DMs, else the workspace's **Direct messages** agent |
| A Teams 1:1 chat | The agent that person picked, else the tenant's **Direct messages and unassigned chats** agent |
| A Teams channel without its own agent | The tenant's **Direct messages and unassigned chats** agent |
| A Slack channel without an agent | Nobody; a mention gets a link to choose an agent |

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](#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:

  ```typescript theme={null}
  export default defineAgent({
    slug: "hr",
    name: "HR assistant",
    description: "Answers HR questions for employees.",
    systemPrompt: "You are the HR assistant…",
    model: "deepseek/deepseek-v4-flash-0731",
    channels: { allow: ["dm"] }, // or ["dm", "group", "channel"]; optional reply: "Write me a DM"
  });
  ```

* **Let an agent speak first** with [`notify`](#agents-that-post-first): 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:

| Option | People… | Set it up | Use it when |
| - | - | - | - |
| **Receptionist** | Just write; the front desk hands them to the right agent | Make a router agent the DM agent and list the others under **Can switch to** | People don't know (or care) which agent they need |
| **Pick an agent** | Choose with `/luno hr`, the Slack App Home, or `use hr` in Teams | List the agents under **Can switch to** | People know who they want, or you want no routing model in between |
| **Dedicated bot** | DM `@HR` directly, like a colleague | Create a Slack app per agent | An agent deserves its own name, avatar and DM list entry |

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

```bash theme={null}
luno channels list receptionist                      # find the DM binding id
luno channels update-binding receptionist <binding-id> --dm-agents hr,contracts,leads
```

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](https://docs.os.meetluno.ai/learn/agents/build-agents.md#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](#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**:

```bash theme={null}
luno templates apply receptionist
luno deploy
```

"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](https://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.

> [!NOTE]
>
> **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):

| UI label | CLI command | Catalog slug | Identity | Use when |
| - | - | - | - | - |
| **Luno OS Slack App** | `connect slack --kind luno` | `slackbot` | Acts as **@Luno OS** | Bot tools **and** External Channels — bind an agent to a specific Slack channel |
| **Custom Slack App** | `channels setup <agent> --custom` | `slack-app:<handle>` (credential instance is still `slackbot`) | Acts as **your bot** (`@handle`) | Give one agent its own name, avatar, and handle; gateway mentions/DMs |
| **Personal Slack Account** | `connect slack --kind personal` | `slack` | Acts as the **installing user** | Posts as that person — **not** for External Channels |

> [!NOTE]
>
> **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`.

> [!NOTE]
>
> **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"`:

```typescript theme={null}
import { defineAgent } from "@meetluno/luno/agent";
import { listMyEmails } from "../actions/list-my-emails"; // credentials: [gmail.scope("user")]

export default defineAgent({
  slug: "assistant",
  name: "Assistant",
  description: "Answers about your own inbox and calendar.",
  model: "anthropic/claude-sonnet-4.6",
  systemPrompt: "You are the personal assistant of whoever writes to you.",
  tools: [listMyEmails],
  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-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.

> [!WARNING]
>
> 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:

```typescript theme={null}
import { defineAction } from "@meetluno/luno/action";
import { z } from "zod";
import { findEmployeeByEmail } from "../lib/hris";

export const myVacationBalance = defineAction({
  slug: "my_vacation_balance",
  name: "My vacation balance",
  description: "Vacation days left for the employee you are talking to.",
  input: z.object({}),
  output: z.object({ name: z.string(), days: z.number() }),
  async run(_input, _credentials, ctx) {
    // ctx.speaker: { platform, id, name?, displayName?, email?, title?, timezone?, isBot,
    //                teamId?, userPrincipalName?, lunoUser?: { id, email } }
    // ctx.conversation: { kind: "dm" | "group" | "channel", platform, channelId, channelName? }
    if (ctx.conversation?.kind !== "dm") throw new Error("Ask me this in a direct message.");
    const email = ctx.speaker?.email;
    if (!email) throw new Error("I can only look this up from Slack or Teams.");
    const employee = await findEmployeeByEmail(email);
    return { name: employee.name, days: employee.vacationDaysLeft };
  },
});
```

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

| | Direct message | Group chat / channel |
| - | - | - |
| Conversation (session) | One per person (Teams 1:1 chat, Slack DM); Slack threads in a DM are their own | One per thread, with several people speaking |
| Author of each message | The person | Each message keeps its author (`[Name · email] text`) |
| Memory (notes, recall) | Per person | Per channel — shared notes, not the first speaker's |
| Credentials tools use | The agent's (`credentials: "speaker"`: also the person's own user-scoped ones) | The agent's (`credentials: "speaker"`: also each speaker's own user-scoped ones) |
| `ctx.conversation.kind` | `dm` | `group` (Slack group DM, Teams group chat) or `channel` |

**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`.

> [!NOTE]
>
> **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](https://docs.os.meetluno.ai/learn/projects/governance.md#channels). 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.

```typescript theme={null}
import { notify, notifyAction } from "@meetluno/luno/channels";

// In a workflow step or action:
await notify({ agent: "leads", platform: "slack", channel: "#ventas", text: "🔥 New lead: **Acme**" });
await notify({ agent: "leads", platform: "teams", user: "ana@acme.com", text: "Acme replied" });

// Or as a ready-made step / agent tool (slug `luno_notify`):
defineAgent({ /* … */ tools: [notifyAction] });
```

| Field | |
| - | - |
| `agent` | Agent slug. It must be bound on that platform (any channel or the DM default). |
| `platform` | `slack`, `teams`, `whatsapp` or `email` |
| `channel` | Slack `#name` or channel id; Teams channel / group chat name or id (the bot must be in it) |
| `user` | Email of the person to message directly (instead of `channel`); the recipient for `email` |
| `phone` | WhatsApp number (`+5491…`) of a customer already chatting with the agent's number |
| `text` | Markdown (required without `report`; the notification fallback with one) |
| `report` | A structured report, rendered natively per channel (see below) |
| `account` | Slack team id / Teams tenant id / WhatsApp phone number id, only when the agent is bound in several |

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`.

| Button `action` | On click |
| - | - |
| `{ kind: "workflow", workflow, input? }` | Starts the workflow as the person who clicked. Their chat email must belong to a member of the organization who may run it. |
| `{ kind: "hook", token }` | Resumes the waiting `hookStep` once with `{ action: <button id>, by: <email or +phone> }`. |
| `{ kind: "link", url }` | Opens the URL. |

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:

```bash theme={null}
luno connect slack --kind luno              # opens the OAuth flow in a browser
luno connect slack --kind luno --print-url  # headless: print the URL to share with the user
luno connect slack --kind luno --wait       # wait for the user to finish OAuth
```

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

```bash theme={null}
luno channels accounts --platform slack
# If empty, connect the Luno OS Slack App (above), then:

luno channels setup support --channel support
luno channels list support
```

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

```bash theme={null}
luno channels setup support --custom
```

### Advanced primitives

Reply behavior matches the External Channels UI:

| `--replies` | UI label | Behavior |
| - | - | - |
| `mentions-followups` (default on bind) | Mentions + follow-ups | @mention to start; keep listening in that thread |
| `mentions` | Mentions only | Only responds when @mentioned |

`--listen all` answers every message in the channel (UI **Respond to → All messages**); `--listen
mentions` goes back to mentions.

```bash theme={null}
luno channels platforms list
luno channels directory --platform slack --account <team-id>
luno channels bind support --platform slack --account <team-id> --channel <channel-id>
luno channels bind support --platform slack --account <team-id> --channel <channel-id> --replies mentions
luno channels unbind support <binding-id>
luno channels update-binding support <binding-id> --replies mentions
luno channels update-binding leads <binding-id> --listen all
```

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](https://docs.os.meetluno.ai/cli.md#channels).

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](https://docs.os.meetluno.ai/learn/agents/build-agents.md#who-can-approve), 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](#whatsapp)). For tools marked [`editable`](https://docs.os.meetluno.ai/learn/agents/build-agents#edit-before-approving), 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](https://docs.os.meetluno.ai/learn/agents/build-agents.md#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](mailto:team@meetluno.ai) and we will add them promptly.

* Telegram
* Linear
* Discord
* Google Chat

## Next steps

> **[Run agents](https://docs.os.meetluno.ai/learn/agents/run-agents.md)**
>
> Prompt the same agent directly while testing channel behavior.

> **[Agent runs](https://docs.os.meetluno.ai/learn/logs/agent-runs.md)**
>
> Inspect Slack-started agent sessions in Runs.

> **[Credentials](https://docs.os.meetluno.ai/learn/credentials/connect-credentials.md)**
>
> Connect OAuth credentials used by tools and integrations.

> **[Build agents](https://docs.os.meetluno.ai/learn/agents/build-agents.md)**
>
> 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.

> [!NOTE]
>
> 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.

> [!WARNING]
>
> 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).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.