Function Calling with OpenAI: Complete Implementation Guide
Introduction
Key Insight
**Function calling** is OpenAI's structured approach to connecting large language models with external tools, APIs, and data sources. Instead of hoping the model generates valid JSON through creative prompting, you define function schemas—and the model returns reliable, machine-readable function calls that your application can execute with confidence.
This capability transforms GPT models from pure text generators into intelligent agents that can retrieve live data, manipulate external systems, and orchestrate multi-step workflows. Whether you're building conversational AI that accesses order databases, data extraction pipelines that parse unstructured documents, or autonomous agents that manage complex business processes, function calling is the foundation.
In this comprehensive guide, we'll cover everything from core concepts through advanced implementation patterns, with practical examples and real-world use cases that you can implement immediately.
What Is Function Calling?
Core Concept
Understanding Function Calling: The Three-Part Process
**Function calling** enables GPT models to generate structured JSON outputs that correspond to predefined function schemas you provide. Here's the essential clarity: the model doesn't execute your functions. Instead, it analyzes the user's request and generates JSON containing the function name and arguments. Your application parses this JSON, validates it, executes the actual function in your backend, and returns the results.
This distinction is critical. Many developers misunderstand function calling as the model directly invoking code. In reality, it's a three-part process:
1. **Model decides**: "This user request needs the `get_order_status` function with order_id='12345'"
2. **Model generates**: Structured JSON with function name and parameters
3. **Your app executes**: Calls your actual backend function with those parameters
This separation of concerns is powerful because it keeps the model focused on understanding intent and generating valid arguments, while your application controls execution, validation, error handling, and security.
Why This Matters More Than Traditional Prompting
Traditional prompt-based JSON generation is notoriously unreliable. You might write:
User: "I need to check my order status"
Prompt: "Generate JSON with the format: {order_id: 'string'}"
Model: {"order_id": "12345", "extra_field": "hallucinated data", "status": "complete"}
The model often adds unexpected fields, uses wrong data types, or generates invalid JSON entirely. Your parser breaks, your user gets an error, and you've wasted tokens.
With function calling:
You define: get_order_status(order_id: string, include_tracking: boolean)
User: "Check my order #12345"
Model: {"name": "get_order_status", "arguments": {"order_id": "12345"}}
Your code: Parses guaranteed structure, validates parameters, executes
Common Mistake
The model generates only what you defined. Nothing extra. No hallucinations. No type mismatches. This is especially powerful with **strict mode**, which guarantees 100% schema adherence—no deviations, no surprise fields, no validation headaches.
The Evolution: From Functions to Tools
OpenAI's API has evolved over time. You'll encounter two different parameter names in documentation and older code:
functionsparameter: The original approach (now deprecated but still functional)toolsparameter: The current standard (introduced with GPT-4 Turbo in late 2023)
The tools parameter is more flexible and future-proof. It supports multiple tool types beyond just functions—including OpenAI's built-in tools like code interpreter and file search. For all new implementations, use tools.
The underlying capability is identical for basic function calling, but tools is the direction the API is moving. Using the deprecated functions parameter will still work, but it's like driving a car with the emergency brake partially engaged—functionally okay, but not optimal.
How Function Calling Works: The Complete Workflow
Step 1: Define Schemas
Step 2: Send Request
Step 3: Parse Response
Step 4: Execute & Return
Function calling follows a four-step pattern that becomes second nature once you understand the flow.
### Step 1: Define Your Function Schemas
Before you can use function calling, you define the functions your model can access using JSON Schema. This schema tells the model:
- What is the function called?
- What does it do?
- What parameters does it need?
- What types are those parameters?
- Which parameters are required?
Here's a complete example defining a weather function:
```json
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather for a specific location. Use this when the user asks about current weather, temperature, conditions, or forecast details.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state/province in format 'City, State' (e.g., 'Toronto, ON' or 'San Francisco, CA')"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Defaults to celsius if user doesn't specify."
}
},
"required": ["location"]
}
}
}
```
Each component serves a purpose:
- **`name`**: How you'll identify this function in the response. Use snake_case, keep it concise and descriptive
- **`description`**: Critical for guiding the model. Explain not just what the function does, but when to use it and what it returns
- **`parameters`**: JSON Schema defining the input structure
- **`type: "object"`**: Always use object for function parameters
- **`properties`**: Define each parameter with type and description
- **`required`**: Array of parameter names the model must include
- **Enum fields**: Use these to restrict choices (e.g., only "celsius" or "fahrenheit")
The description field deserves special attention because it's part of the model's decision-making process. Generic descriptions like "Get weather" or "Retrieve data" won't help the model understand when to call the function. Specific descriptions work better:
```json
// ❌ Vague
"description": "Gets weather"
// ✅ Specific and actionable
"description": "Retrieves current weather conditions for a specified location including temperature, conditions, humidity, and wind speed. Use this when the user asks about current weather, temperature, rain, wind, or other atmospheric conditions. Requires location in 'City, State/Province' format."
```
### Step 2: Send Your Request with Tools Parameter
Once you've defined your function schemas, you include them in your Chat Completions API request. Here's how it looks in Python:
```python
from openai import OpenAI
client = OpenAI()
# Define your functions (as shown above)
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather for a specific location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g., San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}
]
# Make the API call
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[
{"role": "system", "content": "You are a helpful weather assistant."},
{"role": "user", "content": "What's the weather in Toronto?"}
],
tools=tools,
tool_choice="auto" # Let model decide whether to use tools
)
```
And in JavaScript using the Node.js SDK:
```javascript
import OpenAI from "openai";
const openai = new OpenAI();
const tools = [
{
type: "function",
function: {
name: "get_current_weather",
description: "Get the current weather for a specific location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "City and state, e.g., San Francisco, CA"
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
description: "Temperature unit"
}
},
required: ["location"]
}
}
}
];
const response = await openai.chat.completions.create({
model: "gpt-4-turbo",
messages: [
{ role: "system", content: "You are a helpful weather assistant." },
{ role: "user", content: "What's the weather in Toronto?" }
],
tools: tools,
tool_choice: "auto"
});
```
The `tool_choice` parameter controls the model's behavior:
- **`"auto"`** (default): Model decides if it needs to call a function based on context
- **`"none"`**: Disable function calling, force text-only response
- **`"required"`**: Model must call at least one function
- **Force a specific function**: Use `\{"type": "function", "function": \{"name": "specific_function"\}\}`
### Step 3: Parse the Model's Function Call
When the model decides to call a function, the response structure changes. Instead of returning `content`, it returns `tool_calls`. Here's how to detect and extract this:
```python
# Get the response from Step 2
if response.choices[0].finish_reason == "tool_calls":
# Model wants to call a function
message = response.choices[0].message
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
tool_call_id = tool_call.id
print(f"Function: {function_name}")
print(f"Arguments: {function_args}")
print(f"Call ID: {tool_call_id}")
else:
# Model returned text, no function call
print(f"Response: {response.choices[0].message.content}")
```
Key points:
- **`finish_reason == "tool_calls"`**: Indicates the model generated function call(s), not text
- **`tool_calls` array**: Can contain multiple calls (parallel execution, discussed later)
- **`function.arguments`**: A JSON string that must be parsed
- **`id`**: Unique identifier for matching results back to the request
A common mistake is assuming `tool_calls` always exists. Always check `finish_reason` first.
### Step 4: Execute and Return Results
Now comes the crucial part: your application executes the actual function. You parse the arguments, validate them, call your backend, and then return the results to the model for final processing.
```python
def get_current_weather(location: str, unit: str = "celsius") -> dict:
"""Your actual backend function"""
# In reality, you'd call a weather API here
# This is simplified for the example
return {
"location": location,
"temperature": 15,
"unit": unit,
"conditions": "Partly cloudy",
"humidity": 65,
"wind_speed": 12
}
# Execute the function with arguments from Step 3
function_args = json.loads(tool_call.function.arguments)
function_result = get_current_weather(**function_args)
# Return results to the model
final_response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[
{"role": "system", "content": "You are a helpful weather assistant."},
{"role": "user", "content": "What's the weather in Toronto?"},
# Include the original response with tool call
response.choices[0].message,
# Include the tool result with matching ID
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(function_result)
}
],
tools=tools
)
# Now final_response contains the natural language answer
print(final_response.choices[0].message.content)
# Output: "The current weather in Toronto is 15°C with partly cloudy skies and 65% humidity."
```
This loop-back is essential. The model needs to see:
1. The user's original request
2. Its own function call (from Step 2 response)
3. The function result from your backend
Only then can it synthesize a natural language response based on real data.
JSON Schema for Function Parameters
Getting your JSON Schema right is critical because it directly impacts the model's ability to call functions correctly. Let's dive deeper into parameter definition.
Parameter Types and Structures
JSON Schema supports several parameter types, each useful for different scenarios:
{
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "Unique user identifier (e.g., 'usr_12345')"
},
"quantity": {
"type": "integer",
"description": "Number of items to order. Must be positive.",
"minimum": 1,
"maximum": 1000
},
"price": {
"type": "number",
"description": "Price in decimal format (e.g., 19.99)"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "Task priority level"
},
"is_bulk_order": {
"type": "boolean",
"description": "Whether this is a bulk order"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of relevant tags (e.g., ['electronics', 'fragile'])"
},
"metadata": {
"type": "object",
"properties": {
"customer_segment": {
"type": "string"
},
"region": {
"type": "string"
}
},
"description": "Additional metadata about the order"
}
},
"required": ["user_id", "quantity"]
}
Key considerations for each type:
string: Flexible, but be specific in descriptions about format expectations. Are you expecting "[email protected]" format for emails? A specific date format?integer: Whole numbers. Useminimumandmaximumto set boundsnumber: Decimal values for prices, percentages, measurementsboolean: True/false flags for optionsarray: Lists of items. Specifyitemstype to indicate what the list containsobject: Nested structure for complex parameters. Define properties and requirements within
Nested objects are powerful for organizing complex parameters:
{
"type": "object",
"properties": {
"trip_details": {
"type": "object",
"properties": {
"departure": {
"type": "string",
"description": "Departure city"
},
"destination": {
"type": "string",
"description": "Destination city"
},
"departure_date": {
"type": "string",
"description": "Departure date in YYYY-MM-DD format"
}
},
"required": ["departure", "destination", "departure_date"]
}
},
"required": ["trip_details"]
}
Structured Outputs with Strict Mode
Production Best Practice
The latest OpenAI models support **strict mode**, which guarantees that the model's output adheres exactly to your JSON Schema with zero deviations. This is a game-changer for production systems.
When you enable strict mode, the model cannot:
- Add extra fields not in your schema
- Omit required fields
- Use the wrong data types
- Return `null` for fields that don't allow it
Enabling strict mode is simple—just add "strict": true to your function definition:
{
"type": "function",
"function": {
"name": "book_flight",
"strict": true,
"description": "Book a flight with guaranteed schema adherence",
"parameters": {
"type": "object",
"properties": {
"departure_city": {
"type": "string"
},
"destination_city": {
"type": "string"
},
"departure_date": {
"type": "string",
"format": "date"
},
"passengers": {
"type": "integer",
"minimum": 1
},
"cabin_class": {
"type": "string",
"enum": ["economy", "business", "first"]
}
},
"required": ["departure_city", "destination_city", "departure_date", "passengers"],
"additionalProperties": false
}
}
}
Important: When using strict mode, you must include "additionalProperties": false in your schema. This tells the model explicitly that no extra fields are allowed.
The benefit is substantial. Without strict mode, you need to validate that the response matches your expectations. With strict mode, you know it will match—100% of the time. This eliminates validation code and error handling for schema mismatches.
Writing Effective Parameter Descriptions
The descriptions you write for parameters directly impact the model's accuracy. The model uses these descriptions to:
- Decide whether to call the function at all
- Understand what values to provide for each parameter
- Format parameters correctly (date formats, units, special conventions)
Effective descriptions are:
- Specific about format: "Date in YYYY-MM-DD format" not just "Date"
- Clear about purpose: "Primary contact's email address" not just "Email"
- Concrete with examples: "City and state (e.g., 'Toronto, ON')" not just "Location"
- Explicit about constraints: "Positive integer between 1 and 100" not just "Number"
- Helpful about defaults: "Defaults to USD if not specified" prevents errors
// ❌ Weak descriptions
"location": {
"type": "string",
"description": "A location"
}
"date": {
"type": "string",
"description": "Date"
}
"quantity": {
"type": "integer",
"description": "Amount"
}
// ✅ Strong descriptions
"location": {
"type": "string",
"description": "City and state/province in 'City, State/Province' format (e.g., 'Toronto, ON', 'San Francisco, CA', 'London, UK'). Required for accurate weather lookup."
}
"date": {
"type": "string",
"description": "Departure date in ISO 8601 format (YYYY-MM-DD), e.g., '2025-03-15'. Must be today or in the future."
}
"quantity": {
"type": "integer",
"description": "Number of units to order. Must be positive (1 or greater) and not exceed 1000 units per order."
}
Remember that function descriptions consume input tokens. There's a balance between providing enough detail to guide the model and keeping descriptions concise to save tokens. Typically, you want 1-2 sentences per parameter, with critical details included.
Advanced Function Calling Capabilities
Once you've mastered the basics, function calling offers several advanced features that enable sophisticated automation.
Parallel Function Calling
Controlling Function Selection
Multi-Step Workflows
### Parallel Function Calling
Modern GPT models can generate multiple function calls in a single response. This is powerful when you have multiple independent operations that don't depend on each other.
For example, if a user asks "What's the weather in Toronto and New York, and what time is it in London?", the model can generate three function calls simultaneously:
```python
# Model generates multiple function calls
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[
{"role": "user", "content": "What's the weather in Toronto and New York, and the time in London?"}
],
tools=[weather_tool, time_tool],
parallel_tool_calls=True # Explicitly enable parallel calling
)
# Response contains multiple tool_calls
tool_calls = response.choices[0].message.tool_calls
print(f"Number of calls: {len(tool_calls)}")
# Execute all functions in parallel
results = []
for tool_call in tool_calls:
function_name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
# Execute the appropriate function
if function_name == "get_weather":
result = get_weather(**args)
elif function_name == "get_time":
result = get_time(**args)
# Collect result with matching ID
results.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Send all results back in a single request
final_response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[
{"role": "user", "content": "What's the weather in Toronto and New York, and the time in London?"},
response.choices[0].message,
*results # All results included
],
tools=[weather_tool, time_tool]
)
```
Parallel calling is valuable for:
- **Speed**: Execute multiple independent operations concurrently
- **Efficiency**: Single API call, multiple results
- **User experience**: Faster responses with complete information
You can control parallel calling with the `parallel_tool_calls` parameter (default is true for most modern models).
### Controlling Function Selection with tool_choice
The `tool_choice` parameter lets you control how aggressively the model uses function calling. It has several modes:
**`"auto"` (default)**: The model decides whether to call functions based on context. Most of the time, this is what you want. It's intelligent—only calling functions when appropriate.
```python
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[{"role": "user", "content": "Explain quantum computing"}],
tools=[get_weather_tool, search_database_tool],
tool_choice="auto" # Model might not call any function
)
```
**`"none"`**: Force the model to never call functions, even if it would be helpful. Use this when you want explanation without actions:
```python
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[{"role": "user", "content": "What would happen if I searched for product X?"}],
tools=[search_tool],
tool_choice="none" # Force text-only response
)
```
**`"required"`**: Force the model to call at least one function. This guarantees structured output when you need it:
```python
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[{"role": "user", "content": "Extract customer info: John Smith, [email protected], 555-0123"}],
tools=[extract_contact_info_tool],
tool_choice="required" # Must call a function
)
```
**Specific function**: Force the model to call a specific function. Useful when you know exactly which operation is needed:
```python
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[{"role": "user", "content": "Process this order data: ..."}],
tools=[process_order_tool, cancel_order_tool],
tool_choice={
"type": "function",
"function": {"name": "process_order"}
}
)
```
### Multi-Step Agentic Workflows
This is where function calling becomes truly powerful. You can chain multiple function calls across conversation turns, allowing the model to make decisions based on previous results.
Here's a realistic workflow: A customer asks "Find me the cheapest flight to Paris next week."
```
Turn 1: User asks → Model calls search_flights(destination="Paris", dates="next_week")
Functions return: [Flight A: $450, Flight B: $380, Flight C: $520]
Turn 2: Model analyzes results → Calls get_flight_details(flight_id="Flight B")
Functions return: Detailed info for Flight B (cheapest option)
Turn 3: Model wants seat availability → Calls check_availability(flight_id="Flight B")
Functions return: Seats available in economy, business
Turn 4: Model synthesizes response → Returns recommendation to user
Output: "I found the cheapest flight: Air Canada at $380. Here are your options..."
```
Implementing this pattern:
```python
# Initial request
messages = [
{"role": "user", "content": "Find me the cheapest flight to Paris next week"}
]
tools = [search_flights_tool, get_flight_details_tool, check_availability_tool]
# Agentic loop
max_iterations = 10
iteration = 0
while iteration
Real-World Use Cases
Function calling applies across industries. Let's explore practical implementations.
Conversational AI and Chatbots
Customer support is where function calling shines. Instead of a chatbot that can only provide pre-written responses, you can build one that accesses live data.
**Order Status Chatbot**:
```
Customer: "Where's my order?"
Bot (via function call): get_order_status(order_id="12345")
Backend returns: {status: "shipped", tracking: "1Z999AA1...", eta: "Dec 18"}
Bot response: "Your order #12345 has been shipped! Track it with 1Z999AA1... Estimated delivery: December 18th."
```
**FAQ with Live Data**:
```
Customer: "What's the price of your Pro plan?"
Bot (via function call): get_product_pricing(product="Pro plan")
Backend returns: {price: 99, currency: "USD", features: ["..."], included_seats: 5}
Bot response: "The Pro plan is $99/month and includes these features: [...]"
```
Function definitions for a customer support chatbot:
```json
[
{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Retrieve current status, tracking, and estimated delivery of a customer order. Use when customer asks about order status, tracking, or delivery dates.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID (e.g., '#12345' or 'ORD-2025-001')"
}
},
"required": ["order_id"]
}
}
},
{
"type": "function",
"function": {
"name": "get_product_pricing",
"description": "Look up current pricing and plan details for any product or service. Use when customer asks about price, cost, plan features, or what's included.",
"parameters": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "Name of the product or plan (e.g., 'Pro plan', 'Enterprise')"
}
},
"required": ["product_name"]
}
}
}
]
```
Data Extraction and Structured Output
Extract structured data from unstructured text. This is incredibly useful for form automation, resume parsing, or document processing.
**Resume Parsing**:
```
Input: "Hi, I'm John Smith from Acme Corp with 5 years in software engineering.
Technologies: Python, JavaScript, React. [email protected], 555-0123"
Function called: extract_resume_data()
Output:
{
"name": "John Smith",
"company": "Acme Corp",
"experience_years": 5,
"role": "Software Engineer",
"technologies": ["Python", "JavaScript", "React"],
"email": "[email protected]",
"phone": "555-0123"
}
```
**Email Contact Extraction**:
```
Input: "Hi, please contact our sales team at [email protected] or call 1-800-555-0123.
For technical support: [email protected]"
Function called: extract_contacts()
Output:
{
"contacts": [
{"type": "sales", "email": "[email protected]"},
{"type": "support_phone", "number": "1-800-555-0123"},
{"type": "support_email", "email": "[email protected]"}
]
}
```
```json
{
"type": "function",
"function": {
"name": "extract_resume_data",
"strict": true,
"description": "Parse resume text and extract structured professional information",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"company": {
"type": "string"
},
"experience_years": {
"type": "integer"
},
"technologies": {
"type": "array",
"items": {
"type": "string"
}
},
"email": {
"type": "string"
},
"phone": {
"type": "string"
}
},
"required": ["name", "email"],
"additionalProperties": false
}
}
}
```
Database Queries and Business Intelligence
Let customers query databases in plain English without SQL knowledge.
**Sales Dashboard Query**:
```
User: "Show me total sales by region for Q4"
Bot: execute_query(
query="SELECT region, SUM(sales) FROM orders
WHERE date BETWEEN '2024-10-01' AND '2024-12-31'
GROUP BY region"
)
Result: {North America: $2.4M, Europe: $1.8M, Asia: $950K}
Response: "Q4 Sales by Region: North America $2.4M, Europe $1.8M, Asia $950K.
North America led with 52% of total Q4 sales."
```
**Inventory Alert**:
```
User: "Which products are running low on stock?"
Bot: get_low_inventory(threshold=50)
Result: [{product: "Widget A", stock: 12}, {product: "Gizmo B", stock: 8}]
Response: "You have 2 products with
API Integration and System Orchestration
Combine function calling with business processes for end-to-end automation.
**Meeting Scheduling Workflow**:
```
User: "Schedule a demo with John for next Tuesday at 2pm and send him confirmation"
Step 1: check_calendar_availability(date="next Tuesday", duration=30, time="14:00")
✓ Time slot available
Step 2: create_calendar_event(
attendee="[email protected]",
title="Product Demo",
time="2025-01-21 14:00",
duration=30
)
✓ Event created
Step 3: send_email(
to="[email protected]",
template="demo_confirmation",
event_id="evt_12345"
)
✓ Email sent
Response: "All set! Demo scheduled for Tuesday at 2pm with John. Confirmation email sent."
```
**CRM Lead Enrichment**:
```
User: "I have a new lead - [email protected]. Get me more info and add to CRM."
Step 1: lookup_company(email="[email protected]")
✓ Returns: Tech Company Inc., San Francisco, Series B
Step 2: lookup_person(email="[email protected]")
✓ Returns: Jane Doe, VP Engineering, 8 years experience
Step 3: create_crm_contact(
email="[email protected]",
name="Jane Doe",
company="Tech Company Inc.",
role="VP Engineering",
location="San Francisco"
)
✓ Contact added with enriched data
Response: "Jane Doe (VP Engineering at Tech Company Inc.) added to CRM. Series B company, well-funded. Suggested follow-up action: Schedule discovery call."
```
E-Commerce and Booking Systems
Structured output is critical for commerce transactions where accuracy is essential.
**Travel Booking**:
```
User: "Book a trip from Toronto to Amsterdam for 6 people,
non-stop flights only, departing Dec 20"
Function called: search_and_book_flights(
departure_city="Toronto",
destination_city="Amsterdam",
passengers=6,
non_stop_only=true,
departure_date="2025-12-20"
)
Result: Flight booked, confirmation number, total price
Response: "Done! I found Air Canada AC158 departing Dec 20 at 10:15am,
arriving Dec 21 at 8:30am. 6 seats in economy. Total: $3,240."
```
**Product Search with Filters**:
```
User: "Show me laptops under $1000 with at least 16GB RAM"
Function called: search_products(
category="laptops",
max_price=1000,
min_ram_gb=16
)
Result: 8 matching products with detailed specs and availability
```
Content and Marketing Automation
Digital Thrive uses function calling extensively for [SEO](/en/services/seo/) and marketing automation.
**Content Brief Generation**:
```
User: "Create a content brief for 'technical SEO audit'"
Step 1: get_keyword_data(keyword="technical SEO audit")
✓ Returns: search volume, difficulty, CPC
Step 2: get_related_keywords(seed_keyword="technical SEO audit", limit=15)
✓ Returns: 15 related keywords and metrics
Step 3: analyze_serp_competition(keyword="technical SEO audit")
✓ Returns: Top 10 results, content length, backlinks
Step 4: Model synthesizes: "Based on 2,400 monthly searches and 8,200 backlinks
for the top result, here's your content brief..."
```
**Competitor Monitoring**:
```
User: "Monitor competitor pricing and features weekly"
Function: schedule_monitoring(
competitor_domain="competitor.com",
metrics=["pricing", "features", "new_pages"],
frequency="weekly"
)
Weekly reports: Pricing changes detected, 3 new features added, 2 blog posts published
```
Implementation Best Practices
These patterns ensure your function calling implementation is robust, secure, and maintainable.
Critical Practice
### Limit the Number of Functions
OpenAI's API supports up to 128 tools, but accuracy decreases significantly as you add more functions. The model has to "choose" among all available options, and with 100+ options, it makes mistakes.
**Best practice**: Provide fewer than 20 functions per API call.
Why? The model's token budget is finite. With 100 functions in the tools array, you're using thousands of tokens just on function definitions. The model has less context for actually understanding the user's request.
**Strategy for large tool sets**:
```python
# Instead of this (100 functions every time)
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=messages,
tools=ALL_128_FUNCTIONS # ❌ Poor accuracy
)
# Do this (select relevant subset)
# 1. Analyze user intent
relevant_functions = select_functions_by_intent(user_message)
# 2. Provide only relevant functions
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=messages,
tools=relevant_functions[:15] # ✅ Better accuracy, lower cost
)
```
You could use:
- Keyword matching: "search" in user message → include search_products, search_inventory
- Intent classification: Small classifier model determines intent → select functions
- Retrieval-Augmented Generation: Use embeddings to find similar function descriptions
Write Clear, Detailed Descriptions
We've mentioned this, but it bears repeating: function and parameter descriptions are your biggest lever for accuracy.
The model uses descriptions to decide:
- Whether to call the function at all
- Which arguments to provide
- How to format those arguments
// ❌ Bad descriptions → Model often chooses wrong function
{
"name": "get_data",
"description": "Get data"
}
// ✅ Good descriptions → Model chooses correctly
{
"name": "get_customer_orders",
"description": "Retrieve all orders for a specific customer. Use this when the user asks about their order history, past purchases, or all orders placed by a customer. Returns order ID, date, amount, and status."
}
For parameters, be explicit:
// ❌ Insufficient
"date": {
"type": "string",
"description": "Date"
}
// ✅ Detailed and helpful
"date": {
"type": "string",
"description": "Search start date in ISO 8601 format (YYYY-MM-DD), e.g., '2025-01-01'. Must be today or in the past."
}
Always Validate Function Calls
Never execute a function call directly from the model without validation. Treat the model's output like any untrusted user input.
def execute_function_call_safely(function_name, arguments):
# 1. Validate function exists
if function_name not in ALLOWED_FUNCTIONS:
return {"error": f"Unknown function: {function_name}"}
# 2. Validate required parameters
required = FUNCTION_SCHEMAS[function_name].get("required", [])
for param in required:
if param not in arguments:
return {"error": f"Missing required parameter: {param}"}
# 3. Validate parameter types
properties = FUNCTION_SCHEMAS[function_name].get("properties", {})
for param, value in arguments.items():
if param in properties:
expected_type = properties[param].get("type")
if not isinstance(value, get_python_type(expected_type)):
return {"error": f"Invalid type for {param}: expected {expected_type}"}
# 4. Validate parameter values (ranges, enums, etc.)
for param, value in arguments.items():
if param in properties:
schema = properties[param]
# Check enum values
if "enum" in schema and value not in schema["enum"]:
return {"error": f"Invalid value for {param}: must be one of {schema['enum']}"}
# Check numeric ranges
if "minimum" in schema and value = {schema['minimum']}"}
if "maximum" in schema and value > schema["maximum"]:
return {"error": f"{param} must be
Not Checking finish_reason
A dangerous assumption many developers make:
```python
# ❌ DANGEROUS - assumes tool_calls always exists
tool_call = response.choices[0].message.tool_calls[0]
```
The model might return a text response instead of function calls. If `finish_reason` isn't "tool_calls", then `tool_calls` doesn't exist, and you get an AttributeError.
**Always check finish_reason first**:
```python
# ✅ Correct approach
finish_reason = response.choices[0].finish_reason
if finish_reason == "tool_calls":
# Process function calls
tool_calls = response.choices[0].message.tool_calls
for tool_call in tool_calls:
# ... execute function ...
elif finish_reason == "stop":
# Model returned text only
text_response = response.choices[0].message.content
print(text_response)
else:
# Handle other cases (length limit, error, etc.)
print(f"Unexpected finish_reason: {finish_reason}")
```
Forgetting to Parse Arguments JSON
The `arguments` field is a JSON string, not a Python dictionary:
```python
# ❌ WRONG - TypeError
args = tool_call.function.arguments
location = args["location"] # ❌ Can't index a string
# ✅ CORRECT - parse first
import json
args = json.loads(tool_call.function.arguments)
location = args["location"] # ✅ Works
```
This trips up even experienced developers because it feels like it should be a dictionary.
Not Including tool_call_id in Results
The model needs the ID to match results to requests:
```python
# ❌ Missing ID—model doesn't know which result belongs to which call
{
"role": "tool",
"content": json.dumps(result)
}
# ✅ Include the ID
{
"role": "tool",
"tool_call_id": tool_call.id, # ✅ Critical
"content": json.dumps(result)
}
```
Without the ID, the model can't correlate results to function calls, leading to confused responses or errors.
Using Deprecated functions Parameter
The old API used `functions` parameter. It still works, but it's deprecated:
```python
# ❌ Old syntax (deprecated)
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[...],
functions=[...] # Don't use
)
# ✅ Current syntax
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[...],
tools=[{"type": "function", "function": {...}}] # Use this
)
```
The `tools` syntax is the future. Use it for new implementations.
Wrong Function Selection Due to Unclear Descriptions
When the model consistently chooses the wrong function, it's usually because descriptions are ambiguous:
```json
// ❌ Similar functions, unclear descriptions
{
"name": "get_user_info",
"description": "Get user information"
},
{
"name": "get_user_orders",
"description": "Get user orders"
}
// ✅ Distinct, specific descriptions
{
"name": "get_user_info",
"description": "Retrieve user profile information including name, email, phone, and account status. Use when customer asks about their profile or account details."
},
{
"name": "get_user_orders",
"description": "Retrieve customer's order history with status, dates, and amounts. Use when customer asks about their orders, purchases, or order history."
}
```
Debugging Strategies
When function calling isn't working:
**1. Log everything**:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug(f"User input: {user_message}")
logger.debug(f"Tools provided: {[t['function']['name'] for t in tools]}")
logger.debug(f"Model response: {response}")
logger.debug(f"Function chosen: {tool_call.function.name}")
logger.debug(f"Arguments: {tool_call.function.arguments}")
logger.debug(f"Execution result: {result}")
```
**2. Test function selection directly**:
```python
# Create requests that should clearly trigger specific functions
test_cases = [
("What's the weather?", "get_weather"),
("Show me my orders", "get_user_orders"),
("How much does premium cost?", "get_pricing")
]
for user_input, expected_function in test_cases:
response = client.chat.completions.create(
model="gpt-4-turbo",
messages=[{"role": "user", "content": user_input}],
tools=tools
)
if response.choices[0].finish_reason == "tool_calls":
actual_function = response.choices[0].message.tool_calls[0].function.name
status = "✓" if actual_function == expected_function else "✗"
print(f"{status} Input: '{user_input}' → Expected: {expected_function}, Got: {actual_function}")
```
**3. Validate your JSON Schema**:
```python
import jsonschema
# Validate your function schemas
for tool in tools:
try:
# Check if schema is valid
jsonschema.Draft7Validator.check_schema(tool["function"]["parameters"])
print(f"✓ {tool['function']['name']} schema is valid")
except jsonschema.SchemaError as e:
print(f"✗ {tool['function']['name']} has schema error: {e}")
```
**4. Use OpenAI Playground**:
- Go to platform.openai.com/playground
- Paste your function definitions
- Test with real user queries
- Iterate on descriptions quickly without code changes
**5. Check API errors**:
- Invalid schema → API error (your schema has JSON Schema issues)
- Model refuses to call function → Description issue (model doesn't understand when to use it)
- Parsing errors → Check your JSON handling code
---
## Function Calling vs Assistants API
You might be wondering: "Should I use function calling or the Assistants API?" Both support tools, but they're different paradigms.
Function Calling (Chat Completions)
Assistants API (with Tools)
Decision Matrix
### Function Calling (Chat Completions)
**Characteristics**:
- **Stateless**: You manage conversation history in your code
- **Low-level control**: You handle every step of the workflow
- **Flexible**: Complete control over function execution, error handling, and integration
- **Token efficient**: You only include messages and history you actually need
**When to use**:
- Building custom AI agent architectures
- Integrating with existing systems where you need precise control
- Cost-conscious applications where token efficiency matters
- Complex workflows requiring custom orchestration logic
**Real example**: Digital Thrive builds most production agents with function calling because we need to integrate with client systems, implement custom monitoring, and optimize costs.
### Assistants API (with Tools)
**Characteristics**:
- **Stateful**: OpenAI manages conversation threads for you
- **Higher-level abstraction**: You specify what to do, OpenAI handles execution
- **Built-in orchestration**: Automatic multi-turn conversations with function calling
- **Less infrastructure**: No conversation history management code needed
- **File handling**: Built-in support for code interpreter and file retrieval
**When to use**:
- Rapid prototyping where speed matters more than cost
- Standard chatbot patterns that fit OpenAI's model
- When you need file operations (uploads, code execution)
- Simple applications where you don't need custom integrations
### Decision Matrix
| Factor | Function Calling | Assistants API |
|--------|------------------|-----------------|
| **Control** | Maximum | Good |
| **Customization** | Unlimited | Limited to Assistant capabilities |
| **Ease of setup** | More code | Less code |
| **Cost optimization** | Better | Standard |
| **Integration** | Easy with any system | OpenAI ecosystem focus |
| **Scalability** | Excellent | Good |
| **File handling** | Manual | Built-in |
| **Complex workflows** | Better suited | Possible but constrained |
**Our recommendation**: Start with whichever fits your immediate need, but for production systems with serious integration requirements, function calling via Chat Completions offers more control and better economics.
---
## Integration with Digital Thrive Services
Function calling is the backbone of our [AI & Automation services](/en/services/ai-automation/). Here's how it connects to what we build.
AI Agent Development
Function calling is foundational to custom AI agent development. We use it to build intelligent systems that interact with your business processes.
**Real example**:
A logistics company needed automated customer support. We built an agent using function calling that:
1. Understands customer questions naturally
2. Calls `get_shipment_status()` to retrieve tracking
3. Calls `update_delivery_address()` if needed
4. Calls `create_support_ticket()` for complex issues
5. Synthesizes natural responses combining all data
The agent handles 60% of customer inquiries without human intervention.
Web Development Integration
Function calling powers AI features embedded in [web applications](/en/services/web-development/):
- **Live chatbots**: Customer support that accesses order databases
- **Natural language search**: "Show me blue widgets under $50" instead of filters
- **Intelligent forms**: Auto-populate, validate, and suggest based on context
- **Dynamic content**: AI-generated pages pulling from live APIs
Analytics and Data Access
Use function calling to make [analytics](/en/services/analytics/) accessible through conversation:
- **Natural language queries**: "What were our top products last month?"
- **Automated reporting**: AI compiles data and generates reports
- **Real-time dashboards**: Conversational interface to explore metrics
- **Anomaly detection**: AI monitors metrics and alerts on changes
SEO and Content Automation
AI-powered [SEO](/en/services/seo/) workflows using function calling:
- **Keyword research**: Call SEO tool APIs for competitive data
- **Competitor analysis**: Gather and analyze competitor tactics
- **Content optimization**: Check rankings, suggest improvements
- **Technical audits**: Crawl sites and identify issues at scale
---
## Frequently Asked Questions
Does the model actually execute my functions?
No—common misconception. The model generates structured JSON describing which function to call and what arguments to provide. **Your application** executes the actual function. The model's role is understanding intent and generating valid arguments.
Think of it like a restaurant: The customer (user) makes a request, the manager (model) writes an order, and the chef (your code) executes it.
How many functions should I provide?
OpenAI supports up to 128 tools theoretically, but accuracy drops with more than 20 functions. More functions means the model has to choose from more options, and with hundreds of choices, it makes mistakes.
**Best practice**: Fewer than 20 functions per request. Use intent detection to select relevant functions for larger tool sets.
What if the model chooses the wrong function?
Improve function descriptions. Add specific guidance about when to use each function, include examples, and differentiate similar functions clearly.
If descriptions don't help:
- Implement validation to reject inappropriate calls
- Use `tool_choice` to force correct function
- Consider consolidating similar functions
- Ask users for clarification before executing wrong function
Can I force a specific function?
Yes, use `tool_choice`:
```python
tool_choice={
"type": "function",
"function": {"name": "function_name"}
}
```
This guarantees the model will call that function (useful for data extraction where you know which operation is needed).
Which GPT models support function calling?
- GPT-4 and GPT-4 Turbo (all variants)
- GPT-4o and GPT-4o Mini
- GPT-3.5-turbo (1106 version and later)
Check OpenAI's [model documentation](/resources/guides/gpt-models/) for the most current list.
How much do function calls cost?
Function definitions are included in input tokens (your schema descriptions count toward input cost). The model's response is output tokens. Executing your actual backend functions costs whatever your systems charge—OpenAI doesn't charge additional fees for execution.
**Cost optimization**: Fewer functions and shorter descriptions = fewer tokens = lower cost. Learn more about [pricing](/resources/guides/pricing/).
Can I use function calling with streaming?
Technically yes, but it's complex. Function calls stream as deltas, requiring careful accumulation of chunks before you can execute them. For most applications, non-streaming works better when function calling is involved.
What's the difference between JSON Mode and function calling?
- **JSON Mode**: Forces the model to output valid JSON, but structure isn't guaranteed
- **Function Calling**: Model outputs JSON matching your exact schema
- **Strict Mode**: Guarantees 100% schema compliance
Use function calling when you need guaranteed structure. JSON Mode when you need general JSON output without strict schema requirements.
How do I handle long-running functions?
For functions that take under 5 seconds:
- Show loading state to user
- Implement sensible timeout (under 30 seconds typical)
For functions taking >5 seconds:
- Consider async/background execution
- Return a status check function instead: `check_operation_status(operation_id)`
- Notify user when complete via polling or webhooks
Can functions call other functions?
Not directly—the model doesn't chain function calls automatically. But you can implement multi-step workflows:
1. User request → Model calls Function A
2. You execute Function A
3. Return results to model
4. Model analyzes results → Calls Function B
5. You execute Function B
6. Model synthesizes final response
This is the foundation of agentic behavior—loops of function calling and decision-making. Learn more about [AI agents](/resources/guides/ai-agents/).
---
## Summary
Key Takeaways
Function calling is one of the most powerful capabilities in modern LLMs. It bridges natural language understanding with structured API calls, enabling applications that can genuinely integrate with your systems, access real data, and take meaningful actions.
**Key principles to remember**:
- **Function calling ≠ function execution**: The model generates arguments; your code executes functions
- **Use `tools`, not deprecated `functions`**: Modern, flexible, future-proof
- **Descriptions matter enormously**: They guide the model's function selection accuracy
- **Enable strict mode in production**: Guarantees schema adherence, eliminates validation headaches
- **Limit functions to fewer than 20**: Better accuracy, lower token costs
- **Always validate before executing**: Treat model output like untrusted input
- **Start simple**: Single function call first, then parallel calling, then multi-step workflows
Function calling is the foundation for building sophisticated AI agents, automation workflows, and intelligent applications. Whether you're creating customer support bots, data extraction systems, or autonomous workflows, mastering function calling is essential.
Ready to build AI-powered automation? [Contact Digital Thrive](/contact) to discuss how we can help you implement function calling in your systems.
---
## Sources
1. [OpenAI Cookbook - How to Call Functions with Chat Models](https://cookbook.openai.com/examples/how_to_call_functions_with_chat_models)
2. [Vellum.ai - Setting Up OpenAI Function Calling with Chat Models](https://www.vellum.ai/blog/openai-function-calling-tutorial)
3. [Kanaries - OpenAI Function Calling: Examples to Get Started](https://docs.kanaries.net/articles/openai-function-calling)
4. [Mirascope - A Guide to Function Calling in OpenAI](https://mirascope.com/blog/openai-function-calling)
5. [Prompt Engineering Guide - Function Calling with LLMs](https://www.promptingguide.ai/applications/function_calling)
6. [OpenAI Developer Community - Functions vs Tools Discussion](https://community.openai.com/t/functions-vs-tools-what-is-the-difference/603277)
7. [OpenAI Developer Community - Tool Calling Best Practices](https://community.openai.com/t/prompting-best-practices-for-tool-use-function-calling/1123036)