HRM — Attendance & Shifts
When people work, and the schedule that says when they should
These two modules are documented together because they are tightly linked: a shift says when an employee is supposed to work, and attendance records when they actually did — the gap between the two is what tells the system if someone was late, absent, or eligible for overtime.
Part 1 — Attendance
What it does
Attendance records every clock event — check-in, check-out, break start, break end — as a permanent, immutable entry. From those raw events the system derives a daily status (present, late, absent, on leave, half-day) that payroll later reads. Employees who forget to punch can file a correction (regularization) request, which a manager approves or rejects.
Capture methods
A punch can be captured several ways. The server validates the method on every punch — it never trusts what the client claims.
| Method | How it's verified | Typical use |
|---|---|---|
WEB | Allowed if the store permits web check-in | Desk/back-office staff clicking a button |
MOBILE_APP | Same, from a mobile client | Staff on the move |
GEOFENCE | Phone GPS must fall within the store's radius | Field staff who must be physically present |
IP_RESTRICTED | Request IP must match the store's whitelist (CIDR) | On-premise only clock-in |
QR_CODE | Scan of a short-lived, one-time token | A QR poster at the entrance |
BIOMETRIC | Fingerprint/face device punch | Hardware time clocks |
MANUAL | Forced when one person punches for another (needs the manual permission) | HR fixing a missed punch |
GEOFENCE and IP_RESTRICTED only work once the store
has a geofence or IP whitelist configured. Set those with
PATCH /api/v2/stores/:id/geolocation (geoLat,
geoLng, geoRadiusM 1–50000) and
PATCH /api/v2/stores/:id/ip-whitelist. See the
v2 API Reference.
The punch lifecycle
Every event is stored with no updatedAt and no delete — attendance is a
truthful event log. If something is wrong, you don't edit it; you add a
correction that creates a new event.
Clocking in (API)
Dev Only Self: ess.attendance.check-in · For others: hr.attendance.check-in.manual
Omit employeeId to punch yourself. Supplying an employeeId forces
method: MANUAL and requires the manual permission. The same shape applies to
check-out, break-start, and break-end.
Derived daily status
The system computes each day's status on read (and caches it). The logic, in plain terms:
Is it a holiday?
If the date is on the holiday calendar → HOLIDAY.
On approved leave?
If an approved leave request covers the date → ON_LEAVE (or
HALF_DAY).
No punches on a working day?
→ ABSENT. (No punches on a rostered off-day or weekend → not
absent.)
Punches exist?
Pair check-ins with check-outs, subtract breaks, compare against the shift's
start/end + grace period to flag LATE, compute overtime, and
decide PRESENT vs HALF_DAY.
Derived statuses are: PRESENT, LATE, ABSENT, HALF_DAY, WEEKEND,
HOLIDAY, ON_LEAVE, with extra flags like MISSING_CHECKOUT.
Corrections (regularization)
Regularization is time-boxed (default ~7 days, set by the tenant's HR settings). Requests older than the window can only be entered by HR via the manual path, and are audited.
In the app
| Page | Route | What you do there |
|---|---|---|
| Attendance board | /hr/attendance | Summary tiles, today's punches, the event list (scoped to your permission). |
| Corrections | /hr/attendance/corrections | Review, approve, or reject correction requests. |
| New correction | /hr/attendance/corrections/new | File a correction request. |
Attendance permissions
| Capability | ADMIN | HR_MANAGER | MANAGER | EMPLOYEE |
|---|---|---|---|---|
| Own check-in/out | Yes | Yes | Yes | Yes |
| Read team attendance | Yes | Yes | Team | No |
| Read all attendance | Yes | Yes | No | No |
| Manual punch for others | Yes | Yes | Team | No |
| Approve/reject corrections | Yes | Yes | Team | No |
Part 2 — Shifts & Scheduling
What it does
Shifts manage work schedules (this is unrelated to v1 cash-drawer sessions). You build reusable shift templates, publish a roster (assign templates to employees on dates), and let employees swap shifts with a colleague — subject to manager approval.
Template vs instance — the key idea
| Model | What it is | Mutability |
|---|---|---|
| WorkShift (template) | A reusable definition: name, start/end time, break minutes, grace minutes, night-shift premium %. | Edited rarely; long-lived. |
| ShiftSchedule (instance) | One employee, one date — created from a template. | Snapshots the template's times at creation. |
When you roster a shift, its times are copied (snapshotted) into the schedule row. So editing a template later does not rewrite schedules that are already published — past and future stay correct. Payroll always reads the snapshot, never a live template join.
A WorkShift whose endTime is earlier than its startTime is a
crosses-midnight shift (e.g. 22:00 → 06:00) and is handled correctly.
Shift statuses
| Enum | Values |
|---|---|
ShiftScheduleStatus | SCHEDULED · COMPLETED · ABSENT · ON_LEAVE · CANCELLED · SWAPPED |
ShiftSwapStatus | PENDING_PEER · PENDING_MANAGER · APPROVED · REJECTED · CANCELLED · EXPIRED |
Publishing a roster (bulk)
Dev Only POST /api/v2/hr/shifts/schedule · permission hr.shifts.schedule.create
Send 1–500 entries. Each row is checked independently; the response separates
what was created from any conflicts (e.g. a day that's already completed, or
an inactive employee). When isOffDay is true, workShiftId is optional.
An explicit isOffDay row is a rostered rest day — it is not
counted as absent. "Absent" only happens when a real working schedule had no
attendance and no leave.
Shift swaps — a three-step approval
Setting counterpartScheduleId: null makes it a give-away (the counterpart
takes the shift; the requester is unrostered that day). Swaps expire after a
configurable window (default ~48 hours). Each transition notifies the relevant
people.
In the app
| Page | Route | What you do there |
|---|---|---|
| Templates | /hr/shifts | Create & manage shift templates. |
| Roster | /hr/shifts/schedule | View the roster grid; edit/cancel individual cells. |
| New roster | /hr/shifts/schedule/new | Bulk-assign shifts. |
| Swaps | /hr/shifts/swaps | Accept/reject, approve, or cancel swap requests. |
| New swap | /hr/shifts/swaps/new | Request a swap. |
Shift permissions
| Capability | ADMIN | HR_MANAGER | MANAGER | EMPLOYEE |
|---|---|---|---|---|
| Read templates | Yes | Yes | Yes | No |
| Manage templates | Yes | Yes | No | No |
| Read roster | Yes | Yes | Team | Own (via ESS) |
| Create/edit roster | Yes | Yes | Team | No |
| Request / respond to a swap | — | Yes | Yes | Yes (via ESS) |
| Approve a swap | Yes | Yes | Team | No |
How They Connect
When someone clocks in, attendance asks the shifts module "what was this person scheduled to work today?" and uses that window to judge lateness and overtime. If the shifts module isn't in use, attendance falls back to the tenant's default work-week settings — everything keeps working, just without per-day rosters.
Next, set up Leave & Holidays so approved time off shows up correctly on the attendance board instead of as "absent".