Add Phone Calls to Your LangChain Agent

Add Phone Calls to Your LangChain Agent

6 min read
Yanis Mellata
Integrations

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_schema with Pydantic — Gives the LLM structured descriptions of each parameter. The Field(description=...) values are what the model reads to decide how to fill each argument.
  • Descriptive description field — 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

Ready to give your agent a phone?

Get Your API Key →

Written by Yanis Mellata, Founder & CEO