When Should an AI Speak in a Group Chat?
Short answer: an AI in a group chat should speak when someone addresses it or asks a question that is clearly for it, and stay silent when people are talking to each other. When it does speak, it answers the whole burst once, in one or two short texts, and uses a tapback when a sentence would add nothing. It never replies to another bot, never texts the group on a schedule, keeps what members told it privately out of the group, and says up front that it is automated. ZeroSettle's hosted AI agent follows most of these rules on its own; with your own model, you add a small decision step before the main model call.
Key takeaways
- Silence is the default. In a 1:1 thread every message is for the assistant; in a group most are not. Speak when addressed, or when a question only the assistant can answer goes unanswered.
- One answer per burst, short. Wait for people to finish, answer everything since the assistant's last text at once, and keep it to one or two texts.
- No loops, no schedules. ZeroSettle's hosted agent never answers another ZeroSettle number, and its triggers never fire into a group. Your own code should do the same.
- Your own code needs a gate. Cheap checks (its name, an inline reply to its message) first, then a classifier that returns reply, react or silent, before the expensive answer.
- Consent covers every member. Up to 31 people can be in a group, and a STOP from any of them blocks sends into every group they're in.
Put an assistant in a group chat and the first thing people notice is not how smart it is. It is whether it talks too much. An assistant that chimes in on every "sounds good" gets the group muted within a day. One that never answers gets ignored. This post is about the line between those two: the design rules we'd use for any group chat AI, how ZeroSettle's hosted agent already handles groups, and the decision step you'd add in front of your own model.
When should an AI speak in a group chat?
An AI should speak in a group chat in two cases: when someone addresses it, and when someone asks a question that is clearly for it and nobody else can answer. Everything else is a reason to stay quiet.
- Addressed by name: "Harbor & Pine, what time does the crew start?"
- An inline reply to one of its messages: someone swipes to reply to the assistant's last text. In the API that message carries
reply_topointing at the assistant's message. - A question in its lane that is going unanswered: "does anyone know if we need to move the couch?" in a group for a painting job, where the assistant has the prep checklist.
And it should stay silent when:
- a member asks another member something ("Luis, are we still on for Thursday?");
- people are making plans, joking or catching up;
- the message is an acknowledgment ("ok", "thanks Marco", "👍");
- a person in the group has already answered the question;
- it's a tapback, a poll vote, or a rename or someone joining.
When in doubt, silence. A missed answer costs one member a second message ("Harbor & Pine?"). An unwanted answer costs the whole group's attention, every time.
Why is a group chat harder than a 1:1 thread?
A group chat is harder because in a 1:1 thread every message is meant for the assistant, and in a group most messages are not. A 1:1 assistant only has to decide what to say; a group assistant first has to decide whether to say anything.
The stakes are different too:
- Every text reaches everyone. A group on a ZeroSettle number holds up to 31 people. A wrong or chatty answer interrupts all of them.
- People leave or opt out. On ZeroSettle, a STOP from any member blocks sends from all of your numbers into every group that person is in, until they are removed or text START. One annoyed member can shut the assistant out of the group for everyone.
- Bursts are bigger. Three people typing at once produce a dozen bubbles in a minute. Answering each one would flood the thread.
- Other bots may be present. Another business's assistant, or another of your own numbers, can be a member. Two assistants that answer each other can text forever.
- Every text costs something. With ZeroSettle's hosted agent, each text it sends uses a credit, in a group as anywhere else. With your own model, each answer is a model call.
What rules should a group chat AI follow?
A group chat AI should follow nine rules: answer when addressed, stay silent otherwise, answer each burst once, keep it short, prefer tapbacks to filler, never loop with bots, never text on a schedule, keep private things private, and disclose that it is automated. The table shows where each rule lives on ZeroSettle.
| Rule | Hosted agent (Managed by AI Agent) | Your own code (Managed by API) |
|---|---|---|
| Answer when addressed | Built in: answers only when a message is meant for it or asks something it can help with | Name match, a reply_to pointing at its message, then a classifier |
| Silent when people talk to each other | Built in | The classifier's default answer is "silent" |
| One answer per burst | Built in: answers everything since its last text in one go | Wait a few seconds per chat_id and reset the wait on each new message |
| Keep it short | Built in | At most two bubbles in a group; say so in the prompt and enforce it in code |
| Tapback instead of filler | Can send nothing when a reply would add nothing; tapbacks use no credit | A "react" decision maps to POST /api/imessage/reactions |
| Never loop with bots | Never answers another ZeroSettle number; when several of your agent numbers share a group, only one answers | Ignore known bot handles; cap replies per group per hour |
| No scheduled texts | Triggers (the follow-up, daily texts) never fire into a group | Don't point your own scheduled jobs at a chat_id |
| Member privacy | Replies into the group, never in a 1:1 chat | Build the group's context from ?chat_id= only, never from members' 1:1 threads |
| Disclosure | Says it is your automated assistant if sincerely asked | Say so in the message that starts the group |
Tapbacks, poll votes and group events (a rename, someone joining or leaving) never wake the hosted agent. In the API, group events arrive as their own group.event webhook, never as message.received.
Answer the burst, not the bubble
People send one thought as three bubbles, and in a group three people do it at once. Wait until the thread goes quiet for a few seconds, then read everything since the assistant's last text and answer it together. If two members asked two different things, one reply can cover both ("Dana: the crew starts at 8. Marco: the trim is the same white as the doors.").
Keep it short, then stop
In a 1:1 thread the docs suggest one to three bubbles. In a group, aim for one, two at most, and don't end on a question unless the assistant needs the answer to help. A trailing "Anything else I can help with?" invites a reply from everyone, and it is exactly the kind of text that makes people mute a group.
Use tapbacks for acknowledgments
When a member says "perfect, thank you!" to the assistant, a ❤️ is the right answer and a sentence is worse. ZeroSettle sends tapbacks into a group with POST /api/imessage/reactions and the message_id of the group message. A tapback is not a text, so it never uses a credit. More on tapbacks in photos, voice messages and tapbacks over the API.
What does a well-timed answer look like?
A well-timed answer comes after a stretch of silence, answers exactly what was asked, and ends the assistant's turn. Here is a group for a painting job: the homeowner, the project manager, the crew lead, and the painting company's number with its assistant.
- Wed 7:52 AM
- Dana (homeowner): Dana
Luis are you guys still good for Thursday? - Luis (project manager): Luis
Yep, Marco's crew starts at 8 - Marco (crew lead): Marco
We'll need the garage code - Dana (homeowner): Dana
I'll text it to you Marco - No reply: they're talking to each other
- Wed 9:14 AM
- Dana (homeowner): Dana
Harbor & Pine, what do we need to move out of the kitchen before Thursday? - Harbor & Pine assistant: Clear the counters and take down anything on the walls. Small furniture can go to the middle of the room and the crew will cover it.
- Harbor & Pine assistant: Marco's crew handles outlet covers and taping.
- Dana (homeowner): Dana
perfect thank you!❤️ - A tapback, not another text
Notice what it didn't do. It didn't confirm Thursday (Luis owns the schedule), didn't offer to send the garage code, and didn't add "Let me know if you have questions!" Home service businesses can run one group per job; one iMessage group per job walks through setting it up.
How does ZeroSettle's hosted agent behave in a group?
ZeroSettle's hosted agent answers inside a group only when a message is meant for it or asks something it can help with, keeps it short, and stays silent when people are talking to each other. You don't switch anything on: group chats are on for every number, and a number managed by the agent answers in its groups the same way it answers 1:1.
From the AI agent docs, in a group the agent:
- reads the group's recent messages, each labeled with who wrote it (the name you started their conversation with, else their number), and knows the group's name and members;
- replies into the group, with its typing bubble there, never in a 1:1 chat;
- uses one credit per text, as anywhere else, with the same limits per group;
- never fires a trigger (the follow-up, daily texts) into a group;
- isn't woken by tapbacks, poll votes or group events;
- never answers a message from another ZeroSettle number, 1:1 or in a group, so two agents can't keep texting each other;
- when several of your numbers with the agent on are in one group, only one of them answers.
What you control is the business context. The agent's group behavior is built in, but you can narrow what it answers in groups and how it hands off. A few lines are enough:
In group chats:
- You are one member of a job group: the homeowner, our project manager
and the crew lead are the others.
- Answer only questions about the schedule, prep, colors and what happens
next. Price, payment or change orders: say Luis will confirm, then stop.
- Never repeat anything from a private conversation with one member.
- One or two short texts. Don't end on a question unless you need the answer.
Two things to know before you choose it: group messages are not logged in Close or HubSpot, and while the agent manages a number, inbound messages go to the agent instead of your webhook. Step-by-step setup is in AI agent in the console, and how the hosted agent answers iMessages covers its settings and trade-offs.
How do you build the rules into your own code?
With your own model, you put a decision step in front of the main model call: cheap checks first, then a short classifier call that returns reply, react or silent, and only a reply pays for the full answer. Set the number to managed_by: "api" first so group messages reach your webhook.
Group messages arrive as the same message.received event as 1:1 ones, with one difference: message.chat_id is set, and handle is the member who sent it. Your webhook branches on that key. Everything else (the signed receiver, the per-sender queue, the 1:1 reply loop) is the same as in Building iMessage Agents.
// src/group-gate.ts: called by your webhook for each message.received with a chat_id.
// group.event (renames, joins, leaves) arrives as its own event: never route it here.
import { answer, nameFor } from "./agent.js"; // your main model call; names from your CRM
import { gate } from "./gate.js";
type Msg = {
id: number; kind: string; direction: "in" | "out"; handle: string;
text?: string; chat_id?: string; reply_to?: { id: number | null };
};
const BASE = "https://portal.zerosettle.io";
const NAME = /\b(harbor (&|and) pine)\b/i; // what members call it
const BOTS = new Set((process.env.BOT_HANDLES ?? "").split(",").filter(Boolean));
const NOT_SPEECH = new Set(["reaction", "poll_vote", "group_event"]); // rows that never wake it
const waits = new Map<string, NodeJS.Timeout>();
const spoke = new Map<string, number[]>(); // chat_id -> reply times
async function zs(path: string, body?: unknown) {
const r = await fetch(BASE + path, {
method: body ? "POST" : "GET",
headers: { Authorization: `Bearer ${process.env.ZEROSETTLE_TOKEN}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
return r.json();
}
export function onGroupMessage(m: Msg) {
if (NOT_SPEECH.has(m.kind)) return; // tapbacks, votes, group events
if (BOTS.has(m.handle)) return; // never answer a bot
clearTimeout(waits.get(m.chat_id!)); // one answer per burst
waits.set(m.chat_id!, setTimeout(() => void decide(m), 4000));
}
async function decide(latest: Msg) {
const chatId = latest.chat_id!;
waits.delete(chatId);
const q = new URLSearchParams({ chat_id: chatId, before_id: String(latest.id + 1), limit: "30" });
const rows: Msg[] = (await zs(`/api/imessage/messages?${q}`)).messages;
const lastOut = rows.findLastIndex((r) => r.direction === "out");
const burst = rows.slice(lastOut + 1).filter((r) => r.direction === "in" && !NOT_SPEECH.has(r.kind) && !BOTS.has(r.handle));
if (burst.length === 0 || overBudget(chatId)) return;
const ours = new Set(rows.filter((r) => r.direction === "out").map((r) => r.id));
const addressed = burst.some((r) => NAME.test(r.text ?? "") || ours.has(r.reply_to?.id ?? -1));
const d = addressed ? { action: "reply", reaction: null } : await gate(transcript(rows));
if (d.action === "react" && d.reaction) {
await zs("/api/imessage/reactions", { message_id: latest.id, reaction: d.reaction });
} else if (d.action === "reply") {
await zs("/api/imessage/typing", { chat_id: chatId, state: "start" });
const text = await answer(transcript(rows)); // your main model call
for (const bubble of text.split(/\n\s*\n/).filter(Boolean).slice(0, 2)) {
await zs("/api/imessage/send", { chat_id: chatId, text: bubble });
}
spoke.set(chatId, [...(spoke.get(chatId) ?? []), Date.now()]);
}
}
function overBudget(chatId: string) { // a loop breaker
const recent = (spoke.get(chatId) ?? []).filter((t) => t > Date.now() - 3_600_000);
spoke.set(chatId, recent);
return recent.length >= 6;
}
function transcript(rows: Msg[]) {
return rows.map((r) => `${r.direction === "out" ? "Assistant" : nameFor(r.handle)}: ${r.text ?? `[${r.kind}]`}`).join("\n");
}
The gate itself is one short model call with a structured answer. It sees only the group's transcript and returns a decision, never a message to send:
// src/gate.ts
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
const claude = new Anthropic(); // reads ANTHROPIC_API_KEY
const Decision = z.object({
action: z.enum(["reply", "react", "silent"]),
reaction: z.enum(["love", "like", "laugh", "emphasize"]).nullable(),
reason: z.string(),
});
const GATE = `You decide whether Harbor & Pine's assistant should speak in an iMessage group.
The other members are a homeowner and our team, mostly talking to each other.
reply: someone asked the assistant, or asked a question in its lane (schedule, prep,
colors, next steps) that no member has answered.
react: a thanks, an "ok" or good news aimed at the assistant's last message.
silent: everything else, including questions to a named person and small talk.
When unsure, choose silent.`;
export async function gate(transcript: string) {
const r = await claude.messages.parse({
model: "claude-opus-5-5",
max_tokens: 4096, // room for thinking plus the small JSON answer
output_config: { effort: "low", format: zodOutputFormat(Decision) },
system: GATE,
messages: [{ role: "user", content: transcript }],
});
return r.parsed_output ?? { action: "silent", reaction: null, reason: "no decision" };
}
A few details carry the design:
- Cheap signals skip the gate. A name match or an inline reply to the assistant's message goes straight to an answer. The classifier only runs on the ambiguous middle.
- Failure means silence. If the gate's output can't be parsed, the assistant stays quiet. A group forgives a missed answer more easily than a wrong one.
- Log the
reason. When the assistant speaks out of turn, the reason field is how you find out why and fix the prompt. - Handle STOP as final. A send into a group with a member who texted STOP comes back
403 recipient_opted_outwitherror.handle. Don't retry; remove that member or leave the group. - Typing goes to the group.
POST /api/imessage/typingtakeschat_idinstead ofto, so the "…" shows in the group while the answer is written.
Request shapes are in the group chats docs and webhooks, step by step. For a full build, see how to build an AI assistant for iMessage group chats, and for creating, renaming and managing groups, the iMessage group chat API guide.
What should a group chat AI never do?
A group chat AI should never reply to another bot, never text the group on its own schedule, never carry one member's private conversation into the group, and never stay in a group where someone no longer wants to hear from it.
- Never answer another bot. ZeroSettle's hosted agent ignores every ZeroSettle number. In your own code, keep a list of automated handles you know: your own numbers (
handleinGET /api/imessage/line, plus eachnumberin itslineswhen you have several) and any other assistant you've seen in the group. The per-group budget above catches the ones you don't know. - Never text the group on a schedule. A follow-up that works in a 1:1 thread ("just checking in!") lands on every member at once. The hosted agent's triggers never fire into a group; do the same in your code. If the group explicitly asks for a reminder, treat it as one delayed answer, not a recurring job.
- Never bring a private thread into the group. A homeowner may have told the assistant 1:1 about a budget or a lockbox code. Build the group's context from
GET /api/imessage/messages?chat_id=…only;?handle=returns a member's 1:1 conversation, which has no place in a group answer. - Never move members into 1:1 on its own. Starting a private thread with someone because they're in a group is a new conversation they didn't ask for. Answer in the group, or ask them to text the number directly.
- Never add people without consent. A group message reaches every member, and each must have agreed to hear from you. Each member the number has never messaged counts as a new contact when you start the group or add them. More in iMessage and TCPA consent (not legal advice) and how STOP works.
- Never hide that it's automated. Say so in the first message that starts the group or introduces the assistant, so every member sees it once: "Hi all, this is Harbor & Pine's automated assistant. Ask me about the schedule or prep by name; Luis handles everything else." ZeroSettle's terms require “transparent disclosure of AI interaction in systems like chatbots, unless it is contextually evident.”
Broadcast is not a group
POST /api/imessage/broadcast sends each recipient their own 1:1 message, and recipients never see each other. Use a group only when members should see each other's messages, and only with people who agreed to be in it.
Should you put the hosted agent in your groups?
Put the hosted agent in your groups when its answers fit in a written business context and you want group behavior handled for you; build your own gate when the assistant needs your systems, your model or rules the hosted agent doesn't offer.
| If you need | Choose |
|---|---|
| Answers from a brief, files and MCP tools, with nothing to host | Hosted agent |
| Built-in silence, no triggers into groups and no bot loops | Hosted agent |
| Your own model, your own "when to speak" thresholds, or per-group rules | Your own code |
| Group messages in your own database or tools | Your own code (the webhook carries chat_id) |
On price: Pro is $49 a month for one dedicated number, with 5 new contacts a day and 500 free credits for the hosted agent, once. Growth is $199 a month per number (upgrade in the console), with 50 new contacts a day per number (and 3 per 15 minutes), team members and 1,000 free credits, once; from a Growth link it is $599 a month for 3 numbers plus $199 for each extra number. Enterprise is custom pricing through sales for high volume and compliance needs (book a demo). Free credits never refill; packs are 1,000 for $50 or 5,000 for $200 and never expire. Each number allows 10,000 messages a day, sent and received, with known contacts, and the subscription has a 30-day money-back guarantee.
For ideas on where a group assistant earns its place, see a group trip planner and an assistant for teams, clubs and communities. To try it, get a number, start a group with a couple of teammates, and spend a day trying to make the assistant talk out of turn.
Frequently asked questions
Does ZeroSettle's hosted AI agent answer in group chats?
Yes. Group chats are on for every number, and on a number the hosted agent manages it answers inside the group, only when a message is meant for it or asks something it can help with. It never answers another ZeroSettle number, and its triggers (the follow-up, daily texts) never fire into a group.
How does an AI know a group message is meant for it?
The hosted agent decides from the group's recent messages, each labeled with who wrote it. In your own code, use cheap signals first (its name in the text, or an inline reply whose reply_to points at one of its messages), then a short classifier call for everything else, with silence as the default.
Can two AI assistants get stuck replying to each other in a group?
Not with the hosted agent: it never replies to another ZeroSettle number, and when several of your numbers with the agent on share a group, only one of them answers. In your own code, ignore handles you know are automated and cap how many times the assistant can speak in one group per hour.
Does every group message use credits?
No. Only texts the hosted agent sends use credits, one per text. Reading the group, staying silent, typing and tapbacks use none. Free credits come once (500 on Pro, 1,000 on Growth) and packs are 1,000 for $50 or 5,000 for $200. A number your own server answers uses no credits, videos aside.
Do I need consent from everyone in the group?
Yes. A group message reaches every member, and each of them must have agreed to hear from you. A STOP from any member blocks sends into every group they are in, and each member your number has never messaged counts as a new contact when you start the group or add them. This is not legal advice.
Should the assistant tell the group it is an AI?
Yes, up front. Say it in the message that starts the group or introduces the assistant, so every member sees it once. ZeroSettle's terms require transparent disclosure of AI interaction unless it is contextually evident, and the hosted agent says it is your automated assistant if sincerely asked.
Sources
- ZeroSettle iMessage API docs: “Group chats”, “AI agent” (Managed by AI Agent, In group chats, Triggers, Credits, Rules of thumb), “AI agent in the console”, “Send a message” (Broadcast, Replies, Reactions), “Read messages”, “Typing indicators”, “Webhooks, step by step” and “Errors, limits, and compliance”. Checked November 2, 2026.
- ZeroSettle iMessage API Terms of Service, section 4 (Compliance with Legal and Ethical Standards). Checked November 2, 2026.