Blog

Engineering

Building AI Agents That Work in Group Chats

June 25, 2026·9 min read
Building AI Agents That Work in Group Chats

Most messaging products treat group chats as larger 1:1 conversations. That misses the point. Group chats are where coordination happens: friends plan, matchmakers introduce people, concierges loop in vendors, and deals move forward. If you are building an agent for anything social or multi-party, the group thread is not a secondary feature. It is the product surface.

Linq's v3 API treats group chats as first-class objects over iMessage and RCS, with display names, icons, participant management, and webhooks for membership changes. This post covers what that makes possible, how to implement it, and the main product problem teams hit when they add an agent to a group: deciding when the agent should speak.

The examples at the end come from the open-source Linq AI agent example.

Group chats need different product logic

A 1:1 agent has a simple turn-taking model. Every inbound message is addressed to the agent, so the agent replies to everything.

That assumption fails as soon as a third participant joins. In a group, most messages are humans talking to each other. The core product question changes from "what should the agent say?" to "should the agent say anything?"

That shift enables products that cannot work in a 1:1 thread.

Matchmaking and introductions. Ditto describes itself as "your college matchmaker." It learns your type, scans the pool, and texts you a ready-to-go date over iMessage. No swiping, no long pre-date chat. Ditto reports 12,000+ dates delivered, with 70% of users getting a first date within two days.

For a matchmaker, the natural next step is the introduction itself. Put both matches in a group thread, set the group photo to a custom date card, let the agent warm up the conversation, suggest a time and place, then step back. Ditto already describes an agentic system of expert agents for analysis, matchmaking, posting, and scheduling. The group thread is where the scheduler and concierge agents become useful.

Concierge and coordination. A travel or events agent can pull a customer, a vendor, and itself into one thread to handle logistics. The agent answers when asked, stays quiet while humans work through details, and returns to confirm the final plan.

Group commerce and planning. Splitting a bill, planning a trip, and coordinating a gift all need structured work inside a social conversation. The agent can handle totals, scheduling, reminders, and follow-ups without taking over the thread.

Community and cohort bots. A class, club, or founder cohort can have an assistant that answers direct questions and reacts to milestones without replying to every message.

All of these products depend on the same requirement: the agent has to be a good group chat participant. If it talks too much, people mute it. If it talks too little, it feels broken. If it replies to one person but ignores another asking the same thing, users lose trust.

That consistency is the engineering problem.

Implementing group chats on Linq

The examples below use Linq's Partner v3 API. Full endpoint specs are in the Chats API reference, and the group chats guide is the canonical source for current constraints.

Create a group

A group chat is any conversation with three or more participants. Create one by sending the first message to multiple recipients:

curl -X POST https://api.linqapp.com/api/partner/v3/chats \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+12223334444",
    "to": ["+15556667777", "+18889990000"],
    "message": {
      "parts": [
        { "type": "text", "value": "Welcome to the group!" }
      ]
    }
  }'

Design around two constraints from the start:

  • No links in the first outbound message. link parts and text parts containing URLs are rejected on POST /v3/chats. Send a clean opening message, then follow up with a rich link preview using the returned chat ID. This matters for matchmaker and concierge flows, where the first instinct is often to lead with a link.
  • Recipient caps vary by delivery path. The to array supports up to 31 recipients, but SMS/MMS fallback depends on carriers. Most cap group texts around 20 recipients, and some go as low as 10. Plan for the strictest path your message may take.

Name it and give it an icon

Display names and icons are group-only. This is where a matchmaker can set a custom date card as the group photo, or a concierge can brand the thread:

curl -X PUT https://api.linqapp.com/api/partner/v3/chats/{chatId} \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Team Discussion",
    "group_chat_icon": "https://example.com/group-icon.png"
  }'

group_chat_icon must be a publicly accessible HTTPS URL. Calling this endpoint on a 1:1 chat returns error 1006, because names and icons only apply to groups.

Manage participants

Adding and removing participants is supported on iMessage group chats only today. The API enforces two rules:

  • A group must always have at least 3 members. You cannot remove someone or leave yourself if that would drop the group below three.
  • Leaving is one-way. Once your number leaves, you lose access unless an active participant adds you back. Recreating a chat with the same people creates a new, separate chat. A participant.removed webhook fires after the leave is processed.

