Skip to main content

Leave Auto-Action Technical Specification

Version: 1.0.0

Module: OX Bema - Leave Management

Last Updated: January 11, 2026


1. Overview

This document specifies the auto-action logic for leave requests when the reporting manager does not respond within the defined response window.

1.1 Objectives

  • Prevent leave requests from remaining in pending state indefinitely
  • Fair treatment: Employee not penalized for manager inaction
  • Clear accountability: Late applications result in auto-decline
  • Exception handling: Sick/emergency leave always approved

1.2 Core Principle

IF window_expires_before_leave_starts THEN auto_approve
IF leave_starts_before_window_expires THEN auto_decline
IF leave_type IN [SICK, URGENT, BEREAVEMENT] THEN always_auto_approve

2. Auto-Action Logic Flow

                    LEAVE REQUEST SUBMITTED


┌─────────────────────┐
│ Calculate Window │
│ Expiry Time │
└─────────────────────┘


┌─────────────────────┐
│ Is Leave Type │
│ SICK/URGENT/BEREAVE?│
└─────────────────────┘

┌───────────┴───────────┐
│ YES │ NO
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ Set Default: │ │ Compare: │
│ AUTO_APPROVE │ │ window_expiry vs │
│ │ │ leave_start_time │
└─────────────────┘ └─────────────────────┘
│ │
│ ┌───────────┴───────────┐
│ │ │
│ ▼ ▼
│ ┌─────────────────┐ ┌─────────────────┐
│ │ Window expires │ │ Leave starts │
│ │ FIRST │ │ FIRST │
│ └─────────────────┘ └─────────────────┘
│ │ │
│ ▼ ▼
│ ┌─────────────────┐ ┌─────────────────┐
│ │ Set Default: │ │ Set Default: │
│ │ AUTO_APPROVE │ │ AUTO_DECLINE │
│ └─────────────────┘ └─────────────────┘
│ │ │
└───────────┴───────────────────────┘


┌─────────────────────┐
│ Start Timer & │
│ Notify Manager │
└─────────────────────┘


┌─────────────────────┐
│ Manager Responds? │
└─────────────────────┘

┌───────────┴───────────┐
│ YES │ NO (Timeout)
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ Apply Manager's │ │ Execute Default │
│ Decision │ │ Action │
└─────────────────┘ └─────────────────────┘


┌─────────────────────┐
│ Log Action & │
│ Notify Employee │
└─────────────────────┘

3. Working Hours Configuration

3.1 Parameters

{
"working_hours": {
"start_time": "09:30",
"end_time": "19:00",
"timezone": "Asia/Kolkata",
"working_days": ["monday", "tuesday", "wednesday", "thursday", "friday"],
"exclude_holidays": true,
"holiday_calendar_ref": "ox_infy_holidays_2026"
}
}

3.2 Working Hours Per Day

Start: 09:30
End: 19:00
Total: 9 hours 30 minutes (9.5 hours)

3.3 Calculation Algorithm

def calculate_window_expiry(request_time, window_hours, config):
"""
Calculate when the response window expires.

Args:
request_time: DateTime when leave request was submitted
window_hours: Number of working hours for response window
config: Working hours configuration

Returns:
DateTime when window expires
"""
remaining_hours = window_hours
current_time = request_time

while remaining_hours > 0:
# Skip to next working day if current is non-working
if not is_working_day(current_time, config):
current_time = next_working_day_start(current_time, config)
continue

# Calculate available hours today
today_start = max(current_time, working_day_start(current_time, config))
today_end = working_day_end(current_time, config)

# If current time is after working hours, skip to next day
if current_time >= today_end:
current_time = next_working_day_start(current_time, config)
continue

available_today = (today_end - today_start).total_hours()

if available_today >= remaining_hours:
# Window expires today
return today_start + timedelta(hours=remaining_hours)
else:
# Consume today's hours, continue to next day
remaining_hours -= available_today
current_time = next_working_day_start(current_time, config)

return current_time

3.4 Calculation Examples

Example 1: Same Day Expiry

