Skip to main content

Build support AI: Execute tools and take actions

A chatbot answers questions; an agent takes action. The difference between a support chatbot that says "I'll check your account" and an agent that actually checks your account is tool execution. I've seen support teams reduce resolution time by 78% by giving agents the ability to call APIs, look up accounts, and process refunds—all within the same conversation. This article covers production-grade tool design: defining tool schemas, handling errors gracefully, validating inputs, and coordinating multi-step workflows.

Tool schema design and function calling

Every tool your agent can use must have a clear schema (name, description, parameters). Claude's function-calling interface follows this pattern:

import json
from anthropic import Anthropic

# Define available tools
TOOLS = [
{
"name": "lookup_customer_account",
"description": "Retrieve customer account details: name, tier, subscription, payment method, open tickets.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "Unique customer ID (e.g., cust_12345)"
},
"include_payment_method": {
"type": "boolean",
"description": "Include payment method details (requires extra permission check)",
"default": False
}
},
"required": ["customer_id"]
}
},
{
"name": "check_refund_eligibility",
"description": "Check if a customer is eligible for a refund given order/subscription details.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"order_id": {"type": "string", "description": "Order ID (required for one-time purchases)"},
"subscription_id": {"type": "string", "description": "Subscription ID (required for recurring billing)"},
"reason": {"type": "string", "description": "Reason for refund request"}
},
"required": ["customer_id"]
}
},
{
"name": "process_refund",
"description": "Process a refund. Requires explicit authorization.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"order_id": {"type": "string"},
"amount_cents": {"type": "integer", "description": "Amount in cents (e.g., 2999 for $29.99)"},
"reason": {"type": "string"}
},
"required": ["customer_id", "amount_cents", "reason"]
}
},
{
"name": "create_support_ticket",
"description": "Create a ticket for escalation to a human specialist.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"title": {"type": "string", "description": "Ticket summary"},
"description": {"type": "string", "description": "Full description of the issue"},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"default": "medium"
}
},
"required": ["customer_id", "title", "description"]
}
}
]

class SupportToolExecutor:
"""Execute tools safely in a support agent context."""

def __init__(self):
self.client = Anthropic()
self.execution_log = [] # Audit trail

def execute_tool(self, tool_name: str, tool_input: dict) -> str:
"""Execute a tool and return result as JSON string."""

# Validate input schema
validation_error = self._validate_tool_input(tool_name, tool_input)
if validation_error:
return json.dumps({
"status": "error",
"error": f"Invalid input: {validation_error}"
})

# Check authorization (critical for high-risk tools like refunds)
auth_check = self._check_authorization(tool_name, tool_input)
if not auth_check["authorized"]:
return json.dumps({
"status": "error",
"error": f"Authorization failed: {auth_check['reason']}"
})

# Log execution for audit
self.execution_log.append({
"tool": tool_name,
"input": tool_input,
"timestamp": __import__("datetime").datetime.now().isoformat()
})

# Dispatch to implementation
try:
if tool_name == "lookup_customer_account":
return self._impl_lookup_account(tool_input)
elif tool_name == "check_refund_eligibility":
return self._impl_check_refund(tool_input)
elif tool_name == "process_refund":
return self._impl_process_refund(tool_input)
elif tool_name == "create_support_ticket":
return self._impl_create_ticket(tool_input)
else:
return json.dumps({"status": "error", "error": f"Tool {tool_name} not found."})
except Exception as e:
# Log error for investigation
self.execution_log.append({
"error": str(e),
"tool": tool_name,
"timestamp": __import__("datetime").datetime.now().isoformat()
})
return json.dumps({
"status": "error",
"error": f"Tool execution failed: {str(e)}"
})

def _validate_tool_input(self, tool_name: str, tool_input: dict) -> str | None:
"""Validate input against schema. Return error message if invalid."""
tool_def = next((t for t in TOOLS if t["name"] == tool_name), None)
if not tool_def:
return f"Tool {tool_name} not found."

schema = tool_def["input_schema"]
required = schema.get("required", [])

for req_field in required:
if req_field not in tool_input:
return f"Missing required field: {req_field}"

# Type checking
for field, value in tool_input.items():
if field not in schema["properties"]:
return f"Unknown field: {field}"