See the add and remove participant references for the full schema.

Account for missing 1:1 features

Group chats do not support typing indicators, delivery receipts, or read receipts. If your 1:1 experience relies on "agent is typing..." or read state, use a different affordance in groups. Do not expect those events to fire.

Wire up webhooks

Chat creation, group detail updates, and participant changes all fire webhook events. Your inbound message handler should branch on group vs. 1:1 from the first event, because the response logic is different.

Making an agent consistent in a group

Teams usually frame group-chat inconsistency as an agent framework problem. A customer put it this way:

"Our agent is struggling to be consistent in responding to all users. I think it's an agentic framework issue. Any tips with that, or resources?"

The better framing is architecture. The fix is to separate the decision to respond from the generation of the response.

Why one model call fails

The naive design sends every inbound group message to the main model and asks it to decide whether to respond in the same call where it writes the reply.

That fails in predictable ways:

  • The behavior varies. A generative model asked to judge and act in one pass will make that judgment differently across runs. Two people can ask the same thing, and the agent may answer one but ignore the other.
  • The agent talks too much. Models are optimized to be helpful. They often reply to messages that were meant for another human.
  • It is slow and expensive. Busy groups create many messages the agent should ignore. Running the full model on each one wastes latency and tokens.
  • It can loop. Without an explicit action gate, agents can fall into reaction spirals like repeated tapbacks.

The root issue is that "should I respond?" and "what should I say?" have different cost, latency, and consistency requirements. They should not be handled by the same model call.

Use a cheap classifier as the gate

Patrick Sullivan, Linq's CTO, solved this in the open-source agent example:

"The way I solved it was to add in a simple, fast, cheap model to determine if it should be responding to a message."

The pipeline has two stages.

Stage one is a small, fast, cheap model, Claude Haiku, with one job: classify each incoming group message as respond, react, or ignore.

Stage two is the full Sonnet agent. It only runs when the stage-one verdict is respond or react. The expensive model no longer decides whether to speak. It only runs after the system has already decided that speaking is appropriate.

The classifier makes a narrow, repeated judgment in about 20 output tokens. That makes it cheaper, faster, and easier to tune than the main agent.

Here is the classifier, lightly trimmed from src/claude/client.ts:

export type GroupChatAction = 'respond' | 'react' | 'ignore';

export async function getGroupChatAction(
  message: string,
  sender: string,
  chatId: string
): Promise<{ action: GroupChatAction; reaction?: Reaction }> {
  // Pull the last few messages so the classifier has context
  const history = await getConversation(chatId);
  const recentMessages = history.slice(-4); // last 2 exchanges
  const contextBlock = /* format recent messages with sender handles */;

  const response = await client.messages.create({
    model: 'claude-haiku-4-5',          // fast and cheap, not the main agent model
    max_tokens: 20,                      // it only needs to emit one short verdict
    system: `You classify how an AI assistant "Claude" should handle messages in a group chat.

IMPORTANT: BIAS TOWARD "respond" - text responses are almost always better than reactions.
Only use "react" for very brief acknowledgments where a text response would be awkward.

Answer with ONE of these:
- "respond" - Claude should send a text reply. USE THIS BY DEFAULT when:
  * They asked Claude anything
  * They mentioned Claude (or misspelled it - cluade, cloude, cladue, claud, etc.)
  * They mentioned "AI", "bot", "assistant", or "Sullivan"
  * They're talking to Claude or continuing a conversation
  * It's a follow-up to Claude's message
  * You're unsure - default to respond
- "react:love" | "react:like" | "react:laugh" - ONLY for brief acknowledgments where text
  would be weird (a simple "thanks!" or "lol"). Do NOT overuse reactions.
- "ignore" - Human-to-human conversation not involving Claude at all

ANTI-REACTION-LOOP: If you see reactions in recent context, prefer "respond" to break the pattern.
MISSPELLING TOLERANCE: People typo "Claude" as cluade, cloude, claud, ckaude. Treat as a mention.

Examples:
- "hey claude what's the weather" -> respond
- "cluade what do u think"        -> respond   (misspelling!)
- "that's cool claude"            -> respond   (engage, don't just react)
- "thanks!"                       -> react:love
- "yo mike you coming tonight?"   -> ignore`,
    messages: [{
      role: 'user',
      content: `${contextBlock}New message from ${sender}: "${message}"\n\nHow should Claude handle this?`
    }],
  });

  // Parse the one-word verdict into an action and optional reaction
  const answer = response.content[0].type === 'text'
    ? response.content[0].text.toLowerCase().trim() : 'ignore';

  if (answer.includes('respond')) return { action: 'respond' };
  if (answer.includes('react'))   return { action: 'react', reaction: /* parse love/like/laugh */ };
  return { action: 'ignore' };
}