Request:     Monday 10:00 AM
Window: 8 working hours
Available: Monday 10:00 AM to 7:00 PM = 9 hours
Needed: 8 hours

Expiry: Monday 6:00 PM (10:00 AM + 8 hours)

Example 2: Next Day Expiry

Request:     Monday 6:00 PM
Window: 8 working hours
Available: Monday 6:00 PM to 7:00 PM = 1 hour
Remaining: 8 - 1 = 7 hours
Next Day: Tuesday 9:30 AM + 7 hours

Expiry: Tuesday 4:30 PM

Example 3: Weekend Skip

Request:     Friday 5:00 PM
Window: 8 working hours
Available: Friday 5:00 PM to 7:00 PM = 2 hours
Remaining: 8 - 2 = 6 hours
Saturday: Skip (weekend)
Sunday: Skip (weekend)
Monday: 9:30 AM + 6 hours

Expiry: Monday 3:30 PM

Example 4: Holiday Skip

Request:     Wednesday 10:00 AM
Window: 8 working hours
Thursday: Public Holiday (skip)
Available: Wednesday 10:00 AM to 7:00 PM = 9 hours

Expiry: Wednesday 6:00 PM (holiday doesn't affect as window fits in Wed)

4. Leave Type Configuration

4.1 Configuration Schema

{
"leave_types": {
"EARNED_LEAVE": {
"code": "EL",
"response_window_hours": 8,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "DECLINE"
},
"exceptions": []
},
"SICK_LEAVE": {
"code": "SL",
"response_window_hours": 4,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "APPROVE"
},
"exceptions": ["ALWAYS_APPROVE"],
"flags": {
"flag_for_documentation": true,
"flag_pattern_abuse": true
}
},
"URGENT_LEAVE": {
"code": "UL",
"response_window_hours": 4,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "APPROVE"
},
"exceptions": ["ALWAYS_APPROVE"],
"flags": {
"flag_pattern_abuse": true
}
},
"BEREAVEMENT": {
"code": "BV",
"response_window_hours": 4,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "APPROVE"
},
"exceptions": ["ALWAYS_APPROVE"]
},
"MATERNITY": {
"code": "ML",
"response_window_hours": 24,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "APPROVE"
},
"exceptions": ["ALWAYS_APPROVE"]
},
"PATERNITY": {
"code": "PL",
"response_window_hours": 8,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "APPROVE"
},
"exceptions": ["ALWAYS_APPROVE"]
},
"COMP_OFF": {
"code": "CO",
"response_window_hours": 8,
"auto_action_rules": {
"window_expires_first": "APPROVE",
"leave_starts_first": "DECLINE"
},
"exceptions": []
}
}
}

4.2 Response Window Summary

Leave TypeResponse WindowIf Window Expires FirstIf Leave Starts First
Earned Leave8 hoursAuto-ApproveAuto-Decline
Sick Leave4 hoursAuto-ApproveAuto-Approve
Bereavement4 hoursAuto-ApproveAuto-Approve
Maternity24 hoursAuto-ApproveAuto-Approve
Paternity8 hoursAuto-ApproveAuto-Approve
Comp-Off8 hoursAuto-ApproveAuto-Decline

5. Auto-Action Decision Logic

5.1 Pseudocode

def determine_auto_action(leave_request):
"""
Determine what auto-action to take for a leave request.

Returns:
tuple: (action: APPROVE|DECLINE, reason: str)
"""
leave_type = leave_request.leave_type
config = get_leave_type_config(leave_type)

# Check for exceptions (always approve types)
if "ALWAYS_APPROVE" in config.exceptions:
return ("APPROVE", f"{leave_type} is always auto-approved")

# Calculate times
request_time = leave_request.created_at
window_hours = config.response_window_hours
window_expiry = calculate_window_expiry(request_time, window_hours)
leave_start = leave_request.start_date # At working hours start

# Determine which happens first
if window_expiry < leave_start:
# Window expires before leave starts
return (
config.auto_action_rules["window_expires_first"],
"Response window expired before leave start date"
)
else:
# Leave starts before window expires
return (
config.auto_action_rules["leave_starts_first"],
"Leave start date arrived before response window expired"
)

