Status: Experimental. Legacy WhatsApp broadcast arrays remain supported.

Overview

Agent group threads run multiple agents on the same inbound message, using the top-level broadcast config. Each agent runs in its own session. Channel-qualified entries can select participants by mention and allow a bounded number of follow-up rounds so agents can build on sibling replies.

Channel allowlists and group activation rules still apply. For qualified entries on Discord, Slack, and Telegram, an explicit mention of any configured participant can satisfy the room’s mention gate, even when that participant is not the ordinary routed agent. Legacy WhatsApp entries keep their existing admission behavior.

The live WhatsApp QA lane includes whatsapp-broadcast-group-fanout, which verifies that one mentioned group message can produce distinct visible replies from two configured agents.

Configuration

Agent group threads

Use a key in the form "<channel>:<peerId>", such as "discord:123456789", "slack:C0123", "telegram:-100123", or "whatsapp:1203@g.us". The value can be an agent ID array or a strict object:

{
  agents: {
    ownership: "explicit",
    entries: {
      reviewer: {
        name: "Reviewer",
        groupChat: { mentionPatterns: ["@reviewer\\b"] },
      },
      writer: {
        name: "Writer",
        groupChat: { mentionPatterns: ["@writer\\b"] },
      },
    },
  },
  bindings: [{ agentId: "reviewer", match: { channel: "telegram" } }],
  broadcast: {
    "telegram:-100123": {
      agents: ["reviewer", "writer"],
      mentionGating: true,
      maxRounds: 2,
      maxTurns: 4,
    },
  },
}

The ordinary channel route still needs an agent; the binding above selects Reviewer for admission before group dispatch. After the room is allowed by its channel config, send @reviewer @writer Review this draft. Both participants can answer the initial message and, within the budget, add something new in one follow-up round. Send @writer to select only Writer for the initial round.

Object field Default Contract
agents Required Configured agent IDs; at most 16 participants.
mentionGating true Select explicitly mentioned participants; if none match, select all.
maxRounds 1 Integer from 1 to 4, including the initial round.
maxTurns agents.length Integer from 1 to 32; total participant turns started for one inbound message.

Unknown object fields are rejected. Qualified arrays use the same defaults: "slack:C0123": ["reviewer", "writer"] runs one initial round with mention selection. A qualified WhatsApp key takes precedence over an unqualified key for the same peer. Unqualified object entries are not supported.

maxTurns counts agent runs started by the coordinator, including runs that pass or fail. Slots are reserved synchronously before parallel launch, so parallel participants cannot overspend the budget. If the budget is smaller than the eligible participant count, configured order determines which turns start. A turn can produce multiple platform messages through chunks, previews, or message-tool sends. Those deliveries are governed by the agent run and channel transport; maxTurns does not count, buffer, or cap physical messages.

Telegram, Discord, and Slack disable their shared preview and progress drafts for qualified group threads so concurrent participants do not overwrite each other's drafts. Final replies, block replies, and message-tool sends remain available.

The default turn budget covers one turn per configured agent. To let every agent run twice, set maxRounds: 2 and maxTurns to twice the participant count.

Mention selection

Selection uses only explicit @-style matches in the current inbound text, computed once for the participant set. A name in prose or a bare emoji does not select a participant. Mention patterns resolve from the agent’s groupChat.mentionPatterns, then messages.groupChat.mentionPatterns, then its identity-derived patterns. Give participants distinct patterns when you want to address them separately.

With mentionGating: true, a match selects only the matching participants for round 1; no matches selects all. With mentionGating: false, all participants are selected. This option does not turn off the channel’s requireMention policy, sender allowlists, or command authorization.

Bounded follow-up rounds

After a completed round, another round can run only within both maxRounds and maxTurns. Eligible participants are those that produced a final reply in the previous round or were addressed by name in a sibling’s final reply. Each participant's final text is limited to 4,000 characters in the digest; the combined sibling text is limited to 16,000 characters. Each receives an attributed, size-bounded digest of sibling finals from that round, with an instruction to reply only when adding something new and otherwise return NO_REPLY. Passing does not produce a visible final reply.

All participants passing ends the thread. Reaching either limit or cancellation also stops further turns. Each continuation has its own internal identity; it is not a replay of the physical inbound message. Sequential strategy changes launch order within a round; it does not turn that round into a pipeline where each participant sees earlier replies from the same round.

Budget state is in memory, scoped to the channel, account, conversation, thread, and root inbound message. It is not restart-resumable: a Gateway restart loses the active round and budget state. Ordinary inbound deduplication remains a separate protection.

Participant labels

When a qualified entry configures more than one participant, Discord, Slack, and Telegram replies begin with the participant name in bold. The configured count controls labeling, even if mention selection, the turn budget, or silence leaves only one responder. WhatsApp presentation remains unchanged.

Basic setup

Legacy single-pass setup uses unqualified WhatsApp peer IDs as keys and arrays of agent IDs as values:

{
  "broadcast": {
    "120363403215116621@g.us": ["alfred", "baerbel", "assistant3"]
  }
}