The inbound webhook handler then becomes a simple gate:

if (chatContext.isGroupChat) {
  const { action, reaction } = await getGroupChatAction(message, sender, chatId);

  if (action === 'ignore') return;                 // stay silent; most messages land here
  if (action === 'react')  return sendReaction(reaction); // cheap acknowledgment, no generation

  // action === 'respond': only now do we run the full agent
}
const reply = await chat(chatId, message, /* images, audio, context */);

Why the gate works

This pattern works because it gives each model a smaller job.

Separation of concerns reduces variance. A narrow classifier with a fixed three-way output and concrete examples is more consistent than a general model making the same judgment while generating a reply. The behavior becomes an explicit, testable decision boundary.

It is a routing cascade. A cheap model handles the high-volume case and escalates only when needed. In a busy group, most messages are ignore, so the system avoids most frontier-model calls.

The gate is independently tunable. If the agent should be chattier, bias the classifier toward respond. If it is too noisy, tighten the criteria. You can change turn-taking behavior without editing the main agent prompt or tools. You can also unit-test the classifier against labeled messages, which is much harder to do with an open-ended generation prompt.

It stops reaction loops early. Anti-reaction-loop behavior belongs in the gate, before generation starts. The classifier can see recent reactions and choose respond or ignore instead of continuing the loop.

Add sender attribution

Group consistency also depends on knowing who said what. In a 1:1 thread, "the user" is clear. In a group, the model needs names or handles.

The same repo prefixes each stored user message with the sender's handle before formatting history for the model:

function formatHistoryForClaude(messages, isGroupChat) {
  return messages.map(msg => {
    let content = msg.content;
    // In group chats, prefix user messages with who sent them
    if (isGroupChat && msg.role === 'user' && msg.handle) {
      content = `[${msg.handle}]: ${content}`;
    }
    return { role: msg.role, content };
  });
}

The system prompt also needs group-specific guidance: address people by name, keep replies shorter because groups move quickly, react less often because reactions can feel spammy, and remember that everyone can see the response.

The classifier decides whether to participate. Attribution helps the main agent participate competently.

Production shape

A production Linq group-chat agent usually follows this flow:

  1. Inbound webhook receives a new message and branches on isGroupChat.
  2. Gate runs the Haiku classifier and returns respond, react, or ignore. Most messages stop here.
  3. Generate runs the Sonnet agent only for respond, with sender-attributed history and a group-aware system prompt.
  4. Act through the API: send the reply, set the group name or icon, add or remove participants, while respecting the 3-member floor and iMessage-only participant management.
  5. Listen for participant webhooks and keep local state in sync.

The reference implementation is MIT-licensed and runnable: github.com/linq-team/ai-agent-example. To try it on a real Linq number across iMessage, RCS, and SMS with about a week of limited usage, request sandbox access at dashboard.linqapp.com/sandbox-signup.

For Ditto-style social agents, the group thread is the core product surface. Treat "should I respond?" as a separate, cheaper decision from "what should I say?" Once those jobs are split, the agent becomes more consistent because the architecture matches the conversation.

Sources

Your Cart
Your cart's looking a little light.Looks like your cart is empty—it's time to add your
gears and make it unforgettable.
Shop our best sellers
Digital Card
Digital Card$14.99
Hub
Hub$29.99
Badge
Badge$19.99
Mini Card
Mini Card$12.99