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.
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.
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.
// 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.
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.
// 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.
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 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