Build

Groups and multiple agents

Put several people and agents in one conversation, let agents collaborate and run collective votes.

A group is a conversation with several members — people, agents, or both. Every member receives the group's new messages, end-to-end encrypted, and agents answer in the group like anyone else. Groups work the same way with the JavaScript SDK, the Python SDK and the MCP server.

Create a group

Connect each agent

Each agent needs its own agent, token and running program — see the Quickstart. Keep a separate .env file per agent, for example .env-a and .env-b.

Create the group

In the app, open Conversations, tap + and choose Create a group. Give it a name.

Add the agents

Open the conversation options (), pick an agent under Add an agent to the group and add it. Repeat for each agent, then check the member list.

Mentioning @Agent in a message doesn't add it to the group. Agents only receive messages from groups they're a member of, starting with the messages sent after they joined.

Reply in a group

Use reply(): it detects that the message came from a group and answers in the group, linked to the original message.

client.onMessage(async (message, agent) => {
  if (message.text.trim() !== "!status") return;
  await agent.reply(message, { text: "Backups: all green ✅" });
});

Group messages are encrypted once for all members with a group key that the people in the group distribute to each member over the Signal protocol. The SDK fetches and stores that key for you.

Avoid agent loops

Agents in a group also receive each other's messages. If every agent answers every message, they'll talk forever. Only react to what's addressed to you — a command, a mention of your name, or messages from people.

Example: two agents, one command

Run the same program twice, once per agent. Each one answers !demo exactly once.

agent.mjs
import {
  createNodeAgentGatewayClient,
  createNodeSignalSnapshotStore,
} from "@pacerelle/sdk/node";

const agentId = process.env.PACERELLE_AGENT_ID;
const label = process.env.AGENT_LABEL ?? "Agent";
const state = createNodeSignalSnapshotStore(agentId);

const client = createNodeAgentGatewayClient({
  agentId,
  token: process.env.PACERELLE_AGENT_TOKEN,
  e2ee: true,
  signalSnapshot: state.signalSnapshot,
  onSignalSnapshot: state.onSignalSnapshot,
  onOpen: () => console.log(`[${label}] connected`),
});

client.onMessage(async (message, agent) => {
  if (message.text.trim() !== "!demo") return; // ignore everything else, including other agents
  await agent.reply(message, { text: `${label}: received and handled.` });
});

await client.connect();
Terminal
node --env-file=.env-a agent.mjs   # with AGENT_LABEL="Agent A" in .env-a
node --env-file=.env-b agent.mjs   # in a second terminal, with AGENT_LABEL="Agent B"

Send !demo in the group: you get one answer from each agent.

Collective votes

Choice and Confirm widgets can collect one answer per member of the group — people and agents alike. Send them to the group target group:<conversationId> with responseMode: "collective" (response_mode="collective" in Python).

await agent.sendChoiceWidget({
  conversationId: message.conversationId,
  to: `group:${message.conversationId}`,
  responseMode: "collective",
  title: "Which day works for the release?",
  options: [
    { id: "tue", label: "Tuesday" },
    { id: "thu", label: "Thursday" },
  ],
});
  • In groups, choices are collective by default; confirmations need responseMode: "collective". Use responseMode: "individual" to ask a single person.
  • The app shows the votes inside the original card. Each answer reaches your agent as a message: identify the voter with message.from (message.from_id), never with a name written in the payload.
  • A vote is not an automatic decision. Your program decides the rule — majority, unanimity, a quorum — and acts on it.
  • Permission requests always stay individual.

After a restart, the JavaScript SDK's listConversationMessages also returns group votes cast by agent members, so you can recount them.