Hours in Business Made live on timesheets: one bm_timesheet per person per pay period, holding their time entries. A clock-in starts an entry on the sheet that covers today, making the sheet if there isn't one yet. A clock-out closes the entry. Each sheet adds its hours up under the person's time policy into regular, overtime and double-time, plus PTO, holiday and sick. Approved sheets are what a regular pay run pays: see Run payroll.
The screens are under Time: Clock (/time/clock), Timesheets (/time/timesheets), Review (/time/review), Overtime (/time/overtime), Projects (/time/projects) and Policies (/time/policies). Staff use Timesheets in their staff portal (/staff-portal/timesheets). The Business Made docs explain them for people: Clock and timesheets, Projects, overtime and policies and Clock in and submit your timesheet.
What stops someone clocking in (training, an I-9, a certificate) is set up in Training and readiness requirements. Time off booked through requests is in Time off setup and approvals.
Business Made has no AI tools of its own for time. Every route below is under business-made/timesheets/ unless it says otherwise, and goes through call_appengine. Use search_api and describe_endpoint for bodies not shown here.
Ask first
- Overtime rules: after how many hours a week, and a day if the state has a daily rule (for example California). At what rate, and is there double time?
- Rounding: exact, or to the nearest 5, 15 or 30 minutes.
- The longest shift they allow before a clock-out is flagged.
- One rule for everyone, or different ones per state or per location?
- For hours: who, which day, how many, and the type (worked, PTO, holiday, sick, unpaid).
- For approvals: which pay period, and whether every sheet or only some.
Steps
1. Time › Policies: the time policy
Without any policy, every sheet uses the federal fallback: overtime after 40 hours a week at 1.5×, no daily rule, exact time.
A policy is a bm_time_policy record. Time › Policies saves it through the generic repository. Do the same with call_appengine PUT /repository/create:
{
"datatype": "bm_time_policy",
"isNew": true,
"data": {
"name": "California kitchen",
"scope": "location",
"businessLocationId": "<location sk>",
"weeklyOTThreshold": 40,
"dailyOTThreshold": 8,
"doubleTimeThreshold": 12,
"otMultiplier": 1.5,
"doubleTimeMultiplier": 2,
"rounding": "nearest_15min",
"maxShiftHours": 12,
"status": "active"
}
}Without "isNew": true the create is refused with 400 record is not marked as new (tested).
| Field | What goes in |
|---|---|
scope | org-wide, state (with state, a name or USPS code) or location (with businessLocationId). |
weeklyOTThreshold, dailyOTThreshold, doubleTimeThreshold | Hours after which overtime or double time starts. 0 means no such rule. Weeks reset each week, even on a two-week sheet. |
otMultiplier, doubleTimeMultiplier | Defaults 1.5 and 2. |
rounding | exact, nearest_5min, nearest_15min or nearest_30min. It rounds each worked entry's hours, not the punch times. |
maxShiftHours | A clock-out after a longer shift is flagged exceedsMaxShift. 0 means no limit. |
autoBreakDeduct, meal and rest break rules | Unpaid breaks taken off each shift when none is recorded. |
requirePhoto, requireGeofence | What a punch must carry. A punch that fails is flagged for review (step 5). |
status | Only active policies count. |
Which policy applies: the person's location's policy first, then their state's (their work state, else their location's state, else their home address), then the org-wide one, then the fallback. Each sheet records the one it used in appliedPolicy (tested: matchedBy: "location").
The longest-shift check ignores scope. maxShiftHours is read from the first active policy that has one, whoever it belongs to (timesheet.service.ts 930). A limit set for one location flags long shifts everywhere. Overtime and rounding do follow the person's own policy. Until this is fixed, set maxShiftHours the same on every policy, or only on the org-wide one.
Hands on: the policy's sk.
Check: Time › Policies lists it. A sheet totalled after it was made shows it in appliedPolicy.
2. Time › Clock: clocking in and out
Staff clock themselves in on their staff portal's Timesheets page: POST /staff-portal/timesheets/clock-in { "notes": "…" } and POST /staff-portal/timesheets/clock-out. Both are tested. The browser sends where and on what device it is (capture) when it can.
For someone else (a manager at Time › Clock):
| Action | Real action |
|---|---|
| Clock in | POST business-made/timesheets/clock-in/<code> { "notes": "…", "projectId": "…", "taskId": "…" } |
| Clock out | POST business-made/timesheets/clock-out/<code> { "notes": "…" } |
| Are they in? | GET business-made/timesheets/clock-in/<code>/active |
| Who's in now | GET business-made/timesheets/reports/on-the-clock |
<code> is the person's employee code.
What happens (tested):
- Clock-in finds the sheet covering today, or makes one for the pay period. The period is the business's pay period covering today, else one from the person's pay frequency (weekly gave Monday to Sunday), else a two-week period. It adds an open entry, and saves a
bm_clock_event. A second clock-in is refused with 400 Already clocked in. - Readiness. Before the clock-in, the person's readiness requirements are checked. A block refuses it with 423 and the reasons. A manager can let them in with
"override": { "reasonCode": "…", "reason": "…", "expiresAt": "…" }: see Training and readiness requirements. - Clock-out sets the entry's hours to the time between the two punches and its description to the clock-out note. With no open entry it is refused with 400
NOT_CLOCKED_IN. - A clock-in on a sheet that is already submitted still adds the entry to it, and the sheet stays submitted (tested). A clock-in on an approved sheet sends the sheet back to
submitted.
No clock action sends an email.
Check: GET business-made/timesheets/<sheet id> shows the entry with startTime, endTime, hours and countedHours.
3. Time › Timesheets: hours by hand
| Action | Real action | Works on |
|---|---|---|
| Make a sheet | POST business-made/timesheets { "employeeId": "<code>", "payPeriodStart": "2026-10-19", "payPeriodEnd": "2026-10-25" } | — |
| Add hours | POST business-made/timesheets/<id>/entries { "date": "2026-10-20", "hours": 6, "type": "regular", "description": "…" } | draft sheets only |
| Correct an entry | POST business-made/timesheets/<id>/entries/<entryId> with the fields that change | any sheet not yet paid. On an approved sheet, the sheet goes back to submitted (tested). |
| Remove an entry | DELETE business-made/timesheets/<id>/entries/<entryId> | draft sheets only |
| Find a person's sheets | GET business-made/timesheets/employee/<code> | — |
An entry can also carry startTime, endTime (ISO times), breakDuration, projectId and taskId. type is regular, overtime, pto, holiday, sick or unpaid.
| Refusal | Meaning |
|---|---|
| 409 Timesheet already exists for this pay period | Use the sheet there is (tested). |
| 409 already has a timesheet covering some of these days | One sheet per person per day (tested). Add the hours to that sheet. |
| 400 The period ends before it starts | The dates are the wrong way round (tested). |
400 TIMESHEET_NOT_EDITABLE | Adding to a sheet that isn't a draft (tested). Reopen it first (step 4). |
409 ENTRY_OPEN_PUNCH | The entry is still clocked in. Clock out first. |
409 TIMESHEET_PAID_LOCKED | A pay run has paid it. Correct it through payroll. |
How the totals are worked out (tested with daily overtime 8 and nearest 15 minutes):
pto,holidayandsickhours go into their own totals.unpaidcounts nowhere.- Everything else is worked time, including entries typed
overtime. Breaks are taken off and the rounding applied to each entry (9.15 recorded counted as 9.25). Then the policy splits each week into regular, overtime and double time. Tested: a 9.25-hour Monday gave 1.25 overtime hours, and a 2-hour entry typedovertimeon Tuesday counted as regular, because it didn't pass the threshold. - The sheet keeps
recordedWorkedHours(before rounding) next tototalHours,regularHours,overtimeHours,doubleTimeHours,ptoHours,holidayHoursandsickHours.
You can't force overtime by typing an entry overtime. Only the policy's thresholds make overtime. If the business really owes overtime the rules don't produce, change the policy, or pay it as an extra on the pay run.
4. Time › Review: submit, approve, decline, reopen
| Action | Real action | From → to |
|---|---|---|
| Submit | POST business-made/timesheets/<id>/submit | draft → submitted. An empty sheet is refused (EMPTY_TIMESHEET). |
| Approve | POST business-made/timesheets/<id>/approve | submitted → approved |
| Decline | POST business-made/timesheets/<id>/reject { "reason": "Tuesday's hours are wrong" } | submitted → rejected. The reason is kept on the sheet as rejectionReason. The server doesn't require one, but give one. |
| Reopen | POST business-made/timesheets/<id>/reopen { "reason": "…" } | rejected → draft; approved → submitted. A sheet a pay run has paid is refused (TIMESHEET_PAID). |
| Waiting for approval | GET business-made/timesheets/status/pending | — |
All tested. Approving or declining an already decided sheet is refused with 400 Only submitted timesheets can be approved (or rejected). Approving a sheet whose days are already approved or paid on another sheet of the same person is refused with 409 TIMESHEET_DAYS_ALREADY_APPROVED, so the same day is never paid twice.
Staff submit their own sheet in the staff portal (POST /staff-portal/timesheets/<id>/submit). They can pull back their own sheet that is submitted or declined to draft (POST /staff-portal/timesheets/<id>/reopen), but not an approved one.
Timesheets send no email. Submitting doesn't tell the manager, and approving or declining doesn't tell the person. Tell them yourself if the person wants them to know.
Stop: approving makes the hours payable. The next regular pay run for the period pays approved sheets. Say how many sheets and hours will be approved, and wait for yes.
Check: Time › Review (/time/review) for the period. GET business-made/timesheets/reports/team-status?periodStart=…&periodEnd=… counts sheets by status (tested).
5. Time › Review: forgotten clock-ins, flagged punches, missing sheets
| Problem | Real action |
|---|---|
| Someone forgot to clock out | Preview with POST business-made/timesheets/clock-ins/close-stale { "dryRun": true } (tested), which lists clock-ins open longer than 24 hours (olderThanHours). Without dryRun it closes them at 0 hours and flags them for review. Then correct the entry's hours (step 3). Stop before running it for real. |
| Punches flagged for review | GET business-made/timesheets/clock-events/needs-review. A punch is flagged when it is outside the site's area, has no location or no photo when the policy wants one, the site has no area set, or it was closed automatically. Mark one checked with POST business-made/timesheets/clock-events/<id>/review { "note": "…" }. |
| Who has no sheet | GET business-made/timesheets/reports/missing?start=2026-10-19&end=2026-10-25 lists hourly staff with no timesheet for those days (tested). |
| Two sheets for the same days | GET business-made/timesheets/<id>/overlaps, then POST …/<id>/move-entries { "toId": "<other id>", "entryIds": [ … ] } or POST …/<id>/merge-into/<intoId>. |
| One person's hours for a range | GET business-made/timesheets/reports/summary/<code>?startDate=…&endDate=… |
Check: nothing left on the needs-review list for the period, and the missing list is empty or explained.
When it's done
Tell the person:
- the policy made or changed, and who it covers;
- who was clocked in or out, and at what times;
- hours added or corrected, on which sheet, with the new totals (regular, overtime, PTO);
- which sheets were approved, declined (and why) or reopened;
- anything still open: forgotten clock-ins, flagged punches, missing sheets.
Open Time › Review (/time/review) for the period.
What it can't do yet
- No AI tools for time. Everything goes through
call_appengine. - The longest-shift limit isn't per policy (warning in step 1).
- No timesheet emails. Nobody is told about a submit, approval or decline.
- An entry typed
overtimeisn't paid as overtime unless the thresholds make it so (step 3). - Forgotten clock-ins close at 0 hours. Someone has to put the real hours in afterwards.
Don'ts
- Don't approve sheets without a yes. Approved hours are what payroll pays.
- Don't run close-stale without
dryRunfirst. - Don't change a timesheet with
repo_update. Use the entry and status routes, so the totals and history are kept. - Don't make a second sheet for days already covered. Add to the one there is, or move the entries.
- Don't clock someone in past a readiness block without the manager's reason for the override.