#channel bindings across agents

1 messages · Page 1 of 1 (latest)

sick ridge
#

Hi Krill — two related questions about channel bindings across agents.

Our setup:

  • OpenClaw v2026.4.15 running in Docker Desktop on Windows 11 (container: openclaw-gateway)
  • Kingler — main agent, WhatsApp-bound (personal WhatsApp via Baileys), handles inbound messages from 3 family members (Balaji, Reva, Maya). Hub-and-spoke architecture with multiple subagents (Chansey, Pidgey, Paras, JigglyWeb etc.) spawned via sessions_spawn with maxSpawnDepth:2
  • Ditto — diagnostic subagent we're building now. No channel bindings yet. Runs inside the same openclaw-gateway container. Stage 1: manually triggered, no Kingler spawn. Stage 2 (later): Kingler spawns Ditto via allowAgents: ["ditto"]
  • Memory: OpenClaw native memory-core plugin (dreaming at 2 AM IST) + memory-wiki in bridge mode
  • 53 plugins loaded (41 disabled)

Question 1 — Ditto with its own Telegram bot:
Can Ditto have its own Telegram bot binding (separate BotFather token, separate bot) while Kingler continues on WhatsApp? Is this a supported pattern — two agents on the same OpenClaw instance, each with their own channel? Any gotchas with running two bots simultaneously — plugin conflicts, memory isolation, session handling, or anything else specific to our setup?

Question 2 — Kingler with Telegram as a WhatsApp fallback:
Can Kingler have both WhatsApp and Telegram bound simultaneously — so if WhatsApp drops, Telegram still works for the family? Is multi-channel on a single agent supported, and what are the tradeoffs (session continuity, memory context, tool availability per channel, message routing)?

What we're trying to decide:
Whether to give Ditto a Telegram bot for direct operator access (useful for Stage 1 testing and ongoing ops access without needing CLI), and separately whether adding Telegram to Kingler as a WhatsApp fallback channel is advisable or creates more complexity than the redundancy is worth.

unique craneBOT
upbeat rockBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

unique craneBOT
tough axleBOT
#

I’m checking the current docs/source so I don’t make up config behavior.

#

Yes on both, with one big caveat.

Short version

  1. Ditto + its own Telegram bot while Kingler stays on WhatsApp: Yes, supported. This is a normal multi-agent pattern.
  2. Kingler on both WhatsApp + Telegram: Also supported, but it is not transparent failover. It is dual-channel ingress, not automatic channel switchover.

Q1, Ditto with its own Telegram bot

  • OpenClaw supports multiple isolated agents plus multiple channel accounts in one gateway.
  • The clean pattern is:
    • separate agentId
    • separate workspace
    • separate agentDir
    • separate Telegram bot token/accountId
    • a bindings[] rule routing that Telegram account to ditto

So something like:

#
bindings: [
  { agentId: "kingler", match: { channel: "whatsapp", accountId: "kingler-wa" } },
  { agentId: "ditto",   match: { channel: "telegram", accountId: "ditto-ops" } },
]

Gotchas

  • Do not reuse agentDir across Kingler and Ditto, that causes auth/session collisions.
  • Keep separate workspaces if you want real memory/persona isolation.
  • If you intentionally share memory collections / wiki bridge paths, you can still blur that isolation.
  • Subagent spawning later is fine, Ditto does not need a channel binding to be spawnable. The Telegram binding is only for direct operator access.

Q2, Kingler with Telegram fallback

  • You can bind both channels to the same agentId.
  • But OpenClaw routes replies back to the channel the message came from. The model does not automatically choose “fallback Telegram because WhatsApp is down”.
#

So this works as:

  • family can talk to Kingler on WhatsApp or Telegram
  • if WhatsApp dies, they can manually switch to Telegram

It does not work as:

  • WhatsApp message comes in
  • WhatsApp drops
  • OpenClaw silently continues that same exchange on Telegram

Biggest gotcha in your setup
I’m a little worried about your DM context. By default, session.dmScope: "main" means all DMs share one session.