5.2 Decision Matrix

┌─────────────────────────────────────────────────────────────────┐
│ DECISION MATRIX │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Leave Type │ Exception? │ Window vs Leave │ Action │
│ ────────────────────┼─────────────┼────────────────┼────────── │
│ SICK_LEAVE │ YES │ (any) │ APPROVE │
│ URGENT_LEAVE │ YES │ (any) │ APPROVE │
│ BEREAVEMENT │ YES │ (any) │ APPROVE │
│ MATERNITY │ YES │ (any) │ APPROVE │
│ PATERNITY │ YES │ (any) │ APPROVE │
│ ────────────────────┼─────────────┼────────────────┼────────── │
│ EARNED_LEAVE │ NO │ Window first │ APPROVE │
│ EARNED_LEAVE │ NO │ Leave first │ DECLINE │
│ COMP_OFF │ NO │ Window first │ APPROVE │
│ COMP_OFF │ NO │ Leave first │ DECLINE │
│ │
└─────────────────────────────────────────────────────────────────┘

6. Timer and Scheduler

6.1 Timer Implementation

class LeaveAutoActionScheduler:
"""
Scheduler for auto-action on pending leave requests.
"""

def schedule_auto_action(self, leave_request):
"""
Schedule auto-action for a leave request.
"""
config = get_leave_type_config(leave_request.leave_type)

# Calculate trigger time
window_expiry = calculate_window_expiry(
leave_request.created_at,
config.response_window_hours
)
leave_start = get_leave_start_datetime(leave_request)

# Trigger at whichever comes first
trigger_time = min(window_expiry, leave_start)

# Schedule job
job_id = f"leave_auto_action_{leave_request.id}"
scheduler.add_job(
func=self.execute_auto_action,
trigger='date',
run_date=trigger_time,
args=[leave_request.id],
id=job_id,
replace_existing=True
)

return {
"job_id": job_id,
"trigger_time": trigger_time,
"window_expiry": window_expiry,
"leave_start": leave_start
}

def execute_auto_action(self, leave_request_id):
"""
Execute auto-action for a leave request.
"""
leave_request = get_leave_request(leave_request_id)

# Check if already processed
if leave_request.status != "PENDING":
return # Already approved/declined by manager

# Determine and execute action
action, reason = determine_auto_action(leave_request)

if action == "APPROVE":
approve_leave_request(leave_request, auto=True, reason=reason)
else:
decline_leave_request(leave_request, auto=True, reason=reason)

# Log and notify
log_auto_action(leave_request, action, reason)
notify_employee(leave_request, action, reason)
notify_manager(leave_request, action, reason)

def cancel_auto_action(self, leave_request_id):
"""
Cancel scheduled auto-action (called when manager responds).
"""
job_id = f"leave_auto_action_{leave_request_id}"
scheduler.remove_job(job_id)

6.2 Scheduler Jobs

JobTriggerAction
leave_auto_action_{id}min(window_expiry, leave_start)Execute auto-approve/decline
leave_reminder_{id}window_expiry - 2 hoursRemind manager
leave_escalation_{id}75% of window elapsedEscalate to skip-level

7. Notifications

7.1 Notification Schedule

T+0h:       Manager notified of new leave request
T+50%: Reminder to manager (if still pending)
T+75%: Escalation to skip-level manager (CC)
T+90%: Final reminder to manager
T+100%: Auto-action executed, all parties notified

7.2 Notification Templates

New Request (to Manager):

Subject: Leave Request from {employee_name} - Action Required

{employee_name} has requested {leave_type} from {start_date} to {end_date}.

Please respond by {window_expiry} to avoid auto-action.

Auto-action if no response: {default_action}

[Approve] [Decline] [View Details]

Reminder (to Manager):

Subject: Reminder: Pending Leave Request - {employee_name}

You have a pending leave request from {employee_name}.

Time remaining to respond: {time_remaining}
Auto-action if no response: {default_action}

[Approve] [Decline] [View Details]

Auto-Action Executed (to Employee):

Subject: Your Leave Request has been Auto-{action}

Your {leave_type} request from {start_date} to {end_date} has been
automatically {action_lower} due to no response from your manager.

Reason: {reason}

If you have questions, please contact HR.

Auto-Action Executed (to Manager):

Subject: Leave Request Auto-{action} - {employee_name}

The following leave request was automatically {action_lower} as no
response was received within the response window:

Employee: {employee_name}
Leave Type: {leave_type}
Dates: {start_date} to {end_date}
Action: Auto-{action}
Reason: {reason}

8. Escalation

8.1 Escalation Rules

{
"escalation": {
"enabled": true,
"escalate_at_percent": 75,
"escalate_to": "skip_level_manager",
"fallback_if_no_skip_level": "hr_manager",
"notify_original_manager": true
}
}

8.2 Escalation Flow

                    75% of Window Elapsed


┌─────────────────────┐
│ Is Request Still │
│ Pending? │
└─────────────────────┘

┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────┐ ┌─────────────────────┐
│ Do Nothing│ │ Get Skip-Level │
│ │ │ Manager │
└───────────┘ └─────────────────────┘

┌───────────┴───────────┐
│ Found │ Not Found
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Add Skip-Level │ │ Add HR Manager │
│ as Approver │ │ as Approver │
└─────────────────┘ └─────────────────┘
│ │
└───────────┬───────────┘

┌─────────────────────┐
│ Notify All Parties │
│ of Escalation │
└─────────────────────┘

9. Edge Cases

9.1 Manager on Leave

Scenario: Employee's manager is on leave

Solution:
1. Check if manager has an active leave during response window
2. If yes, auto-escalate to skip-level manager immediately
3. Response window applies to skip-level manager

9.2 Holiday During Window

Scenario: Public holiday falls within response window

Solution:
1. Holiday hours are excluded from window calculation
2. Window extends to next working day
3. Example:
- Request: Wednesday 4 PM, Window: 8 hours
- Thursday: Holiday
- Window expires: Friday 2:30 PM (not Thursday)

9.3 Leave Request Modified

Scenario: Employee modifies pending leave request (changes dates)

Solution:
1. Cancel existing auto-action job
2. Recalculate window based on new dates
3. Schedule new auto-action job
4. Reset notification schedule

9.4 Leave Request Cancelled

Scenario: Employee cancels pending leave request

Solution:
1. Cancel auto-action job immediately
2. No auto-action needed
3. Notify manager of cancellation

9.5 Multiple Managers

Scenario: Employee has multiple reporting managers

Solution:
1. Any one manager can approve/decline
2. Window applies to first response
3. If no manager responds, auto-action executes

10. Audit Trail

10.1 Log Schema

{
"leave_request_id": "string",
"timestamp": "datetime",
"event_type": "enum[CREATED, REMINDED, ESCALATED, AUTO_APPROVED, AUTO_DECLINED, MANAGER_APPROVED, MANAGER_DECLINED, CANCELLED]",
"actor": "string (system|user_id)",
"details": {
"action": "string",
"reason": "string",
"window_expiry": "datetime",
"leave_start": "datetime",
"time_elapsed": "duration"
}
}

10.2 Sample Audit Log

[
{
"leave_request_id": "LR-2026-001",
"timestamp": "2026-01-10T10:00:00+05:30",
"event_type": "CREATED",
"actor": "emp_123",
"details": {
"leave_type": "EARNED_LEAVE",
"start_date": "2026-01-12",
"end_date": "2026-01-12",
"window_expiry": "2026-01-10T18:00:00+05:30"
}
},
{
"leave_request_id": "LR-2026-001",
"timestamp": "2026-01-10T14:00:00+05:30",
"event_type": "REMINDED",
"actor": "system",
"details": {
"reminder_type": "50_percent",
"notified": ["mgr_456"]
}
},
{
"leave_request_id": "LR-2026-001",
"timestamp": "2026-01-10T18:00:00+05:30",
"event_type": "AUTO_APPROVED",
"actor": "system",
"details": {
"action": "APPROVE",
"reason": "Response window expired before leave start date",
"window_expiry": "2026-01-10T18:00:00+05:30",
"leave_start": "2026-01-12T09:30:00+05:30"
}
}
]

