By Sagar Shankaran, Founder of CallSphere
Build an AI agent that handles returns and refunds by enforcing company policies, managing approval workflows, processing partial refunds, and handling exceptions with proper escalation for edge cases.
Key takeaways
Returns and refunds seem straightforward until you account for the real-world complexity: time-window policies, restocking fees, partial refunds for bundles, different rules for digital versus physical products, and exceptions for VIP customers or defective items. A returns agent must encode all of this business logic while remaining conversational and helpful.
The key insight is separating policy evaluation from the conversation. The agent talks to the customer; a policy engine determines what is allowed.
The policy engine evaluates return eligibility based on structured rules. It takes the order details and return reason and produces a clear allow/deny decision with explanations.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
class ReturnReason(Enum):
DEFECTIVE = "defective"
WRONG_ITEM = "wrong_item"
NOT_AS_DESCRIBED = "not_as_described"
CHANGED_MIND = "changed_mind"
DUPLICATE_ORDER = "duplicate_order"
class RefundType(Enum):
FULL = "full"
PARTIAL = "partial"
STORE_CREDIT = "store_credit"
DENIED = "denied"
@dataclass
class ReturnPolicy:
return_window_days: int = 30
restocking_fee_pct: float = 0.15
free_return_reasons: list = None
digital_returnable: bool = False
def __post_init__(self):
if self.free_return_reasons is None:
self.free_return_reasons = [
ReturnReason.DEFECTIVE,
ReturnReason.WRONG_ITEM,
]
@dataclass
class ReturnEvaluation:
eligible: bool
refund_type: RefundType
refund_amount: float
restocking_fee: float
reason: str
requires_approval: bool
class PolicyEngine:
def __init__(self, policy: ReturnPolicy):
self.policy = policy
def evaluate(
self,
order_date: datetime,
order_total: float,
return_reason: ReturnReason,
is_digital: bool = False,
is_vip: bool = False,
) -> ReturnEvaluation:
days_since_order = (datetime.utcnow() - order_date).days
# Digital products
if is_digital and not self.policy.digital_returnable:
if return_reason not in (
ReturnReason.DEFECTIVE,
ReturnReason.WRONG_ITEM,
):
return ReturnEvaluation(
eligible=False,
refund_type=RefundType.DENIED,
refund_amount=0,
restocking_fee=0,
reason="Digital products are non-refundable",
requires_approval=False,
)
# Return window check
window = self.policy.return_window_days
if is_vip:
window = int(window * 1.5)
if days_since_order > window:
if return_reason == ReturnReason.DEFECTIVE:
return ReturnEvaluation(
eligible=True,
refund_type=RefundType.FULL,
refund_amount=order_total,
restocking_fee=0,
reason="Defective items eligible outside return window",
requires_approval=True,
)
return ReturnEvaluation(
eligible=False,
refund_type=RefundType.DENIED,
refund_amount=0,
restocking_fee=0,
reason=f"Return window of {window} days has passed",
requires_approval=False,
)
# Calculate refund
if return_reason in self.policy.free_return_reasons:
return ReturnEvaluation(
eligible=True,
refund_type=RefundType.FULL,
refund_amount=order_total,
restocking_fee=0,
reason="Full refund for defective/wrong item",
requires_approval=False,
)
# Changed mind — restocking fee applies
fee = order_total * self.policy.restocking_fee_pct
return ReturnEvaluation(
eligible=True,
refund_type=RefundType.PARTIAL,
refund_amount=order_total - fee,
restocking_fee=fee,
reason=f"Refund minus {self.policy.restocking_fee_pct*100:.0f}% restocking fee",
requires_approval=False,
)
Some returns require manager approval — high-value orders, out-of-window exceptions, or repeated returns from the same customer. The approval workflow queues these cases and tracks their resolution.
from enum import Enum as PyEnum
import uuid
class ApprovalStatus(PyEnum):
PENDING = "pending"
APPROVED = "approved"
DENIED = "denied"
@dataclass
class ApprovalRequest:
id: str
order_id: str
evaluation: ReturnEvaluation
customer_id: str
status: ApprovalStatus = ApprovalStatus.PENDING
reviewer: Optional[str] = None
notes: str = ""
class ApprovalWorkflow:
def __init__(self):
self.pending: dict[str, ApprovalRequest] = {}
def submit(
self, order_id: str, customer_id: str, evaluation: ReturnEvaluation
) -> ApprovalRequest:
request = ApprovalRequest(
id=str(uuid.uuid4()),
order_id=order_id,
evaluation=evaluation,
customer_id=customer_id,
)
self.pending[request.id] = request
return request
def approve(self, request_id: str, reviewer: str, notes: str = ""):
req = self.pending.get(request_id)
if req:
req.status = ApprovalStatus.APPROVED
req.reviewer = reviewer
req.notes = notes
def deny(self, request_id: str, reviewer: str, notes: str = ""):
req = self.pending.get(request_id)
if req:
req.status = ApprovalStatus.DENIED
req.reviewer = reviewer
req.notes = notes
The conversational agent collects information from the customer, evaluates the policy, and either processes the return immediately or submits it for approval.
from agents import Agent, function_tool
policy_engine = PolicyEngine(ReturnPolicy())
approval_workflow = ApprovalWorkflow()
@function_tool
def evaluate_return(
order_id: str,
order_date: str,
order_total: float,
return_reason: str,
is_digital: bool,
is_vip: bool,
) -> str:
"""Evaluate whether a return is eligible per company policy."""
reason_enum = ReturnReason(return_reason)
dt = datetime.fromisoformat(order_date)
result = policy_engine.evaluate(
order_date=dt,
order_total=order_total,
return_reason=reason_enum,
is_digital=is_digital,
is_vip=is_vip,
)
if result.requires_approval:
req = approval_workflow.submit(order_id, "customer", result)
return (
f"Return requires approval. Request ID: {req.id}. "
f"Reason: {result.reason}. "
f"Estimated refund: ${result.refund_amount:.2f}"
)
if result.eligible:
return (
f"Return approved. Refund: ${result.refund_amount:.2f}. "
f"Restocking fee: ${result.restocking_fee:.2f}. "
f"Reason: {result.reason}"
)
return f"Return denied. Reason: {result.reason}"
returns_agent = Agent(
name="Returns Agent",
instructions="""You handle return and refund requests.
Collect these details before evaluating:
1. Order ID
2. Reason for return (defective, wrong_item, not_as_described,
changed_mind, duplicate_order)
3. Confirm the item and order details
Use evaluate_return to check eligibility. Explain the result
clearly to the customer. If a restocking fee applies, explain
why. If approval is needed, tell the customer their request
has been submitted and they will hear back within 24 hours.
Never override policy decisions. Never promise outcomes you
cannot guarantee.""",
tools=[evaluate_return],
)
Break the bundle into individual item prices and apply the return policy to each item separately. If the customer returns only one item from a three-item bundle, refund that item's prorated price minus any bundle discount they received. Store individual item prices at order time to make this calculation possible.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
For standard, policy-compliant returns below a dollar threshold (e.g., under $100), yes — the agent should process automatically. For high-value orders, out-of-window exceptions, or repeated returns, require human approval. This balances speed with financial risk management.
Track return frequency per customer and flag accounts that exceed a threshold (e.g., more than three returns in 90 days). The policy engine should consider return history as an input. For flagged accounts, route all returns to human review regardless of the policy evaluation.
#Returns #RefundAutomation #WorkflowAutomation #PolicyEnforcement #AIAgents #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco