Widget flow model
A tenant’s embeddable widget is not one surface — it is a family of tenant-owned configurations that share one storage home (the widget configuration’s feature flags) and one public session bootstrap. Four of them decide what happens for a visitor: the conversation flow, the AI voice button, the sign-up forms, and the trust policy that gates browser-resident AI agents. Two more support them: a bounded web-call context for browser voice, and a guardrail layer those agents must clear before they act. This page explains how the pieces fit together. The end-to-end walkthrough lives in Web support end to end; the install and voice-button setup walkthrough lives in Embed a voice or capture widget.Widget objects: one storage home, one safe-read pattern
Each widget object is a self-contained JSONB blob on the widget configuration’s feature flags — a settings write, not a schema change. They share one deliberate read-side convention: a safe parser returns a fully-valid default (or an empty set) for absent, drifted, or malformed stored values, so a bad row can never break the public widget surface.
Every object is version-stamped so future shape changes are explicit, and each names the exact tenant-owned controls below. The flow and the voice button double as consent surfaces (the pre-chat form, the AI disclosure + consent copy); the sign-up form is a consent capture surface; the trust policy is a tenant-owned security gate. Between them they define everything the widget does — and nothing here ever picks a voice or SMS provider.
The flow runtime — deciding “what happens next”
The no-code conversation flow you publish and the embed runtime reads back is a pure, side-effect-free evaluator. It consumes (a) the flow object and (b) a small runtime context the caller computes — business-hours state, pre-chat completion, the visitor’s latest message, the current page path — and answers one question: what should the widget do right now? Its evaluation order is deterministic:- On first open — outside business hours → the configured offline behavior (
show_away_message,hide_launcher, orcollect_email); a mid-conversation returning visitor is not re-gated. Otherwise, if the pre-chat form is enabled and not yet completed →pre_chat_form; else it opens routing. - On each visitor message — first: explicit routing rules (keyword or URL-path match, first match wins) → keyword-triggered AI handoff → FAQ deflection auto-match (highest keyword score; ties break toward the earlier-authored entry — the builder’s priority signal) →
immediateorafter_deflection_missAI handoff → the fallback default (human_inbox/ai_agent/faq/collect_email).
Web call context — the bounded screen-pop fields
When a visitor starts a browser call, the widget collects the page they’re calling from (URL, title, referrer) plus an optional bag of host-supplied attributes (plan tier, cart value, account id), and posts it on session bootstrap. That context rides along as the visitor’s room-participant metadata so the answering agent — or, on a warm AI→human handoff, the human’s screen-pop — sees where on the site the call originated and who the visitor is, instead of a cold “anonymous web visitor.” Because the context arrives from the public, origin-gated browser surface, it’s treated as untrusted at the trust boundary:- Every field is length-bounded (URLs ≤ 2048 chars, title ≤ 300, attribute map capped at 20 entries of ≤ 64-char keys / ≤ 512-char values) and URLs are scheme-restricted to
http(s)(javascript:/data:/file:are dropped — the screen-pop surfaces them as a clickable origin hint). - The whole serialized blob is hard-capped to 6 KB before it’s stamped onto a room token — staying well under the media layer’s participant-metadata cap.
- The sanitizer never throws and never blocks the call: a malformed field is dropped, because a bad attribute must not stop the visitor from starting their call. Non-string attribute values are coerced when trivially representable (number / boolean), otherwise dropped; if the structured blob would exceed the budget, the attribute map is dropped first while the load-bearing page URL / title / referrer survive.
WebMCP guardrails — gating browser-resident AI agents
A browser-resident AI agent (WebMCP /navigator.modelContext) can drive the widget’s in-page write surface — and before it acts, its request passes through six tenant-owned controls, each independently observable:
- Action rate lane — a sliding-window cap (5 callbacks/hour, 30 messages/hour by default) per visitor session, layered on top of the cluster-wide visitor rate lane, and scoping the action verbs specifically.
- Per-session caps — a cumulative ceiling (50 actions / 100 preview-cost units per session by default) so a long-lived session can’t grind under the hourly lane; a request that would breach either ceiling is rejected and charges nothing. Both defaults are an abuse brake, not a business limit — a human-driven flow never approaches them.
- Audit — a structured, sanitized log line for every agent-invoked call (allowed or denied) so you can reconstruct what a browser agent did.
- Attestation — the agent is verified and classified (Web Bot Auth inbound signature, and/or a scoped agent identity token on its own header, never the visitor token) so your policy can allow / deny / rate by agent identity. A presented-but-invalid token is a hard deny — a caller that asserts an identity it can’t prove is never trusted.
- Reputation (KYA) — attestation proves who signed; a per-tenant behavioral score (velocity vs. declared rate, burst anomalies, prior-abuse ratio) can then tighten — never loosen — the attestation verdict: downgrading
allowtorate, or forcing a hard deny once abuse crosses a floor. This is what lets an otherwise-allowlisted agent that starts misbehaving get reined in automatically. - Compliance (quiet hours) — an agent-scheduled callback is a future outbound voice leg, so it runs through your own tenant-configured quiet-hours / dialing window (channel
voice). Default-OPEN: when you haven’t opted the voice channel into quiet hours the gate allows; when you have and recipient-local time is outside the window it rejects with your reason andnextAllowedAt. It’s your window — not a platform hard gate.
The transcript service — a bounded, sanitized email body
Behind the widget’s “email me a copy of this chat” endpoint, a pure renderer turns a visitor’s own conversation into an HTML + plain-text email body. The route owns auth, ownership, recipient resolution, and the send; keeping rendering pure and side-effect-free means the transcript content can be specified and tested without the transport. The same trust-boundary discipline applies to the output:- Every visitor-controlled fragment — message bodies, display names, attachment URLs — is HTML-escaped before it lands in the markup; newlines survive as
<br/>so formatting doesn’t corrupt. - The brand accent color is validated to a strict hex value before it reaches a
styleattribute (drifted / tenant-controlled values fall back to the default); unparseable timestamps degrade to the raw string, not “Invalid Date”, so the rendered output stays byte-stable.
The trust policy — how agent identities gate every widget action
The single tenant-owned gate that decides whether a browser agent may act at all is the trust policy object: a list of foreign agent directories (for example a partner agent platform) you choose to trust, each with a grant scope:allow— grant the surface’s normal posture.deny— revoke a previously trusted directory without deleting the row, keeping the audit trail.rate— allow, but with the directory’srate_limit_per_hourinstead of the surface’s normal per-session cap.
- Nothing is trusted by default. An empty list (or no stored row) means every foreign-signed request still classifies invalid and is denied — exactly as before the object existed. You must deliberately opt each directory in by exact HTTPS URL.
- The posture matrix decides per agent. The master writes posture is
allow_all(default-open, still rate-capped),deny_all,allowlist, ordenylist; explicit deny entries always win over allow, and a verified foreign signature trusted by an opted-in directory isn’t blocked by anallowlistposture (that posture gates unknown callers; it doesn’t gate a directory you deliberately named). - The runtime can only tighten. Reputation scoring then narrows — never widens — the verdict this policy produced, and the policy object is read back through the same fail-closed safe parser as every other widget object.
See also
- Voice widget install walkthrough — install the embed, bind an AI voice agent, get the snippet
- Web support end to end — widget → Inbox → co-browse → replay as one workflow
- Co-browse session model — the companion privacy/consent model the same widget family carries