If Kingler is handling DMs from Balaji, Reva, and Maya, that is risky already, even before Telegram.

I’d strongly recommend:

#
session: {
  dmScope: "per-channel-peer"
}

If you’ll have multiple accounts per channel and want the strongest separation:

session: {
  dmScope: "per-account-channel-peer"
}

If you later want “same human on WhatsApp + Telegram = same memory thread”, use identity links deliberately. Otherwise keep them isolated.

Tradeoff summary

#
  • Ditto on its own Telegram bot: I’d recommend this. Clean ops path, low ambiguity.
  • Kingler on Telegram as fallback: workable, but think of it as manual fallback, not HA failover.
  • Complexity cost: more bindings, more allowlists, more operator policy, and more care around DM session isolation.
  • Tooling: same agent tools mostly still work, but channel UX differs. Telegram has more native chat affordances than WhatsApp, so behavior won’t be perfectly identical.

Docs

If you want, I can sketch the exact JSON5 layout for:

  1. Kingler WA only + Ditto TG only, or
  2. Kingler on both WA+TG + Ditto on separate TG bot.

If this fixed it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow can index it.

sick ridge
#

can you pls sketch the layout for opton 2 (kingler on both and Ditto on TG Bot

tough axleBOT
#

I’m verifying the exact config keys before I sketch it, just so I don’t hand you fake JSON5.

#

Yep, here’s a safe sketch for option 2: Kingler on WhatsApp + Telegram, and Ditto on its own Telegram bot.

{
  session: {
    // Important for your setup. Avoid one shared DM context across family members.
    dmScope: "per-channel-peer",
    // If you later add more accounts per channel and want stricter isolation:
    // dmScope: "per-account-channel-peer",

    // Optional, only if you WANT the same person on WA + TG to share context.
    // Otherwise leave this out.
    // identityLinks: {
    //   balaji: ["whatsapp:+15551230001", "telegram:123456789"],
    //   reva:   ["whatsapp:+15551230002", "telegram:234567890"],
    //   maya:   ["whatsapp:+15551230003", "telegram:345678901"],
#
    // },
  },

  agents: {
    defaults: {
      subagents: {
        maxSpawnDepth: 2,
      },
    },
    list: [
      {
        id: "kingler",
        default: true,
        workspace: "~/.openclaw/workspace-kingler",
        agentDir: "~/.openclaw/agents/kingler/agent",
#

        // Stage 2, let Kingler explicitly spawn Ditto
        subagents: {
          allowAgents: ["ditto"],
          requireAgentId: true,
        },
      },
      {
        id: "ditto",
        workspace: "~/.openclaw/workspace-ditto",
        agentDir: "~/.openclaw/agents/ditto/agent",
      },
    ],
  },
#
  bindings: [
    // Kingler primary channel
    { agentId: "kingler", match: { channel: "whatsapp", accountId: "kingler-wa" } },

    // Kingler fallback / second ingress
    { agentId: "kingler", match: { channel: "telegram", accountId: "kingler-tg" } },

    // Ditto direct operator bot
    { agentId: "ditto", match: { channel: "telegram", accountId: "ditto-ops" } },
  ],

  channels: {
    whatsapp: {
      defaultAccount: "kingler-wa",
      accounts: {
#
        "kingler-wa": {
          // For a family setup, allowlist is usually safer than pairing long term.
          dmPolicy: "allowlist",
          allowFrom: [
            "+15551230001", // Balaji
            "+15551230002", // Reva
            "+15551230003", // Maya
          ],
        },
      },
    },

    telegram: {
      defaultAccount: "kingler-tg",
#
      // If both Telegram bots are DM-only, this keeps things simple.
      groupPolicy: "disabled",

      accounts: {
        "kingler-tg": {
          botToken: "123456:KINGLER_BOT_TOKEN",
          dmPolicy: "allowlist",
          allowFrom: [
            "tg:123456789", // Balaji
            "tg:234567890", // Reva
            "tg:345678901", // Maya
          ],
        },

        "ditto-ops": {
#
          botToken: "987654:DITTO_BOT_TOKEN",
          dmPolicy: "allowlist",
          allowFrom: [
            "tg:123456789", // operator
          ],

          // Optional, nice if Ditto may need exec approvals in Telegram DMs
          execApprovals: {
            enabled: true,
            target: "dm",
            approvers: ["123456789"],
          },
        },
      },
    },
#
  },
}

What this gives you

  • Kingler answers on WhatsApp and its Telegram bot
  • Ditto answers on its own Telegram bot
  • Kingler can later spawn Ditto with agentId: "ditto"
  • Memory/session/auth stay isolated as long as workspace + agentDir stay separate

Important notes

  • This is dual-channel, not auto-failover. If WhatsApp is down, family must message Kingler’s Telegram bot. OpenClaw will not silently reroute a WhatsApp conversation to Telegram.
  • Do not reuse agentDir or workspace between Kingler and Ditto.
  • If you want same-person continuity across WA + TG, add session.identityLinks. If not, leave it out and keep channels separate.
  • Telegram bots use BotFather token in config, not channels login. WhatsApp still uses:
#

If you want, I can do a second pass and turn this into a more production-shaped version for your exact family/operator policy, for example:

  • family on Kingler TG bot
  • only you on Ditto TG bot
  • optional identityLinks for Balaji/Reva/Maya
sick ridge
#

what is identity links and how does it work across channels

tough axleBOT
#

I’m checking the session docs/source for the exact identityLinks behavior, because this is one of those settings where details matter.

#

identityLinks is an explicit map of “these different sender IDs are actually the same person.”

It only affects DM session grouping. It does not:

  • grant access
  • pair accounts
  • reroute messages
  • merge groups/topics/channels automatically

What it looks like

session: {
  dmScope: "per-peer",
  identityLinks: {
    balaji: [
      "whatsapp:+15551230001",
#
      "telegram:123456789",
    ],
    reva: [
      "whatsapp:+15551230002",
      "telegram:234567890",
    ],
  },
}

How it works

OpenClaw normally keys DM sessions by the raw sender ID.

So without identityLinks, Balaji would look like two different people:

  • WhatsApp: +15551230001
#
  • Telegram: 123456789

With identityLinks, OpenClaw can replace both with one canonical identity, like balaji.


The important part: behavior depends on dmScope

1. dmScope: "main"

All DMs already share one session, so identityLinks basically does nothing useful.

2. dmScope: "per-peer"

This is the real cross-channel merge mode.

With the config above:

  • Balaji on WhatsApp → agent:kingler:direct:balaji
  • Balaji on Telegram → agent:kingler:direct:balaji
#

So WA + TG reuse the same DM session.

3. dmScope: "per-channel-peer"

This keeps channels separate, but normalizes the person inside each channel.

You’d get:

  • WhatsApp Balaji → agent:kingler:whatsapp:direct:balaji
  • Telegram Balaji → agent:kingler:telegram:direct:balaji

So same human name, different sessions per channel.

4. dmScope: "per-account-channel-peer"

Same idea, but also split by account if you have multiple bots/accounts.


Practical recommendation for your setup

#

If your goal is:

“Balaji on WA and TG should feel like one continuous thread”

Use:

session: {
  dmScope: "per-peer",
  identityLinks: {
    balaji: ["whatsapp:+15551230001", "telegram:123456789"],
  },
}

“Keep WA and TG separate, but still know they’re the same person”

Use:

#
session: {
  dmScope: "per-channel-peer",
  identityLinks: {
    balaji: ["whatsapp:+15551230001", "telegram:123456789"],
  },
}

For your family setup, I’d usually start with per-channel-peer, because it’s safer and easier to reason about.

One more important limit

identityLinks applies to direct-message scopes, not group/topic routing. It’s really a DM identity normalization feature.

Docs:

#

If you want, I can sketch exactly how I’d set identityLinks for Balaji/Reva/Maya in your Kingler config, with the safer dmScope choice.

sick ridge
#

i want to test dmScope: "per-peer" + identityLinks for the main family member; can you sketch kingler config

tough axleBOT
#

I’m just checking the session CLI spelling so I can give you a clean test loop, not a hand-wavy one.

#

Yep. If you want to test only the main family member with dmScope: "per-peer" + identityLinks, I’d do something like this for Kingler.

{
  session: {
    // Cross-channel DM continuity for linked identities
    dmScope: "per-peer",

    // Start with ONE linked person only, so the blast radius stays small
    identityLinks: {
      balaji: [
        "whatsapp:+15551230001", // Balaji WA number
        "telegram:123456789",    // Balaji Telegram user id
      ],
    },
  },
#

  agents: {
    list: [
      {
        id: "kingler",
        default: true,
        workspace: "~/.openclaw/workspace-kingler",
        agentDir: "~/.openclaw/agents/kingler/agent",
        subagents: {
          allowAgents: ["ditto"],
          requireAgentId: true,
        },
      },
    ],
  },
#

  bindings: [
    { agentId: "kingler", match: { channel: "whatsapp", accountId: "kingler-wa" } },
    { agentId: "kingler", match: { channel: "telegram", accountId: "kingler-tg" } },
  ],

  channels: {
    whatsapp: {
      defaultAccount: "kingler-wa",
      accounts: {
        "kingler-wa": {
          dmPolicy: "allowlist",
          allowFrom: [
            "+15551230001", // Balaji
            "+15551230002", // Reva
#
            "+15551230003", // Maya
          ],
        },
      },
    },

    telegram: {
      defaultAccount: "kingler-tg",
      groupPolicy: "disabled",
      accounts: {
        "kingler-tg": {
          botToken: "123456:KINGLER_BOT_TOKEN",
          dmPolicy: "allowlist",
          allowFrom: [
            "tg:123456789", // Balaji
#
            "tg:234567890", // Reva
            "tg:345678901", // Maya
          ],
        },
      },
    },
  },
}

What this does

With that config:

  • Balaji on WhatsApp and Balaji on Telegram both land in the same DM session
  • Reva and Maya are still allowed, but not linked cross-channel
  • Their sessions stay separate unless you add them to identityLinks
#

So Balaji’s shared session key should look like:

agent:kingler:direct:balaji

Good first test

  1. DM Kingler on WhatsApp as Balaji:

    my test token is mango-47

  2. Then DM Kingler on Telegram as Balaji:

    what was my test token?

If identityLinks is working, Kingler should remember mango-47.

Verify from CLI

#
openclaw agents list --bindings
openclaw channels status --probe
openclaw sessions --agent kingler --json

You want to see a session key like:

agent:kingler:direct:balaji

Important caution

This gives you shared memory/context, not automatic transport failover.

So:

  • Balaji can continue the same conversation on WA or TG
#
  • replies still go back to whichever channel the latest message came from

If you want, I can also sketch the next-step version where:

  • Balaji is linked first
  • Reva/Maya stay unlinked
  • Ditto TG bot is added alongside this without changing the Kingler session behavior
sick ridge
#

Proposed config additions:

session: {
  dmScope: "per-peer",
  identityLinks: {
    balaji: [
      "whatsapp:+91XXXXXXXXXX",
      "telegram:XXXXXXXXX"
    ]
  }
}

bindings: [
  { agentId: "main", match: { channel: "whatsapp" } },
  { agentId: "main", match: { channel: "telegram", accountId: "kingler-tg" } },
  { agentId: "ditto", match: { channel: "telegram", accountId: "ditto-ops" } }
]

channels: {
  telegram: {
    defaultAccount: "kingler-tg",
    groupPolicy: "disabled",
    accounts: {
      "kingler-tg": {
        botToken: { source: "env", provider: "default", id: "TELEGRAM_KINGLER_BOT_TOKEN" },
        dmPolicy: "allowlist",
        allowFrom: ["tg:XXXXXXXXX"]
      },
      "ditto-ops": {
        botToken: { source: "env", provider: "default", id: "TELEGRAM_DITTO_BOT_TOKEN" },
        dmPolicy: "allowlist",
        allowFrom: ["tg:XXXXXXXXX"]
      }
    }
  }
}

Three questions:

  1. WhatsApp binding without accountId — our current config has no named WhatsApp account (accountId), WhatsApp just works via the channels.whatsapp block. Is { agentId: "main", match: { channel: "whatsapp" } } the correct binding form when there's no named account? Or does introducing a bindings block require us to also name the WhatsApp account explicitly?

  2. identityLinks format — is "telegram:NUMERIC_USER_ID" the correct format for a Telegram numeric user ID in identityLinks? Or should it be "telegram:tg:ID" or some other form?

  3. Introducing bindings for the first time — are there any gotchas when adding a bindings block to a config that previously had none? Does it override or supplement the existing channel routing, or does adding bindings change how all channel traffic is routed from that point on?

tough axleBOT
#

I’m verifying the routing edge cases in the docs/source, especially the “omitted accountId” bit, because that one bites people later.

#

Yep, your sketch is basically sane. Answers inline:

1) WhatsApp binding without accountId

