Laava LogoLaava
ai-agents

Channels as first-class citizens

Laava Team
Multi-channel customer support

The standard approach to building an AI agent goes like this: you build the agent. You get it working in a notebook or a CLI. You test with hardcoded input. It works. Then someone says: "Great, now make it work via email." And that's when the pain starts.

We've had that pain often enough to form an opinion. And that opinion is: channels are not an integration you bolt on later. Channels are the first architecture decision.

Why channels are so hard

At first glance, a channel adapter is simple. Email comes in, you parse it, you hand it to the agent, the agent returns a response, you send it back. Done.

But in practice:

Email via Microsoft 365 requires you to validate incoming webhook notifications with a clientState. To detect replay attacks — Microsoft sometimes sends the same notification two or three times. To maintain thread context so that a follow-up email carries the right conversation context.

Slack sends webhook events that you need to validate with signature verification. Events can be replayed within a replay window. Slash commands have a 3-second timeout within which you must respond, even if your agent needs more time. And Slack's Block Kit has its own markup language that has nothing to do with the markdown your model generates.

Microsoft Teams has yet another auth model, different message length constraints, and a different way of managing threads.

Voice is an entirely different story — real-time streaming, interrupt handling, and latency requirements that are fundamentally different from text-based channels.

Every channel looks simple until you put it in production.

The mistake everyone makes

Most teams build their agent logic first and add channels later. The result: the agent is tightly coupled to a single input/output format. When a second channel comes along, you have to choose: adapt the agent (and break the first channel) or write an adapter that translates input/output (and introduce a new abstraction that feels bolted on after the fact).

Both options are painful. And they get more painful with every channel you add.

How we do it

In our platform, channels are first-class citizens. That means:

The agent doesn't know which channel a message came from. The agent receives a normalized message with metadata (sender, thread context, attachments) and returns a normalized response. The channel adapter translates to and from the channel-specific format.

Each channel has its own middleware. Signature validation for Slack, clientState checks for M365 email, replay deduplication for both. That middleware lives in the channel adapter, not in the agent.

Channel adapters are interchangeable. The same agent runs over email, Slack, Teams, or a chat widget without changing a single line of agent code. The only difference is in the adapter.

Channels share state. A customer who first emails and then calls has the same context in both channels. That only works if state is not managed per channel but centrally.

Why this matters

In practice, almost every client asks for multiple channels. Not on day one — on day one it's "we want an email agent." But as soon as that works, the question comes: "Can this also work via our Slack?" And then: "Our customers want it via chat on the website."

If you've built channels as an afterthought, every new channel is a mini-project. If you've designed them as first-class citizens, it's a configuration change.

That difference is the difference between weeks of work and an afternoon.

Want to discuss how this applies to your business?

Let's have a conversation about your specific situation.

Book a conversation

Ready to get started?

Get in touch and discover what we can do for you. No-commitment conversation, concrete answers.

No strings attached. We're happy to think along.

Channels as first-class citizens | Laava Blog | Laava