Channels
Connect Telegram and WhatsApp to your workflows: per-user connections, pairing, bindings, and result delivery.
Maturity: preview (55 %)
Messaging channels. Telegram and WhatsApp connect a person's messaging account to project workflows: unknown senders pair first, messages fire workflows under the normal permission and quota rules, and answers return to the same chat.
- Telegram and WhatsApp are the only messaging platforms.
- Delivery sends finished answers, not a live stream.
- Channels run in a single process; a second replica would answer every message twice.
A channel connects an external messaging account — a Telegram bot or a WhatsApp Business number — to your project's workflows. Once connected and paired, a message you send from your phone fires a workflow, the run executes inside Neuralis with all the usual permission and quota rules, and the result comes back to the same chat.
Three ideas define the design:
- Connections are personal. Every user connects their own account
(
channels.connectis granted to members by default). Your bot token is stored encrypted in your credential scope; other members cannot see your connection at all. Project-wide shared connections exist too, managed by owners/admins. - Unknown senders never reach an agent. Inbound senders are identified by pairing: a stranger messaging your bot gets an expiring 8-character code and nothing else. Only after you approve the code does that sender's message ever fire anything.
- Delivery is finished answers, not a live stream. A run delivers each answer it finishes back to the chat, as that answer ends — chunked to the platform's limits, code blocks kept intact. Usually that is one message; when the agent hands work to a background subagent it is two, the hand-off and then the result. Thinking and tool events never leave the platform.
Tutorial: connect Telegram
Telegram uses long-polling by default — your Neuralis instance needs no public URL. This makes it the best first channel for self-hosted deploys: the whole flow below works on a laptop behind NAT.
1. Create a bot
- In Telegram, message @BotFather.
- Send
/newbot, pick a display name, then a unique username ending inbot(e.g.acme_assistant_bot). - BotFather replies with an HTTP API token (
123456:ABC-...). Copy it — you paste it into Neuralis in the next step, and nowhere else. Never paste a bot token into a chat with an agent; the wizard is the only place that asks for it.
2. Connect it in the Calendar widget
- Open the Calendar widget from the dock and click Channels in the toolbar.
- Under My channels, pick Telegram.
- Give the connection a label, paste the bot token, keep the transport on Polling (no public URL needed) and click Validate & connect.
Neuralis validates the token live against the Bot API (getMe) and stores
it encrypted in your own credential scope. The connection card shows the
bot's @username and a green status dot; a polling worker starts
immediately.
3. Pair yourself
- From your own Telegram account, send any message to your new bot.
- The bot replies with a pairing code (e.g.
K7MWPR2X) — this is the only reply an unpaired sender ever gets. Codes expire after 1 hour and at most 3 may be pending per connection. - Back in the Channels panel, your connection card now shows an amber badge. Expand the card and approve the request — the sender is linked to you.
Approving links the peer to your own user. Linking a peer to a different
user is an owner/admin operation (channels.manage).
4. Bind a workflow
- Create or open a workflow in the Calendar (any instruction works — try "Summarize what I send you and list action items").
- In the editor's Channel bindings section — it is there when creating, installing a template, and editing alike — click Bind, pick your connection, leave Reply with the run result checked, and add the binding. (On a brand-new workflow the binding is staged and attaches the moment the workflow is created.)
- Make sure the workflow's status is active.
A connection may be bound to several workflows; in that case one inbound message fires all of them — the editor and the connection card warn you when you are about to add a second binding to the same account.
The binding attaches a message trigger to the workflow. A binding is the
only path from a channel into an agent — there is no "route to any agent"
fallback. The trigger is system-managed from then on: editing the workflow's
schedule never touches it; it disappears only when you delete the binding.
You can also just ask your agent in chat — the manage-workflows skill's
binding scripts list, add and remove bindings under the same permissions, so
"create a workflow that answers my Telegram messages and bind it to my bot" is
a one-message setup: the workflow tool creates it, a script binds it. The
manage-channels skill lets the agent check a connection, disconnect or
reconnect it, unlink a peer, and delete a connection. For a delete, the skill
tells the agent to wait for your explicit ask and to tell you how many workflows
are bound to that connection first; the script refuses unless the live binding
count still matches the one the agent reported. Creating a connection stays with
you — the route refuses an agent's call — because it takes a secret.
The fastest path to a chat-style bot is the shipped Channel assistant workflow template: it installs with the two settings below already right (manual-only schedule, one continuous thread) plus channel etiquette built into its instruction — install it, bind your connection, done.
For a chat-style bot, two settings matter:
- Schedule: Manual only. A schedule is not required — the message trigger fires on every inbound message regardless. Add a cron only if the same workflow should also run on a timer.
- Mode: One thread. Every message then continues the same conversation, so the agent remembers the whole exchange — a continuous chat. Fresh per fire starts a blank run per message (right for stateless tasks like "summarize what I send you", wrong for back-and-forth conversation).
5. Test it
Message your bot again. Within a couple of seconds the bound workflow fires; you'll see the run on the calendar (and the typing indicator in Telegram while it works). The final answer arrives back in the chat, split into 4,000-character chunks with code fences kept intact.
Your message rides into the run as untrusted, delimited context appended
after the workflow's instruction — it is never parsed into template
placeholders, and the run executes as the workflow's creator with their
current permissions, exactly like a scheduled fire. The context's source
line also names the Neuralis user the sender is linked to (from the
pairing record — trustworthy, never derived from message content), so the
agent knows who it is talking to: telegram message from linked peer 6133194221 (Kiss Laci) — linked to Neuralis user alex@example.com.
Tutorial: connect WhatsApp
WhatsApp uses the Business Cloud API — the official Meta platform.
Unlike Telegram it is webhook-only: your Neuralis instance must be
reachable on a public HTTPS URL (a reverse proxy or tunnel in front of your
deployment). Declare that proxy in NEURALIS_TRUSTED_PROXIES
(deployment checklist):
undeclared, every webhook call arrives from the proxy's own address and the
webhook rate cap applies to all senders together.
1. Get the Cloud API credentials
In Meta for Developers, create (or open) a Business app with the WhatsApp product added. On WhatsApp → API setup collect four values:
- Access token — generate a permanent token via a system user in Meta Business Settings (the dashboard's temporary token expires in 24 h).
- Phone number ID — the numeric id under the phone number selector (not the phone number itself).
- App secret — App settings → Basic. This signs every webhook delivery; Neuralis rejects anything unsigned.
- Verify token — a string you invent; you'll paste the same value in two places (Neuralis and the Meta webhook form).
When your Meta app supports it, the wizard also offers Sign in with
WhatsApp (OAuth) which fills the access token automatically — an
administrator first stores the app registration as the platform credentials
oauth.whatsapp.clientId / oauth.whatsapp.clientSecret, and registers the
redirect URI https://<your-host>/api/oauth/whatsapp?action=callback with the
provider — Neuralis sends exactly that URI, with no per-request query beyond
action, because Meta matches a registered redirect exactly. The callback must
return to the same signed-in user and project that started the flow. The four fields
above remain the universal path.
2. Connect it in the Calendar widget
Open Calendar → Channels → WhatsApp, give the connection a label and
fill the four fields. On Validate & connect, Neuralis verifies the
token + phone number id live against the Graph API and stores everything
encrypted in your own credential scope. Note the connection id (ch-…)
shown on the card. The card also lists the workflows bound to the
connection (click one to jump to it on the calendar).
Project-scope connections work differently: an owner/admin creates the
connection first, and its card prints the exact derived credential ids
(telegram.botToken.ch-…-style, one per field) to create as project-scope
custom credentials in the admin Credentials tab — channel secrets are
always resolved per connection under these derived ids, never under a
shared static id.
3. Subscribe the webhook at Meta
In the Meta app's WhatsApp → Configuration → Webhook:
- Callback URL:
https://your-neuralis-host/api/webhooks/channels/<connectionId> - Verify token: the same string you entered in the wizard.
Meta sends a GET handshake (hub.challenge) which Neuralis answers
automatically; then subscribe to the messages field. Every delivery is
verified with HMAC (X-Hub-Signature-256) over the raw body before anything
is parsed.
4. Pair and bind
The flow is identical to Telegram from here: message the business number from your phone → pairing code arrives → approve it in the Channels panel → bind a workflow → message again and the result returns to the chat. (WhatsApp's 24-hour customer-service window applies: the business number can reply freely for 24 h after the user's last inbound message.)
Inbound policies
Each connection has a dmPolicy:
| Policy | Behavior |
|---|---|
pairing (default) | Unknown senders get an expiring code; approval creates a peer link. |
allowlist | Only listed stable platform ids may even hold a pairing request; everything else drops silently. |
disabled | All unlinked inbound drops silently. |
There is deliberately no open mode. Group chats are stricter still: a
binding must explicitly allow a group id, the sender must still be a linked
peer, and pairing codes are never sent into groups.
Duplicate webhook/polling deliveries are deduplicated by provider message
id, and rapid consecutive messages from the same peer within 2 seconds
coalesce into a single fire. Each connection is additionally capped to a
configurable number of workflow fires per minute (the
channelInboundFiresPerMinute platform setting, default 30, 0 disables) —
over-cap messages are dropped silently toward the peer and recorded in the
audit log. The project's spend limits still govern every fire that does run;
the cap only keeps a flooding peer from burning the budget window.
Delivery
- A message-fired run replies to the originating chat per its binding's reply setting. Failures report to the binding's failure destination (a chosen peer) or the primary chat.
- Answers are sent as the agent finishes them, not once when the run record closes. This matters whenever the agent hands work to a background subagent: it answers "I have started the research", that reply goes out immediately, and the actual result — written minutes later, once the subagent reports back — is sent as its own message. Both reach the chat.
- A binding's settings are fixed at bind time — there is no edit path, by
design. To change the reply style, delete the binding and add it again; the
workflow's
messagetrigger re-attaches to the new one automatically. - The binding's Reply style picks one of three: Off (nothing reaches the chat), Final answers only (the default — every answer the agent finishes, including one a background subagent reports back later), or Everything it writes, which adds the short notes it writes between tool calls, as it writes them. The last one is a lot of messages on a busy workflow. Those between-tool notes go only to a chat the run was addressed to: an announced scheduled run sends its linked peers the finished answers, whichever of the two styles the binding uses.
- If you type into the same conversation from the web chat, the agent's reply is mirrored to your phone as well — but only to your direct chat, only when you are the person that chat is linked to, and only while the conversation is bridged to a single chat. A teammate's turn in a shared conversation is never mirrored, and a group chat never receives one.
- Thinking, tool calls, tool results and error details never leave the platform. A failed run sends a fixed, neutral notice; the real error stays on the run row and the audit log. A run stopped before it started gets a fixed notice too — when someone cancelled it before it started (a retry that is still waiting counts, and its notice only says the run was cancelled), or when the workflow was paused, archived, waiting for a template upgrade or had its package switched off. A run cancelled while it was already working stays silent.
- A scheduled or manual run can also deliver: set the workflow's delivery policy to announce and select bindings — linked peers receive its answers as a DM (the Announce scheduled runs toggle on the binding form, and beside the workflow's bindings in its editor, where it can be turned off too). They arrive the same way a reply does: one per answer, as the turn that wrote it ends, so an answer a background subagent reports back minutes later reaches the peers too. Turning the policy off stops the next one, mid-run included. This is a separate opt-in: bridging a conversation never grants a scheduled run a voice on the channel.
- A run whose final text is exactly
NO_REPLYdelivers nothing — use it in instructions like "If nothing significant happened, answer NO_REPLY." The check is on the entire reply:NO_REPLYappended after a real answer suppresses nothing. The token is also hidden from the chat timeline, so an agent that uses it correctly leaves no stray marker behind.
The fire endpoint
Any workflow with a webhook trigger can also be fired over plain HTTP:
curl -X POST https://your-neuralis/api/webhooks/workflow/<workflowId>/fire \
-H "Authorization: Bearer wfk1...." \
-H "Content-Type: application/json" \
-d '{"text": "optional context for this run"}'Create the token from the workflow's trigger (it is shown once; only a
salted hash is stored — rotating generates a new token and kills the old
one). The optional text (≤ 4,000 chars) rides as the same untrusted
delimited context as channel messages.
Security posture
- Connection records store credential references only — secrets live in the encrypted credential store at the owning scope and are never echoed by any API response or UI.
- Members write channel secrets only into their own user scope, and only for channel credential ids — the route and the host port both enforce the whitelist, so this is not a general credential-write path.
- Webhook ingress verifies signatures over the raw body before anything else; an unknown connection id is indistinguishable from a bad signature.
- Deleting a connection, unlinking a peer and removing a binding are each recorded in the audit log, whoever — person or agent — did it.
- The channel data folder is unreadable through the filesystem tools even for members — connection metadata is reachable through the channel routes only, which scope every answer to the caller.
Telegram transports
| Transport | Public URL | Notes |
|---|---|---|
| Polling (default) | Not needed | One long-poll worker per connection. Don't run two replicas against the same bot token (Telegram answers 409). |
| Webhook | Required | Per-connection secret header token, generated automatically. |
WhatsApp (Business Cloud API) is webhook-only and requires a publicly reachable URL — see the WhatsApp tutorial above.