---
title: "Connect a chat channel"
description: "Bind a chat app to agents and map the people who may use them."
canonical_url: "https://docs.oz0.ai/build/apps/chat-channels"
---
# Connect a chat channel

> Bind a chat app to agents and map the people who may use them.

For: application developers.

Connect a chat channel to let people run agents from their chat system. A mention of the app or `/oz0 <request>` starts an allowed agent, whose answer returns to a thread.

The agent's [questions](https://docs.oz0.ai/agents/asking-people) and [approvals](https://docs.oz0.ai/agents/approvals) appear there as buttons. A thread reply continues the [conversation](https://docs.oz0.ai/agents/conversations).

Three verified plugins bring channels: [Slack](https://docs.oz0.ai/build/plugins/verified/slack), [Discord](https://docs.oz0.ai/build/plugins/verified/discord) and [Microsoft Teams](https://docs.oz0.ai/build/plugins/verified/teams). The platform itself knows no chat system: a channel is an [event source](https://docs.oz0.ai/build/parts/event-sources) that carries chat messages, plus a function that posts, so any chat system is a plugin away.

Everything comes in through the edge's HTTPS port, `POST /events/<tenant>/<source>`: Slack's Events API, slash commands and interactivity, Discord's interactions endpoint, and the Bot Framework's messaging endpoint for Teams. The edge keeps no connection open to Slack, Discord or Teams, and nodes only dial out, as always.

## Create your own app, about two minutes

Slack and Discord send an app's events to one URL, and yours go to your own edge, so every cluster uses a Slack app and a Discord application of its own. [Connect](https://docs.oz0.ai/admin/connections#your-own-app-about-two-minutes) makes each close to one click, from the console (Credentials, Connections) or `oz0 connection start slack` / `discord`:

- Slack: a create link opens Slack with your app's manifest filled in (the `/oz0` command, the events and interactivity on your edge, the bot's scopes). Paste the app's Client ID, Client Secret and Signing Secret on the edge's page, click Retry next to the Request URL on Slack's Event Subscriptions page, and allow the app in your workspace ([Slack](https://docs.oz0.ai/build/plugins/verified/slack#create-your-own-slack-app-about-two-minutes)).
- Microsoft Teams: an Azure Bot of your own in your Microsoft Entra tenant, in three checked steps (its IDs, a client secret your tenant's token endpoint takes, then the messaging endpoint and the Teams app package the edge builds for you to upload); about ten minutes, and it needs an edge the internet reaches ([Teams](https://docs.oz0.ai/build/plugins/verified/teams#create-your-own-bot-about-ten-minutes)).
- Discord: Discord has no manifest, so the edge's pages guide you: make an application in the developer portal, paste its Application ID, Public Key, Client Secret and bot token, paste the Interactions Endpoint URL the page shows into the portal, and pick the server ([Discord](https://docs.oz0.ai/build/plugins/verified/discord#create-your-own-discord-application-about-two-minutes)).

Each step is checked before the next: the format of each field, the client secret with Slack's `oauth.v2.access` or a Discord client credentials grant, the bot token with Slack's `auth.test` or Discord's `GET /users/@me` and `GET /applications/@me`, and the signing secret or public key by the signed request Slack or Discord sends to your edge. A wrong field is caught at its own step, and its page says what to do.

Troubleshooting. The usual causes, with what to do, are on [Connections](https://docs.oz0.ai/admin/connections/troubleshooting#troubleshooting): for Slack, the Request URL not verified (click Retry after pasting; Slack must reach your edge), a wrong signing secret, missing scopes; for Discord, the PING failing or a wrong public key (the portal will not save the URL), another application's token. A cluster whose edge Slack or Discord cannot reach cannot use a channel yet; events over an outbound connection are being evaluated for it.

## How a message becomes a job

1. The chat system sends a delivery. The edge checks its signature with a tenant secret only the edge reads (Slack's signing secret, Discord's public key) and refuses one signed more than five minutes ago; for Teams it checks the delivery's bearer token against the keys the Bot Connector publishes, and that it is for your bot. A forged delivery gets `401` and reaches no node.
2. The edge answers at once, from rules the plugin declares: Slack and Discord give three seconds. A slash command is acknowledged, a Discord command is deferred ("thinking…", which only the person who typed it sees).
3. A node with the plugin reads the delivery into a neutral message: a mention, a command, a reply in a thread, a click on a button, a form sent back.
4. The edge finds the channel's binding for that workspace and room, checks who wrote, and starts the agent through the same path as `oz0 run`: [kill switches](https://docs.oz0.ai/govern/kill-switches), [quotas](https://docs.oz0.ai/spend/budgets-and-quotas), budgets and the person's role all hold, and the audit trail names the account.
5. The edge posts to the thread through the plugin: who asked what, then each question or approval with buttons, then the answer. A post that fails for a while (a rate limit, the chat system briefly down) is posted again, up to six times over about eight minutes. When the opening cannot be posted at all, for example because the app is not a member of the room, the person is told why in a message only they see, and the job's answer is in the console.

The person's message is outside text ([sandboxes](https://docs.oz0.ai/concepts/security/sandbox-tiers#sandboxes)): the agent reads it as labelled data, the job runs as outside-started (its tools and sessions in the sandbox on tier-1 nodes), and what the agent answers is posted so that it mentions nobody but the person who asked.

## Bind a channel to agents

A binding says which agents a channel may start, where, and who may start them. Admins of the tenant make them:

```bash [Terminal]
oz0 channel list
oz0 channel bind helpdesk --channel slack --agent support-triage --agent billing --room C0123ABCD
oz0 channel bind lobby --channel discord --agent greeter --allow everyone --team community
```

- The first `--agent` is the default. A message whose first word is another of the agents starts that one: `/oz0 billing why was I charged twice?`. Discord's `/oz0` has an `agent` option too.
- `--workspace` (a Slack workspace or Discord server ID) and `--room` (a channel ID) narrow the binding; the most specific binding for a message's place wins.
- `--allow members` (the default) lets only mapped people start the agents, each as their own account, which needs the operator role and the [content permission](https://docs.oz0.ai/admin/tenants) in the tenant: the answers are posted to the room.<br />

Each post asks again: while that account may not see the job's content (it lost the permission, or a [wall](https://docs.oz0.ai/govern/taint-and-labels/label-policies#walls) of the tenant's label policies closes the job to it), the thread gets only what the job did, how it ended and a link to OZ, never the answer, its result card, an approval's arguments or a question's words.<br />

`--allow everyone` lets anyone in the room start them, as the binding's service identity `service:channel/<name>` (unless they are mapped to an account that may start them itself); give such a binding a `--team` with [quotas](https://docs.oz0.ai/spend/budgets-and-quotas). Everyone in the room then reads the answers, so bind it only where that is fine.
- A channel starts agents, not flows.

## Map people to accounts

People in the chat system are mapped to accounts of the tenant by their ID there (a Slack user ID such as `U0123ABCD`, a Discord user ID):

```bash [Terminal]
oz0 channel member add slack U0123ABCD alice --workspace T0456EFGH
oz0 channel member add discord 80351110224678912 alice
oz0 channel members
```

A mapped person starts jobs, answers questions and decides approvals as that account, with its membership in the tenant: readers may not start or decide, operators and above may decide approvals, and starting a thread or answering a question also takes the [content permission](https://docs.oz0.ai/admin/tenants). A cluster role counts for nothing in a chat: a cluster admin's account acts with its membership in the tenant, and needs one. Someone who is not mapped, where the binding allows only members, is told so in the chat with the command that maps them; nothing starts.

An admin maps people only to accounts whose grant in the tenant is not more than the admin's own (no higher role, no content permission the admin lacks, no agent the admin's own agent grant does not let them start), and only a cluster admin maps a person to a cluster admin's account; a mapping to an account wider than the admin's is not theirs to remove, nor to replace by mapping the same person again.

In the same way, a binding names only agents its admin may start when it lets everyone in the room start them, and an admin whose grant names agents binds only those, and removes or replaces (by saving one of the same name) only such bindings. Each mapping is in the audit log as `channel.member.set`, with who made it.

## Link your own chat identity

People can link themselves, so an admin need not map everyone by hand. Anyone with a membership in the tenant, signed in to the console, opens Chat channels, chooses Get a link code under *Your chat identity*, and sends the code to the bot within ten minutes:

```text
link K7QM-3XPD-W2HN-9RTA
```

Send it in a direct message to the Slack app, or with the slash command (`/oz0 link K7QM-3XPD-W2HN-9RTA` on Slack, `/oz0 prompt: link K7QM-3XPD-W2HN-9RTA` on Discord), where nobody else reads it. The bot answers, to you alone, which account you now act as. From the terminal:

```bash [Terminal]
oz0 channel link            # a code for any of the tenant's channels; --channel slack for one
oz0 channel links           # the chat identities linked to your account
oz0 channel unlink slack U0123ABCD
```

The chat system's signature proves who sent the code, and the code proves which account asked for it, so the link maps exactly that chat person, in that workspace, to that account. A link never raises anyone: in the chat you act with your account's own membership in the tenant, the same as with a mapping an admin made (starting agents still takes the operator role and the content permission). A code works once, for ten minutes, and making a new one replaces the one before.

It has sixteen characters, at least one of them a digit, and only a message in that shape (`link` and the code with its dashes, or the sixteen characters run together) is read as one, so `@bot link this repo` goes to the agent like any other message. A code posted in a room others read stops working at once, since someone else could send it first: make a new one. Each link is in the run audit trail as `channel.link` (with the account it replaced, if any), and each refused code as `channel.link.refused`.

On Slack, subscribe the app to the bot event `message.im` (and the `im:history` scope) for direct messages.

A direct message to the app is for linking: anything else sent there is answered with how to start an agent (mention the app in a room, or use the slash command), and nothing starts. An admin who wants an agent to answer in a person's direct messages binds it to that direct message's room ID (`D…` on Slack) with `oz0 channel bind --room`; a binding for the whole workspace does not cover direct messages.

## When nothing starts

The person who wrote always hears why, in a message only they see, and the refusal is in the run audit trail as `channel.refused` with its kind:

<table>
<thead>
  <tr>
    <th>
      Kind
    </th>
    
    <th>
      For example
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        not_allowed
      </code>
    </td>
    
    <td>
      not mapped where the binding allows only members, a reader, no content permission
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        no_agent
      </code>
    </td>
    
    <td>
      no binding for the room, an agent the binding does not allow, an agent that is not installed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        kill_switch
      </code>
    </td>
    
    <td>
      a <a href="https://docs.oz0.ai/govern/kill-switches">
        kill switch
      </a>
      
       stops the agent or the tenant
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        quota
      </code>
    </td>
    
    <td>
      a <a href="https://docs.oz0.ai/spend/budgets-and-quotas">
        quota
      </a>
      
       or a budget is used up
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        input
      </code>
    </td>
    
    <td>
      the message does not fit what the agent takes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        busy
      </code>
      
      , <code>
        stale
      </code>
    </td>
    
    <td>
      the thread's job still works on the last message; a button of something decided already
    </td>
  </tr>
</tbody>
</table>

When the platform itself cannot route a message (its database or Temporal is away for minutes), the edge tries again for a few minutes; on the last try it tells the person that nothing was started and to send the message again, and records `channel.failed`. A delivery the plugin cannot read at all names no person, so nobody can be told; it is audited as `event.failed`.

## Related tasks

- [Chat channel conversations](https://docs.oz0.ai/build/apps/chat-channels/conversations): Handle replies, questions, approvals and cards in a thread.
- [Build a chat channel](https://docs.oz0.ai/build/apps/chat-channels/build): Implement channel delivery and posting functions in a plugin.

<legacy-sections>

- <span id="build-a-channel">

Section

</span>

: [build a channel](https://docs.oz0.ai/build/apps/chat-channels/build#build-a-channel)
- <span id="console-tabs">

Section

</span>

: [console tabs](https://docs.oz0.ai/build/apps/chat-channels/build#console-tabs)
- <span id="go-on-in-the-thread">

Section

</span>

: [go on in the thread](https://docs.oz0.ai/build/apps/chat-channels/conversations#go-on-in-the-thread)
- <span id="questions-and-approvals-in-the-thread">

Section

</span>

: [questions and approvals in the thread](https://docs.oz0.ai/build/apps/chat-channels/conversations#questions-and-approvals-in-the-thread)
- <span id="results-as-cards">

Section

</span>

: [results as cards](https://docs.oz0.ai/build/apps/chat-channels/conversations#results-as-cards)
- <span id="the-ai-label">

Section

</span>

: [the ai label](https://docs.oz0.ai/build/apps/chat-channels/conversations#the-ai-label)

</legacy-sections>


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