Yes, that form is correct if WhatsApp is currently your only/default WhatsApp account:

{ agentId: "main", match: { channel: "whatsapp" } }

That does not force you to name the WhatsApp account.

The important nuance is:

  • omitting accountId = matches the default account only
  • accountId: "*" = matches all accounts on that channel

So for your current setup, this is fine.

#

The gotcha is later. If you eventually add a second WhatsApp account, that same binding will still only match the default one.


2) identityLinks format

Yes, use:

"telegram:NUMERIC_USER_ID"

and

"whatsapp:+91XXXXXXXXXX"
#

So your example:

identityLinks: {
  balaji: [
    "whatsapp:+91XXXXXXXXXX",
    "telegram:XXXXXXXXX"
  ]
}

is the right shape.

I would not use:

#
"telegram:tg:ID"

That extra tg: is not the right form here.

Small distinction:

  • allowFrom can use tg:123456789
  • identityLinks should use provider-prefixed peer ids, like telegram:123456789

3) Adding bindings for the first time

It supplements and changes routing, not just “adds a comment on the side”.

From that point on, inbound routing works like this:

#
  1. try the most specific matching bindings
  2. if nothing matches, fall back to the default agent

So adding bindings means:

  • matched traffic now routes by those rules
  • unmatched traffic still falls back to your default agent (main in your case)

