Build a restaurant booking agent¶
This tutorial builds a complete voice agent for a restaurant. By the end you will have a working agent that greets callers, collects a reservation, and confirms the details.
You will use the ADK to define the agent, iterate on it, and push it for testing. Merging and deployment still happen in Agent Studio.
What you will build¶
The agent handles calls to Maison, a fictional restaurant. When a caller asks to make a reservation, the agent:
- Enters a booking flow and collects the caller's name, party size, and preferred date and time
- Confirms the details back to the caller
- Stores the booking on the conversation state
What you will learn¶
This tutorial covers:
- creating a new project and pulling it down locally
- working on a branch
- defining entities for structured data collection
- building a multi-step flow with default steps and a function step
- writing a start function and a booking function
- triggering a flow from a topic
- tuning speech recognition with keyphrase boosting and transcript corrections
- adjusting spoken output with pronunciation rules
- previewing changes with
poly statusandpoly diff - pushing and testing with
poly chat
Prerequisites¶
- the ADK installed and you're signed in — see Getting started if you haven't already
Part 1 — Set up the project¶
Create the project¶
This creates a new project in Agent Studio and initializes it locally in one step — see Getting started for how project creation works in general, and poly project for the full set of flags.
Initializing project <account_id>/<project_id>...
✓ Project initialized at /Users/yourname/maison/<account_id>/<project_id>
The project starts with just the base agent settings and voice configuration — see Working locally for what a full project structure looks like. Resources like config/entities.yaml, flows/, and topics/ don't exist yet; you'll create them as you go.
Create a working branch¶
You are now on the booking-flow branch — pushes go here instead of main, leaving it untouched until you merge. See Branches, push, and pull for why this matters.
Check which branch you are on
Run poly status at any time to see your current branch, region, and when the project was last pulled.
Part 2 — Define the agent¶
Persona¶
Open agent_settings/persona.txt and describe who the agent is. This is the free-text field Agent Studio shows as Role:
You are the reservations agent for Maison, an upscale French restaurant.
You are polite, calm, and warm, and you keep answers short.
Keep identity and behavior separate
The persona says who the agent is. Standing instructions about what it should do belong in rules.txt below.
Supported references
The persona accepts {{attr:...}} and {{vrbl:...}} references, so it can vary per variant or per call. Behavioral references such as {{fn:...}} and {{ho:...}} belong in rules.txt.
Rules¶
Open agent_settings/rules.txt. Rules give the agent standing instructions that apply across every turn. Add the following:
You are the reservations agent for Maison, an upscale French restaurant.
Always be warm and welcoming.
When a caller wants to make a reservation, use {{fn:start_booking_flow}} to begin the booking process.
Never tell the caller their reservation is confirmed until the booking flow has completed.
{{fn:start_booking_flow}} references a global function you will define later. The model uses these references to understand when to call them.
{{fn:...}} only works for global functions
Only functions in the top-level functions/ directory can be referenced with {{fn:...}} in rules and topics. Flow function steps (files inside flows/{name}/function_steps/) are called automatically by the flow — they cannot be referenced this way.
Part 3 — Define the entities¶
Entities are the structured data values the agent can collect from a caller. Create config/entities.yaml:
entities:
- name: customer_name
description: The caller's full name for the reservation
entity_type: name_config
config: {}
- name: party_size
description: Number of guests for the reservation, between 1 and 20
entity_type: numeric
config:
has_decimal: false
has_range: true
min: 1
max: 20
- name: reservation_date
description: The date the caller wants to dine, such as "Friday" or "the 15th"
entity_type: date
config:
relative_date: true
- name: reservation_time
description: The time the caller wants to dine, such as "7pm" or "half past seven"
entity_type: time
config:
enabled: true
start_time: "12:00"
end_time: "22:00"
These four entities will be collected across the steps of the booking flow.
Automatic ASR biasing
When a default step lists these entities in extracted_entities, the platform automatically configures speech recognition to be more accurate for that kind of input — dates, times, numbers, and names.
Part 4 — Build the booking flow¶
Flows live under flows/. Each flow gets its own directory. The directory name must be the snake_case version of the flow's name field — so a flow named Booking Flow must live in flows/booking_flow/.
Create the directory structure:
Flow configuration¶
Create flows/booking_flow/flow_config.yaml:
name: Booking Flow
description: Collects reservation details and confirms a table booking
start_step: Collect Name
Step 1 — Collect name¶
Create flows/booking_flow/steps/collect_name.yaml:
step_type: default_step
name: Collect Name
prompt: |
## Collect the caller's name
Ask the caller for the name the reservation should be under. Be warm and conversational.
Do not repeat the question if the caller has already given a name.
Collected so far: {{entity:customer_name}}
conditions:
- name: has_name
condition_type: step_condition
description: Caller has provided their name
required_entities:
- customer_name
child_step: Collect Party Size
extracted_entities:
- customer_name
Step 2 — Collect party size¶
Create flows/booking_flow/steps/collect_party_size.yaml:
step_type: default_step
name: Collect Party Size
prompt: |
## Collect party size
Ask how many guests will be dining. The restaurant accepts between 1 and 20 guests.
If the caller gives a number outside that range, explain politely and ask again.
Name on reservation: {{entity:customer_name}}
Party size so far: {{entity:party_size}}
conditions:
- name: has_party_size
condition_type: step_condition
description: Caller has given a valid party size
required_entities:
- customer_name
- party_size
child_step: Collect Date
extracted_entities:
- party_size
Step 3 — Collect date and time¶
Create flows/booking_flow/steps/collect_date.yaml:
step_type: default_step
name: Collect Date
prompt: |
## Collect reservation date and time
Ask the caller when they would like to dine. Collect both the date and the time.
Maison is open for lunch from noon and for dinner until 10pm.
Party: {{entity:customer_name}}, {{entity:party_size}} guests
Date collected: {{entity:reservation_date}}
Time collected: {{entity:reservation_time}}
conditions:
- name: has_date_and_time
condition_type: step_condition
description: Caller has given both date and time
required_entities:
- customer_name
- party_size
- reservation_date
- reservation_time
child_step: confirm_booking
extracted_entities:
- reservation_date
- reservation_time
child_step: confirm_booking uses the Python filename (without .py) rather than a step name — that is the convention for pointing to a function step.
Step 4 — Confirm booking (function step)¶
Function steps are deterministic Python. They run without model interpretation. Create flows/booking_flow/function_steps/confirm_booking.py:
from _gen import * # <AUTO GENERATED>
def confirm_booking(conv: Conversation, flow: Flow):
"""Confirm the reservation and store the details."""
name = conv.entities.customer_name.value if conv.entities.customer_name else "Guest"
size = conv.entities.party_size.value if conv.entities.party_size else "?"
date = conv.entities.reservation_date.value if conv.entities.reservation_date else "?"
time = conv.entities.reservation_time.value if conv.entities.reservation_time else "?"
# Store the confirmed details in conversation state
conv.state.booking_confirmed = True
# Exit the flow and return a context string for the model
conv.exit_flow()
return f"Booking confirmed: table for {size} under {name} on {date} at {time}."
After this function runs, the model receives the returned context string and uses it to continue the conversation.
Part 5 — Add a global function to enter the flow¶
Global functions live in the top-level functions/ directory. Create functions/start_booking_flow.py:
from _gen import * # <AUTO GENERATED>
@func_description("Start the table reservation process for Maison restaurant")
def start_booking_flow(conv: Conversation):
"""Enter the booking flow."""
conv.goto_flow("Booking Flow")
This is what {{fn:start_booking_flow}} in your rules resolves to. When the model decides to call it, the agent enters the booking flow at its start step.
Part 6 — Add a start function¶
The start function runs once at the beginning of every call, before the first user input. Use it to initialize state.
Create or update functions/start_function.py:
from _gen import * # <AUTO GENERATED>
def start_function(conv: Conversation):
"""Initialize conversation state at call start."""
conv.state.booking_confirmed = False
Part 7 — Add a topic¶
Topics tell the agent what kinds of caller utterances map to which actions. Create topics/make_a_reservation.yaml:
name: Make a Reservation
enabled: true
content: The caller wants to make a table reservation at Maison restaurant.
example_queries:
- I'd like to book a table
- Can I make a reservation?
- Do you have any tables available on Saturday?
- I want to reserve a table for four
actions: Use {{fn:start_booking_flow}} to begin collecting the reservation details.
The model uses content and example_queries to understand when this topic applies, and actions to know what to do.
Part 8 — Tune speech recognition¶
Voice agents listen on a noisy channel, and domain-specific vocabulary is usually the first thing ASR gets wrong. The ADK exposes two speech-recognition resources that do not require any code: keyphrase boosting and transcript corrections. Both live under voice/speech_recognition/.
Keyphrase boosting¶
Create voice/speech_recognition/keyphrase_boosting.yaml:
keyphrases:
- keyphrase: Maison
level: maximum
- keyphrase: reservation
level: boosted
- keyphrase: party of
level: boosted
The recognizer biases toward these phrases when it is uncertain. Boost the brand name at maximum so the agent never mis-hears it, and apply lighter boosts to the phrases that disambiguate booking intent.
Transcript corrections¶
Spoken times and numbers often come through in forms that are hard to parse. Create voice/speech_recognition/transcript_corrections.yaml:
corrections:
- name: Time normalization
description: Collapse common spoken time forms
regular_expressions:
- regular_expression: half past (\d{1,2})
replacement: \1:30
replacement_type: partial
- regular_expression: quarter past (\d{1,2})
replacement: \1:15
replacement_type: partial
- regular_expression: quarter to (\d{1,2})
replacement: \1:45
replacement_type: partial
These rules are applied after the recognizer returns text but before the model sees it. They only touch the transcript — the caller's audio is unaffected.
See the speech recognition reference for the full field list, interaction styles, and barge-in settings.
Part 9 — Adjust how the agent speaks¶
Response control sits on the other side of the conversation: it shapes what the agent says before TTS. For a French-brand restaurant, the most common issue is pronunciation. Create voice/response_control/pronunciations.yaml:
pronunciations:
- regex: "\\bMaison\\b"
replacement: May-zon
case_sensitive: false
description: Ensure the restaurant name is spoken in the intended French style
You can add entries for any word or phrase the TTS mispronounces. See the response control reference for the full field list, including phrase filtering.
Part 10 — (Optional) Send an SMS confirmation¶
SMS is an enterprise feature
SMS templates require an enterprise Agent Studio account. Skip this part if you're on a self-serve account.
Skip this part if you are only testing via chat
conv.send_sms_template needs conv.caller_number, which is only populated on the voice channel. In poly chat, the template won't actually be dispatched — but the code path is wired the same way and will fire the moment a real call comes in.
Define the template¶
Create config/sms_templates.yaml:
sms_templates:
- name: booking_confirmation
text: >-
Hi {{vrbl:booking_name}}, your table for {{vrbl:booking_size}} at Maison
is confirmed for {{vrbl:booking_date}} at {{vrbl:booking_time}}.
We look forward to seeing you.
env_phone_numbers:
sandbox: ""
pre_release: ""
live: "+15551234567"
The {{vrbl:...}} placeholders pull from conv.state values — so the function step has to set them before sending.
Send it from the function step¶
Replace the body of flows/booking_flow/function_steps/confirm_booking.py with:
from _gen import * # <AUTO GENERATED>
def confirm_booking(conv: Conversation, flow: Flow):
"""Confirm the reservation, store details on state, and send an SMS."""
name = conv.entities.customer_name.value if conv.entities.customer_name else "Guest"
size = conv.entities.party_size.value if conv.entities.party_size else "?"
date = conv.entities.reservation_date.value if conv.entities.reservation_date else "?"
time = conv.entities.reservation_time.value if conv.entities.reservation_time else "?"
# Populate the variables the SMS template consumes
conv.state.booking_name = name
conv.state.booking_size = str(size)
conv.state.booking_date = str(date)
conv.state.booking_time = str(time)
conv.state.booking_confirmed = True
# Send the SMS if we have a caller number (voice channel only)
if conv.caller_number:
conv.send_sms_template(
to_number=conv.caller_number,
template="booking_confirmation",
)
conv.exit_flow()
return f"Booking confirmed: table for {size} under {name} on {date} at {time}."
Because the decision to send happens in Python, the model doesn't need to resolve {{twilio_sms:...}} at all.
See the SMS templates reference for the full field list, environment-specific sender numbers, and the conv.send_sms helper for free-form messages.
Part 11 — Review your changes¶
╭─────────────── Project Status ────────────────╮
│ Region <region> │
│ Account ID <account_id> │
│ Project ID <project_id> │
│ Last Pulled 2026-04-21T09:15:00 │
│ Current Branch booking-flow │
╰───────────────────────────────────────────────╯
New files:
/Users/yourname/maison/<account_id>/<project_id>/config/entities.yaml
/Users/yourname/maison/<account_id>/<project_id>/flows/booking_flow/flow_config.yaml
/Users/yourname/maison/<account_id>/<project_id>/flows/booking_flow/steps/collect_name.yaml
/Users/yourname/maison/<account_id>/<project_id>/flows/booking_flow/steps/collect_party_size.yaml
/Users/yourname/maison/<account_id>/<project_id>/flows/booking_flow/steps/collect_date.yaml
/Users/yourname/maison/<account_id>/<project_id>/flows/booking_flow/function_steps/confirm_booking.py
/Users/yourname/maison/<account_id>/<project_id>/functions/start_booking_flow.py
/Users/yourname/maison/<account_id>/<project_id>/functions/start_function.py
/Users/yourname/maison/<account_id>/<project_id>/topics/make_a_reservation.yaml
/Users/yourname/maison/<account_id>/<project_id>/voice/speech_recognition/keyphrase_boosting.yaml
/Users/yourname/maison/<account_id>/<project_id>/voice/speech_recognition/transcript_corrections.yaml
/Users/yourname/maison/<account_id>/<project_id>/voice/response_control/pronunciations.yaml
/Users/yourname/maison/<account_id>/<project_id>/variables/booking_confirmed
Variables in poly status
The variables/ entries appear because the ADK scans your function code for conv.state.* assignments and tracks each one as a variable. These entries are virtual — they do not correspond to files on disk and are not something you need to create or manage. This is expected output.
Run poly diff to see the exact content difference for any file before pushing.
Part 12 — Push to Agent Studio¶
Pushing local changes for <account_id>/<project_id>...
Pushed <account_id>/<project_id> to Agent Studio.
The agent is now deployed to the booking-flow branch. Sandbox remains on main and is unaffected.
Part 13 — Test on the branch¶
poly chat defaults to --environment branch, which chats against your current branch's last pushed state — exactly what you just pushed, without touching main:
Starting chat for <account_id>/<project_id> (booking-flow)...
Type your message. Press Ctrl+C to exit.
Agent: Welcome to Maison. How can I help you today?
You: I'd like to book a table for this Saturday
Agent: Of course! I'd be happy to help with that. Could I take the name for the reservation?
You: It's under Sarah Chen
Agent: Thank you, Sarah. And how many guests will be dining?
You: Just two of us
Agent: Perfect. And what time were you thinking for Saturday?
You: Around 7:30 in the evening
Agent: Wonderful. Let me confirm that — a table for 2 under Sarah Chen this Saturday at 7:30pm. Does that sound right?
You: Yes, that's perfect
Agent: Your reservation is confirmed. We look forward to welcoming you to Maison on Saturday. Is there anything else I can help you with?
Useful chat flags
poly chat --state— printsconv.stateafter each turn so you can verify values are being set correctlypoly chat --functions— shows which functions the model called each turnpoly chat --push— pushes your latest changes before starting the session, useful during rapid iteration
poly chat --push can create conflict markers
If the remote state has diverged from your local copy, poly chat --push may write merge-conflict markers (<<<<<<<, =======, >>>>>>>) directly into your YAML files. If this happens, open the affected file, resolve the conflict by hand, and push again before continuing.
Keep editing, pushing, and chatting against the branch until you're happy with the behavior. Only merge once it works the way you want.
Part 14 — Merge to main¶
Merge from the CLI with poly branch merge '<commit message>' (run from the booking-flow branch), or open the project in Agent Studio, switch to the booking-flow branch, and merge through the web UI:
See poly branch merge for the full conflict-resolution flow, including --interactive and --resolutions.
Part 15 — After the merge¶
poly branch merge switches you back to main locally and pulls the merged state automatically — there's no separate poly branch switch or poly pull needed.
Merging into main also deploys to sandbox automatically, so there's no separate deploy step either. To confirm:
Then chat against sandbox to see the merged agent in action:
YAML key order changes after a round-trip
After a push and pull, the platform returns YAML with keys in alphabetical order. Fields you wrote in logical order (such as name: before description:) will be reordered. This is cosmetic and does not affect behavior, but poly diff will show changes after the first round-trip even when the content is the same.
What to explore next¶
This tutorial covered a single flow with four steps. From here you can extend the agent in several directions.
Multi-location support with variants: If Maison has multiple locations, use config/variant_attributes.yaml to define per-location phone numbers, opening hours, and capacity limits. The agent reads the right values for each location at runtime using {{attr:...}} in prompts and conv.variant.attribute_name in code.
Handling cancellations: Add a second topic and flow for callers who want to cancel or modify a reservation. The flow structure is similar — collect a name or reference, confirm the record, then update state.
External API calls: Replace the stub booking logic in confirm_booking.py with a real HTTP call to your reservation system. Function steps are the right place for this — they run deterministically and can store results in conv.state for the model to reference.
Richer error paths: Add an explicit error step to the flow for when the booking cannot be completed. Route to it from confirm_booking.py using flow.goto_step("Error") and return a context string explaining what happened.
Call handoffs: Define SIP transfer destinations in config/handoffs.yaml and trigger them from code with conv.call_handoff(...). Like SMS, handoffs require an enterprise Agent Studio account. See the handoffs reference.
Related pages¶
-
Build an agent with the ADK
The general CLI and AI-agent workflows this tutorial applies to a concrete example. Open the workflow tutorial
-
Flows
Full reference for flow configuration, step types, and conditions. Open flows
-
Entities
All entity types and their configuration fields. Open entities
-
Functions
How global functions, transition functions, and function steps differ. Open functions
-
Topics
How topics connect caller intent to agent actions. Open topics
-
Speech recognition
Keyphrase boosting, transcript corrections, and ASR settings. Open speech recognition
-
Response control
Pronunciations and phrase filtering for spoken output. Open response control
-
Variants
Per-location configuration without duplicating your project. Open variants