By Sagar Shankaran, Founder of CallSphere
Build an AI agent that handles employee clock-in/out, displays work schedules, manages timecard exceptions, and routes approval workflows — replacing clunky time tracking interfaces with conversational interactions.
Key takeaways
Time and attendance systems are notoriously frustrating. Employees forget to clock in, navigate confusing web portals to view schedules, and fill out paper forms for exceptions. Managers spend hours each pay period reviewing timecards and chasing down missing punches. An AI agent wraps all of this into a simple conversational interface: "Clock me in," "What is my schedule next week?", "I forgot to clock out yesterday at 5 PM."
The architectural challenge is ensuring accuracy — payroll depends on correct time records, so the agent must validate every operation and maintain a clear audit trail.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
from dataclasses import dataclass, field
from datetime import date, datetime, time, timedelta
from typing import Optional
from enum import Enum
from agents import Agent, Runner, function_tool
import json
class PunchType(Enum):
CLOCK_IN = "clock_in"
CLOCK_OUT = "clock_out"
BREAK_START = "break_start"
BREAK_END = "break_end"
class ExceptionType(Enum):
MISSED_PUNCH = "missed_punch"
EARLY_DEPARTURE = "early_departure"
LATE_ARRIVAL = "late_arrival"
OVERTIME_REQUEST = "overtime_request"
SCHEDULE_CHANGE = "schedule_change"
@dataclass
class TimePunch:
punch_id: str
employee_id: str
punch_type: PunchType
timestamp: datetime
source: str # "agent", "kiosk", "manual"
verified: bool = True
@dataclass
class ScheduleEntry:
employee_id: str
date: date
start_time: time
end_time: time
department: str
position: str
@dataclass
class TimeException:
exception_id: str
employee_id: str
exception_type: ExceptionType
date: date
description: str
corrected_time: Optional[datetime] = None
status: str = "pending" # "pending", "approved", "denied"
approved_by: Optional[str] = None
PUNCHES_DB: dict[str, list[TimePunch]] = {}
SCHEDULE_DB: dict[str, list[ScheduleEntry]] = {}
EXCEPTIONS_DB: dict[str, list[TimeException]] = {}
The clock tool validates punches against the employee's schedule and flags anomalies like double clock-ins or punches far outside scheduled hours.
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
@function_tool
def clock_in_out(employee_id: str, punch_type: str) -> str:
"""Record a clock-in or clock-out punch for an employee."""
now = datetime.now()
valid_types = {"clock_in": PunchType.CLOCK_IN, "clock_out": PunchType.CLOCK_OUT,
"break_start": PunchType.BREAK_START, "break_end": PunchType.BREAK_END}
if punch_type not in valid_types:
return json.dumps({"error": f"Invalid punch type. Use: {list(valid_types.keys())}"})
# Check for duplicate punches
existing = PUNCHES_DB.get(employee_id, [])
recent = [p for p in existing if (now - p.timestamp).seconds < 300
and p.punch_type == valid_types[punch_type]]
if recent:
return json.dumps({"error": "Duplicate punch detected. "
"A similar punch was recorded within the last 5 minutes."})
# Validate sequence (cannot clock out without clocking in)
if punch_type == "clock_out":
today_punches = [p for p in existing if p.timestamp.date() == now.date()]
clock_ins = [p for p in today_punches if p.punch_type == PunchType.CLOCK_IN]
clock_outs = [p for p in today_punches if p.punch_type == PunchType.CLOCK_OUT]
if len(clock_outs) >= len(clock_ins):
return json.dumps({"error": "No matching clock-in found for today."})
punch = TimePunch(
punch_id=f"P-{employee_id[:4]}-{now.strftime('%H%M%S')}",
employee_id=employee_id,
punch_type=valid_types[punch_type],
timestamp=now,
source="agent",
)
PUNCHES_DB.setdefault(employee_id, []).append(punch)
# Check if late or early
schedule = _get_today_schedule(employee_id)
alerts = []
if schedule and punch_type == "clock_in":
scheduled_start = datetime.combine(now.date(), schedule.start_time)
if now > scheduled_start + timedelta(minutes=5):
alerts.append(f"Late arrival: {int((now - scheduled_start).seconds / 60)} minutes")
return json.dumps({
"status": "recorded",
"punch_type": punch_type,
"timestamp": now.isoformat(),
"alerts": alerts,
})
def _get_today_schedule(employee_id: str) -> Optional[ScheduleEntry]:
entries = SCHEDULE_DB.get(employee_id, [])
today = date.today()
return next((e for e in entries if e.date == today), None)
@function_tool
def get_schedule(employee_id: str, week_offset: int = 0) -> str:
"""Get an employee's schedule for the current or upcoming week."""
today = date.today()
week_start = today - timedelta(days=today.weekday()) + timedelta(weeks=week_offset)
week_end = week_start + timedelta(days=6)
entries = SCHEDULE_DB.get(employee_id, [])
week_schedule = [
e for e in entries if week_start <= e.date <= week_end
]
result = []
for entry in sorted(week_schedule, key=lambda e: e.date):
result.append({
"date": str(entry.date),
"day": entry.date.strftime("%A"),
"start": entry.start_time.strftime("%I:%M %p"),
"end": entry.end_time.strftime("%I:%M %p"),
"department": entry.department,
})
total_hours = sum(
(datetime.combine(date.min, e.end_time) - datetime.combine(date.min, e.start_time)).seconds / 3600
for e in week_schedule
)
return json.dumps({
"week": f"{week_start} to {week_end}",
"shifts": result,
"total_scheduled_hours": round(total_hours, 1),
})
@function_tool
def submit_time_exception(
employee_id: str,
exception_type: str,
exception_date: str,
description: str,
corrected_time: str = "",
) -> str:
"""Submit a timecard exception for manager review."""
valid_types = {t.value: t for t in ExceptionType}
if exception_type not in valid_types:
return json.dumps({"error": f"Invalid type. Use: {list(valid_types.keys())}"})
exc_date = date.fromisoformat(exception_date)
if (date.today() - exc_date).days > 14:
return json.dumps({"error": "Exceptions older than 14 days require HR review."})
corrected = datetime.fromisoformat(corrected_time) if corrected_time else None
exception = TimeException(
exception_id=f"EXC-{employee_id[:4]}-{exc_date.isoformat()}",
employee_id=employee_id,
exception_type=valid_types[exception_type],
date=exc_date,
description=description,
corrected_time=corrected,
)
EXCEPTIONS_DB.setdefault(employee_id, []).append(exception)
return json.dumps({
"status": "submitted",
"exception_id": exception.exception_id,
"type": exception_type,
"date": exception_date,
"routed_to": "Direct manager for approval",
})
attendance_agent = Agent(
name="TimeBot",
instructions="""You are TimeBot, a time and attendance assistant.
Help employees clock in/out, view schedules, and submit timecard exceptions.
Always confirm the action before recording a punch.
For missed punches, require the employee to specify the correct time.
Never modify past punches directly — route all corrections through exceptions.""",
tools=[clock_in_out, get_schedule, submit_time_exception],
)
Store all timestamps in UTC internally and convert to the employee's local time zone for display. The employee profile includes a time zone field, and the agent uses it for all time-related operations. Schedule entries are stored in the employee's local time zone since shifts are location-specific.
Implement geofencing or IP-based validation as additional verification layers. The agent can check whether the request originates from an approved location or network. For remote workers, use periodic activity checks rather than location verification.
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.
The agent tracks total hours worked per day and per week. When a clock-out would push daily hours past 8 or weekly hours past 40, the agent flags the overtime and routes a notification to the manager. Some jurisdictions require daily overtime calculations, while others use weekly — the configuration is location-specific.
#TimeTracking #Attendance #ScheduleManagement #WorkforceManagement #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.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
An agentic-AI perspective on Anthropic Skills system, covering orchestration patterns, tool use, and how agent tooling fits production agent stacks.
Enterprise CIO Guide perspective on Comet's general-availability launch put an agentic browser in front of millions of consumers, and it works better than the demos suggested.
Enterprise CIO Guide perspective on Harvey AI's enterprise rollout numbers show legal agents have moved past the pilot stage at AmLaw 100 firms.
Enterprise CIO Guide perspective on Hippocratic AI's deployment numbers show healthcare voice agents are moving from pilot to production across major US health systems.
An agentic-AI perspective on Claude Agent SDK loops, covering orchestration patterns, tool use, and how agent orchestration fits production agent stacks.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco