RIFF how-to · 2026-07-06 · design: Config Cascade & Per-Org Branding
Brand Your Flow's Follow-Along Page
Give every phone line its own colors, badge, tagline — and its own caller-friendly progress labels — in one YAML block.
0 · What you get without doing anything
- The menu is already yours. "Press 1 for … 2 for …" comes from your pack's capabilities — every flow has its own.
- The progress is already yours. The page mirrors the live call: your flow's states, your intake stages, your collected (whitelisted) answers.
- A fallback look. Flows with no declared brand get a deterministic color palette derived from the flow id (distinct, but not chosen), or a legacy keyword theme (law, plumbing…). The linter reminds you:
companion-brand-missing.
1 · Declare the brand (one block)
Preferred home: your company's org file, so every one of its lines matches automatically.
# orgs/capsule_law.org.yaml
org_id: capsule_law
display_name: "Capsule Law"
brand:
name: "Capsule Law" # header title + page <title>
badge: "CL" # the little avatar chip (1–3 chars)
tagline: "your intake, live" # subtitle under the name
colors:
primary: "#c8a24a" # accents / glow
highlight: "#e2c56f" # highlights / gold
bg: "#0b0e19" # page background (optional)
panel: "#131730" # card background (optional)
journey: # optional — see §4
cap_intake__stage_matter: "Your case"
cap_intake__stage_contact: "How to reach you"
Then point each of the company's flows at it — one line in the flow/pack YAML:
org: capsule_law
config:
brand: {name: "Riff Cafe", badge: "RC", colors: {primary: "#38bdf8"}}
A flow-level block also overrides its org's brand per key — capsule_law's immigration line can keep the org colors but swap the badge.2 · Which color does what
| token | page variable | what the caller sees |
|---|---|---|
primary | --glow | accents, live indicators, links' glow |
highlight | --gold | emphasis text, collected-answer highlights |
bg | --bg | page background |
panel | --panel | cards / sections |
Pick primary/highlight with good contrast against your bg — a contrast lint gate is planned; until then, eyeball it in the preview (§5).
3 · What lights up as the phone reads
The page doesn't just sit there while the agent talks — three surfaces highlight in real time,
and all three take your brand's highlight color automatically (no extra config):
| surface | when it lights up | how it uses highlight |
|---|---|---|
| Menu options | as each option is read aloud, or hinted for tapping | the key glows and fills at low opacity (--menu-highlight) |
| Karaoke word-sweep | word-by-word as the agent speaks its line | the current word gets a soft 16% wash + glow (derived --gold-soft) |
| Fresh-slot flash | the moment a just-heard answer lands in "heard so far" | the row's border, wash, and ring flash in highlight |
4 · Journey labels — how your progress READS
Without journey:, the page prettifies state ids ("Cap Service Request Stage Problem"). With it, callers see your words:
journey:
cap_service_request__stage_problem: "Problem"
cap_service_request__stage_location: "Address"
cap_service_request__stage_timing: "Time"
cap_service_request__confirm: "Confirm"
cap_service_request__done: "Booked!"
grep "^ cap_" flows/<flow>.yaml, the Line Page, or python -m riff.flow_eval --describe. Unmapped states fall back to prettified ids — map only the steps callers should recognize.5 · Verify
- Lint:
python -m riff.flow_eval.linter <flow_id>— thecompanion-brand-missingwarn disappears; a danglingorg:is an error. - Tokens:
python -c "from riff.branding import brand_for_flow; print(brand_for_flow('<flow_id>'))"— you should see your colors/journey, not the fallback. - Live: place a test call, say "text link", open the SMS'd companion URL — your colors, your badge, your step names. Org-file edits show on the page's next poll (no restart); Python changes still need a server restart.
6 · How it works (30 seconds)
The effective-config cascade (call → state → state group → flow → org → global) resolves your brand: block; brand_for_flow() projects it to public tokens only (no internal ids, no provenance) into /api/companion/state; the single companion.html template maps tokens onto its CSS variables and label lookup. Resolution order: declared → legacy keyword theme → derived palette.
companion_public_slots + the privacy tier, which only tighten down the cascade. You cannot loosen privacy by branding a page.7 · Gallery — the running flows, branded
These brands are LIVE in the flow YAMLs (flow-level config.brand; org files lift shared identity up in PR4). Each mock mirrors the follow-along page: header, that flow's REAL menu options, and the "heard so far" slot echo — the value renders in the brand's highlight color.
The exact block behind the plumbing card, as it appears in flows/austin_plumbing.yaml:
config:
brand:
name: "David Austin Plumbing Pros"
badge: "AP"
tagline: "plumbing service"
colors: {primary: "#38bdf8", highlight: "#facc15"}
journey:
greeting: ["Welcome", "The call is getting started."]
triage: ["Your problem", "Tell us what's going on."]
scheduling_collect_possible_days: "Days that work"
scheduling_collect_blocked_days: "Days to avoid"
scheduling_request_summary: ["Request summary", "Reading your visit request back."]
complete: ["All set!", "Our dispatcher will confirm shortly."]
cap_outlet_switch__stage_outlet_switch_3…) aren't self-describing, and a WRONG label is
worse than the prettified fallback. Label the states you're sure of; extend as you review each
vertical's stages. The 4 in-flight vertical packs (locksmith, barbershop, pet shop, café) get
their brands when that work lands — their files are active WIP.8 · Troubleshooting
| symptom | cause → fix |
|---|---|
| page shows space-channel / generic look | no declared brand reached the server — check org: spelling (lint catches dangling refs), and that the block is brand: under the org root or flow config: |
| colors ignored but name/badge applied | colors must be a mapping with keys from §2 — a stray string is skipped |
| step labels not applied | journey keys must match state ids EXACTLY (case, double underscores) |
| two flows of one company look different | one of them overrides the org brand at flow level — check its config.brand |