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.
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.
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();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". UseresponseMode: "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.