The Theater Agent: Building a Voice AI for the Box Office
The Amish Country Theater has a box office. It's staffed during business hours, and the people working it are great. The problem isn't them — it's the hours between.
Calls come in at 9pm, on Sundays, on the holidays the box office is closed for. Those calls used to go to voicemail. Most callers don't leave one. They google the next venue and book there instead.
I built the Theater Agent to handle that overflow — specifically, the calls that come in when the box office is closed. The agent never picks up while a human is at the desk. It picks up only when there's nobody there to answer. Every after-hours call now gets a real conversation about what's playing, and a ticket link in the caller's hands before they hang up.

What an After-Hours Caller Experiences
You dial the theater outside business hours. The agent picks up — sounds human, doesn't sound scripted, doesn't put you through a menu. You can say things like:
- "What shows are coming up next weekend?"
- "Do you have anything by that bluegrass guy?"
- "Tell me about the Land Cruise dates in July."
The agent answers in real time, reading from live data. If you want tickets, it confirms your phone number and texts you a direct link to the show page — branded for the theater, hosted on VBO, ready to check out. The whole thing takes a minute and a half. No voicemail. No callback.
The Tools Under the Hood
ElevenLabs handles the voice. The conversation logic and tool calls go through a Node + TypeScript webhook service I wrote, exposing the actions the agent needs:
- list_shows — pulls upcoming shows from VBO Ticketing for a date range.
- find_show_by_name — fuzzy-matches a caller-spoken title across a rolling date window. This one took the most engineering: ASR mishears split titles, drops syllables, and confuses similar-sounding names. The matcher does multiple tokenization passes against overlapping date ranges to be resilient to all of that.
- get_show_details — pulls full details for one show by ID.
- get_land_cruise_availability — separately handles Land Cruise inquiries, pulling dates from RegFox.
- send_ticket_link_sms — Twilio SMS with the hosted ticket URL.
- get_current_datetime — theater-local time, so "next weekend" actually resolves correctly.
The Part Nobody Talks About: QA
The dirty secret of voice agents is that the demo is the easy part. Running one in production, where every call is a real customer with real expectations, requires a layer almost no demo shows you: post-call quality assurance.
Every call ends with a webhook from ElevenLabs. The agent service pulls the full transcript, scores it against a QA rubric, and posts a notification to a Slack channel. If something went sideways — agent got confused, gave bad info, didn't send the link — I can see exactly what happened, in the customer's own words, within minutes of the call ending.
The QA layer isn't a nice-to-have. It's the difference between an agent that works and an agent that quietly degrades for weeks before anyone notices.
There's also a QA dashboard at /qa/dashboard with summary metrics, recent reports, and a dead-letter view for any post-call events that failed to process. The dead letters get retried by a sync job, so a Slack outage doesn't silently lose QA data.
Engineering Decisions That Mattered
A few choices that aged well:
- Intro-only call filtering — early on, the QA was flooded by hang-ups and one-second test calls. Filtering anything under a real engagement threshold cleaned that up.
- Agent allowlist — the service rejects webhook events from any agent ID I haven't explicitly registered. Stops weird traffic cold.
- Non-actionable noise suppression — QA evaluations skip transcripts that don't have a real customer turn. Cuts the false-issue rate dramatically.
The Result
The box office stays in charge during the day. When they close, the phone keeps working. The calls that used to die in voicemail now turn into tickets — quietly, automatically, while everybody is asleep.