Introduction
In this next AI agent pattern, we dive into an Employee Leave Request Agent. The purpose of this agent is to streamline employee leave requests by verifying eligibility, checking for overlaps, confirming policy compliance, and processing the request once all conditions are met. The agent’s job is to ensure that the leave request complies with the company’s leave policies, handles approval workflows, and ensures that no steps are skipped or overlooked. Just like previous examples, it will never claim success unless the task is fully completed, with all actions tracked and receipts issued.
The Use Case
Employees often submit requests for vacation, sick leave, or personal time off (PTO). The agent’s role is to read the request, verify that the employee has enough leave balance, ensure that the requested time does not conflict with other approved leaves, and check for any other policy requirements. The agent either processes the request automatically or sends it for manual approval if needed. All actions (approvals, denials, or requests for more information) are logged, with receipts for every action taken.
Prompt Contract (agent interface)
# file: contracts/leave_request_v1.yaml
role: "LeaveRequestAgent"
scope: >
Process employee leave requests by checking leave balance, eligibility, and policy compliance.
Ask once for missing fields (employee_id, leave_type, start_date, end_date, justification).
Propose tool calls; never assert success without a receipt.
output:
type: object
required: [summary, decision, leave_details, citations, next_steps, tool_proposals]
properties:
summary: {type: string, maxWords: 80}
decision: {type: string, enum: ["approve", "reject", "need_approval", "need_more_info"]}
leave_details:
type: object
required: [employee_id, leave_type, start_date, end_date, balance_after_leave]
properties:
employee_id: {type: string}
leave_type: {type: string, enum: ["vacation", "sick", "personal"]}
start_date: {type: string}
end_date: {type: string}
balance_after_leave: {type: integer} # remaining leave balance in hours or days
citations: {type: array, items: {type: string}}
next_steps: {type: array, items: {type: string}, maxItems: 5}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name: {type: string, enum: [CheckLeaveBalance, CheckLeavePolicy, ApproveLeave, RequestApproval]}
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "leave_policy.v5"
citation_rule: "1–2 minimal-span claim_ids per factual sentence"
decoding:
narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
bullets: {top_p: 0.82, temperature: 0.45}
Example claims (context given to the model)
[
{"claim_id":"policy:leave:vacation_max_days","text":"Employees are allowed to take up to 15 days of vacation per year.",
"effective_date":"2025-01-01","source_id":"doc:leave_policy_v5","span":"15 days of vacation"},
{"claim_id":"policy:leave:sick_leave","text":"Sick leave can be used for up to 10 days per year.",
"effective_date":"2025-01-01","source_id":"doc:leave_policy_v5","span":"10 days per year"},
{"claim_id":"policy:leave:advance_approval_required","text":"All leave requests need at least 1 week of advance notice.",
"effective_date":"2025-01-01","source_id":"doc:leave_policy_v5","span":"advance notice required"}
]
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel
from datetime import date
from typing import Optional, List, Dict
class CheckLeaveBalanceArgs(BaseModel):
employee_id: str
leave_type: str
class CheckLeavePolicyArgs(BaseModel):
employee_id: str
leave_type: str
start_date: date
end_date: date
class ApproveLeaveArgs(BaseModel):
employee_id: str
leave_type: str
start_date: date
end_date: date
balance_after_leave: int
class RequestApprovalArgs(BaseModel):
employee_id: str
leave_type: str
start_date: date
end_date: date
approver_id: str
class ToolReceipt(BaseModel):
tool: str
ok: bool
ref: str
message: str = ""
data: Optional[Dict] = None
# adapters.py (demo logic)
from tools import *
from datetime import datetime, timedelta
LEAVE_BALANCE = {"U001": {"vacation": 120, "sick": 40, "personal": 30}} # hours remaining
LEAVE_POLICY = {
"vacation": {"max_days": 15, "advance_notice_required": 7},
"sick": {"max_days": 10, "advance_notice_required": 0},
"personal": {"max_days": 7, "advance_notice_required": 0}
}
def check_leave_balance(a: CheckLeaveBalanceArgs) -> ToolReceipt:
balance = LEAVE_BALANCE.get(a.employee_id, {}).get(a.leave_type, 0)
return ToolReceipt(tool="CheckLeaveBalance", ok=True, ref=f"balance-{a.employee_id}",
message="Leave balance checked", data={"balance": balance})
def check_leave_policy(a: CheckLeavePolicyArgs) -> ToolReceipt:
leave_days_requested = (a.end_date - a.start_date).days + 1
max_days_allowed = LEAVE_POLICY.get(a.leave_type, {}).get("max_days", 0)
if leave_days_requested > max_days_allowed:
return ToolReceipt(tool="CheckLeavePolicy", ok=False, ref="policy-limit",
message=f"Exceeded maximum {a.leave_type} days ({max_days_allowed} days allowed).")
advance_notice = (a.start_date - datetime.utcnow().date()).days
if advance_notice < LEAVE_POLICY[a.leave_type]["advance_notice_required"]:
return ToolReceipt(tool="CheckLeavePolicy", ok=False, ref="policy-advance-notice",
message=f"Leave request must be submitted at least {LEAVE_POLICY[a.leave_type]['advance_notice_required']} days in advance.")
return ToolReceipt(tool="CheckLeavePolicy", ok=True, ref="policy-ok", message="Leave policy checked")
def approve_leave(a: ApproveLeaveArgs) -> ToolReceipt:
new_balance = LEAVE_BALANCE[a.employee_id][a.leave_type] - (a.end_date - a.start_date).days
LEAVE_BALANCE[a.employee_id][a.leave_type] = new_balance
return ToolReceipt(tool="ApproveLeave", ok=True, ref=f"approve-{a.employee_id}",
message="Leave approved", data={"balance_after_leave": new_balance})
def request_approval(a: RequestApprovalArgs) -> ToolReceipt:
return ToolReceipt(tool="RequestApproval", ok=True, ref=f"approval-{a.employee_id}",
message=f"Approval requested for {a.leave_type} leave.", data={"approver_id": a.approver_id})
Agent Loop (proposal → verification → execution → receipts)
# agent_leave.py
import uuid, json
from typing import Any, Dict, List
from tools import *
from adapters import *
ALLOWED_TOOLS = {"CheckLeaveBalance", "CheckLeavePolicy", "ApproveLeave", "RequestApproval"}
def new_idem() -> str:
return f"idem-{uuid.uuid4()}"
def verify_proposal(p: Dict[str, Any]) -> str:
required = {"name","args","preconditions","idempotency_key"}
if not required.issubset(p): return "Missing proposal fields"
if p["name"] not in ALLOWED_TOOLS: return "Tool not allowed"
return ""
def execute(p: Dict[str, Any]) -> ToolReceipt:
n, a = p["name"], p["args"]
if n == "CheckLeaveBalance": return check_leave_balance(CheckLeaveBalanceArgs(**a))
if n == "CheckLeavePolicy": return check_leave_policy(CheckLeavePolicyArgs(**a))
if n == "ApproveLeave": return approve_leave(ApproveLeaveArgs(**a))
if n == "RequestApproval": return request_approval(RequestApprovalArgs(**a))
return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")
# --- Model shim returning a plan per contract (replace with your LLM call) ---
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], leave_request: Dict[str,Any]) -> Dict[str,Any]:
decision = "approve" if leave_request["leave_type"] == "sick" else "need_approval"
return {
"summary": f"Leave request for {leave_request['employee_id']} checked and ready for approval.",
"decision": decision,
"leave_details": {
"employee_id": leave_request["employee_id"],
"leave_type": leave_request["leave_type"],
"start_date": leave_request["start_date"].isoformat(),
"end_date": leave_request["end_date"].isoformat(),
"balance_after_leave": LEAVE_BALANCE[leave_request["employee_id"]][leave_request["leave_type"]] - (leave_request["end_date"] - leave_request["start_date"]).days
},
"citations": ["policy:leave:vacation_max_days","policy:leave:sick_leave","policy:leave:advance_approval_required"],
"next_steps": ["Check leave balance", "Check leave policy", "Approve leave or request approval"],
"tool_proposals": [
{"name":"CheckLeaveBalance","args":{"employee_id":leave_request["employee_id"],"leave_type":leave_request["leave_type"]},
"preconditions":"Check balance for the requested leave.","idempotency_key": new_idem()},
{"name":"CheckLeavePolicy","args":{"employee_id":leave_request["employee_id"],"leave_type":leave_request["leave_type"],
"start_date":leave_request["start_date"],"end_date":leave_request["end_date"]},
"preconditions":"Ensure policy compliance (max days, advance notice).","idempotency_key": new_idem()},
{"name":"ApproveLeave","args":{"employee_id":leave_request["employee_id"],"leave_type":leave_request["leave_type"],
"start_date":leave_request["start_date"],"end_date":leave_request["end_date"],
"balance_after_leave": LEAVE_BALANCE[leave_request["employee_id"]][leave_request["leave_type"]] - (leave_request["end_date"] - leave_request["start_date"]).days},
"preconditions":"Approve leave if compliant.","idempotency_key": new_idem()},
{"name":"RequestApproval","args":{"employee_id":leave_request["employee_id"],"leave_type":leave_request["leave_type"],
"start_date":leave_request["start_date"],"end_date":leave_request["end_date"],"approver_id":"M001"},
"preconditions":"Request manager approval for over-policy leave.","idempotency_key": new_idem()}
]
}
def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
idx = {r.tool:r for r in receipts}
lines = [model_json["summary"], ""]
lines.append(f"Decision: {model_json['decision']}")
lines.append(f"Leave Details: {model_json['leave_details']}")
lines.append("")
lines.append("Next steps:")
for s in model_json["next_steps"]:
lines.append(f"• {s}")
if idx.get("ApproveLeave") and idx["ApproveLeave"].ok:
lines.append(f"\nLeave approved: {idx['ApproveLeave'].ref}")
if idx.get("RequestApproval") and idx["RequestApproval"].ok:
lines.append(f"Approval requested: {idx['RequestApproval'].message}")
lines.append("\nCitations: " + ", ".join(model_json["citations"]))
return "\n".join(lines)
def handle(leave_request: Dict[str,Any]) -> str:
contract = open("contracts/leave_request_v1.yaml").read()
claims: List[Dict[str,Any]] = [] # load real claims
plan = call_model(contract, claims, leave_request)
receipts: List[ToolReceipt] = []
for prop in plan["tool_proposals"]:
reason = verify_proposal(prop)
if reason:
receipts.append(ToolReceipt(tool=prop["name"], ok=False, ref="blocked", message=reason)); continue
rec = execute(prop)
receipts.append(rec)
if not rec.ok and prop["name"] in {"ApproveLeave"}:
break
return render_response(plan, receipts)
if __name__ == "__main__":
example_leave_request = {
"employee_id":"E001",
"leave_type":"vacation",
"start_date": date(2025,10,10),
"end_date": date(2025,10,15),
"justification":"Vacation in the mountains"
}
print(handle(example_leave_request))
The Prompt You’d Send to the Model (concise and testable)
System:
You are LeaveRequestAgent. Follow the contract:
- Ask once if employee_id, leave_type, start_date, end_date, or justification is missing.
- Cite 1–2 claim_ids per factual sentence using provided claims.
- Propose tools; never assert success without a receipt.
- Output JSON with keys: summary, decision, leave_details, citations[], next_steps[], tool_proposals[].
Claims (eligible only):
[ ... JSON array of policy claims like above ... ]
User:
Request leave for employee E001 from 2025-10-10 to 2025-10-15, vacation, justification: "Vacation in the mountains"
How to adapt quickly
Replace the mock leave balance, policy compliance, approvals, and payment methods with your internal HR and payroll systems. Implement idempotency for leave requests, time-bound responses, and SLA enforcement. Load claims from your HR and leave policy systems, ensuring leave caps, advance notice requirements, and leave balances are accurately tracked. Add a validation layer to ensure correct formats, lexicon, and compliance. Deploy the contract, policy bundle, and decoder settings behind a feature flag for easy testing, canary deployment, and rollback support.