Channels
Added in: @mastra/core@1.22.0
Channels connect agents to messaging platforms. Configure them via the channels property on the Agent constructor. See the Channels guide for concepts and platform setup instructions.
Usage exampleDirect link to Usage example
import { Agent } from '@mastra/core/agent'
import { createSlackAdapter } from '@chat-adapter/slack'
import { createDiscordAdapter } from '@chat-adapter/discord'
export const supportAgent = new Agent({
id: 'support-agent',
name: 'Support Agent',
instructions: 'You are a helpful support assistant.',
model: 'openai/gpt-5.5',
channels: {
adapters: {
slack: createSlackAdapter(),
discord: createDiscordAdapter(),
},
},
})
ParametersDirect link to Parameters
adapters:
handlers?:
inlineMedia?:
inlineLinks?:
tools?:
state?:
userName?:
threadContext?:
chatOptions?:
resolveResourceId?:
Per-adapter optionsDirect link to Per-adapter options
Wrap an adapter in a ChannelAdapterConfig object to set per-adapter options:
import { Agent } from '@mastra/core/agent'
import { createDiscordAdapter } from '@chat-adapter/discord'
import { createSlackAdapter } from '@chat-adapter/slack'
const agent = new Agent({
name: 'Example',
instructions: '...',
model: 'openai/gpt-5.5',
channels: {
adapters: {
discord: {
adapter: createDiscordAdapter(),
toolDisplay: 'text',
cors: {
origin: ['https://customer-saas.example'],
credentials: true,
},
gateway: false,
},
slack: createSlackAdapter(), // Plain adapter uses defaults
},
},
})
adapter:
gateway?:
cards?:
cors?:
formatError?:
formatToolCall?:
streaming?:
toolDisplay?:
typingStatus?:
Tool display modesDirect link to Tool display modes
toolDisplay controls how tool calls render in chat. The default 'cards'
posts a "Running…" card per tool and edits it with the result, matching the
behavior in earlier versions. 'text' is the same lifecycle but without rich
Block Kit, useful for platforms that don't render cards well.
'timeline' and 'grouped' stream tool state as inline task_update chunks
alongside the agent's text. These modes require streaming: true and rely on
the chat adapter to render the chunks. Slack supports both natively; other
adapters may render a placeholder until they ship support. If streaming is
disabled, the channel logs a one-time warning and falls back to 'cards'.
'hidden' executes tools silently. Only the typing status indicates work in
progress.
Pass a function to toolDisplay for fully custom rendering. The function
receives a ToolDisplayEvent (running / result / error / approval)
and a ToolDisplayContext ({ mode, platform }); return { kind: 'post', message } for a discrete post/edit, { kind: 'stream', chunk } to push into
the active streaming widget, or undefined to skip rendering that event.
Approve/deny prompts (requireApproval) always render as a separate card
regardless of mode, because inline task entries can't carry interactive
buttons.
import { Agent } from '@mastra/core/agent'
import { createSlackAdapter } from '@chat-adapter/slack'
const agent = new Agent({
name: 'Streaming Agent',
instructions: '...',
model: 'openai/gpt-5.5',
channels: {
adapters: {
slack: {
adapter: createSlackAdapter(),
streaming: true, // already the Slack default
toolDisplay: 'timeline',
},
},
},
})
Custom typing statusDirect link to Custom typing status
Pass a function to typingStatus to customize the status copy. The function is
called once per stream chunk; return a string to set the status, or false /
null / undefined to leave the current status unchanged. Return values are
de-duplicated so the platform only sees a call when the status changes.
defaultTypingStatus is exported from @mastra/core/channels so you can
fall back to the built-in defaults for chunks you don't handle.
import { Agent } from '@mastra/core/agent'
import { defaultTypingStatus } from '@mastra/core/channels'
import { createDiscordAdapter } from '@chat-adapter/discord'
const agent = new Agent({
name: 'Custom Typing Agent',
instructions: '...',
model: 'openai/gpt-5.5',
channels: {
adapters: {
discord: {
adapter: createDiscordAdapter(),
typingStatus: (chunk, ctx) => {
if (chunk.type === 'tool-call' && chunk.payload.toolName === 'searchDocs') {
return 'is searching docs…'
}
return defaultTypingStatus(chunk, ctx)
},
},
},
},
})
HandlersDirect link to Handlers
Override built-in event handlers. Each handler can be:
- Omitted: uses the default Mastra handler (routes the message through the agent and posts the response)
false: disables the handler entirely- A function
(thread, message, defaultHandler) => Promise<void>: wraps or replaces the default
import { Agent } from '@mastra/core/agent'
import { createSlackAdapter } from '@chat-adapter/slack'
const agent = new Agent({
name: 'Custom Handler Agent',
instructions: '...',
model: 'openai/gpt-5.5',
channels: {
adapters: {
slack: createSlackAdapter(),
},
handlers: {
onMention: async (thread, message, defaultHandler) => {
console.log('Received mention:', message.text)
await defaultHandler(thread, message)
},
onDirectMessage: false,
},
},
})
onDirectMessage?:
onMention?:
onSubscribedMessage?:
The ChannelHandler function signature:
type ChannelHandler = (
thread: Thread,
message: Message,
defaultHandler: (thread: Thread, message: Message) => Promise<void>,
) => Promise<void>
Resource ID resolutionDirect link to Resource ID resolution
By default a channel thread's memory resourceId is ${platform}:${message.author.userId}. The sender owns the memory, scoped per platform. For apps with a shared identity, such as single sign-on (SSO), this splits memory: the same user gets feishu:user_123 in a Feishu DM but user_123 on the web.
Pass resolveResourceId to decide memory ownership separately from the sender. It runs only when a new thread is created. Reused threads keep their stored resourceId and never call the hook, so existing conversations don't depend on the resolver being available. Return ctx.defaultResourceId to fall back to the built-in behavior.
import { Agent } from '@mastra/core/agent'
import { createSlackAdapter } from '@chat-adapter/slack'
const agent = new Agent({
name: 'SSO Agent',
instructions: '...',
model: 'openai/gpt-5.5',
channels: {
adapters: {
slack: createSlackAdapter(),
},
resolveResourceId: async ({ thread, message }) => {
// DM: share resource-level memory with the web app by using the bare SSO id
if (thread.isDM) {
return await resolveSsoUserId(message)
}
// Group chat: the conversation owns the memory; the sender stays the actor
return thread.channelId
},
},
})
The ResolveResourceIdContext passed to the function:
platform:
thread:
message:
defaultResourceId:
Inline mediaDirect link to Inline media
Controls which attachment types (images, video, PDFs, etc.) are sent as file parts to the model. Types that do not match are described as text summaries so the agent knows about the file without crashing models that reject unsupported types.
The default (['image/png', 'image/jpeg', 'image/webp', 'application/pdf']) matches the formats supported by major vision models. Override inlineMedia to expand the list (e.g. ['image/*', 'audio/*']) or replace it entirely with a predicate function.
Supported glob patterns:
| Pattern | Matches |
|---|---|
image/* | All image types (image/png, image/jpeg, etc.) |
video/* | All video types |
* or */* | All types |
application/pdf | Exact type match |
For platforms with private CDNs (e.g. Slack), attachments are fetched with authenticated credentials from the Chat SDK. For platforms with public CDNs (e.g. Discord), the URL is passed directly to the model.
Inline linksDirect link to Inline links
Promotes URLs found in message text to file parts so the model can process linked content instead of seeing raw URL text. Each entry can be a string (domain pattern) or an object with a forced mime type.
String entries match a domain and perform a HEAD request to detect the Content-Type. The resolved type is checked against inlineMedia and only matching types become file parts.
Object entries match a domain and force a specific mime type, skipping the HEAD request and bypassing the inlineMedia check. This is useful for sites like YouTube where a HEAD request returns text/html, but the model treats the URL as video content.
type InlineLinkEntry =
| string // Domain pattern (HEAD determines mime type)
| { match: string; mimeType: string } // Domain + forced mime type (skips HEAD)
RelatedDirect link to Related
- Channels guide: Concepts, quickstart, and platform setup
- Agent class: Constructor parameters and methods
- Chat SDK adapters: Adapter configuration and platform setup