Configuring a channel
The context tool, reject conditions, thread ids, data context and reply templates, and which variables each one can use.
The fields
| Field | Required | What it decides |
|---|---|---|
| Name | yes | Unique within the tenant. |
| Channel type | yes | sms, slack or teams. |
| Channel config | yes | Provider credentials and settings. Use ${secrets.X} references. |
| Agent | yes | Which agent answers. |
| Agent version | no | latest, published, or a number. Defaults to the agent's own resolution. |
| Context tool | no | Runs before the agent. |
| Reject condition | no | Requires a context tool. |
| Thread id expression | no | What counts as one conversation. |
| Data context | no | What the agent receives besides the message. |
| Reply template | no | How the outgoing message is composed. |
| Enabled | yes | Whether the channel accepts messages. |
Expressions throughout are JEXL, written as ${...}, the same language as event trigger
handlers.
Variables available
The context differs per stage, because each stage runs at a different point and sees what exists by then.
| Variable | Is |
|---|---|
body | The message text. |
from, to | Sender and recipient identifiers. |
phoneNumber | The sender's number, normalised. SMS only. |
userId, channelId | The provider's user and channel. Slack and Teams. |
payload | The provider's raw request body. |
headers, query | The raw request headers and query string. |
toolResult | What the context tool returned. Its data, not the envelope. |
agentResult | The agent's structured output, or its text if it has none. |
Which are available where:
| Stage | Available |
|---|---|
| Context tool mapping | body, from, to, phoneNumber, userId, channelId, payload |
| Reject condition and message | the above, plus toolResult |
| Thread id expression | body, from, to, phoneNumber, userId, channelId, payload, headers, query, toolResult |
| Data context | body, from, to, phoneNumber, userId, channelId, payload, toolResult |
| Reply template | agentResult, toolResult, body, from, to, phoneNumber, userId, channelId |
The reply template is the exception worth remembering: it has agentResult and it does not
have payload. By then the provider's raw request is no longer in scope, so a template reaching
for ${payload.From} resolves to nothing and the reply goes out with a gap in it.
The context tool
A tool that runs before the agent, with its inputs mapped from the incoming message:
contextMapping:
customerId: ${payload.From}
query: ${payload.Body}Its result becomes toolResult, available to the reject condition, the data context and
anything downstream.
Use it to answer "who is this" before the agent has to. A text from a phone number is anonymous; a context tool that looks the number up turns it into a customer with an account and a history, and the agent starts from facts instead of asking.
The tool is referenced by id with a version selector, so a channel can pin a tool version the way anything else can.
Reject conditions
An expression that, when true, answers with a fixed message and never invokes the agent:
rejectCondition: ${toolResult.status == 'inactive'}
rejectMessage: Sorry ${from}, your account is ${toolResult.status}. Please call support.This needs a context tool, because without one there is nothing to decide on.
It exists so that the cases you already know the answer to cost nothing. An inactive account, an unrecognised number, a region you do not serve: all answerable without a model, and cheaper and more predictable for it.
The reject message is itself an expression, so it can name the caller and the reason. A fixed string would be a worse answer than the agent's, which defeats the purpose.
Thread ids
The thread id decides which messages are one conversation. Without an expression, one is generated from the channel and the sender, which is usually right.
Set one when "the same conversation" means something specific:
${phoneNumber} one conversation per sender, for ever
${payload.From}-${payload.To} one per sender and recipient pair
${payload.ThreadTs} whatever the provider already calls a threadA thread id that is too broad merges unrelated conversations, so the agent brings up last month's order. Too narrow starts fresh every message, so the agent asks for context the sender just gave. The second failure is more common and easier to spot.
Data context
What the agent receives besides the message itself. Two forms:
# Field by field
{ "customerId": "${toolResult.id}", "phone": "${phoneNumber}" }
# The whole tool result, unquoted
{ "data": ${toolResult} }Prefer naming the fields. Passing an entire tool result hands the agent everything the upstream service returned, including fields you did not intend to expose to a model and would rather not have in a prompt.
Reply templates
Without a template, the agent's text response is sent as-is. With one, the reply is composed from the agent's structured output:
Hello ${from}! Your order status: ${agentResult.status}, arriving ${agentResult.eta}.This is worth using whenever the agent has an output schema. The agent decides what is true, and the template decides what the message says, which means wording changes do not need a new agent version and the channel can stay within the provider's length limits.
A template referring to a field the agent did not produce resolves to nothing, and the reply goes out with a gap in it rather than failing. Keep the template and the agent's output schema in step, and read one real reply after changing either.
Enabling, and names
A channel can be disabled, which stops it accepting messages while keeping its configuration.
Names are unique per tenant, enforced by the database rather than checked in the application, so two people creating "Support SMS" at once cannot both succeed.