Handle incoming messages as conversations
Incoming messages are filed into conversation threads linked to the case they concern, each with a stated level of confidence about who sent it, and a handler can reply on the same thread.
- What it is
- Mechanism — something you declare.
- What it does
- One thread model for every channel. A thread holds the exchange with one counterpart on one channel and can be linked to the case it concerns. Its messages record both directions. What a message was on its channel (bodies, headers, attachments) stays in that channel's own record.
- What you decide
- Write the handler with defineInboundChannelHandler: the channel, which messages it takes, the minimum sender assurance, whether it replies, an optional handler for messages below the minimum, and its acting roles.
- Where it stops
- Email is the only channel with a provider behind it.

A channel is how a message reached the application. A channel handler runs on each message after the engine has routed it to an organization, worked out how much is known about who sent it, and filed it in a conversation thread. The handler can then answer on that thread.
The problem it solves
Service work arrives as messages: a client writes, the firm answers, the client replies. Treating each message as an isolated event loses the conversation, and every handler ends up re-inventing how to tie a reply back to its case.
Messages also claim a sender they cannot prove. An email address or a domain can be forged, and acting on it as though it identified a person would let anyone who can send a message steer the application.
What it does
One thread model for every channel. A thread holds the exchange with one counterpart on one channel and can be linked to the case it concerns. Its messages record both directions. What a message was on its channel (bodies, headers, attachments) stays in that channel’s own record.
A stated level of confidence about the sender. Each message carries a sender-assurance level from its channel’s own ladder. For email: unverified, domain aligned (the sending domain passed DMARC), a known contact of the organization, or bound to a reply address the application issued for this thread. For a signed webhook: unverified or signed by the source. These levels identify; they never authenticate a person.
Assurance gates, it never grants. A handler declares the lowest level it will act on. A message below it never reaches that handler. It goes to a declared handler for such messages, or is stored and audited and dispatched to nobody. Whatever the level, the handler acts only with the roles it declares.
Replies stay on the thread. A handler declared to reply can answer through the channel’s outbound path. The reply is recorded on the thread and sent at most once, even when the handler or the sending worker is retried. A handler declared not to reply cannot. Closing a thread revokes its reply addresses.
What you decide
Write the handler with defineInboundChannelHandler: the channel, which messages it takes, the
minimum sender assurance, whether it replies, an optional handler for messages below the minimum,
and its acting roles.
Screen senders the application cannot confirm
The example application gives its +screened address a handler for messages from senders whose
domain does not pass DMARC. Selected from
todo-email.inbound-channel-handler.ts;
the shared intake code, the default handler and the screening handler’s body are omitted.
export const todoEmailScreenedHandler = defineInboundChannelHandler({
ref: EMAIL_SCREENED_REF,
source: { kind: InboundSourceKind.CHANNEL, channel: InboundChannelKind.EMAIL, routeTag: TODO_EMAIL_SCREENED_ROUTE_TAG },
replyMode: InboundReplyMode.THREAD,
minimumSenderAssurance: InboundSenderAssurance.DOMAIN_ALIGNED,
belowMinimumHandlerRef: EMAIL_SCREENING_REF,
actingRoles: { organization: [CORE_ORG_ROLES.ORG_MEMBER] },
handle: (ctx, message) => intake(ctx, message),
});
// …
export const todoEmailScreeningHandler = defineInboundChannelHandler({
ref: EMAIL_SCREENING_REF,
source: { kind: InboundSourceKind.CHANNEL, channel: InboundChannelKind.EMAIL },
replyMode: InboundReplyMode.NONE,
minimumSenderAssurance: InboundSenderAssurance.UNVERIFIED,
actingRoles: { organization: [CORE_ORG_ROLES.ORG_MEMBER] },
// …
A domain-aligned message to the screened address reaches the first handler, which may answer on the
thread. Anything below that goes to the screening handler, which is declared not to reply, so
ctx.reply is not available to it. It has neither a route tag nor the default, so it is reachable
only as the other handler’s below-minimum handler. Both act with the same organization-member role:
the sender’s assurance decides which handler runs, never what it may do.
Limits
Email is the only channel with a provider behind it. The mechanism has run end to end against a stand-in for the email provider, but not yet against a real provider account, and no running application has yet shown a record’s linked conversations in its interface.