HR Systems hr-leave

HR Leave Request Workflows

One template handles annual, sick, extended, and emergency leave — each with a different chain. Emergency leave skips the manager and goes straight to HR. Extended leave adds department head sign-off. An out-of-office provider covers manager absences.

Annual / Sick (≥3d)
Manager
→
HR Admin
→
Approved
Extended (>10 days)
Manager
→
Dept Head
→
HR Admin
→
Approved
Emergency
HR Admin
→
Approved

One template, four leave types

The template has a manager level and an HR level; conditions read leaveType and durationDays from the request data to add the department head level or skip levels.

  • Short sick leave skips HR (under 3 days, manager only)
  • Extended leave adds dept head between manager and HR
  • Emergency leave skips the manager and waits on HR only
  • All leave types maintain a full audit trail
hr-leave-template.ts
import { ApprovalEngine } from 'hierarchical-approval';
import { PostgresAdapter } from 'hierarchical-approval/adapters/postgres';

const engine = new ApprovalEngine({
  adapter: new PostgresAdapter({ connectionString: process.env.DATABASE_URL }),
  tenantId: 'acme-corp',
  // Resolves every { type: 'role' } approver below
  orgProvider: { getUsersByRole: (role) => directory.usersWithRole(role) },
});

// A { type: 'dynamic' } approver: look up the submitter's direct manager
engine.registerResolver('direct-manager', (submittedBy) => hrSystem.managerIdOf(submittedBy));

await engine.defineTemplate({
  name: 'hr-leave',
  documentType: 'leave-request',
  levels: [
    {
      level: 1,
      name: 'Manager',
      approvers: [{ type: 'dynamic', resolver: 'direct-manager' }],
      mode: 'any',
    },
    {
      level: 3,
      name: 'HR',
      approvers: [{ type: 'role', role: 'hr-admin' }],
      mode: 'any',
    },
  ],
  conditions: [
    // Extended leave (> 10 days) adds the department head between the two
    {
      when: { field: 'durationDays', operator: '>', value: 10 },
      addLevels: [
        {
          level: 2,
          name: 'Department head',
          approvers: [{ type: 'role', role: 'dept-head' }],
          mode: 'any',
        },
      ],
    },
    // Short sick leave (< 3 days) skips HR
    {
      when: {
        all: [
          { field: 'leaveType', operator: '==', value: 'sick' },
          { field: 'durationDays', operator: '<', value: 3 },
        ],
      },
      skipLevels: [3],
    },
    // Emergency leave skips the manager and goes straight to HR
    {
      when: { field: 'leaveType', operator: '==', value: 'emergency' },
      skipLevels: [1],
    },
  ],
});

What goes wrong in real HR systems.

These edge cases cause silent failures in hand-rolled approval scripts.

01

Manager is on leave — out-of-office cover

An employee submits leave, but their manager is also on leave. Without delegation, the request sits unreviewed until the manager returns — breaching SLA.

delegate.ts
// Standing cover: consulted whenever a level's approvers are resolved,
// so a manager who is away is replaced before the request reaches them
const engine = new ApprovalEngine({
  adapter,
  tenantId: 'acme-corp',
  orgProvider,
  outOfOfficeProvider: {
    getDelegateFor: (userId, at) => hrSystem.coverFor(userId, at), // string | null
  },
});

// One-off hand-off of a request that is already waiting on someone
await engine.delegate(instance.id, {
  fromApprover: 'manager-bob',
  toApprover: 'manager-carol',
  reason: 'Away 2-13 November',
  until: new Date('2026-11-13'),
});

// manager-carol can now call engine.approve(instance.id, { approverId: 'manager-carol' })

The outOfOfficeProvider is asked for a stand-in whenever a level's approvers are resolved, so absence stays in your HR system. For a request already waiting, engine.delegate() hands it to someone else.

02

Team leave quota exceeded

Half the engineering team requests the same week off. Without quota checking, all requests would be individually approved — leaving projects understaffed.

quota-check.ts
// Count approved leave in the same department that overlaps the request
async function overlappingLeave(departmentId: string, startDate: string, endDate: string) {
  const { items } = await engine.queryInstances(
    { documentType: 'leave-request', status: 'approved', data: { departmentId } },
    { limit: 500, offset: 0 }
  );
  // ISO dates compare correctly as strings
  return items.filter(
    (leave) => leave.data.startDate < endDate && leave.data.endDate > startDate
  ).length;
}

// Call before engine.submit()
const onLeave = await overlappingLeave(departmentId, startDate, endDate);
if (onLeave >= (await getTeamMaxConcurrent(departmentId))) {
  throw new Error(`Team quota exceeded: ${onLeave} already on leave`);
}

Query approved leave with engine.queryInstances() and block before calling engine.submit(). The quota itself is your rule, not the engine's.

03

Same-day emergency leave

An employee has a family emergency at 8am and can't come in. They need approval confirmation immediately — not after a 24-hour review cycle.

emergency.ts
// Emergency leave: the manager level is skipped, HR is the only approver
const instance = await engine.submit({
  templateName: 'hr-leave',
  documentType: 'leave-request',
  documentId: 'leave-2026-0412',
  submittedBy: 'emp-0712',
  data: {
    leaveType: 'emergency',
    startDate: '2026-11-02',
    endDate: '2026-11-02',
    durationDays: 1,
  },
});
// instance.status === 'pending', waiting on level 3 (HR) only

await engine.approve(instance.id, { approverId: 'hr-erin' });
// instance.status === 'approved' — no manager round trip

The employee skips the manager level, so the request waits only on HR. There is no auto-approve: HR still makes the call.

Your next approval workflow
ships faster than the last one.

Drop it into any Node.js project. No database required for local development.

$ npm install hierarchical-approval

Peer dep for Postgres: npm install pg @types/pg