By Sagar Shankaran, Founder of CallSphere
Build an AI agent that automates document collection for accounting firms, tracks tax filing deadlines, manages client portal access, and provides real-time status updates during the hectic tax season.
Key takeaways
Every January, accounting firms begin the same stressful cycle: chasing clients for W-2s, 1099s, mortgage interest statements, and dozens of other documents. Staff spend hours making phone calls and sending emails that say "we still need your..." The documents trickle in over weeks, creating bottlenecks that push everything toward the April deadline. An AI agent transforms this reactive document chase into a proactive, automated workflow.
This tutorial builds an agent that tracks which documents each client needs, sends reminders, processes submissions, and keeps clients informed about their filing status.
Accounting firms need to track each client's filing type, required documents, submission status, and deadlines. The data model captures these relationships.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent for financial services 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, field
from datetime import datetime, date, timedelta
from enum import Enum
from typing import Optional
class FilingType(Enum):
INDIVIDUAL_1040 = "1040"
BUSINESS_1120 = "1120" # C-Corp
PARTNERSHIP_1065 = "1065"
S_CORP_1120S = "1120S"
TRUST_1041 = "1041"
class DocumentStatus(Enum):
NEEDED = "needed"
REQUESTED = "requested"
RECEIVED = "received"
REVIEWED = "reviewed"
ISSUE = "issue" # document has a problem
class FilingStatus(Enum):
NOT_STARTED = "not_started"
GATHERING_DOCS = "gathering_docs"
IN_PREPARATION = "in_preparation"
REVIEW = "review"
READY_TO_FILE = "ready_to_file"
FILED = "filed"
EXTENDED = "extended"
@dataclass
class RequiredDocument:
name: str
description: str
status: DocumentStatus = DocumentStatus.NEEDED
received_date: Optional[date] = None
issue_note: str = ""
@dataclass
class TaxClient:
id: str
name: str
phone: str
email: str
filing_type: FilingType
filing_status: FilingStatus = FilingStatus.GATHERING_DOCS
documents: list[RequiredDocument] = field(default_factory=list)
deadline: date = date(2026, 4, 15)
assigned_preparer: str = ""
notes: str = ""
last_contact: Optional[date] = None
Different filing types require different sets of documents. We define these requirements so the agent knows exactly what to request from each client.
DOCUMENT_REQUIREMENTS = {
FilingType.INDIVIDUAL_1040: [
RequiredDocument("W-2", "Wage and income statements from all employers"),
RequiredDocument("1099-INT", "Interest income from banks and investments"),
RequiredDocument("1099-DIV", "Dividend income statements"),
RequiredDocument("1099-NEC", "Non-employee compensation (freelance income)"),
RequiredDocument("1098", "Mortgage interest statement"),
RequiredDocument("Property Tax Bills", "Annual property tax statements"),
RequiredDocument("Charitable Donations", "Receipts for charitable contributions over $250"),
RequiredDocument("Health Insurance (1095)", "Health insurance coverage form"),
RequiredDocument("Prior Year Return", "Last year's tax return for reference"),
],
FilingType.BUSINESS_1120: [
RequiredDocument("Income Statement", "Profit and loss statement for the fiscal year"),
RequiredDocument("Balance Sheet", "Year-end balance sheet"),
RequiredDocument("Bank Statements", "All business bank statements (12 months)"),
RequiredDocument("Payroll Reports", "Annual payroll summary and W-3"),
RequiredDocument("Depreciation Schedule", "Fixed asset and depreciation details"),
RequiredDocument("Accounts Receivable", "Outstanding receivables aging report"),
RequiredDocument("Accounts Payable", "Outstanding payables aging report"),
RequiredDocument("Loan Documents", "Business loan statements and interest paid"),
],
}
def create_client_checklist(filing_type: FilingType) -> list[RequiredDocument]:
"""Create a fresh document checklist for a client based on filing type."""
templates = DOCUMENT_REQUIREMENTS.get(filing_type, [])
return [
RequiredDocument(doc.name, doc.description)
for doc in templates
]
Tax season has firm deadlines with serious consequences for missing them. The agent must track deadlines and prioritize accordingly.
TAX_DEADLINES = {
FilingType.PARTNERSHIP_1065: date(2026, 3, 16),
FilingType.S_CORP_1120S: date(2026, 3, 16),
FilingType.INDIVIDUAL_1040: date(2026, 4, 15),
FilingType.BUSINESS_1120: date(2026, 4, 15),
FilingType.TRUST_1041: date(2026, 4, 15),
}
def get_deadline_status(client: TaxClient) -> dict:
today = date.today()
days_left = (client.deadline - today).days
docs_needed = sum(
1 for d in client.documents
if d.status in (DocumentStatus.NEEDED, DocumentStatus.REQUESTED)
)
docs_total = len(client.documents)
docs_received = docs_total - docs_needed
if days_left < 0:
urgency = "OVERDUE"
elif days_left <= 7:
urgency = "CRITICAL"
elif days_left <= 30:
urgency = "APPROACHING"
else:
urgency = "ON_TRACK"
return {
"client": client.name,
"deadline": client.deadline.isoformat(),
"days_left": max(days_left, 0),
"urgency": urgency,
"documents_received": f"{docs_received}/{docs_total}",
"filing_status": client.filing_status.value,
"recommendation": (
"File extension" if urgency in ("OVERDUE", "CRITICAL") and docs_needed > 3
else "Prioritize outstanding documents" if urgency == "CRITICAL"
else "On track"
),
}
from agents import Agent, Runner, function_tool
CLIENTS_DB = {
"chen": TaxClient(
"c1", "Robert Chen", "555-0301", "robert@email.com",
FilingType.INDIVIDUAL_1040,
documents=create_client_checklist(FilingType.INDIVIDUAL_1040),
assigned_preparer="Amy Liu",
),
}
# Simulate some documents received
CLIENTS_DB["chen"].documents[0].status = DocumentStatus.RECEIVED # W-2
CLIENTS_DB["chen"].documents[0].received_date = date(2026, 2, 1)
CLIENTS_DB["chen"].documents[8].status = DocumentStatus.RECEIVED # Prior year
@function_tool
def check_client_status(client_name: str) -> str:
"""Check a client's document submission status and deadline."""
key = client_name.lower().split()[-1]
client = CLIENTS_DB.get(key)
if not client:
return f"Client '{client_name}' not found."
status = get_deadline_status(client)
outstanding = [
d.name for d in client.documents
if d.status in (DocumentStatus.NEEDED, DocumentStatus.REQUESTED)
]
result = (
f"Client: {status['client']}\n"
f"Filing: {client.filing_type.value}\n"
f"Deadline: {status['deadline']} ({status['days_left']} days left)\n"
f"Documents: {status['documents_received']} received\n"
f"Status: {status['urgency']}\n"
)
if outstanding:
result += f"Still needed: {', '.join(outstanding)}\n"
result += f"Recommendation: {status['recommendation']}"
return result
@function_tool
def send_document_reminder(client_name: str, documents: str) -> str:
"""Send a reminder to a client about outstanding documents."""
key = client_name.lower().split()[-1]
client = CLIENTS_DB.get(key)
if not client:
return f"Client not found."
doc_list = [d.strip() for d in documents.split(",")]
client.last_contact = date.today()
return (
f"Reminder sent to {client.name} ({client.email})\n"
f"Documents requested: {', '.join(doc_list)}\n"
f"Message: 'Hi {client.name.split()[0]}, we still need the following "
f"documents to complete your {client.filing_type.value} filing: "
f"{', '.join(doc_list)}. Your deadline is {client.deadline.isoformat()}. "
f"Please upload them to your client portal or email them to us.'"
)
@function_tool
def mark_document_received(
client_name: str, document_name: str
) -> str:
"""Mark a document as received for a client."""
key = client_name.lower().split()[-1]
client = CLIENTS_DB.get(key)
if not client:
return "Client not found."
for doc in client.documents:
if document_name.lower() in doc.name.lower():
doc.status = DocumentStatus.RECEIVED
doc.received_date = date.today()
remaining = sum(
1 for d in client.documents if d.status == DocumentStatus.NEEDED
)
return (
f"Marked '{doc.name}' as received for {client.name}. "
f"{remaining} documents still outstanding."
)
return f"Document '{document_name}' not found in {client.name}'s checklist."
@function_tool
def request_extension(client_name: str, reason: str) -> str:
"""File for a tax deadline extension for a client."""
key = client_name.lower().split()[-1]
client = CLIENTS_DB.get(key)
if not client:
return "Client not found."
client.filing_status = FilingStatus.EXTENDED
new_deadline = date(2026, 10, 15)
client.deadline = new_deadline
return (
f"Extension request initiated for {client.name}.\n"
f"New deadline: {new_deadline.isoformat()}\n"
f"Reason: {reason}\n"
f"Note: Extension extends time to file, not time to pay. "
f"Estimated tax payments may still be due by the original deadline."
)
accounting_agent = Agent(
name="Tax Season Assistant",
instructions="""You are a professional tax season assistant for an accounting firm.
1. When a client calls, use check_client_status to see their current
document status and deadline urgency.
2. If documents are outstanding, explain which ones are still needed
and why they are important.
3. Use send_document_reminder for clients who need follow-up.
4. When a client says they have submitted a document, use
mark_document_received to update their record.
5. If the deadline is critical and documents are unlikely to arrive
in time, discuss the option to request_extension.
IMPORTANT:
- Never provide specific tax advice. Direct tax questions to the
assigned preparer.
- Be understanding about document collection — many clients find
the process confusing.
- Emphasize the deadline and consequences of late filing.""",
tools=[check_client_status, send_document_reminder, mark_document_received, request_extension],
)
Set up a webhook from your client portal (such as TaxDome, Canopy, or a custom system) that fires when a document is uploaded. The webhook handler calls mark_document_received with the client name and document type. This keeps the agent's status view in sync with actual uploads without manual intervention.
Still reading? Stop comparing — try CallSphere live.
See the financial services AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
Yes. Build a batch reminder function that queries all clients with outstanding documents and a deadline within 30 days, then sends personalized reminders for each. Run this as a weekly cron job during January through March, increasing to daily in April. The agent handles individual follow-ups, while the batch process handles proactive outreach at scale.
The agent does not decide whether to file an extension — that is the preparer's judgment. The agent identifies clients at risk (critical urgency with many missing documents) and flags them for the preparer. If the preparer approves, the agent handles filing the extension request and communicating the new deadline to the client.
#Accounting #TaxSeason #DocumentCollection #ClientPortal #Python #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.
Togolese professional-services firms lose new clients when calls go unanswered. See how lawyers, accountants, notaries, and consultants use CallSphere to capture and qualify every enquiry 24/7 in French and Ewe from around 30,000 XOF a month.
Maltese professional-services firms in Valletta and Sliema use CallSphere AI voice and chat agents to answer, qualify and book new-client inquiries in English, Maltese and Italian without interrupting billable work.
Tunisian professional firms lose clients and billable time to unanswered calls. See how law offices, accountants and consultancies in Tunis use CallSphere AI voice and chat agents to run first intake 24/7 in French and Arabic and protect fee earners.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco