Skip to main content

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, or collect_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) → immediate or after_deflection_miss AI handoff → the fallback default (human_inbox / ai_agent / faq / collect_email).
Proactive engagement triggers are a second, separate evaluation pass the runtime runs on scroll / timer / exit-intent ticks with freshly-measured behavioral context (page time, scroll depth, URL, visit count, cart value). Only enabled triggers that haven’t exhausted their frequency cap are considered, ordered by priority with builder-order tie-break; the first trigger whose conditions all hold fires, at most one directive per tick. An unknown (future-version) condition never matches — so a drifted rule degrades to doesn’t fire, never fires unconditionally. A rule pointing at a missing FAQ id degrades safely to a human — and every directive the runtime returns is inbox / web-chat / AI-agent routing; it never returns or implies an outbound voice or SMS action. The evaluator is clock-free and network-free by design, which keeps the widget’s “what happens next” logic fully testable in isolation.

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.
The visitor’s session bootstrap and the agent’s answering view both receive this same bounded snapshot, so the screen-pop and any audit trail see an identical, sanitized contract.

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:
  1. 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.
  2. 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.
  3. Audit — a structured, sanitized log line for every agent-invoked call (allowed or denied) so you can reconstruct what a browser agent did.
  4. 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.
  5. 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 allow to rate, 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.
  6. 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 and nextAllowedAt. It’s your window — not a platform hard gate.
A plain human visitor (no agent signature or token) pays no verification cost and their flow is byte-for-byte unchanged — these are attenuation / rejection layers only; they never originate a call.

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 style attribute (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.
A visitor’s transcript is their conversation history delivered to their mailbox; the renderer’s job is to make that self-service touch safe by construction — nothing it produces executes in anyone’s browser context.

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’s rate_limit_per_hour instead of the surface’s normal per-session cap.
Three rules hold the whole posture:
  1. 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.
  2. The posture matrix decides per agent. The master writes posture is allow_all (default-open, still rate-capped), deny_all, allowlist, or denylist; explicit deny entries always win over allow, and a verified foreign signature trusted by an opted-in directory isn’t blocked by an allowlist posture (that posture gates unknown callers; it doesn’t gate a directory you deliberately named).
  3. 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