Channels
A channel is whatever lets a user send a message in and get a reply back. Sessions persist in SQLite and are keyed to their channel identity. Tasks, projects, memory, schedules, and rooms provide the intentional shared state between surfaces.
CLI, HTTP, the bundled web UI, and Discord ship with the main install. Slack ships as a first-party plugin. The HTTP API is the foundation; everything else is either a consumer of it or a channel registered with the runtime.
For workflow triggers (file-drop, email polling, RSS, calendar) see Workflows → Triggers. Triggers are event-driven workflow inputs, not user-facing channels.
CLI
Comes with @tailored-ai/cli. The tai command and tai -m "…" for
one-shot. See Quick start and the
CLI package page for the full surface.
HTTP API
tai (with no arguments) starts an HTTP server on port 3000. Routes
cover sessions, agents, workflows, tasks, projects, memory, resources,
approvals, and webhooks. SSE streams responses token-by-token.
See @tailored-ai/server for the route reference. Every other channel here is a consumer of this surface; if you're building a custom UI or integrating into your own stack, this is your interface.
By default the server binds to 127.0.0.1. For programmatic access on another
interface, set server.authToken; it gates every /api/* route. For the
bundled browser UI, use server.proxyAuth, which exchanges a password for the
session cookie needed by SSE connections. See Self-hosting
before exposing the server.
Web UI
tai (with no arguments) mounts a built React SPA at /. Chat sidebar,
agent picker, session list, tasks board, memory view, workflow runner.
The bundled UI is one consumer of the HTTP API. If you'd rather build your own:
- Build a separate Next.js / SvelteKit / Vite app against
/api/sessions,/api/agents, etc. - Serve it separately against the HTTP API, or register a UI provider plugin
and select it with
server.ui.provider.
To disable the bundled UI, set server.ui.enabled: false.
Discord
channels:
discord:
enabled: true
token: ${DISCORD_BOT_TOKEN}
owner: ${DISCORD_OWNER_ID}
respondToDMs: true
respondToMentions: true
allowedGuilds: [] # empty = any; otherwise list guild ids
perChannelMapping: {} # channel id → project id
Setup:
- Create a bot at discord.com/developers.
- Copy the token into
.envasDISCORD_BOT_TOKEN=…. - Right-click your own user in any Discord client, "Copy User ID", put
in
.envasDISCORD_OWNER_ID=…. - Add the bot to your server with the
botandapplications.commandsscopes plus message-content intent. - Run
tai. The bot logs in and registers slash commands (/new,/agent,/project, …).
DMs and @mentions route to the agent. Per-user sessions persist, so
the bot remembers context across messages from the same user.
Outbound DM tool
If you want an agent to initiate a Discord DM, enable the discord_dm
tool and list it in the relevant agent's tools: array. The agent calls
discord_dm({ message: "…" }) and the configured owner gets a DM.
The right channel for online-mode escalations. Preferred over gmail send for FYI-style pings, since the chat thread is more conversational
than email.
Slack
Install the first-party Socket Mode channel:
tai plugin install @tailored-ai/channel-slack
channels:
slack:
enabled: true
token: ${SLACK_BOT_TOKEN}
appToken: ${SLACK_APP_TOKEN}
respondToDMs: true
respondToMentions: true
It handles DMs and channel mentions, replies in threads, and can map Slack
channels to registered projects. See the
@tailored-ai/channel-slack README
for app scopes and Socket Mode setup.
Other channels
For Telegram, iMessage, SMS, or another transport, three paths are available:
- Register a channel factory from a plugin. The CLI starts every
registered channel whose config block has
enabled: true. See Custom channels below. - Embed the runtime in your own Node script. Construct an
AgentRuntime, write a small channel class that wires your transport torunAgentLoop, start it directly. See Extending in code. - Wrap the HTTP API. If your platform has a webhook or polling
bridge already, point it at
POST /api/sessions/:id/messagesand the agent works without code changes.
The Slack plugin is the compact reference implementation. The Discord channel
(packages/core/src/channels/discord.ts)
shows the more complete surface, including slash commands and rooms.
Custom channels
Implement the Channel interface, then register a factory:
import type { AgentRuntime, Channel, Plugin } from "@tailored-ai/core";
class ExampleChannel implements Channel {
id = "example";
type = "example";
// ...connect(), disconnect(), send()
}
const plugin: Plugin = (ctx) => {
ctx.channels.register("example", async (runtime: AgentRuntime, cfg) => {
const channel = new ExampleChannel(runtime, cfg);
await channel.connect();
return { channel, disconnect: () => channel.disconnect() };
});
};
export default plugin;
Enable it in config.yaml:
channels:
example:
enabled: true
token: ${EXAMPLE_TOKEN}
The CLI starts every registered factory whose config has enabled: true. Failures are logged and skipped so one bad channel can't keep the
others from starting.
Discord now uses the same channel factory and outbound registry as plugins. Cron, tasks, workflows, and notifications resolve delivery by channel id rather than depending on Discord directly.
Writing your own channel
A channel is an object that:
- Listens for incoming messages (HTTP, WebSocket, file change).
- Resolves the right session via
findOrCreateSession(db, userKey, model, provider, projectId). - Calls
runAgentLoop(content, runtime.buildLoopOptions({ session, agentName })). - Sends the response back over its transport.