11. API Endpoints

11.1 Get Auto-Action Status

GET /api/leave/requests/{id}/auto-action-status

Response:
{
"leave_request_id": "LR-2026-001",
"status": "PENDING",
"auto_action": {
"scheduled": true,
"trigger_time": "2026-01-10T18:00:00+05:30",
"default_action": "APPROVE",
"reason": "Window expires before leave starts"
},
"window": {
"start": "2026-01-10T10:00:00+05:30",
"expiry": "2026-01-10T18:00:00+05:30",
"hours": 8,
"elapsed_percent": 50
},
"leave": {
"start": "2026-01-12T09:30:00+05:30"
}
}

11.2 Override Auto-Action (Admin)

POST /api/leave/requests/{id}/override-auto-action

Request:
{
"action": "APPROVE|DECLINE",
"reason": "string",
"admin_id": "string"
}

Response:
{
"success": true,
"message": "Auto-action overridden successfully"
}

12. Database Schema

12.1 Leave Request Table Extension

ALTER TABLE leave_requests ADD COLUMN auto_action_config JSON;

-- Example auto_action_config:
{
"window_expiry": "2026-01-10T18:00:00+05:30",
"leave_start_datetime": "2026-01-12T09:30:00+05:30",
"default_action": "APPROVE",
"scheduled_job_id": "leave_auto_action_LR-2026-001",
"escalated": false,
"escalated_to": null
}

12.2 Auto-Action Log Table

CREATE TABLE leave_auto_action_logs (
id SERIAL PRIMARY KEY,
leave_request_id VARCHAR(50) NOT NULL,
event_type VARCHAR(50) NOT NULL,
actor VARCHAR(50) NOT NULL,
details JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

FOREIGN KEY (leave_request_id) REFERENCES leave_requests(id)
);

CREATE INDEX idx_auto_action_logs_request ON leave_auto_action_logs(leave_request_id);
CREATE INDEX idx_auto_action_logs_event ON leave_auto_action_logs(event_type);

13. Testing Scenarios

13.1 Test Cases

#ScenarioExpected Result
1EL applied Mon 10AM, leave Wed, no responseAuto-Approve (Mon 6PM)
2EL applied Mon 6PM, leave Tue, no responseAuto-Decline (Tue 9:30AM)
3SL applied Mon 6PM, leave Tue, no responseAuto-Approve (exception)
4EL applied Fri 5PM, leave Mon, no responseAuto-Decline (leave starts before window)
5EL applied Mon 10AM, leave Fri, manager approves TueManager decision (Approved)
6EL applied Mon 10AM, leave Fri, manager declines TueManager decision (Declined)
7Bereavement any timeAuto-Approve (exception)
8Request during holiday weekWindow excludes holidays
9Manager on leaveEscalate to skip-level
10Employee cancels requestCancel auto-action job

14. Configuration Summary

{
"auto_action_config": {
"working_hours": {
"start": "09:30",
"end": "19:00",
"timezone": "Asia/Kolkata",
"exclude_weekends": true,
"exclude_holidays": true
},
"leave_types": {
"EARNED_LEAVE": { "window": 8, "exception": false },
"SICK_LEAVE": { "window": 4, "exception": true },
"URGENT_LEAVE": { "window": 4, "exception": true },
"BEREAVEMENT": { "window": 4, "exception": true },
"MATERNITY": { "window": 24, "exception": true },
"PATERNITY": { "window": 8, "exception": true },
"COMP_OFF": { "window": 8, "exception": false }
},
"notifications": {
"remind_at_percent": [50, 75, 90],
"channels": ["email", "app"]
},
"escalation": {
"enabled": true,
"at_percent": 75,
"to": "skip_level_manager"
}
}
}

Document Control

VersionDateChangesAuthor
1.0January 11, 2026Initial specification-