Ensemble Docs

Configuring a channel

The context tool, reject conditions, thread ids, data context and reply templates, and which variables each one can use.

The fields

FieldRequiredWhat it decides
NameyesUnique within the tenant.
Channel typeyessms, slack or teams.
Channel configyesProvider credentials and settings. Use ${secrets.X} references.
AgentyesWhich agent answers.
Agent versionnolatest, published, or a number. Defaults to the agent's own resolution.
Context toolnoRuns before the agent.
Reject conditionnoRequires a context tool.
Thread id expressionnoWhat counts as one conversation.
Data contextnoWhat the agent receives besides the message.
Reply templatenoHow the outgoing message is composed.
EnabledyesWhether 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.

VariableIs
bodyThe message text.
from, toSender and recipient identifiers.
phoneNumberThe sender's number, normalised. SMS only.
userId, channelIdThe provider's user and channel. Slack and Teams.
payloadThe provider's raw request body.
headers, queryThe raw request headers and query string.
toolResultWhat the context tool returned. Its data, not the envelope.
agentResultThe agent's structured output, or its text if it has none.

Which are available where:

StageAvailable
Context tool mappingbody, from, to, phoneNumber, userId, channelId, payload
Reject condition and messagethe above, plus toolResult
Thread id expressionbody, from, to, phoneNumber, userId, channelId, payload, headers, query, toolResult
Data contextbody, from, to, phoneNumber, userId, channelId, payload, toolResult
Reply templateagentResult, 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 thread

A 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.

On this page