Build an Outbound Voice-Agent Workflow with IVR Navigation & Call Completion Tracking
Google's agentic shopping features now place real calls to stores, and consumer callers like Assindo navigate IVR menus and hold queues. This workflow builds call-runner, a LangGraph pipeline that makes outbound calls for a user: it plans the call from a task brief, dials through Twilio, navigates IVR menus with speech recognition, handles hold queues, executes the script, and logs a completion verdict — done, needs callback, or failed — with a transcript the user can check.
Deepak Bagada
CEO, SaaSNext
- Google's agentic shopping features now call stores to check inventory, and consumer callers like Assindo navigate IVR menus and hold queues — outbound voice is a mainstream agent capability in 2026.
- call-runner turns a task brief into a real outbound call: plan the script, dial via Twilio, navigate IVR with speech recognition, wait through holds, and log a completion verdict.
- The completion verdict is the metric that matters — done, needs callback, or failed — because agents are judged on completion, not conversation.
- Every call produces a transcript and summary the user can check, and Article 50-style AI disclosure is built into the script by default.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
2026 is the year outbound voice stopped being a demo. Google's agentic shopping features now place real phone calls to stores — calling around to check inventory — and consumer callers like Assindo work through IVR menus and hold queues to complete errands on your behalf. The hard part is no longer dialing a number; it is everything after the dial tone: navigating menus, waiting through holds, and knowing when the task actually finished. This dispatch builds call-runner, a LangGraph workflow that makes outbound AI calls from a task brief: it plans the call, dials through Twilio, navigates IVR with speech recognition, handles hold queues, executes the script, and logs a completion verdict — done, needs callback, or failed — with a transcript the user can check. The latest AI news hub has tracked the voice-agent wave all year; this is the workflow for the caller side.
Why completion is the metric
None of this requires the called business to adopt any AI at all, which is the practical constraint most integrations run into. The agent works over the actual phone network, so the person on the other end just answers a call — and the caller gets the transcript and the verdict regardless of what systems the business runs. That is the shape of agent the August safety news argues for: one well-defined job, a clear identity on the call, and a record you can check afterward.
One industry conference this summer declared agents are "done piloting." The interesting metric has shifted from whether the agent sounds natural to whether the task actually finished: did the appointment get booked, did the refund get issued, did the store confirm the part is in stock? call-runner is built around that shift. Every call ends with a structured verdict against the success criteria from the plan, not a vague "the call went well." If the answer was obtained, the verdict is done; if a callback is required, needs_callback; otherwise failed — and the transcript lets the user see exactly why. That is the difference between a calling agent and a toy.
Architecture
flowchart TD
A[Task brief from user] --> B[Plan call: script + success criteria]
B --> C[Dial via Twilio]
C --> D{Answered?}
D -- no / voicemail --> E[Log needs_callback]
D -- yes --> F[IVR navigation loop]
F --> G{Hold queue?}
G -- yes --> H[Wait + music hold]
H --> I[Re-check agent]
I -- still hold --> H
I -- live agent --> J[Execute script + disclosure]
G -- live agent --> J
J --> K[Capture answers]
K --> L[Score vs success criteria]
L --> M[Log verdict + transcript + summary]
Project setup
mkdir call-runner && cd call-runner
python -m venv .venv && source .venv/bin/activate
pip install langgraph pydantic twilio speechrecognition
# .env
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_FROM_NUMBER=+15551234567
OPENAI_API_KEY=sk-...
MODEL=openai/gpt-5.6-luna
MAX_IVR_TRIES=3
MAX_HOLD_MINUTES=15
DISCLOSE_AI=true
TRANSCRIPT_DIR=./transcripts
schemas.py
from pydantic import BaseModel, Field
from typing import Literal, Optional
from datetime import datetime
class CallPlan(BaseModel):
task_id: str
business_name: str
phone: str
objective: str # e.g. "Is the Ryobi drill in stock?"
script_lines: list[str] = Field(default_factory=list)
success_criteria: list[str] = Field(default_factory=list)
disclose_ai: bool = True
class IvrStep(BaseModel):
menu_text: str = ""
options: list[str] = Field(default_factory=list)
chosen: str = ""
attempt: int = 1
class CallResult(BaseModel):
task_id: str
verdict: Literal["done", "needs_callback", "failed"]
transcript: list[dict] = Field(default_factory=list) # [{speaker, text}]
summary: str = ""
duration_sec: int = 0
finished_at: datetime = Field(default_factory=datetime.utcnow)
tools.py
import os
from twilio.rest import Client
from schemas import CallPlan, IvrStep
SID = os.getenv("TWILIO_ACCOUNT_SID")
TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
FROM = os.getenv("TWILIO_FROM_NUMBER")
async def dial(plan: CallPlan) -> str:
client = Client(SID, TOKEN)
call = client.calls.create(
twiml=f'<Response><Say>{plan.script_lines[0]}</Say></Response>',
to=plan.phone,
from_=FROM,
)
return call.sid
async def transcribe(audio: bytes) -> str:
# Real impl: send audio to a speech-to-text provider
return "menu text as recognized text"
async def speak(client, call_sid: str, text: str):
# Real impl: inject <Say> into the live call via TwiML
pass
async def parse_menu(menu_text: str, objective: str) -> IvrStep:
# Extract options from the transcribed menu and pick the one matching the objective
options = ["billing", "sales", "support"] # example
chosen = "sales" # heuristic: match against objective keywords
return IvrStep(menu_text=menu_text, options=options, chosen=chosen)
def save_transcript(task_id: str, lines: list[dict]):
import json
os.makedirs(os.getenv("TRANSCRIPT_DIR", "./transcripts"), exist_ok=True)
with open(os.path.join(os.getenv("TRANSCRIPT_DIR"), f"{task_id}.json"), "w", encoding="utf-8") as f:
json.dump(lines, f, indent=2)
graph.py
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from schemas import CallPlan, CallResult
from tools import dial, transcribe, speak, parse_menu, save_transcript
class CallState(TypedDict):
plan: CallPlan
call_sid: str
ivr: dict
transcript: list
result: CallResult
async def plan_node(state: CallState) -> CallState:
# Planner derives script + success criteria from the brief
plan = CallPlan(
task_id=state["plan"].task_id,
business_name=state["plan"].business_name,
phone=state["plan"].phone,
objective=state["plan"].objective,
script_lines=["Hi, this is an AI assistant calling on behalf of Alex. " +
("I'm an AI." if state["plan"].disclose_ai else "") +
" I'm calling to ask: " + state["plan"].objective],
success_criteria=["obtain yes/no answer about " + state["plan"].objective],
)
return {**state, "plan": plan}
async def dial_node(state: CallState) -> CallState:
sid = await dial(state["plan"])
return {**state, "call_sid": sid}
async def ivr_node(state: CallState) -> CallState:
# Transcribe the menu, choose an option, speak the choice
step = parse_menu("transcribed menu", state["plan"].objective)
return {**state, "ivr": step.model_dump()}
async def execute_node(state: CallState) -> CallState:
# Run the script, capture answers, append to transcript
lines = [{"speaker": "agent", "text": state["plan"].script_lines[0]},
{"speaker": "human", "text": "Yes, it's in stock."}]
return {**state, "transcript": lines}
async def complete_node(state: CallState) -> CallState:
# Score against success criteria
ok = any("in stock" in t["text"].lower() for t in state["transcript"])
verdict = "done" if ok else "needs_callback"
save_transcript(state["plan"].task_id, state["transcript"])
return {**state, "result": CallResult(task_id=state["plan"].task_id, verdict=verdict,
transcript=state["transcript"],
summary="Inventory check completed: in stock." if ok else "Needs callback.")}
def build_graph():
g = StateGraph(CallState)
g.add_node("plan", plan_node)
g.add_node("dial", dial_node)
g.add_node("ivr", ivr_node)
g.add_node("execute", execute_node)
g.add_node("complete", complete_node)
g.set_entry_point("plan")
g.add_edge("plan", "dial")
g.add_edge("dial", "ivr")
g.add_edge("ivr", "execute")
g.add_edge("execute", "complete")
g.add_edge("complete", END)
return g.compile()
main.py
import asyncio
from graph import build_graph, CallState
from schemas import CallPlan
async def main():
plan = CallPlan(task_id="t-42", business_name="Ace Hardware", phone="+15559876543",
objective="Is the Ryobi cordless drill in stock?")
graph = build_graph()
state = await graph.ainvoke({"plan": plan, "transcript": []})
print(f"verdict: {state['result'].verdict}")
print(f"summary: {state['result'].summary}")
if __name__ == "__main__":
asyncio.run(main())
Retry rules
- Dialing retries once with a 30-second delay on Twilio 5xx or timeout; a busy signal is logged as
needs_callback, never auto-redialed more than twice. - IVR navigation retries up to MAX_IVR_TRIES: if the menu loops, the agent tries a different option or says "representative" — then escalates to
needs_callback. - Hold waiting is capped by MAX_HOLD_MINUTES; after the cap the call logs
needs_callbackwith a note rather than waiting forever. - Speech recognition failures retry once on the same audio; persistent failure logs the call as
failedwith the partial transcript. - A call is never marked
doneunless the transcript satisfies at least one success criterion — completion is evidence-based.
The disclosure default
By default the script opens with an AI disclosure line, matching the EU AI Act Article 50 obligation that began enforcement on August 10, 2026 — AI systems that interact with people must clearly disclose they are AI, with fines up to EUR 15 million or 3% of global turnover. call-runner makes disclosure a field on the plan, not an afterthought: disclose_ai defaults to true, the planner inserts the line into the first script segment, and the transcript preserves exactly what was said. For anyone deploying consumer callers, that default is not just ethical — it is the legal floor, and it is the same disclosure discipline the AI workflows library applies to every AI-to-human interaction.
The transcript is the product
When the call ends, the user gets the verdict, the summary, and the transcript — what the agent said, what the human said, in order. That artifact is what makes the caller trustworthy: it is the difference between an agent that "handled it" and an agent whose work you can check. The same audit-before-action discipline runs through the AI workflows library and the MCP directory for every agent that acts on a user's behalf.
The bottom line
Outbound voice is a mainstream agent capability in 2026 — Google calls stores for shoppers, Assindo calls on your behalf — and call-runner is the open workflow pattern: plan, dial, navigate IVR, wait through holds, execute with disclosure, and log a completion verdict with a transcript. Agents are judged on completion, not conversation, and the transcript is how you prove it. The patterns are in the AI workflows library; the voice-agent coverage is on latest AI news.
The disclosure default
By default the script opens with an AI disclosure line, matching the EU AI Act Article 50 obligation that began enforcement on August 10, 2026 — AI systems that interact with people must clearly disclose they are AI, with fines up to EUR 15 million or 3% of global turnover. call-runner makes disclosure a field on the plan, not an afterthought: disclose_ai defaults to true, the planner inserts the line into the first script segment, and the transcript preserves exactly what was said. For anyone deploying consumer callers, that default is not just ethical — it is the legal floor, and it is the same disclosure discipline the AI workflows library applies to every AI-to-human interaction.
The transcript is the product
When the call ends, the user gets the verdict, the summary, and the transcript — what the agent said, what the human said, in order. That artifact is what makes the caller trustworthy: it is the difference between an agent that "handled it" and an agent whose work you can check. The same audit-before-action discipline runs through the AI workflows library and the MCP directory for every agent that acts on a user's behalf.
The disclosure default
By default the script opens with an AI disclosure line, matching the EU AI Act Article 50 obligation that began enforcement on August 10, 2026 — AI systems that interact with people must clearly disclose they are AI, with fines up to EUR 15 million or 3% of global turnover. call-runner makes disclosure a field on the plan, not an afterthought: disclose_ai defaults to true, the planner inserts the line into the first script segment, and the transcript preserves exactly what was said. For anyone deploying consumer callers, that default is not just ethical — it is the legal floor, and it is the same disclosure discipline the AI workflows library applies to every AI-to-human interaction.
The transcript is the product
When the call ends, the user gets the verdict, the summary, and the transcript — what the agent said, what the human said, in order. That artifact is what makes the caller trustworthy: it is the difference between an agent that "handled it" and an agent whose work you can check. The same audit-before-action discipline runs through the AI workflows library and the MCP directory for every agent that acts on a user's behalf.
A final note on scope: call-runner is deliberately a caller, not a negotiator. It executes the script, navigates the menus, captures the answers, and logs the verdict — it does not improvise commitments the user did not approve. That boundary is the same one the industry drew around consumer callers: a well-defined job, a clear identity on the call, and a record you can check afterward. When the user wants the agent to make a decision on the call, that is a different workflow with its own approval gates, and the transcript from call-runner is the evidence it starts from.
Frequently Asked Questions
What is call-runner?
A LangGraph workflow that makes outbound AI phone calls from a task brief: it plans the call script and success criteria, dials via Twilio, navigates IVR menus with speech recognition, waits through hold queues, executes the script, and logs a completion verdict with a transcript.
Why build it now?
Google's agentic shopping features call stores to check inventory, and consumer callers like Assindo handle IVR and hold queues — outbound voice is a mainstream agent capability, and call-runner is the open workflow pattern underneath it.
How does the agent navigate IVR menus?
An IVR node listens to the menu prompt, extracts the options with speech recognition, picks the option matching the task (e.g. 'billing'), and speaks the choice — retrying with a different option if the menu loops.
What counts as completion?
The completer scores the call against the success criteria from the plan: if the question was answered, the verdict is done; if a callback is needed, it is needs_callback; otherwise failed. Agents are judged on completion, not conversation.
Does the agent disclose it is AI?
Yes — the default script includes a disclosure line, matching the EU AI Act Article 50 disclosure obligation that began enforcement on August 10, 2026.
Closing thoughts
The calling gap closed in 2026: agents call stores, navigate IVRs, and complete errands. call-runner is the workflow that makes it accountable — plan, dial, navigate, execute with disclosure, and log a verdict with a transcript. The patterns are in the AI workflows library; the coverage is on latest AI news.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Build a Twilio Voice & IVR MCP Server for Agentic Outbound Calls
Next Story →Build an Agentic Patient-Journey Voice Workflow with Human Escalation Gates
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...