Result: when Apus would reply in this chat, it runs all three agents.

Every listed agent ID must exist in the configured roster: config validation rejects unknown IDs in both arrays and objects. Deleting an agent prunes it from both forms.

Runtime membership uses the canonical agents.entries roster. Run apus doctor --fix to migrate a legacy agents.list roster before startup.

Processing strategy

broadcast.strategy sets how agents process the message:

Strategy Behavior
parallel (default) All agents process simultaneously; replies arrive in any order.
sequential Agents process in array order; each waits for the previous to finish.
{
  "broadcast": {
    "strategy": "sequential",
    "120363403215116621@g.us": ["alfred", "baerbel"]
  }
}

Complete example

{
  "agents": {
    "ownership": "explicit",
    "defaults": {
      "authInheritance": { "agentId": "code-reviewer" },
      "heartbeat": { "agentId": "code-reviewer" },
      "systemAgent": { "agentId": "code-reviewer" }
    },
    "entries": {
      "code-reviewer": {
        "name": "Code Reviewer",
        "workspace": "/path/to/code-reviewer",
        "sandbox": { "mode": "all" }
      },
      "security-auditor": {
        "name": "Security Auditor",
        "workspace": "/path/to/security-auditor",
        "sandbox": { "mode": "all" }
      },
      "docs-generator": {
        "name": "Documentation Generator",
        "workspace": "/path/to/docs-generator",
        "sandbox": { "mode": "all" }
      }
    }
  },
  "bindings": [
    { "agentId": "code-reviewer", "match": { "channel": "whatsapp", "accountId": "*" } }
  ],
  "talk": { "agentId": "code-reviewer" },
  "broadcast": {
    "strategy": "parallel",
    "120363403215116621@g.us": ["code-reviewer", "security-auditor", "docs-generator"]
  }
}

How it works

Message flow

Incoming message arrives

A channel message arrives.

Route and admission

Apus applies channel allowlists, group activation rules, and configured ACP binding ownership.

Broadcast check

If no configured ACP binding owns the route, Apus checks the qualified channel/peer key, then the legacy peer key for WhatsApp.

If broadcast applies

- Selected participants process the message within the round and turn limits.
- Each agent has its own session key and isolated context.
- Agents process in parallel (default) or sequentially.
- WhatsApp audio attachments are transcribed once before fan-out, so agents share one transcript instead of making separate STT calls.

If broadcast does not apply

Apus dispatches the ordinary route or the configured ACP session route selected during routing.

Group threads do not bypass channel allowlists, command authorization, or exclusive ACP bindings. Participant mention admission extends the room mention gate as described above.

Session isolation

Each agent in a broadcast group maintains completely separate:

On Discord, Slack, and Telegram, reply delivery and completion hooks use the responding participant's session, and local media resolves with that participant's media roots. This also applies to qualified entries with one participant, whose replies do not have a participant name label.

