AI agent integration, channel by channel: website, WhatsApp, Slack, Teams and Jira
Pasting the embed takes twenty minutes. Everything behind it is a different project in every channel, and half of the estimate is somebody else's approval queue.
Every page that ranks for integrate an AI agent is selling a script tag. Paste this, the pitch goes, and your product has an assistant by lunchtime.
That part is true. Pasting the tag takes twenty minutes. It is also the only twenty minutes of the project that looks the same no matter what you are building, which is exactly why it is the only part anybody writes about.
The work is everything behind the tag, and it is not one project. The agent that answers questions on your marketing site cannot be moved to WhatsApp without redesigning how conversations start, and cannot be moved to Teams until somebody in your customer's IT department clicks approve. The model code barely changes between channels. The calendar changes enormously.
One agent core, five thin adapters
The failure mode I get called in to fix is a codebase with the channel baked into the agent. A Slack handler that calls the model directly. Then, four months later, a WhatsApp handler that also calls the model directly, with a second prompt, a divergent tool list and its own private idea of what a conversation is. Two channels, two agents, and every bug fixed twice.
Build it the other way round, from the first channel, even when the first channel is the only one anybody has asked for: a core that takes a normalised message and returns a normalised response, and an adapter per channel that knows nothing about the model.
// Every channel collapses to this before the agent sees it.
type InboundMessage = {
channel: "web" | "whatsapp" | "slack" | "teams" | "jira";
externalUserId: string; // Slack user, phone number, Entra oid, Atlassian account id
threadKey: string; // the stable id this conversation hangs off
text: string;
attachments: Attachment[];
idempotencyKey: string; // the channel's own message id, not one you generate
};
interface ChannelAdapter {
parse(payload: unknown): InboundMessage;
render(reply: AgentReply): Promise<void>; // Block Kit, Adaptive Card, ADF, SSE
}A message arrives from some channel
Slack event, WhatsApp webhook, Teams activity, Jira webhook, or a fetch from your own UI.
The adapter normalises it
Channel-shaped payload becomes an InboundMessage. This is the last line that knows which channel it is.
Resolve the external id to a user in your system
↳ not linked? the agent answers as a stranger, with a stranger's permissions
Agent core: load history, pick tools, call the model
The only place in the codebase that knows a model exists.
Tools run as the resolved user, never as the integration
The adapter renders the reply in its native format
Block Kit, an Adaptive Card, Atlassian Document Format, or streamed tokens.
One transcript, one audit trail, five surfaces
Everything channel-specific lives in the two adapter steps. Get that split right and the second channel costs a third of the first. Get it wrong and every channel is the first channel again.
Website: the only surface you fully own
On your own site nobody is waiting to approve anything, which is both the appeal and the catch: every decision is yours. You own the streaming transport, so you own what happens when the model stalls mid-sentence. You own session identity, which means deciding what an anonymous visitor is allowed to ask before signing in. You own the rate limit, and it needs to be per session rather than per IP, because an office of forty people shares one address.
You also own the two things vendors never mention. Any page content you feed the agent as context is untrusted input, so a competitor's embedded comment is a prompt injection. And token spend per session is unbounded until you cap it, which is the same argument as not sending every call to the expensive model.
WhatsApp: a 24-hour window you design around
WhatsApp is the channel that changes the product rather than the code. Through the Cloud API you can reply freely only inside a customer service window that opens when the user messages you and closes 24 hours later. Outside it, a business-initiated message has to be a template, pre-registered and approved by Meta, with variables in fixed slots.
Read that twice before promising a proactive agent. We will follow up tomorrow with what we found is not a sentence your agent can compose at runtime. It is a template you wrote weeks earlier and had approved, and the agent gets to fill four blanks in it.
- A phone number is not an identity. It is an unverified claim about who is typing. Account linking is a real feature with a real flow, not a lookup, and until it exists the agent is talking to an anonymous stranger who happens to know your customer's order number.
- Meta meters the conversation. Your token bill is the small half of the cost. The messaging bill is the half finance asks about.
- There are no threads. One flat conversation per number, forever, so conversation state is entirely your problem and so is deciding when a topic has ended.
- Voice notes arrive. People send them constantly. Transcription is in scope, and so is what the agent does with ninety seconds of ambient noise.
- Webhooks retry. Deduplicate on Meta's message id or you will answer the same question three times.
Slack and Teams: the same job, a month apart
These two look interchangeable on a proposal and are not. Slack wants an acknowledgement within three seconds, so the real work goes on a queue and the reply comes back later through the response URL or a fresh post. It retries on timeout with a X-Slack-Retry-Num header, which is the second time idempotency has come up and not the last. In exchange it hands you threads for free: thread_ts is a conversation key you did not have to invent, which makes Slack the cheapest channel to build well.
Teams is a comparable week of engineering and a different month. The agent needs an Entra ID application and a bot registration, replies render as Adaptive Cards rather than Block Kit, and proactive messages require a conversation reference you stored earlier. None of that is hard. The gate is that installing it into your customer's tenant needs their administrator's consent, and if the agent reads mail or files through Graph, the permissions it asks for will be read by somebody whose job is to say no to exactly this.
So the honest sequencing advice: build Slack first to get the core right, even if Teams is the one the deal depends on. The adapter you write second is always better, and with Teams you want the waiting to happen while the code is already finished.
Jira: the loop you will build on day one
Everybody writes this bug once. The agent posts a comment, the comment fires the comment_created webhook, the agent reads its own comment, considers it a new instruction and replies to itself. On a quiet Saturday that is a few hundred comments on one ticket.
// The first guard in the handler, before anything else.
if (event.comment.author.accountId === AGENT_ACCOUNT_ID) return;
// And the second: the payload is a summary, not the issue.
// Re-fetch before you reason about state.
const issue = await jira.getIssue(event.issue.key);After that, Jira is mostly about what the agent is allowed to see. It acts either as a licensed user or as an app, and either way the project permission scheme decides its visibility, so an agent that summarises all open issues is summarising all open issues it has access to, which is a different and much more interesting sentence to put in front of a customer. Comments are Atlassian Document Format rather than markdown, which surprises everyone once. And the build target matters: a Forge app for anything distributed, webhooks plus the REST API when it is internal and you want it shipped this month.
The four things every channel needs anyway
Underneath the per-channel detail, every integration I have shipped needed these four, and the ones that skipped them needed them later and more expensively.
| What | Why it exists | What happens without it |
|---|---|---|
| Identity mapping | A channel id is not a user. It needs resolving to an account before a single tool runs. | The agent answers with data belonging to whoever shares that Slack channel or that phone. |
| Conversation state | Threads exist in Slack and Jira, loosely in Teams, not at all in WhatsApp. | The agent either forgets the previous message or remembers a conversation from March. |
| Idempotency | Every one of these platforms retries, and none of them promise exactly-once. | Duplicate replies, duplicate tool calls, duplicate refunds. |
| Audit trail | What was asked, what tools ran, as whom, and what came back. | No answer to the only question that matters after an incident: what did it actually do? |
That last row is the one buyers ask about and demos never show. It is the same discipline as bounding the loop and putting humans where the risk is: an agent you cannot audit is an agent you cannot put in front of customers.
Where the time actually goes
Rough shape of how I scope these, assuming one agent core already exists and this is a channel being added to it. The right-hand column is the part no amount of engineering speeds up.
| Channel | Typical build | The gate you do not control |
|---|---|---|
| Your own website | 2-4 weeks | None. You ship when it is ready, which is why it goes first. |
| Slack | 1-2 weeks | A workspace admin installs it. Days, not weeks, unless you list it publicly. |
| 2-3 weeks | Meta business verification, then template review for every proactive message. | |
| Microsoft Teams | 2-3 weeks | Your customer's tenant administrator consenting to the permissions you asked for. |
| Jira | 1-2 weeks | A project admin, plus Atlassian review if it is a distributed app. |
The embed is twenty minutes. The integration is identity, state, idempotency and an audit trail, repeated once per front door, while somebody else's approval queue runs in the background.
If you are deciding between renting a widget and building something you own, the useful question is not which is cheaper this quarter. It is which one still works when you need a second channel, a real identity model, or an answer about what the agent did last Tuesday. You can see the engineering I do around this on the AI capabilities page and what it has shipped into in selected work.
Tell me which channels you actually need and what the agent has to be able to do in each, and I will come back with a fixed scope and a fixed price per channel, with the approval gates marked on the timeline. Start that conversation here.
Building something like this?
I design and ship these systems for clients: retrieval over private data, agents that complete real tasks, and the Laravel platforms underneath them.