expected_type = schema["properties"][field].get("type")
if expected_type == "integer" and not isinstance(value, int):
return f"Field {field} must be an integer, got {type(value).__name__}"
elif expected_type == "string" and not isinstance(value, str):
return f"Field {field} must be a string, got {type(value).__name__}"

return None

def _check_authorization(self, tool_name: str, tool_input: dict) -> dict:
"""Check if this tool call is authorized."""
# High-risk tools require explicit authorization
high_risk = {"process_refund", "create_support_ticket"}

if tool_name in high_risk:
customer_id = tool_input.get("customer_id")
if not customer_id:
return {"authorized": False, "reason": "Customer ID required"}

# In production, query your authorization service
# For now, assume all calls are authorized (in real code, check ACLs)
return {"authorized": True}

return {"authorized": True}

def _impl_lookup_account(self, tool_input: dict) -> str:
"""Lookup customer account (mock implementation)."""
customer_id = tool_input["customer_id"]
# In production, query your customer database
return json.dumps({
"status": "success",
"customer_id": customer_id,
"name": "John Doe",
"tier": "premium",
"subscription_status": "active",
"monthly_spend": 99.99,
"open_tickets": 1,
"payment_method": "***4242" if tool_input.get("include_payment_method") else "hidden"
})

def _impl_check_refund(self, tool_input: dict) -> str:
"""Check refund eligibility (mock)."""
customer_id = tool_input["customer_id"]
# In production, query refund policy service
tier = "premium" # from lookup

eligibility = {
"free": 0, # days
"basic": 7,
"premium": 30
}

days_eligible = eligibility.get(tier, 0)

return json.dumps({
"status": "success",
"eligible": days_eligible > 0,
"days_remaining": days_eligible,
"message": f"Customer is eligible for {days_eligible}-day refund window."
})

def _impl_process_refund(self, tool_input: dict) -> str:
"""Process a refund (high-risk operation)."""
customer_id = tool_input["customer_id"]
amount_cents = tool_input["amount_cents"]
reason = tool_input["reason"]

# In production, call payment processor (Stripe, etc.)
refund_id = f"refund_{customer_id}_{__import__('uuid').uuid4().hex[:8]}"

return json.dumps({
"status": "success",
"refund_id": refund_id,
"amount": f"${amount_cents / 100:.2f}",
"customer_id": customer_id,
"reason": reason,
"initiated_at": __import__("datetime").datetime.now().isoformat(),
"message": f"Refund of ${amount_cents / 100:.2f} initiated. Processing typically takes 3-5 business days."
})

def _impl_create_ticket(self, tool_input: dict) -> str:
"""Create support ticket for escalation."""
customer_id = tool_input["customer_id"]
title = tool_input["title"]
description = tool_input["description"]
priority = tool_input.get("priority", "medium")

ticket_id = f"ticket_{__import__('uuid').uuid4().hex[:8]}"

return json.dumps({
"status": "success",
"ticket_id": ticket_id,
"customer_id": customer_id,
"title": title,
"priority": priority,
"message": f"Ticket {ticket_id} created with {priority} priority. A specialist will review within 24 hours."
})

This design ensures:

  • Clear input validation — schema is the source of truth
  • Authorization checks — high-risk tools (refunds) verify permissions
  • Audit trail — every tool call is logged
  • Graceful error handling — failures return JSON errors, never crash

Error handling and fallback strategies

Not every tool call succeeds. A customer lookup might timeout, a refund might fail due to insufficient balance, or an API might be down. Design graceful fallbacks:

def handle_tool_error(
tool_name: str,
tool_input: dict,
error: str,
fallback_strategy: str = "escalate"
) -> str:
"""Handle tool failures with fallback strategies."""

strategies = {
"escalate": "escalate to human",
"retry": "retry after 5 seconds",
"cached": "use cached data from last 24 hours",
"explain": "explain limitation to customer"
}

if tool_name == "lookup_customer_account":
if "timeout" in error.lower():
# Cache hit: return cached data if available
return json.dumps({
"status": "partial",
"message": "Account lookup was slow; using cached data from earlier today.",
"data_freshness": "~2 hours old"
})
else:
return json.dumps({
"status": "error",
"message": "Unable to retrieve account. Escalating to a specialist."
})

elif tool_name == "process_refund":
# Never fall back on refund processing; always escalate
return json.dumps({
"status": "error",
"message": "Refund processing temporarily unavailable. Escalating to our billing team."
})

