docs
/
AI skills

Run payroll

A pay run in Business Made › Payroll from start to paid. The employer setup it needs. A draft for the period, calculated and reviewed with its flags. Sent to the payroll approvers through the Approvals tray. Recorded once it is approved, with the bank file or Pay now after that. Bonus and off-cycle runs, cancelling and voiding. Every email and money step each part sets off.

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:

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 addressPOST 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 typesPOST 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 profileEach 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:

FieldWhat it is
payPeriodStart, payPeriodEnd, payDateYYYY-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.
employeeIdsThe people's bm_employee sk.
runType and typeregular, bonus or off-cycle. The screen sends both.
supplementalMethodflat (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, totalDeductions and totalCompanyCost.
  • 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:

LineWho paysAmount
Federal income taxEmployee$156.15
Social SecurityEmployee$124.00
MedicareEmployee$29.00
FUTAEmployer$12.00
Texas SUTAEmployer$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.

KindWhat it means
negative_netMust fix. The deductions are more than the pay.
hours_spike, overtime_spike, rate_changedDifferent from the previous run.
zero_net, inactive_person, ghost_person, duplicate_personSomeone is being paid nothing, or shouldn't be paid.
bank_account_changed, duplicate_bank_account, no_pay_methodSomething about where the money goes.
missing_clock_out, hours_on_leave_daySomething about the timesheets.
missing_formFor 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 gives FLAG_NOT_FOUND. Once the run has been sent for approval, the call is refused with 409 PAYROLL_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-requested message, and puts the run in their Approvals tray.
  • It sets the run to pending_approval and records approvalTaskId and approveBy. approveBy is the deadline that still makes the bank cutoff.
  • The first time anyone in the business sends a run, it creates the payroll-approval workflow, 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):

StageWho decidesHow many
Payroll reviewthe HR or Manager role. If nobody has those roles, the Owner.2
Payment releasethe Finance, Owner or Controller role2

Hands on: the approvalTaskId. Check: the Approval step reads "It is with now". A run that is sent back shows Sent back by with their comment.

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.paid message ("Your pay stub is ready"), and each held person a payroll.held message.

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 ". See this run's pay stubs opens /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/download returns the direct-deposit (NACHA) file for the person to upload to their bank. The download itself makes the deposits: one bank_transfer per person, with the stubs marked in file. Downloading again gives the same file. ?regenerate=1 voids 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/pay sends 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 with PAYOUT_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.

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

NeedDo
Stop a run that hasn't been paidPOST business-made/payroll/runs/<sk>/cancel with { reason }. This works at any status except processing, completed or paid.
Remove a draftDELETE 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 paidA 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 runPOST 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-approval workflow off, sending a run is refused (PAYROLL_APPROVAL_OFF). The screen then has no way to pay. Only the one-step run/execute route 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-approval checks 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/execute to 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.