Add Phone Calls to Your LangChain Agent
What You're Building
You have a LangChain agent. It chains tools together — search, database queries, API calls. At some point in a workflow, it needs to call a business by phone. Book an appointment, verify information, follow up on an order.
This post gives you a PhoneCallTool — a LangChain BaseTool subclass that calls the AgentPhone API. Your agent invokes it like any other tool and gets structured results back.
Prerequisites:
- Python 3.10+
pip install langchain langchain-openai httpx- An AgentPhone API key (get one free — 5 calls, no credit card)
The Phone Tool
LangChain tools extend BaseTool from langchain_core. Here's the complete implementation:
import os
import time
import httpx
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
AGENTPHONE_API_KEY = os.environ["AGENTPHONE_API_KEY"]
AGENTPHONE_BASE = "https://agentphone.app/api/v1"
AGENTPHONE_HEADERS = {
"x-api-key": AGENTPHONE_API_KEY,
"Content-Type": "application/json",
}
class PhoneCallInput(BaseModel):
to_phone_number: str = Field(
description="Phone number to call in E.164 format (e.g. +14155551234)"
)
objective: str = Field(
description="What the call should accomplish. Be specific — include "
"names, dates, reference numbers, and relevant details."
)
business_name: str = Field(
default="",
description="Name of the business being called."
)
website: str = Field(
default="",
description="URL of the business website (provides additional context)."
)
class PhoneCallTool(BaseTool):
name: str = "place_phone_call"
description: str = (
"Place a real phone call to a business or person. Use when a workflow "
"step requires calling someone by phone — booking appointments, "
"verifying information (insurance, account status), following up on "
"orders, getting quotes, or checking availability. The call is handled "
"by a voice AI that navigates IVR menus, waits on hold, and has a "
"natural conversation. Returns the outcome, summary, and transcript."
)
args_schema: type[BaseModel] = PhoneCallInput
def _run(
self,
to_phone_number: str,
objective: str,
business_name: str = "",
website: str = "",
) -> str:
# Create the call
response = httpx.post(
f"{AGENTPHONE_BASE}/calls",
headers=AGENTPHONE_HEADERS,
json={
"to_phone_number": to_phone_number,
"objective": objective,
"business_name": business_name,
"website": website,
},
)
if response.status_code != 202:
return f"Failed to create call: {response.json()}"
call_id = response.json()["data"]["call_id"]
# Poll until the call finishes
while True:
time.sleep(4)
result = httpx.get(
f"{AGENTPHONE_BASE}/calls/{call_id}",
headers=AGENTPHONE_HEADERS,
).json()["data"]
if result["status"] in ("completed", "failed", "canceled"):
break
return (
f"Call Status: {result['status']}\n"
f"Outcome: {result.get('outcome', 'unknown')}\n"
f"Details: {result.get('outcome_details', '')}\n"
f"Summary: {result.get('summary', '')}\n"
f"Duration: {result.get('duration_seconds', 0)}s\n"
f"Transcript:\n{result.get('transcript', 'N/A')}"
)
Key design decisions:
args_schemawith Pydantic — Gives the LLM structured descriptions of each parameter. TheField(description=...)values are what the model reads to decide how to fill each argument.- Descriptive
descriptionfield — Lists specific use cases so the LLM knows when to reach for this tool vs others. - Blocks until done — Phone calls take 30-120 seconds. The tool blocks and returns the result. LangChain handles this gracefully.
- Returns
str— LangChain tool outputs are strings. We format the result as readable text.
Wiring It to an Agent
Create an agent with the phone tool using create_tool_calling_agent:
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o")
phone_tool = PhoneCallTool()
prompt = ChatPromptTemplate.from_messages([
("system",
"You are a helpful assistant that can place phone calls to businesses. "
"Use the place_phone_call tool when a task requires calling someone. "
"Always use E.164 format for phone numbers (+1 followed by 10 digits "
"for US/Canada). After a call, report the outcome to the user clearly."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, [phone_tool], prompt)
executor = AgentExecutor(agent=agent, tools=[phone_tool], verbose=True)
Running It
result = executor.invoke({
"input": "Call Riverside Auto at +15105550456 and ask if they can do an "
"oil change tomorrow afternoon. My car is a 2022 Toyota Camry."
})
print(result["output"])
> Entering new AgentExecutor chain...
Invoking: `place_phone_call` with {
'to_phone_number': '+15105550456',
'objective': 'Check if they can do an oil change tomorrow afternoon for a 2022 Toyota Camry',
'business_name': 'Riverside Auto'
}
Call Status: completed
Outcome: achieved
Summary: Riverside Auto can do a synthetic oil change tomorrow at 2:30 PM...
...
I called Riverside Auto and they can fit you in tomorrow at 2:30 PM for a
synthetic oil change. It'll be $89.99 and take about 45 minutes. Want me
to confirm the appointment?
> Finished chain.
Multi-Tool Chains
Phone calls become powerful when combined with other tools. Here's an agent with search + phone:
from langchain_community.tools.tavily_search import TavilySearchResults
search_tool = TavilySearchResults(max_results=3)
phone_tool = PhoneCallTool()
prompt = ChatPromptTemplate.from_messages([
("system",
"You help users find and contact local businesses.\n\n"
"Workflow:\n"
"1. Search the web to find the business and their phone number\n"
"2. Call the business to accomplish the user's request\n"
"3. Report the results clearly\n\n"
"Always search first to get the correct phone number. "
"Never guess or make up phone numbers."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, [search_tool, phone_tool], prompt)
executor = AgentExecutor(agent=agent, tools=[search_tool, phone_tool], verbose=True)
Now the agent handles end-to-end requests like:
- "Find a pet groomer near downtown Portland that can take my golden retriever this week"
- "Find a plumber in the Castro that does weekend emergency calls and ask about their rates"
- "Look up the number for Tartine Bakery and ask if they can do a custom cake order for March 25"
The agent searches, finds the number, calls, and reports back.
Error Handling
Phone calls fail. Build error handling into your system prompt:
prompt = ChatPromptTemplate.from_messages([
("system",
"You help users with phone-based tasks.\n\n"
"After each call, check the outcome:\n"
"- 'achieved': Report the confirmed details to the user.\n"
"- 'not_achieved': Tell the user what happened and suggest alternatives.\n"
"- 'partial': Report what was accomplished and what remains.\n"
"- If status is 'failed': The call didn't connect. Suggest retrying later.\n\n"
"Never retry a failed call without asking the user first."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
The agent needs explicit instructions for failure cases. Without them, it might silently swallow errors or retry indefinitely.
Getting Started
Get an API key at agentphone.app (5 free calls, no credit card). Full API reference: agentphone.app/docs.
Other frameworks: OpenAI Agents SDK | CrewAI | MCP