else:
return json.dumps({
"status": "error",
"message": f"Tool {tool_name} failed: {error}. Escalating for manual handling."
})

Multi-step workflows: Coordinating tools

Some customer problems require multiple tools in sequence. A refund request might require: (1) lookup account, (2) check eligibility, (3) process refund, (4) create ticket for confirmation. The agent's loop handles this:

def agent_refund_workflow(customer_id: str, executor: SupportToolExecutor, client: Anthropic) -> str:
"""Example: Agent handles refund request using multiple tools."""

system_prompt = """You are a support agent handling a refund request. Follow this workflow:

1. Use lookup_customer_account to get customer details.
2. Use check_refund_eligibility to verify they can get a refund.
3. If eligible, use process_refund to initiate it.
4. Use create_support_ticket to create a confirmation ticket.

If any step fails, escalate to a human."""

messages = [
{
"role": "user",
"content": f"Customer {customer_id} is requesting a refund for their subscription."
}
]

# Agentic loop
for iteration in range(5): # Max 5 tool calls
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=system_prompt,
tools=TOOLS,
messages=messages
)

# Extract tool calls
tool_calls = [
b for b in response.content if b.type == "tool_use"
]

if not tool_calls:
# No more tools; return final response
return next(
(b.text for b in response.content if b.type == "text"),
"Refund processing complete."
)

# Execute all tool calls
for tool_call in tool_calls:
result = executor.execute_tool(tool_call.name, tool_call.input)

# Append tool result to messages
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": result
}
]
})

return "Refund workflow incomplete; escalating to specialist."

Limiting tool access and permissions

Not all agents should have all tools. A tier-1 support agent can look up accounts and create tickets but not process refunds. Implement role-based tool access:

TOOL_PERMISSIONS = {
"tier_1_support": ["lookup_customer_account", "create_support_ticket"],
"tier_2_support": ["lookup_customer_account", "check_refund_eligibility", "create_support_ticket"],
"billing_team": ["lookup_customer_account", "check_refund_eligibility", "process_refund", "create_support_ticket"],
"admin": ["lookup_customer_account", "check_refund_eligibility", "process_refund", "create_support_ticket"]
}

def filter_tools_by_role(agent_role: str) -> list[dict]:
"""Return only tools this agent is authorized to use."""
allowed_tools = TOOL_PERMISSIONS.get(agent_role, [])
return [t for t in TOOLS if t["name"] in allowed_tools]

Key Takeaways

  • Tool schema is your contract — define it clearly (name, description, input schema, required fields) so the model understands what each tool does.
  • Validate inputs and check authorization — never blindly execute a tool call; validate schema compliance and check ACLs before execution.
  • Design graceful error fallbacks — tool failures are inevitable; cache old data, explain limitations to customers, escalate when appropriate.
  • Tools enable multi-step workflows — use the agentic loop to call tools in sequence, passing results from one tool into the next.
  • Implement role-based tool access — not all agents need all tools; restrict refunds to billing team, lookups to tier-1+, etc.

Frequently Asked Questions

What's the difference between a tool and an API call?

A tool is a structured function definition (schema) that an LLM can choose to call. An API call is the actual HTTP request. Tools abstract away HTTP details so the model can focus on logic. Tools are safer because they force schema validation and authorization checks before execution.

How do I prevent agents from using tools incorrectly?

Validation (schema check the inputs), examples in the system prompt (show correct usage), and tests (verify tools work before deploying). Also set tool-specific constraints: "process_refund can only be called if check_refund_eligibility returned true."

Can a customer see which tools the agent used?

You can choose to show or hide this. For transparency, cite tool results: "According to our records (lookup_customer_account), you have 3 open tickets." For privacy, hide technical details but mention the lookup: "I checked your account and found..."

What if a tool returns data the agent shouldn't share?

Filter tool results before appending to the conversation history. For example, lookup_account returns a payment method; mask it (***4242) before the agent sees it. Tools should also respect permissions: don't return sensitive data the agent shouldn't access.

How do I test tool workflows?

Unit test each tool (mock backend, verify happy path and errors). Integration test the agentic loop (simulate multi-turn conversation, verify tool sequence). End-to-end test with real customer data in staging (verify refund flow works without actually processing refunds).

Further Reading