Skip to main content
Wildo.ai Coming soon

Events and external data

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.
Incoming messages carry checked sender assurance, join a case’s thread, and receive replies on that same thread.
Drawn for Events and external data, the area this capability belongs to.

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.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.