One Menu Module, Every Business

Co-written by Gemini (narrative + ideation) and Claude/Codex (technical verification), 2026-07-04. Design: docs/menu-module-v1-design.md.

Hey there! Welcome to your first deep dive into how we build real, production-grade voice applications. If you've ever called a business, you've used a phone menu (an "IVR"). You've probably also noticed they can be... a mixed bag. Some are slow, some don't let you interrupt, and they all look and feel different.

We had the same problem. Our engineers were building custom menus for every new client—a locksmith, a law firm, a futuristic space network. It was slow and inconsistent.

So, we built one, universal menu module. A single, reusable system that can power the phone menu for any business, just by feeding it a simple configuration file.

This tutorial will walk you through how it works. By the end, you'll add a brand new feature to a live phone menu yourself, using the exact same process our own developers use.

1. The Foundation: What We're Building On

Before we built the new module, we already had some powerful tools in our toolbox. Think of these as the Lego bricks we had lying around.

These were great parts, but they weren't working together to create a perfect, reusable menu.

2. The Problem: The Annoying Little Things

So, what was wrong? Why did we need a dedicated "Menu Module"?

We needed to fix this. We needed one system that guaranteed a great voice, perfect visual sync, and consistent styling for every single menu, every single time.

3. The Solution: A Menu in a Box

The Menu Module solves all these problems by being clever and consistent. It's not about writing new code for every menu; it's about describing the menu in a simple data file.

The Blueprint: A YAML "Pack"

Everything about a business's phone menu is defined in one file, called a Business Pack. It's a YAML file, which is just a simple, human-readable way to organize data.

Here's a look at the pack for the Space Channel Network Operations line.


# packs/space_channel_widgets.pack.yaml

schema_version: riff_business_pack_v0
business:
  id: space_channel_widgets
  name: Space Channel Network Operations
  # ... and other business details
system_prompt: You are Operations Droid SC-1... # Instructions for the AI
capabilities:
- id: launch_command
  kind: content
  label: Launch Command
  aliases:
  - '1'
  - one
  - launch
  dtmf: '1'
  text: Five launches on the board...
  prerender: true
- id: iss_operations
  kind: content
  label: ISS Operations
  aliases:
  - '2'
  - two
  - iss
  dtmf: '2'
  text: Station nominal with seven crew aboard.
  prerender: true
# ... more capabilities ...
menu:
  greeting_line: This is Operations Droid SC-1 with Space Channel network operations.
  prompt: Say 1 for launch command, 2 for iss operations, 3 for webb observatory, 4 for daily
    briefing, 5 for intelligence feed. You can also say text link to
    follow along, or report a bug.
  prerender: true
# ... other settings ...

Let's break down the important parts:

One Style to Rule Them All

To fix the styling chaos, all menus now use a single CSS stylesheet. A business can change its branding by just changing a few "theme tokens" (which are just CSS variables).

For example, the highlight color for the companion page is defined in one place:


/* web/assets/menu-theme.css */

:root {
  --menu-highlight: var(--gold, #f5c542);
  /* ... other theme tokens for background, fonts, etc. */
}

Every menu companion page imports this file. Want to change the highlight to blue for a new client? You change one line of code.

4. Your Turn: Add a "Joke of the Day"

Time to get your hands dirty. We're going to add a new option to the Space Channel menu: a "Joke of the Day" on key 6. This is exactly how the feature was shipped for real.

Honesty note: the Space Channel pack is generated by an importer from a live data feed, and the 12-hour refresh will overwrite hand edits — the real joke-of-the-day shipped by editing the importer (riff/business_pack/importers/space_channel_widgets.py). The exercise below is exactly right for hand-authored packs like packs/pet_shop.pack.yaml; for generated packs, make the same change in the importer instead.

Step 1: Open the Pack File

Open the business pack file located at packs/space_channel_widgets.pack.yaml.

Step 2: Add the New Capability

Scroll down to the capabilities: list. At the very end of the list, add a new block for our joke. Make sure the indentation matches the other items perfectly (YAML is picky about spaces!).


# Add this block to the end of the capabilities list

- id: joke_of_the_day
  kind: content
  label: Joke of the day
  aliases:
  - joke
  - jokes
  - joke of the day
  - tell me a joke
  - make me laugh
  - funny
  dtmf: '6'
  text: Are you as bored as I am — that sentence still makes sense if you say it backwards.
  prerender: true

You've just defined everything the system needs to know about this new option: what to call it, how to trigger it (by voice or keypad), and what to say.

Step 3: Update the Menu Prompt

Now, we need to tell the caller that this new option exists. Find the menu: section and edit the prompt: line to include "6 for joke of the day."


# Find this section and edit the prompt line

menu:
  greeting_line: This is Operations Droid SC-1 with Space Channel network operations.
  prompt: Say 1 for launch command, 2 for iss operations, 3 for webb observatory, 4 for daily
    briefing, 5 for intelligence feed, 6 for joke of the day. You can also say text link to
    follow along, or report a bug.

Step 4: Compile!

Save the file. Now for the magic. Go to your terminal and run the pack compiler. This command tells the system to read your blueprint and build a new, updated version of the voice application, including pre-rendering all the new audio.


python -m riff.business_pack compile packs/space_channel_widgets.pack.yaml

You'll see a bunch of output as it works — the compiler refuses to write anything until your flow passes three validation gates (the loader, ~35 lint rules, and the riffc pre-flow compiler), so a broken menu never reaches a caller. When it's done, the new menu is live on the next call. That's it! You've just shipped a new feature.

5. Prove It: Test Your Work

A true developer always tests their code. Here's how to validate every aspect of the menu module.

Grab your phone and call the Space Channel number. Have the companion web page open on your computer.

You've just successfully validated the entire system.

6. The "Why": Smart Decisions for a Great Experience

You might be wondering about some of the details. Why do it this way?

```bash

# This script runs on a 12-hour cron schedule

scripts/refresh-space-widgets.sh

```

This script fetches new data, regenerates the YAML file, and runs the compiler, ensuring the menu content (like launch schedules) is never stale.

Congratulations!

You've just seen how a simple, data-driven approach can create powerful, consistent, and high-quality user experiences. You took a blueprint (the YAML pack), added a new component (the joke capability), and used the factory (the compiler) to produce a finished product. This "one menu module" pattern saves us thousands of hours and ensures every client gets our best work, every time.