kalagh

n8n community nodes for Kalagh — send and receive WhatsApp, Telegram, Viber, Instagram, Facebook Messenger, email and website-chat messages through a Kalagh provider.

Documentation

n8n-nodes-kalagh

n8n community nodes for Kalagh — send and receive WhatsApp, Telegram, Viber, Instagram DM,
Facebook Messenger, email and website-chat messages through a Kalagh provider.

A Kalagh provider is a single-platform messaging adapter with a REST facade (POST /api/kalagh/*),
a media route (GET /storage/{key}) and an event bus. This package turns all of that into n8n
nodes: four of them, plus one credential.

Node What it does
Kalagh 45 operations across message, chat, contact, channel and log
Kalagh Trigger Starts a workflow on provider events, consumed over AMQP (RabbitMQ)
Kalagh Webhook Trigger Starts a workflow on provider events, delivered over HTTP
Kalagh Message Type Routes a message to a separate branch per message type

Installation

In n8n: Settings → Community nodes → Install, then enter n8n-nodes-kalagh.

Or from the command line, in your n8n custom-nodes directory:

npm install n8n-nodes-kalagh

Requires n8n 1.x or 2.x and Node.js 20.15+.

Credential

Create one Kalagh API credential per provider process — a provider serves exactly one platform,
so a WhatsApp provider and a Telegram provider are two credentials.

Field Required Notes
Base URL yes e.g. http://kalagh-whatsapp:4000, no trailing slash
API Key yes the provider's API_KEY; sent as x-api-key
AMQP URL only for AMQP trigger e.g. amqp://user:pass@rabbitmq:5672
Queue Prefix rarely must match the provider's QUEUE_PREFIX; leave empty unless you set one

Test connection calls POST /api/kalagh/channels/list, which is authenticated — a wrong key
fails the test rather than passing silently.


Which trigger should I use?

Both emit identical items, so a workflow can be moved from one to the other without rewiring.

Kalagh Webhook Trigger — works against any provider, no broker access

The provider POSTs every channel event to the URL stored in that channel's webhookUrl field, with
three retries and backoff. Activating this node writes the n8n webhook URL into that field;
deactivating restores whatever was there before.

  • ✅ No RabbitMQ access needed. Works over the public internet (n8n Cloud included), as long as the
    provider can reach your n8n WEBHOOK_URL.
  • ⚠️ A channel stores exactly ONE webhook URL. If something else already receives that channel's
    webhooks, this node takes it over for as long as it is active. Use the AMQP trigger instead when
    you need several listeners on one channel.
  • ⚠️ Kalagh does not sign these deliveries today. The n8n webhook path is the shared secret, so keep
    the URL private; the node additionally drops any event whose channelId it was not configured for.

Kalagh Trigger — AMQP, multi-consumer, replayable

Binds its own durable queue to the KALAGH_PROVIDER_EVENTS topic exchange, so it never takes
events away from the Kalagh backend or from another n8n workflow. Failures dead-letter to
kalagh.dlx, and the ack mode is configurable (immediately / on finish / on success).

Requires:

  1. n8n able to reach the RabbitMQ broker;
  2. the provider running with PROVIDER_EVENTS_EXCHANGE_ENABLED=true (see below).

Provider events reach the exchange only when that flag is on. With it off the provider publishes
straight to its per-platform queue exactly as before, and this trigger will connect successfully
but never receive anything. The webhook trigger is unaffected either way.


Item shape — built for zero expressions

Every event item is flattened so common workflows need no expressions at all:

{
  "event": "message.new",
  "channelId": "20251114-0832-0a04-0000-000000007b17",
  "timestamp": "2026-08-03T09:41:22.145Z",

  "id": "20251114-0841-0a22-0000-00000000c31f",
  "chatId": "20251114-0833-0a05-0000-000000007b18",
  "chatPlatformId": "905551112233@s.whatsapp.net",
  "text": "do you ship to Izmir?",
  "me": false,

  "messageType": 1,
  "messageTypeName": "Text",     // ← every numeric enum has a …Name sibling
  "status": 4,
  "statusName": "Delivered",
  "hasAttachments": false
}

Because the enum names are there, an IF or Switch node can be built by dragging
messageTypeName and typing Image — no {{ }} anywhere. The …Name values are the enum MEMBER
names from the Kalagh wire contract (Text, Delivered, AuthenticateRequired), so they stay
stable even if a display label is reworded.

Options (same on both triggers):

  • Split Message Batches (default on) — one item per message/chat/contact instead of one item per
    batch event.
  • Only Inbound Messages (default on) — drops messages the channel sent itself. Keep it on for
    auto-reply workflows, or the reply retriggers the workflow that sent it.
  • Download Attachments (default off) — fetches each attachment as binary attachment_0,
    attachment_1… With it off, items carry mediaUrl (absolute) and the attachment's storage key, so
    you can pass a link along without moving bytes through n8n.

Every event, not just messages

All 29 wire events are shaped the same way — envelope fields (event,
channelId, timestamp) on every item, ids lifted to the top level, and a
…Name sibling beside every numeric enum. A test walks the full event list and
fails if one is ever added without shaping, so this cannot quietly rot.

Event Lifted onto the item
message.new/.sync text, chatId, chatPlatformId, messageTypeName, statusName
message.delete one item per message: messageId, messagePlatformId, deleted
message.reaction emojis, reactionCount, removed
message.status statusName
chat.* (collections) one item per chat, plus typeName, modeName, syncStatusName
chat.delete one item per chat: chatId, chatPlatformId, deleted
chat.participant one item per member: participantPlatformId/Name/Role, action
chat.avatar mediaUrl
contact.* one item per contact, plus displayName, statusName
contact.delete one item per contact: contactId, contactPlatformId, deleted
channel.open/close stateName, and mediaUrl when an avatar came with it
channel.error errorMessage, errorCode (message alone reads as a chat message)
channel.credentials hasCredentials, savedAt — the credential blob is never emitted

channel.credentials is redacted deliberately. That event carries the
session material that authenticates as the connected account, and n8n stores
every item in its execution history. The signal survives; the secret does not.

Turning Split Message Batches off keeps the raw arrays on a single item
instead, for all of the above.

Seeing the fields before a message arrives

n8n builds its schema view from run data, so a trigger that has never fired
gives you nothing to drag into the next node. Three ways round it, easiest first:

  1. Import a demo workflow (below). Each one ships the sample as n8n's own
    pinned data, so the schema is populated the moment you open it.
  2. Kalagh Trigger (AMQP): click Fetch Test Event. With an empty queue it
    returns a representative message rather than an error — turn that off with
    the Sample Output When Testing option. This never affects a live run.
  3. Paste it yourself: open the trigger, choose Edit Output, and paste
    workflows/sample-message-item.json from this package.

Narrowing to one kind of message

Both triggers have a Message Types dropdown — real message types, not
numbers to look up. Tick Image and only images start the workflow. It has no
effect on chat, contact or channel events, which carry no message type.

For branching rather than filtering, use the Kalagh Message Type node below.


The Kalagh Message Type node

A Switch built for exactly one job: one output per message type.

Tick the types you care about and each becomes its own labelled branch — Text,
Image, Document — with an optional trailing Other for everything else
(the alternative is to discard it). No conditions to write, no enum numbers to
remember, and nothing to keep in step when the wire contract gains a type.

It reads the type off the item itself, so it works straight from either trigger
and from the Kalagh node's own message output: it checks messageType, then
type, then content.type, and finally resolves messageTypeName back to its
number. Matching is always on the numeric type, so renaming a display label can
never silently re-route a live workflow.


The Kalagh node

Resource → Operation, 45 operations, each mapped to a real provider route.

Message — Send · Send Media · Reply · Forward · Edit · Delete · React · Remove Reaction ·
Mark as Read · Get · Get Many · Search · Get History · Sync · Download Media

Chat — Get · Get Many · Create · Update · Delete · Search · Sync · Set State (mute/archive/pin
and their inverses) · Mark as Read · Get Participants · Add Participant · Remove Participant

Contact — Get · Get Many · Create · Delete · Search · Sync · Block · Unblock

Channel — Get · Get Many · Create · Update · Restart · Terminate · Unload · Request QR Code

Log — Get Many · Get Webhook Deliveries

Picking a recipient

Send To offers Chat, Contact, and — only where the platform can resolve them — Phone Number and
Username. Chats and contacts are searchable, paginated pickers; they also accept a pasted ID, which
is validated so a platform id (905551112233@s.whatsapp.net) pasted into an ID field is caught
immediately instead of failing at the provider.

Media

Send Media takes either a binary field on the incoming item or a file URL the node fetches
first, and uploads it as multipart. Message Type defaults to Detect Automatically, which lets
the provider infer image/video/audio/document from the MIME type.

Download Media takes an attachment's storage key (or any absolute URL) and returns binary data.
The same operation works for avatars — anything under GET {baseUrl}/storage/….

Capability gating

Platforms differ, and the node knows how. Editing is unavailable on Viber, Instagram, Facebook
Messenger and every email flavour; Instagram cannot forward; reactions and group chats are missing
on several. Rather than failing with a provider stack trace, the node throws:

Editing messages is not supported on Viber. Supported platforms: Bot, WhatsApp (Personal),
WhatsApp (Business), Telegram (Personal), Telegram (Bot), Website Chatbot.

These tables are code-generated from the Kalagh provider wire contract, so they cannot drift from
what the provider actually enforces.

Output

Read operations default to Simplified output — the fields you normally want, with …Name
siblings for every enum, matching the trigger item shape. Switch Output to Raw for the
provider response untouched.

Using it as an AI tool

The Kalagh node sets usableAsTool, so an AI Agent can send messages, look up chats and search
contacts directly. Simplified output keeps the agent's context small. Your n8n must run with
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true.

Deliberately not included

Channel → Delete. It triggers a hard cascade on the provider (channel, chats, messages,
attachments). That is not something an automation should be able to do by accident; delete a channel
from the provider panel instead.


Enabling the AMQP exchange on the provider

Set on the provider process:

PROVIDER_EVENTS_EXCHANGE_ENABLED=true

With the flag off (the default) the broker topology is byte-identical to before. With it on
the provider asserts the KALAGH_PROVIDER_EVENTS topic exchange, binds its existing per-platform
queue with {platformKey}.#, and publishes there instead of sendToQueue — so the Kalagh backend
keeps receiving exactly what it did, while this trigger's own queue receives copies.

Routing keys are {platformKey}.{eventName}, e.g. whatsapp.message.new.

Troubleshooting

The trigger never fires. Check the channel's Raise Events setting: with it off the channel
publishes to no sink at all — no webhook, no queue, no socket. The Kalagh node (Channel → Update)
can turn it back on. For the AMQP trigger, also confirm PROVIDER_EVENTS_EXCHANGE_ENABLED=true.

Credential test fails with 401. The API Key must equal the provider's API_KEY env var exactly.

"property X should not exist". The provider rejects unknown request fields. This usually means
the provider is older than this package; update the provider.

Webhook trigger activates but nothing arrives. n8n's WEBHOOK_URL must be an address the
provider can actually reach — localhost inside a container is not.

IDs

Every ID this package deals with is a provider ID (a Kalagh incremental GUID such as
20251114-0832-0a04-0000-000000007b17) or a platform ID (a WhatsApp JID, a Telegram user id).
The Kalagh central application's own GUIDs never appear here, and n8n never talks to it.

Demo workflows

Four ready-to-import workflows ship in workflows/ (they are in the npm tarball, so after
installing you will find them under node_modules/n8n-nodes-kalagh/workflows/). Import one with
Workflows → Import from File, set the credential and channel on each Kalagh node, then activate.

File What it demonstrates
01-echo-auto-reply.json The smallest end-to-end proof: every inbound message gets a reply
02-keyword-auto-reply.json Routing on text with an IF node and no expressions in the condition
03-inbound-image-download.json Attachments as binary, branching on messageTypeName
04-channel-health.json Reacting to a disconnect by requesting a fresh login QR
05-route-by-message-type.json One branch per message type with the Kalagh Message Type node

Start with 01. If it replies, the credential, the webhook path and both directions all work.

Every demo carries pinned sample data on its trigger, so the field schema is
already there before a single real message has arrived.

Development

npm install --ignore-scripts   # n8n-workflow pulls in isolated-vm, which we never run
npm run verify                 # codegen drift check → build → lint → structural validation → tests

npm run codegen regenerates src/nodes/shared/generated/enums.ts from the Kalagh wire contract;
codegen:check fails the build when the committed file has drifted, which is what keeps the enum
tables honest without a runtime dependency on any Kalagh package.

Two live checks are opt-in because they need a broker:

KALAGH_AMQP_URL=amqp://user:pass@host:5672 npm run test:live

drives the real trigger against a real exchange, and — from the monorepo — the producer half:

KALAGH_VERIFY_AMQP_URL=amqp://user:pass@host:5672 \
  node providers/packages/core/scripts/verify-events-exchange.mjs

Licence

MIT. n8n itself is a separate product under its own licence; this package neither embeds
nor modifies it.

Discussion