Work Orders
A work order drives a manufacturing run from scheduling through completion — tracking the Product being built, planned and produced quantities, materials consumed, operations performed, and quality checks recorded along the way.
A work order is the unit of production in the Manufacturing module. You create one when you need to build a quantity of a composite Product — the order tracks everything from the initial plan through the last quality check: who is working on it, which materials are required, how many units were actually produced, and what each step cost. Work orders consume inventory units as materials and produce finished units that land in your chosen output location.
Work orders can only target composite Products — Products with a bill of materials (BOM). If a Product is not marked composite, it will not appear in the Output Product picker. See Products for details on the BOM flag.
Creating a work order #
New work orders are created from Manufacturing → Work Orders → New Work Order. The form asks for:
- Output Product — the composite Product you are producing. Required. Only Products marked as composite (Has Bill of Materials) appear in the list.
- Planned Quantity — how many units you intend to produce. Must be greater
than zero. Defaults to
1. - Status — Draft or Scheduled. A new work order can only open at the start line; every later status is reached by working the order (issuing materials, starting production, recording a quality check). The record refuses a work order created as Completed or Cancelled whichever door it comes through, because such an order has no legal transition out of itself.
- Due Date — optional target completion timestamp.
- Scheduled Start — optional timestamp for when production is planned to begin.
- Output Location — the inventory location where finished units will land when the order is completed. Optional; if omitted the units must be placed manually.
- Assigned To — a user responsible for the order. Optional.
- Notes — free-form text for any additional context.
When a work order is saved, it is automatically assigned a sequential
number prefixed WO- (e.g. WO-0042).
Work order statuses #
A work order moves through a defined lifecycle. The status badge is visible in the list and on the order's page:
- Draft — created but not yet scheduled. Can move to Scheduled, In Progress, or Cancelled.
- Scheduled — planned and queued for production. Can move to In Progress or Cancelled.
- In Progress — actively being produced. Can move to Quality Check, Completed, or Cancelled.
- Quality Check — production is done; output is awaiting inspection. Can move back to In Progress, forward to Completed, or Cancelled.
- Completed — finished. The quantity produced is recorded. Terminal — cannot be changed further.
- Cancelled — stopped before completion. All non-terminal operations are automatically skipped, and the material the order issued goes back to stock (see Cancelling a work order). Terminal.
The edit form enforces this. Its Status field offers only the statuses the order can reach from where it is now — the rest are greyed out — and it says in so many words which moves are open to it. Completed and Cancelled are greyed out there too: an order is completed with Complete Work Order on its page, which records the output and makes the finished units, and cancelled with Cancel, which gives back the material it issued. A move that is not allowed is refused with a message naming the legal next steps, never silently dropped.
The API enforces the same lifecycle. The rule belongs to the work order, not
to the screen, so a status jump the edit form refuses is refused through the
API too, with a 422 carrying that same message. The
two moves the API takes that the edit form leaves to the page are completing
and cancelling: it completes an order when sent "status": "completed", as
Complete Work Order does, and cancels one when sent "status": "cancelled",
as Cancel does. Previously the API accepted any status, so which door a change came
through decided whether it was allowed. Sending the status an order already has
is not a move and is still accepted.
Cancelling a work order #
When you cancel a work order, all pending and in-progress operations are skipped automatically so the order closes cleanly, and the material it issued goes back to stock. Each unit the order took from gets back what the order took: a bag the order used part of is topped up, and a bag it used up returns to the bin it came from. Each issued line becomes Returned, with a Consumed Qty of 0 and no cost, and every return is recorded as a Material Returned movement. Cancelling a work order that is already cancelled gives back nothing a second time.
Cancelling with Cancel on the page and with "status": "cancelled" on the
API do the same.
A line that was issued before work orders could give material back can't be returned automatically. The work order is still cancelled, that line stays Issued, and a message says its material was not returned to stock. Put it back by adjusting the stock by hand.
A line for a kind of material rather than a product took nothing off a shelf through the work order, so cancelling just marks it Returned.
A Reject quality result also cancels the order, but the work was done, so its material is not given back.
Deleting a work order #
A work order that has used material or made units is not deleted. The page says why, and the API answers 409 with the same reason. If it is still open, has made nothing, and its material is Issued and not yet Consumed, you are asked to cancel it first, because cancelling gives the material back; once it is cancelled and its lines are Returned, it can be deleted. A Draft work order with nothing issued is deleted at once.
A finished work order, one cancelled by a Reject quality result, and one with any Consumed line are kept: they made units or used material that was not given back.
When you delete several work orders from the list, the ones that can't be deleted are skipped and named, and the rest are deleted.
The work order list #
Manufacturing → Work Orders shows each order's number, output Product, planned and produced quantities, status badge, assigned user, and due date. Filter by status (multi-select) or by Product, and sort by any column. Trashed orders can be restored via the Trashed filter.
Materials #
The Materials tab on a work order lists the components required to build it. Each material line records:
- Material Product or Material Category — either a specific Product or any unit from a category qualifies. Category-based lines are useful when any equivalent substitution is acceptable.
- Planned Qty and Unit of Measure — how much is needed per the BOM, and what that figure is measured in. This is the point the whole tab turns on: a BOM line asking for 100 lb per finished unit on an order for ten units needs 1,000 lb, not 1,000 of anything. If the material is stocked in 50 lb bags, that is twenty bags.
- In Stock — how much is on the shelf at the work order's output location, in the same unit of measure, colour-coded green (covered), amber (some but not enough), or red (none). Stock at your other locations is not counted, because it is not stock this job can use.
- Consumed Qty — how much actually left inventory, derived from the stock movements rather than from the plan. When a job uses part of a bag, only the part it used is counted, and the rest of the bag stays in stock.
- Planned Cost and Actual Cost — Planned Cost is the quantity converted
into stocked units and priced at the product's cost per stocked unit, in cents
(stored in the database as an integer, e.g.
1500= $15.00). It stays there after the materials are issued, so the two can be compared. Once the materials are issued, Actual Cost is a total recorded for the line: it adds up what the units that went in actually cost, where a unit with a cost of its own counts at that cost, one without at the product's, and a part-used unit only for the part used. The total is rounded once, to the cent, and fixed when the line is issued, so changing the product's cost afterwards doesn't change it. 1,000 lb of a $7.98 50 lb bag costs $159.60 — twenty bags — not $7,980. - Status — Pending, Issued (pulled from inventory and on the production floor), Consumed, or Returned (the work order was cancelled and the material went back to stock).
Materials can be added and edited manually. You can add a line for any Product or category in your catalog. A line's Consumed Qty and Status are set by issuing its materials, and can't be edited by hand. Once a line has been issued, it records what left the shelf, so it can't be edited (apart from its notes) or deleted on its own. When a work order is completed the consumed-qty entries serve as the record of what left inventory.
Issuing materials #
Issue Materials on the work order page takes every pending line off the shelf at once. Before it moves anything it checks that each line can be covered — if any is short the whole issue is refused, nothing is consumed, and the message names the material, what the order needs, what is in stock and where. Fix the shortage (move stock in, or reduce the planned quantity) and issue again.
A work order with no output Location takes its units from any Location, so Issue Materials on it needs permission to update work orders at every Location. Someone who has that permission at only some Locations is asked to set the work order's output Location first, on the page, over the API and over the connector.
The API does the same with
POST /api/v1/manufacturing/work-orders/{id}/issue-materials, and so does the
MCP connector: every pending line is
issued in one go, a shortage refuses the whole issue with a 422 under
materials, and a draft or scheduled work order is started. A completed or
cancelled work order takes no materials. The units come from the stock the user
can see, as on screen: the key's user over the API, the connected user over the
connector. That user needs to be able to see that stock as well as update work
orders.
What comes off the shelf is decided in the material's unit of measure and then converted into whole units: 1,000 lb of a product stocked in 50 lb bags takes twenty bags from the work order's own location. The last bag opened keeps what the job did not use, which stays in stock, and the job is charged only the quantity it asked for. Each bag taken records its own movement.
Naming the units that go in
When a material's units can be told apart, Issue Materials asks which ones go in, the same way shipping does:
- One by one, when the material's category tracks serial numbers, or a unit on the shelf has a printed label or a value in a custom field marked "Identifies a unit". Scan or choose each unit. Name exactly the units the line needs: too few is refused as a shortage, and a unit the line wouldn't need is refused too.
- By lot, when the category tracks lot numbers or expiration dates. Choose the lot (or expiry), and how much of it, in the line's unit of measure. Several lots can make up a line, and together they must add up to what the line needs. Within a lot, the oldest units go first.
The units offered are the ones in stock at the work order's own location. Which way a line is named is decided when it's issued, so if a category starts tracking lots while a job is under way, the lines not yet issued are named by lot, and the lines already issued keep the units they took.
For any other material, the units taken are the oldest in stock at that location. They're recorded as the units that went in, but nobody chose them, so they may not be the ones actually picked up.
Category-based lines have no product to take off a shelf, so they are marked issued and left for a person to fulfil.
Through the API, the same choices go in named, keyed by the material line's id.
A line that needs naming and is left out is refused with a 422 under
named.<line id>, and nothing is issued:
curl -X POST "https://your-domain.com/api/v1/manufacturing/work-orders/<work-order-id>/issue-materials" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"named": {
"<serial-tracked-line-id>": {"units": ["<unit-id>", "<unit-id>"]},
"<lot-tracked-line-id>": {"batches": [{"lot_number": "L-17", "expiration_date": "2027-01-31", "quantity": 100}]}
}
}'
quantity is in the line's own unit of measure. Leave named out for a work order
whose lines are all taken oldest first.
Operations #
The Operations tab lists the production steps for the order, in sequence order. Each operation records:
- Step # — the sequence position (1, 2, 3…).
- Name — a description of the step (e.g. Cut, Assemble, Paint).
- Work Center — the work center where the step is performed. Drives labor cost calculation via the work center's cost per hour.
- Status — Pending, In Progress, Completed, or Skipped.
- Est. Minutes — planned duration, used for planned labor cost.
- Assigned To — the user responsible for this step.
- Started At / Completed At — timestamps set when an operation is started or completed. Actual minutes (and therefore actual labor cost) are computed from the difference.
From the operations table you can Start, Complete, or Skip an individual step without leaving the work order page. Steps run in sequence order: while an earlier step is still pending or in progress, Start and Complete are greyed out and say which step is holding this one up. Finish that step, or Skip it — skipping is deliberately always available, because a step somebody decides not to perform moves the routing forward honestly, unlike completing one that never happened. Steps sharing a sequence number do not block each other.
Each of the three is recorded in the work order's History, with who did it.
Operations sourced from a routing template (see Routings & Operations) are pre-populated when the order is created from a BOM that references a routing; you can still add ad-hoc steps at any time.
Cost roll-up: planned total cost = sum of planned material costs + sum of planned labor costs. Actual total cost uses consumed quantities and actual operation minutes, plus any unaccounted material (see below). Both figures are visible on the work order's detail page.
Unaccounted material #
The materials issued to a work order tell you how far it could physically get: a run with concrete mix for ten units and epoxy for two makes two, because the shortest material is the one that decides. Producing more units than the materials account for is allowed — a batch planned for ten genuinely does sometimes yield eleven — but you are asked where the extra came from, and the work order carries the shortfall from then on.
The Cost Summary then shows an Unaccounted Material figure in red: the material those units should have taken, priced at the planned material cost per unit. Actual Material Cost stays honest about what left the shelf; Actual Total includes the shortfall, so a run that produced more than it consumed can no longer report "Actual = Planned" to the penny. Produced Qty is marked in red too, saying what the materials cover and by how much the run is over.
An explanation covers the figure it was recorded with. Output is recorded once, when the work order is completed, so there is no later figure for it to be compared with.
This is enforced on the work order itself, not on the form, so every door meets
it: the QC modal, Complete Work Order, and PATCH /api/v1/manufacturing/work-orders/{id} with "status": "completed" and
quantity_produced. The API refuses an unexplained overrun with a 422 under
overrun_reason and leaves the work order where it was; send overrun_reason
alongside quantity_produced to record a genuine yield gain, and read the
shortfall back from unaccounted_quantity and unaccounted_material_cost. How
much the explanation covers comes back as overrun_reason_quantity.
A product with no bill of materials has no material lines and so nothing to check its output against. The work order says so on its own page rather than showing a row of zeroes that reads as "no material used" — add components to the product to have material cost and shortfalls tracked.
Quality checks #
The Quality Checks tab is where inspection results are logged. A quality check records:
- Result — Pass, Rework, or Reject.
- Checked At — the timestamp of the inspection; always the moment it was recorded, and not settable to any other time.
- Qty Inspected and Qty Passed — the difference implies the rejection count. Qty Passed cannot exceed Qty Inspected: on a Pass it becomes the work order's produced quantity and creates that many units, so a typo here would mint stock that was never made. And on a Pass, Qty Passed above what the issued materials account for asks you where the extra units came from before it will record anything — see Unaccounted material. A Rework or Reject creates no stock and is never held up by this. Neither quantity may reach 1,000,000. Recording a check over the API follows the same rules as recording one here, with one difference: the Record QC Result action only appears once a work order reaches Quality Check status, while the API accepts a result from any status the resulting move is legal from (a Pass from In Progress moves straight to Completed, for example) and refuses with a 422 from any status it is not.
- Inspector — the user who performed the check.
- Defect Notes — description of what was wrong.
- Corrective Action — what was done to address the defect.
Each inspection is stored as a separate row with its own result. The tab is read-only — a check cannot be edited or deleted once recorded, because deleting one would not reverse whatever it had done, such as the stock a Pass created. When a work order is in Quality Check status, record a result with the Record QC Result action; a Pass completes it, a Rework sends it back to In Progress, and a Reject cancels it.
A Pass makes the finished units, so, like Complete Work Order, it needs an output Location first, and you need permission to edit work orders there. The app and the API refuse it otherwise, and nothing is recorded.
Relationship to inventory #
Completed work orders produce units: when you record quantity_produced on
completion, the finished units appear in the output location as new
inventory units, in that location's receiving
bin — or, failing that, wherever the product was last put away there. This is
the same rule receiving follows, and it is why an in-stock unit always has a bin
somebody can walk to. On the material side, consumed units are marked used and
their movement history records the work order as the source. This keeps the full
audit trail intact across both the work order and the inventory unit.
Completing over the API does the same as Complete Work Order.
PATCH /api/v1/manufacturing/work-orders/{id} with "status": "completed", or
the same through the MCP update tool, makes the finished units at the output
location and marks the issued materials consumed, in one step. Add
quantity_produced to record a different figure; without it the quantity
planned is used. The work order needs an output location first, or the request
is refused with a 422 and nothing changes. Sending quantity_produced without
completing is refused with a 422, and so is a second figure on a work order
that is already completed.
For the Product's BOM see Products; for the location the output lands in see Locations & Bins.
Doing it from the API #
# Create a work order
curl -X POST "https://your-domain.com/api/v1/manufacturing/work-orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "<product-id>",
"quantity_planned": 10,
"output_location_id": "<location-id>",
"due_at": "2026-07-01T08:00:00Z"
}'
# List work orders filtered by status
curl "https://your-domain.com/api/v1/manufacturing/work-orders?status=in_progress" \
-H "Authorization: Bearer $TOKEN"
# Advance a work order one stage. Only the moves listed under
# "Work order statuses" above are accepted — anything else comes back 422:
# {"message": "A draft work order cannot be moved straight to that status.
# From Draft this work order can move to: Scheduled, In Progress
# or Cancelled.",
# "errors": {"status": ["A draft work order cannot be moved straight ..."]}}
# Nothing else in a refused request is saved, so retrying with a legal status
# is safe.
curl -X PATCH "https://your-domain.com/api/v1/manufacturing/work-orders/<work-order-id>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "in_progress"}'
# Record a quality check result. A Pass creates stock and completes the work
# order the same way recording one in the app does, so the work order has to
# already be in a status a Pass may complete from (In Progress or Quality
# Check) — from Draft or Scheduled this comes back a 422 under "status". A
# Pass also needs an output location, or it comes back a 422 under
# "output_location_id".
curl -X POST "https://your-domain.com/api/v1/manufacturing/quality-checks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"work_order_id": "<work-order-id>",
"result": "pass",
"quantity_inspected": 10,
"quantity_passed": 10,
"inspector_id": "<user-id>"
}'
# A Pass beyond what the issued materials account for needs overrun_reason,
# the same as completing with quantity_produced over the API (see above) — otherwise
# it's refused with a 422 under "overrun_reason".
curl -X POST "https://your-domain.com/api/v1/manufacturing/quality-checks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"work_order_id": "<work-order-id>",
"result": "pass",
"quantity_inspected": 10,
"quantity_passed": 10,
"overrun_reason": "Eight finished from offcuts left over from last week."
}'
Material and operation records are also available at
/api/v1/manufacturing/work-order-materials and
/api/v1/manufacturing/work-order-operations — filter both by work_order_id
to scope them to a specific order. A material record's status and
quantity_consumed are read-only there: they're set by Issue Materials, and
sending either is refused. See the full
API reference for all available
filters and fields.