On WhatsApp, one input is shared on purpose: the group context buffer (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered. It is cleared once after the fan-out completes.

This allows each agent to have different personalities, models, skills, and tool access (for example read-only vs. read-write).

Example: isolated sessions

In group 120363403215116621@g.us with agents ["alfred", "baerbel"]:

Alfred's context

```text
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: ~/apus-alfred/
Tools: read, write, exec
```

Baerbel's context

```text
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: ~/apus-baerbel/
Tools: read only
```

Use cases

Best practices

1. Keep agents focused
Give each agent a single, clear responsibility (`formatter`, `linter`, `tester`) instead of one generic "dev-helper" agent.
2. Use descriptive ids and names
```json
{
  "agents": {
    "ownership": "explicit",
    "defaults": {
      "authInheritance": { "agentId": "security-scanner" },
      "heartbeat": { "agentId": "security-scanner" },
      "systemAgent": { "agentId": "security-scanner" }
    },
    "entries": {
      "security-scanner": {
        "name": "Security Scanner",
        "workspace": "~/.apus/workspace"
      },
      "code-formatter": { "name": "Code Formatter" },
      "test-generator": { "name": "Test Generator" }
    }
  },
  "talk": { "agentId": "security-scanner" }
}
```
3. Configure different tool access
```json
{
  "agents": {
    "ownership": "explicit",
    "defaults": {
      "authInheritance": { "agentId": "reviewer" },
      "heartbeat": { "agentId": "reviewer" },
      "systemAgent": { "agentId": "reviewer" }
    },
    "entries": {
      "reviewer": {
        "workspace": "~/.apus/workspace",
        "tools": { "allow": ["read", "exec"] }
      },
      "fixer": { "tools": { "allow": ["read", "write", "edit", "exec"] } }
    }
  },
  "talk": { "agentId": "reviewer" }
}
```

`reviewer` is read-only. `fixer` can read and write.
4. Monitor performance
With many agents, prefer `"strategy": "parallel"` (default), keep broadcast groups to a handful of agents, and use faster models for simpler agents.
5. Failures stay isolated
Agents fail independently. One agent's error is logged (`Broadcast agent <id> failed: ...`) and does not block the others.

Compatibility

Providers

Channel-qualified entries use the shared core dispatch path across channel plugins. Discord, Slack, and Telegram additionally support participant mention admission and name labels. Legacy unqualified entries apply only to WhatsApp (web channel).

Routing

Broadcast groups work alongside existing routing:

{
  "bindings": [
    {
      "match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } },
      "agentId": "alfred"
    }
  ],
  "broadcast": {
    "GROUP_B": ["agent1", "agent2"]
  }
}

Precedence: broadcast takes priority over ordinary route bindings. Configured ACP bindings (bindings[].type="acp") are exclusive: when one matches, Apus dispatches to the configured ACP session instead of fan-out broadcast.

Troubleshooting

Agents not responding
**Check:**

1. Agent IDs exist in `agents.entries` (config validation rejects unknown ids).
2. The qualified channel/peer key matches the room. Legacy WhatsApp keys use a group JID like `120363403215116621@g.us`, or E.164 like `+15551234567` for DMs.
3. The message passed normal gating (mention/activation rules still apply).

**Debug:**

```bash
apus logs --follow | grep -i broadcast
```

A successful fan-out logs `Broadcasting message to <n> agents (<strategy>)`.
Only one agent responding
**Check:** explicit mentions may select one participant, `maxTurns` may allow only one run, or the others may pass. Also check whether the peer is only in ordinary route bindings or matches an exclusive configured ACP binding.

**Fix:** add ordinary route-bound peers to the broadcast config, or remove/change the configured ACP binding if fan-out broadcast is desired.
Performance issues
If slow with many agents: reduce the number of agents per group, use lighter models, and check sandbox startup time.

Examples

Example 1: Code review team
```json
{
  "broadcast": {
    "strategy": "parallel",
    "120363403215116621@g.us": [
      "code-formatter",
      "security-scanner",
      "test-coverage",
      "docs-checker"
    ]
  },
  "agents": {
    "ownership": "explicit",
    "defaults": {
      "authInheritance": { "agentId": "code-formatter" },
      "heartbeat": { "agentId": "code-formatter" },
      "systemAgent": { "agentId": "code-formatter" }
    },
    "entries": {
      "code-formatter": {
        "workspace": "~/agents/formatter",
        "tools": { "allow": ["read", "write"] }
      },
      "security-scanner": {
        "workspace": "~/agents/security",
        "tools": { "allow": ["read", "exec"] }
      },
      "test-coverage": {
        "workspace": "~/agents/testing",
        "tools": { "allow": ["read", "exec"] }
      },
      "docs-checker": { "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
    }
  },
  "bindings": [
    { "agentId": "code-formatter", "match": { "channel": "whatsapp", "accountId": "*" } }
  ],
  "talk": { "agentId": "code-formatter" }
}
```

One code snippet in the group can produce four perspectives: formatting fixes, a security finding, a coverage gap, and a docs nit.
Example 2: Multi-language pipeline
```json
{
  "broadcast": {
    "strategy": "sequential",
    "+15555550123": ["detect-language", "translator-en", "translator-de"]
  },
  "agents": {
    "ownership": "explicit",
    "defaults": {
      "authInheritance": { "agentId": "detect-language" },
      "heartbeat": { "agentId": "detect-language" },
      "systemAgent": { "agentId": "detect-language" }
    },
    "entries": {
      "detect-language": { "workspace": "~/agents/lang-detect" },
      "translator-en": { "workspace": "~/agents/translate-en" },
      "translator-de": { "workspace": "~/agents/translate-de" }
    }
  },
  "bindings": [
    { "agentId": "detect-language", "match": { "channel": "whatsapp", "accountId": "*" } }
  ],
  "talk": { "agentId": "detect-language" }
}
```

API reference

Config schema

type BroadcastGroupConfig = {
  agents: string[];
  mentionGating?: boolean;
  maxRounds?: number;
  maxTurns?: number;
};

type BroadcastConfig = {
  strategy?: "parallel" | "sequential";
  [key: string]: string[] | BroadcastGroupConfig | "parallel" | "sequential" | undefined;
};

Fields

strategy "parallel" | "sequential"

How to process eligible agents within each round. parallel launches reserved turns together; sequential runs them in configured order.

[channel:peerId] string[] | BroadcastGroupConfig

Channel-qualified peer ID. Arrays use the group-thread defaults; objects configure mention selection, rounds, and participant-turn budgets. At most 16 agents.

[peerId] string[]

Legacy WhatsApp group JID or E.164 phone number. Every listed agent processes one turn, with no internal follow-up rounds or participant selection.

Limitations

  1. Shared context: follow-up digests contain bounded sibling finals, not full sibling sessions or tool histories.
  2. Message ordering: parallel responses may arrive in any order.
  3. Rate limits: participants share the channel account’s transport limits; one turn can produce several platform messages.
  4. Recovery: round and turn-budget state is in memory and cannot resume after a Gateway restart.
  5. Control UI: a dedicated team-thread session is not yet available. Each participant keeps its own session.

Related