Inbound AI enrichment
Inbound AI enrichment is the detect-then-translate pass every inbound message can run after Orbit has resolved ownership and routed the conversation. One helper detects the message body’s source language, auto-translates non-English bodies into English, and persists the results onto the message’s metadata, where the inbox, routing rules, and the voice gateway read them. This page explains that pass end to end: where it sits in the pipeline, the exact metadata keys it writes, the minimum-length short-circuit, which channels participate, and the tenant setting that switches it off.Where it runs in the pipeline
The inbound pipeline resolves before it enriches. Tenant ownership is settled first — inbound message resolution decides which tenant receives an inbound message or delivery receipt, and the tenant-level rule engine layers keyword, language, and queue distribution on top of that. The enrichment pass runs after the message is stored and acknowledged (at the router level): it is deliberately fire-and-forget, so the sender’s webhook acknowledgment never waits on a language-model round-trip, and a failing enrichment never blocks or drops the message. Because the write lands after the initial store, enrichment also signals the inbox a second time: when the metadata update lands, Orbit pushes a real-time refresh to any agent with the thread open, so the detected language and translation appear live instead of on the next poll. If that push fails — a realtime outage — the enrichment result is still persisted; it simply surfaces on the inbox’s normal poll cadence. The language condition in inbox routing rules consumes the detected language this pass produces — and the dry-run panel on a routing rule shows the same detector’s read on a sample body before the rule goes live.The two outputs on message metadata
Every enrichment result lands on the message’smetadata as plain key-value entries you can read through the API, webhooks, or the dashboard:
detected_language— the ISO 639-1 code the detector read from the body (for exampletr,es, oren).detected_locale— a mirror ofdetected_language, written in the same pass so both keys stay in sync. New consumers can adoptdetected_localewithout dual-reading the older key; existing consumers keep usingdetected_language.translation_en— the English translation of a non-English body. This powers the per-message Show translation affordance in the inbox, where an agent toggles between the original body and the English rendering.
detected_language and detected_locale are recorded as en — so consumers can distinguish “checked, was English” from “never checked” — and no translation_en is written, because English is the operator-inbox-native language. When detection cannot name a language with confidence, nothing is recorded at all; a longer follow-up message from the same conversation is a fresh chance to enrich.
The 20-character short-circuit
Bodies shorter than 20 characters skip the language-model round-trip entirely. The rationale: short bodies — “ok”, ”👍”, “yes” — are not worth confident detection, and the model rarely identifies the source language reliably on that little signal. The threshold is 20 characters, shared by design with the WhatsApp AI pipeline’s historical minimum, so traffic that previously funneled through that pipeline sees identical short-circuit behavior. Practical consequence: expect the first substantive message in a conversation to be the one that carries language metadata. Tactical replies under the threshold simply inherit the conversation’s already-detected context.Channel coverage
The enrichment pass is channel-agnostic. It was originally WhatsApp-only, embedded in the WhatsApp AI ingestion pipeline; it has since been extracted into a shared helper that every inbound ingest path calls after inserting the message and before acknowledging the sender. Channels that funnel through the generic inbound ingestion carry it: WhatsApp, SMS, email, RCS, Viber, LINE, Telegram, Messenger, Instagram, and fax. The native web-chat widget inbound path — the surface that bypassed the generic ingestion — now calls the same helper directly, closing the last gap.The tenant gate
Enrichment is tenant-controlled. Under Organization settings the inbox-AI translation toggle switches the pass off for your workspace: when disabled, enrichment short-circuits before any language-model round-trip or metadata write — message bodies are never sent to a model provider for translation. The default is on. A failure to read your settings is fail-open: a settings outage cannot silently disable translation across workspaces. Consumers read the metadata, never the display text. The inbox’s Show translation toggle, the routing-rule language condition, and the voice gateway all key offdetected_language / translation_en on the stored message — so gating enrichment off starves every consumer of new values at once, while existing metadata on older messages remains readable.
Where the results show up
Three shipped surfaces read the enrichment output. This table names each consumer, what it shows, and what it does when enrichment has never run on a message:
The per-message Show translation affordance. In the inbox thread, an inbound message that the pass has enriched carries a
Translated from TR (or ES, DE, and so on) button beneath the original body. Clicking flips it into Hide translation and reveals the English text; clicking again collapses it back. The button only appears when the message has translation_en — an English body (no translation written) and a message enriched while translation was on but under 20 characters (skipped) both render the body without the affordance.
Tenant-level walkthrough: the Organization settings toggle
The inbox-AI translation toggle lives under Settings → Compliance in the dashboard, on the Inbox AI privacy section — alongside the auto-categorize and auto-summarize gates. The write path is owner-only. The toggle persists toorganizations.settings.inbox_ai.translate_enabled.
To verify the current state from the API:
- On (the default). Every inbound body of 21 characters or more runs the detect + translate pass. Existing
translation_envalues stay on older messages, and each new non-English inbound gets itstranslation_enwritten. TheShow translationbutton appears under every new non-English bubble. - Off. Inbound bodies short-circuit before any language-model round-trip — no call to the provider, no
translation_enwrite. Older messages keep whatever metadata they already have; theShow translationbutton still opens the archive on those. The language condition in routing rules goes quiet on live traffic — no freshdetected_language, so no rule with a language condition fires again — and the dry-run panel responds as it always did (the detector reads the sample inline). The voice gateway’s live language detection also pauses. - Fail-open. If the settings read fails, the gate falls back to the platform default — still on — because a settings outage cannot silently disable translation across the workspace. This is a deliberate “still collect when the read blips” posture, the same fail-open the spam-policy and ticket-deflection configs use.
- Cache. The gate reads are cached per tenant for 60 seconds, so a flip takes up to a minute to propagate to every inbound ingest path.
Show translation control, new bubbles simply stop gaining one. To an operator scrolling backwards this reads as “the older messages translated, the recent ones did not” — which is exactly what happened.
Edge cases, concretely
The outline in The 20-character short-circuit names the classes; here is what each actually shows:- Exactly 20 characters (for example the Turkish greeting
"Merhaba, nasilsin bugun"at 20 chars including spaces): does run the detector — the short-circuit is strictlylength < 20. Nothing writes if the detector cannot name a language with confidence; a longer follow-up message from the same conversation is a fresh chance to enrich. - 19 characters (one char under): does not run at all. No model call, no
detected_language, notranslation_en, noShow translationbutton. The conversation shows the original body only. - English body of any length ≥ 20 chars: the detector runs and reports
en;detected_languageanddetected_localeare both stampeden. Notranslation_enis written (English is the inbox-native language), so theShow translationbutton never appears. - Low-confidence detection — a mixed-code-switch body, a gibberish string, a body that is mostly emoji: the detector declines to name a language, nothing is written, and the message renders as original-only.
- Realtime push outage: the metadata write lands, but the fire-and-forget
message.updatedpush to any agent with the thread open fails. The desktop inbox falls back to its poll cadence and the translation appears on the next refresh; the data is not lost. - Toggle-off mid-thread: messages before the flip keep their metadata; messages after the flip have no new metadata. Rolling the toggle back on later picks up exactly where the past interruption ended.
Spanish → ES team rule stopped matching. The dry-run panel still shows detected_language: es for a sample body (the detector runs against the sample inline), which hides the loss — check the toggle state first when a once-working language rule goes quiet.
Reading the metadata via API
The enrichment keys are plainmetadata entries on the stored message, so the message-detail endpoint returns them:
metadata:
metadata keeps whatever other keys it carries.
The same keys ride the inbound webhook envelope under data.metadata, so a receiver can branch on them without a follow-up GET:
When the inbox shows nothing
The failure modes an operator actually reports:- The button never appears on any message: check Settings → Compliance → Inbox AI privacy first; if
translate_enabledisfalsethe whole pass is short-circuiting. - The button never appears on a few messages: those bodies were under 20 characters, were English (
detected_language: en, no translation written), or the detector declined to name a language with confidence. - The button disappears between one page of a thread and the next: someone toggled translation off mid-thread — older bubbles keep the affordance; newer ones do not.
- A routing-rule language condition stopped matching live traffic: same root cause — enrichment is off, so
detected_languagenever gets written. The dry-run panel still previews the detector reading the sample inline, so the sample check will not reveal the stall. - Voice live-language detection quiet: the same toggle gates the voice gateway’s multilingual STT linkage; flipping it back on restores detection live.
See also
- Inbound message resolution — the tenant-ownership layer the enrichment pass runs beneath
- Inbound message routing — the tenant-level rule engine (match types, targets, fall-through) layered on top of resolution
- Routing rules — the language condition and the dry-run panel that previews the detector before a rule goes live
- Inbox API reference — the routing-rules endpoints, including
detected_languagein the dry-run response - Conversation intelligence — the broader inbound-analysis family this pass belongs to