It does not replace channel auth/access policy:

  • dmPolicy
  • allowFrom
  • groupPolicy
  • etc.

Those still apply separately.

Practical effect for your sketch

With:

#
bindings: [
  { agentId: "main",  match: { channel: "whatsapp" } },
  { agentId: "main",  match: { channel: "telegram", accountId: "kingler-tg" } },
  { agentId: "ditto", match: { channel: "telegram", accountId: "ditto-ops" } }
]

you’d get:

  • WhatsApp default account → main
  • Telegram account kingler-tg → main
  • Telegram account ditto-ops → ditto
  • anything else unmatched → fallback to default agent

#

Two small cautions

A) dmScope: "per-peer" is powerful

For Balaji, WA + TG will become one shared DM session.

That is exactly what you want for the test, but it also means context can jump channels for that linked user.

B) Your WhatsApp binding is okay now, but future-sensitive

Today:

{ agentId: "main", match: { channel: "whatsapp" } }

Later, if you add another WhatsApp account and want all WhatsApp to stay on main, you’d need:

#
{ agentId: "main", match: { channel: "whatsapp", accountId: "*" } }

My recommendation

For your current test, I’d keep exactly this shape:

session: {
  dmScope: "per-peer",
  identityLinks: {
    balaji: [
      "whatsapp:+91XXXXXXXXXX",
      "telegram:XXXXXXXXX"
#
    ]
  }
}

and

{ agentId: "main", match: { channel: "whatsapp" } }

That’s the cleanest low-risk first pass.

After applying it, I’d verify with:

#
openclaw agents list --bindings
openclaw channels status --probe
openclaw sessions --agent main --json

If you want, I can do one more pass and turn your exact snippet into a final cleaned-up JSON5 block with comments removed, ready to paste.