Payroll lives in the Business Made app (not Studio). Its Payroll › Run payroll screen is /payroll/run, and one run is /payroll/run?run=<sk>. The screen walks five steps: Period, Review, Approval, Pay and Bank file & payslips. Each step uses a route under business-made/payroll/runs… (appengine business-made/services/payroll.service.ts and payroll-approval.service.ts). Call those routes with call_appengine; they are what the screen itself calls.
A run's status goes draft → review → pending_approval → approved → completed. A run that is sent back returns to review. A run can be cancelled before it is paid, and voided after.
Nothing in this skill moves money except Pay now (step 7). Recording the run (step 6) writes the pay stubs and books the run. Making the bank file (step 7) gets the deposits ready for the bank to send. Business Made doesn't make tax deposits.
For how a person sees all this, read the Business Made docs:
- Run payroll
- People, periods and schedules, which covers pay profiles, deductions, bank accounts and pay periods.
Tax forms and filings are covered in Contractors, taxes, forms and filings. For the API, see the Business Made module reference.
Ask first
- The period and pay date. For example: Sep 28 – Oct 11, paid Oct 16. Run payroll offers the pay periods already on file. Never guess dates.
- Who is paid. Is it everyone owed pay, or only some people?
- A regular run, a bonus run or an off-cycle run?
- A regular run pays the period's approved timesheets and salaries.
- A bonus run or an off-cycle run pays only the amounts you enter.
- Anything extra. Ask about bonuses, tips, commission, or hours that should override a timesheet, and for whom.
- For a bonus: how federal tax is withheld. It is either a flat 22% (the default) or added to regular pay.
- Who approves. Approval goes to people with the HR or Manager role, and then to Finance, Owner or Controller. Does the business have those people?
- How money goes out. It goes either by a bank file the business uploads to its bank, or by Pay now through Stripe ACH or Plaid Transfer. The choice is set up under Payroll › Direct deposit (
/payroll/setup/ach).
Steps
1. Payroll › Employer & SUTA: what a run needs
Every calculation checks the business's setup. When something is missing, the run can be reviewed, but it can't be sent for approval or paid (PAYROLL_SETUP_INCOMPLETE). These are the items it checks. All were tested.
| Missing item (as the run reports it) | Fix |
|---|---|
| Legal business name, Federal EIN, Business address | POST business-made/payroll/employer-setup with legalName, ein (##-#######), address { street1, city, state, zip }. On screen: Payroll › Employer & SUTA (/payroll/setup/employer). |
<ST> state registration (for example, TX state registration) | Send the same route with states: [{ state: "TX", sutaRate: 0.027, unemploymentAccountId, withholdingAccountId }]. sutaRate is a fraction (0.027 means 2.7%), not a percentage. |
| Earning types | POST business-made/payroll/earning-types/seed adds the standard set: regular, overtime, bonus, tips and the rest. On screen: Payroll › Earning types (/payroll/setup/earnings). |
| A person's pay profile | Each person needs a bm_payroll_profile with a pay type, a rate or salary, a pay frequency and a pay method. Set it on Payroll › People & pay (/payroll/people). |
employer-setup merges what you send with what is already stored. states is replaced whole, so send every state. Federal tax tables ship built in, so a 2026 run needs no tax rules.
These come from the business's own records: its EIN, its state account numbers and its rates. Never guess them. Ask the person, and save only what they give you.
Hands on: the employer setup and the people being paid.
Check: GET business-made/payroll/employer-setup reads back what was saved.
2. Payroll › Run payroll: start the run (Period)
POST business-made/payroll/runs with:
| Field | What it is |
|---|---|
payPeriodStart, payPeriodEnd, payDate | YYYY-MM-DD. All three are required (PAYROLL_RUN_DATES_REQUIRED). If the end comes before the start, the run is refused with PAYROLL_RUN_BAD_PERIOD. |
employeeIds | The people's bm_employee sk. |
runType and type | regular, bonus or off-cycle. The screen sends both. |
supplementalMethod | flat (22% federal on bonuses) or aggregate (added to regular pay). |
manualInputs | {} for now. Extras are set in step 3. |
The run is always saved as a draft, whatever status you send. Tested: a status: "approved" sent here was ignored. The run gets a number like RUN-20261016-965467 and a statusHistory.
On screen, Continue to review → does this step and step 3 together. If a run for the period is already under way, the screen shows Continue that run → instead.
Hands on: the run's sk.
Check: GET business-made/payroll/runs/<sk> shows status: "draft" and the run number.
3. Payroll › Run payroll: calculate and review (Review)
POST business-made/payroll/runs/<sk>/calculate. The run goes draft → review, and the call stores:
payrollEntries: each person's earnings, taxes, deductions and net.totals:totalGrossPay,totalNetPay,totalEmployeeTaxes,totalDeductionsandtotalCompanyCost.readiness:{ ready, missing[], warnings[] }.review: the flags, the checklist, a comparison with the previous run, and overtime.
If the calculation fails, the run goes back to draft with a calculationError. A run that isn't fully set up still calculates (tested); it only shows readiness.ready: false.
Tested on a $52,000 salary, paid bi-weekly in Texas, with a single filer and no W-4. Gross pay was $2,000.00 and net pay was $1,690.85:
| Line | Who pays | Amount |
|---|---|---|
| Federal income tax | Employee | $156.15 |
| Social Security | Employee | $124.00 |
| Medicare | Employee | $29.00 |
| FUTA | Employer | $12.00 |
| Texas SUTA | Employer | $54.00 |
Flags (review.flags) are per person. Each one has an id, a kind, a severity and a message. Some also have a fix link.
| Kind | What it means |
|---|---|
negative_net | Must fix. The deductions are more than the pay. |
hours_spike, overtime_spike, rate_changed | Different from the previous run. |
zero_net, inactive_person, ghost_person, duplicate_person | Someone is being paid nothing, or shouldn't be paid. |
bank_account_changed, duplicate_bank_account, no_pay_method | Something about where the money goes. |
missing_clock_out, hours_on_leave_day | Something about the timesheets. |
missing_form | For example, no W-4 ("Federal tax will be withheld as Single…") or a late I-9. These are warnings and don't stop pay. |
A person can be held. This happens when a requirement rule's payroll gate blocks them. The rest of the run still pays, and the held person is told by email in step 6.
The checklist (review.checklist) covers the whole business, not only this run. Tested: a person outside the run who had no timesheet still made "Everyone who worked has a timesheet" fail.
Extras and notes: POST business-made/payroll/runs/update with the whole run, changing only these fields:
manualInputs, keyed by the person's employee code (EMP-…):{ "EMP-SKHR7A": { "hours": { "REG": 80 }, "flatEarnings": { "BON": 100, "TIP": 40 }, "skipDeductionIds": [] } }. Then calculate again.reviewResolutions: [{ flagId, note }]. This marks a flag as looked at. A note is required (REVIEW_NOTE_REQUIRED). An unknown flag givesFLAG_NOT_FOUND. Once the run has been sent for approval, the call is refused with 409PAYROLL_REVIEW_LOCKED.
This route never changes status. Tested: the status sent was ignored, and the note was stamped with who wrote it and when. Tested: a BON: 100 added a Bonus line, with federal tax withheld at the flat 22%.
Show the person the totals, each person's net pay, and every flag. Fix or note each Must fix flag before step 4: the API doesn't stop on them, but the screen does.
Hands on: the totals, the flags, and what the person changed.
Check: on /payroll/run?run=<sk>, the Review step shows the same totals and flags. Calculate again reruns step 3.
4. Payroll › Run payroll: send it for approval (Approval)
POST business-made/payroll/runs/<sk>/submit-for-approval with { comment }. This needs status review and readiness.ready. Otherwise it is refused with INVALID_STATUS or PAYROLL_SETUP_INCOMPLETE (both tested). On screen, the button is Send for approval.
Stop. Sending does these things:
- It emails each approver an
approval-requestedmessage, and puts the run in their Approvals tray. - It sets the run to
pending_approvaland recordsapprovalTaskIdandapproveBy.approveByis the deadline that still makes the bank cutoff. - The first time anyone in the business sends a run, it creates the
payroll-approvalworkflow, switched on. From then on, every run for the business needs approval.
Show the person the totals, the pay date and who will be asked. Wait for a yes.
The workflow as shipped (appengine src/workflow/workflow-setup.service.ts 244):
| Stage | Who decides | How many |
|---|---|---|
| Payroll review | the HR or Manager role. If nobody has those roles, the Owner. | 2 |
| Payment release | the Finance, Owner or Controller role | 2 |
- "How many" is capped at the number of people on the stage. The person who sent the run is never one of them.
- An unanswered stage chases after 24 hours, then adds the Owner after another 24.
- To change who approves or how many, edit the workflow under Operations › Pipelines › Payroll approval. The fields are the same as in Approvals: publishing review and access requests, step 2. For timers and stages, see Stage workflow with SLA timers.
- To give someone one of those roles, see Invite staff and set roles.
Hands on: the approvalTaskId.
Check: the Approval step reads "It is with
5. Approvals: the approvers decide
Each approver approves or sends the run back from their Approvals tray. They can also do it on the run's own Approval step: Approve, or Send back, which needs a comment. The routes behind these are POST approval/task/<taskId>/approve and POST approval/task/<taskId>/reject with { note }.
- When both stages approve, the run becomes
approved. - When the run is sent back, it returns to
review, its flags can be worked again, and it is resubmitted with Resubmit for approval.
An Owner or ConfigAdmin who isn't on a stage can act for it, but must give a reason (overrideReason). That counts as one approval. The screen hides the decide box from the person who prepared the run.
The AI never approves or sends back a run for anyone. Tell the person who it is waiting on: GET business-made/payroll/runs/<sk> and the task. If they ask for a nudge, point them to the Approvals tray.
Hands on: the run's status.
Check: the Pay step reads "Approved by
6. Payroll › Run payroll: record the pay (Pay)
POST business-made/payroll/runs/<sk>/process. This needs status approved (otherwise INVALID_STATUS, tested). It rechecks the setup and refuses to pay anyone twice for the period. On screen, the button is Pay N people, then a confirm: "This records everyone's pay stub, updates year-to-date totals and loan balances, and books the run."
Stop. Processing does these things, and they can only be undone by voiding:
- It writes each person's pay stub, exactly as approved.
- It adds to the year-to-date totals and loan balances.
- It marks the timesheets as paid in this run.
- It posts the payroll journal to Books.
- It emails each person paid a
payroll.paidmessage ("Your pay stub is ready"), and each held person apayroll.heldmessage.
Show the person the number of people, the net total and the pay date. Wait for a yes.
The run ends completed. A process cut short leaves the run processing. Finish paying (the same call) completes it, and nobody is paid twice.
Hands on: the run, now completed.
Check: the Bank file & payslips step reads "Paid: N people, $… net on /payroll/history?openId=<sk>.
7. Payroll › Run payroll: the bank file or Pay now (Bank file & payslips)
These are the only routes that get money to people.
- Bank file.
GET business-made/payroll/runs/<sk>/ach/downloadreturns the direct-deposit (NACHA) file for the person to upload to their bank. The download itself makes the deposits: onebank_transferper person, with the stubs marked in file. Downloading again gives the same file.?regenerate=1voids the pending deposits and builds a new file. The direct-deposit setup must be done first. On screen, the button is Download bank file. - Pay now.
POST business-made/payroll/runs/<sk>/ach/paysends the pay through Stripe ACH or Plaid Transfer. This only works when the business chose one of those in Direct deposit setup. With a bank file setup it is refused withPAYOUT_BANK_FILE.- The run must be
completed(PAYROLL_NOT_RELEASED). - Each person is paid at most once.
- Anyone the provider can't pay falls back to the bank file or a check.
- The run must be
Stop. Each is a payment step. A run that is in a bank file or paid out can no longer be voided. Pay now sends real money. Confirm the run, the total and the method, and wait for a yes. Never do this on your own.
Hands on: the file name, or the payout result. Check: the Bank file card reads "Made already: N direct deposits · $…".
Corrections
| Need | Do |
|---|---|
| Stop a run that hasn't been paid | POST business-made/payroll/runs/<sk>/cancel with { reason }. This works at any status except processing, completed or paid. |
| Remove a draft | DELETE business-made/payroll/runs/<sk>. This works on a draft only. A cancelled run stays as a record (tested: CANNOT_DELETE_PAYROLL_RUN). |
| Pay a bonus, or someone who was missed, after the period was paid | A new run with runType: "bonus" or "off-cycle", and the amounts in manualInputs. It never pays the period's timesheets or salary again. A second regular run for a period already paid is refused. |
| Undo a recorded run | POST business-made/payroll/runs/<sk>/void with { reason }, on Payroll › History (/payroll/history?openId=<sk>). It works on a completed or paid run only (tested: a run in review gives CANNOT_VOID, "cancel it instead"). See the list below. |
Voiding does these things:
- It reverses the journal, dated today.
- It voids the stubs.
- It takes the year-to-date totals back off.
- It frees the timesheets for a corrected run.
It is refused once any of the run's pay went out (in a bank file, paid, or by check), or once the year was e-filed. Voiding is a Stop: show the person what it reverses, and wait for a yes.
There is also a one-step route, POST business-made/payroll/run/execute. It records and books a run straight from a preview, with no draft and no approval. The screen never uses it. Once the payroll-approval workflow exists and is on, it is refused with 409 PAYROLL_APPROVAL_REQUIRED. Use the steps above. POST business-made/payroll/run/preview with the same dates writes nothing. It is safe for "how much will payroll be?" (also GET business-made/payroll/reports/upcoming-run?start&end&payDate).
When it's done
Tell the person the run number, the period, the pay date, the number of people, the gross, net and employer cost, who was held and why, and where it stands:
- waiting on which approvers;
- recorded;
- in a bank file;
- paid.
Open /payroll/run?run=<sk>.
"Why can't I run payroll?": GET business-made/payroll/reports/blockers?start=<date>&end=<date>. It lists the whole business's missing timesheets, unapproved timesheets, drafts and missing payroll profiles for those dates. Add the run's readiness.missing from step 3.
What it can't do yet
- No approval means no pay from the screen. The Pay step opens only for an approved run. If the business switches the
payroll-approvalworkflow off, sending a run is refused (PAYROLL_APPROVAL_OFF). The screen then has no way to pay. Only the one-steprun/executeroute works. - The checklist covers the whole business. It counts people outside the run (step 3).
- Must fix flags aren't enforced by the API.
submit-for-approvalchecks only setup readiness. - No tax deposits. Business Made doesn't pay EFTPS or state deposits. The run's taxes are recorded and reported, and the business pays them.
- Benefit deductions set monthly or annually are taken every pay. See Benefits enrollment.
Don'ts
- Don't send a run for approval, record it, make the bank file, use Pay now, or void a run without the person's yes. Each one emails people, books the run or moves money.
- Don't approve or send back a run for anyone, and don't use an owner override to get a run through.
- Don't use
run/executeto skip approval. - Don't guess an EIN, a state account number, a SUTA rate, pay dates or anyone's pay.
- Don't run a second regular run to fix a paid period. Use a bonus or off-cycle run, or void the run and run it again.
- Don't promise a deposit date. The bank decides that once the file is uploaded.