# Channels

> Putting an agent on Telegram, Slack, Discord, WhatsApp or email, in a chat box on your website, or opening it to another system over HTTP. What to set for each, and every option.

A channel is how people reach an agent, and how a job's question reaches a
person. An agent is on one because its `agent.ts` names it in `channels`, keyed
by its name like a tool and made by its function from `@chloejs/core/channels`,
like `channels: { telegram: telegramChannel({ allowFrom: [123456789] }), api: apiChannel() }`.
A channel you write yourself goes in the same way, so nothing about a channel
chloe ships is built in. The dashboard's Channels page shows every channel
chloe ships with a switch, the options that are plain values as a form, and a
place for its tokens; it adds or removes the call and its import in `agent.ts`,
and checks the agent still loads. An option that needs code, like `tools` or
`job`, is written in the file and shown there read only. Each channel has a
page of its own.

| Channel | Who may write | What it needs | Opens a port? |
|---|---|---|---|
| [Telegram](https://chloejs.org/docs/channels-telegram) | Telegram user ids in `allowFrom` | a bot token | no |
| [Slack](https://chloejs.org/docs/channels-slack) | Slack member ids in `allowFrom` | two app tokens | no |
| [Discord](https://chloejs.org/docs/channels-discord) | Discord user ids in `allowFrom` | a bot token | no |
| [WhatsApp](https://chloejs.org/docs/channels-whatsapp) | numbers in `allowFrom`, or anybody when unset | a number registered with Meta | a route your web server passes on |
| [Email](https://chloejs.org/docs/channels-email) | addresses in `allowFrom`, and only once the agent wrote first | the Google sign-in, or a mailbox of your own | no |
| [A web page](https://chloejs.org/docs/channels-web) | visitors to the sites in `origins` | a token on your site's server | a few routes your web server passes on |
| [HTTP](https://chloejs.org/docs/channels-http) | holders of a token | a token | no |

A channel's tokens are secrets, so they go in `.env`, and `chloe.config.ts`
hands them over under `agents` and the agent's id:

```ts
// Every agent this copy runs, and how the runtime behaves. An agent is on this
// list or it does not exist.
//
// `settings` is every choice, as deep as it goes, and what it leaves out is the
// default. This file is in source control, so a key goes in .env beside it and
// is named here as `process.env.` and its name in .env. chloe reads no key it
// is not handed here.
import { defineConfig } from "@chloejs/core";

import shop from "./agent.ts";

export default defineConfig({
  agents: [shop],
  settings: {
    // What an agent asks when its own agent.ts names none, and the gateway's key.
    model: { defaultModel: "anthropic/claude-sonnet-5.5", key: process.env.CHLOE_MODEL_KEY },
    // The shop's own channels: its Telegram bot and its WhatsApp number.
    agents: {
      shop: {
        telegram: process.env.CHLOE_AGENTS_SHOP_TELEGRAM,
        whatsapp: {
          phone_number_id: process.env.CHLOE_AGENTS_SHOP_WHATSAPP_PHONE_NUMBER_ID,
          token: process.env.CHLOE_AGENTS_SHOP_WHATSAPP_TOKEN,
          app_secret: process.env.CHLOE_AGENTS_SHOP_WHATSAPP_APP_SECRET,
        },
      },
    },
  },
});
```

Saving the config or `.env` starts the channel, with no restart.

## Options they share

Telegram, Slack, Discord, WhatsApp and email take these. A web page and HTTP have their
own, on their pages.

| Option | Default | What it controls |
|---|---|---|
| `allowFrom` | nobody (anybody on WhatsApp; required on email) | Who may reach the agent, as the platform names them. While the list is empty, a private message is answered with the sender's id, which is what goes in it. On Telegram, Slack, Discord and WhatsApp, the first entry is who the agent's jobs ask when they name nobody. |
| `name` | the kind, like `"telegram"` | Only needed when an agent has two of one kind. It is the start of every address on the channel: `telegram:12345`. |
| `chatHistory` | `{ messages: 10 }` | How much of a conversation each turn is shown: the last `messages`, none older than `days`. Nothing is deleted. |
| `sendWhileWorking` | off | Sends what the model writes on its way to an answer, not only the answer. Not on email. |
| `tools` | every tool the agent has | The only tools a turn on this channel gets. See [below](#what-a-message-gets-tools-or-a-job). |
| `job` | none | A job every message goes to, instead of a turn. See [below](#what-a-message-gets-tools-or-a-job). |

## What a message gets: tools or a job

Without either option, a message is a turn with the agent and every tool it
has.

**`tools`** keeps a turn to the tools named, for when the person writing should
not reach the rest. Name each by the tool itself, so a typo is an error in your
editor, or by its name in the agent's `tools`. The agent's memory and skills
come with them, and for its owner the tools that read its own files and runs;
`selfWriteFile` only when named. On [a web page](https://chloejs.org/docs/channels-web), only what is named, and `chatAsk`.

**`job`** sends every message to one job instead of a turn, so code goes first:
look up who wrote, read their orders, and decide whether a model is needed at
all. The job reads the message from `work.input`, where `userId` is the
sender's id on that channel, and what it returns is the reply:

```ts
// A customer's message, answered with their own orders at hand. The shop's
// WhatsApp number (channels/whatsapp.ts) hands it every message, in place of a
// turn.
//
// Code goes first. The number they wrote from is looked up in the customer
// system, and a number on no account is answered without asking a model at
// all. Only then is one model step given that customer's orders and their
// message, and asked for the reply.
import { z } from "zod";

import { defineJob } from "@chloejs/core";

import { customerByPhone } from "../services/customersService.ts";
import { ordersBy } from "../services/ordersService.ts";

export default defineJob({
  id: "answer-whatsapp-customer",
  description: "A customer who wrote on WhatsApp has an answer that knows their orders.",
  run: async (work) => {
    // Their id on the channel it came in on, which on WhatsApp is their number.
    const who = await work.step("look them up", () => customerByPhone(work.input.userId));
    if (!who) return "This number is not on any of our accounts. Write from the number you order with, and we will find you.";
    const orders = await work.step("their orders", () => ordersBy(who.id));
    const { reply } = await work.model("write the reply", {
      instructions:
        "You answer a shop's customer, briefly and warmly, from their orders alone. " +
        "When the answer is not in them, say somebody from the shop will get back to them today.",
      output: z.object({ reply: z.string().min(1).max(1000) }),
      prompt: `${who.name} wrote: ${work.input.text}\n\nTheir orders: ${JSON.stringify(orders)}`,
    });
    return reply;
  },
});
```

- Name the job on the channel only, not in the agent's `jobs`. It must be code
  with `run` and no cron line. Only a message there starts it.
- One conversation runs it once at a time, and two conversations run it side by
  side.
- A job that asks with `work.ask` asks in that conversation, and the next
  message is the answer.
- A slash is only text there, except `/clear`.
- A run that fails tells the person only that something went wrong. The error
  is in the run.
- `work.state` is shared by every conversation: keep anything per person under
  `work.input.userId`.

An agent that should be a different agent for some people is a second agent,
with its own channel.

## A message while it is answering

One reply at a time in a conversation, on every channel. A message from the
same person while a reply is going is read by that reply before its next step,
and the reply answers both; nothing is sent for the second message on its own.
A message from anybody else, or one with files, waits for the reply to end and
then gets its own.

## Commands in a chat

A message starting with `/<job id>` runs that job, and the words after it fill
the job's `args`: see [A job is a workflow](https://chloejs.org/docs/jobs#starting-one-with-something).
Four commands work on every channel: `/clear` starts a new conversation and
keeps the old one on the page, `/compact` has the agent's quick model sum up the
conversation so far, after which the agent is shown that summary and what came
after it (the messages it sums up stay on the page), and `/models` and `/model`
pick the model, see [Models](https://chloejs.org/docs/models). Any other slash message is an
ordinary message.

## Writing one

A channel chloe does not ship is a file in the agent's `channels/` folder,
written with `defineChannel(kind, options, start)` from
`@chloejs/core/channels`. Its options extend `Shared`, so it takes the shared
options above, and `defineChannel` handles them. `start` does only the
platform: it listens, turns each message into an `Incoming`, calls
`receive(agent(), incoming, rulesOf({ ...options, bound }))`, and sends back the
text that returns.

`receive()` decides what happens to a message, the same for every channel, in
this order:

1. Nobody outside `allowFrom` is answered.
2. The answer to a sign-in goes to the connection that asked, and the answer to
   a job waiting on that chat goes to the job.
3. In a group, a message not meant for the agent is left alone.
4. `/<job id>` runs that job, and `/clear`, `/compact`, `/models` and `/model` are answered.
5. Anything else is a turn, or goes to the channel's `job`. A reply that reads
   `/<job id>` starts nothing, because the model may have read a page written
   to ask for it.

Hand `receive()` a `send` to allow `sendWhileWorking`, `calling` to be told as
each tool starts, and `writing` for the words as they are written. A reply can
carry `buttons`, and `multiple` when several may be picked: send the ticked
ones as one message with `together()`. `commands(agent)` is the list to show in the platform's own
command menu.
