Printing & labels
Printer routing, ZPL and PDF labels, and the print-job queue that turns a receipt or a finished job into a label on a shelf.
Carbon can print a physical label automatically when you post a receipt, split a batch, or complete a job's first operation. The printing system consists of a print-job queue, printer routes that map location and context to a device, and renderers for thermal-label ZPL and document PDF.
This system is separate from generated PDFs such as invoices, purchase orders, quotes, and job travelers. Those documents are delivered as files; printing sends output to a physical printer. Its default output is a thermal ZPL label.
Two renderers, one queue
A print job renders in one of two formats, chosen by the printer it's routed to:
- ZPL is the text command language thermal label printers speak. It's the default for part and storage labels. A ZPL label needs a media size with ZPL dimensions configured, or it can't render.
- PDF is a real document, rendered by the same React-PDF engine that produces invoices and travelers. Kanban cards are PDF only, so a kanban card sent to a ZPL-only printer fails unless a template covers it.
Three document types exist (packages/printing/src/registry.ts:10):
Receipt, Shipment, Operation, Entity, Job, Split, or StockTransfer.StorageUnit.Kanban.The source document is what maps to the doc types worth printing. Posting a shipment yields product labels; scanning a storage unit yields a storage label. getDocumentTypesForSource does that lookup (registry.ts:49).
Printer routes
A printer route is one physical printer (printerRoute table, 20260326000000_print-manager.sql:37). Each route carries:
https://pbx-XXXX.pbxz.cloud/api/v1/print/….X-API-Key header on delivery.You manage routes under Settings → Printing (apps/erp/app/routes/x+/settings+/printing.tsx). Each route has a Test action that sends a small ZPL test label straight to its printerUrl, so you can prove a printer is reachable before wiring it into a workflow. Test print only works for ZPL printers with a media size, and returns "Test label sent to printer" on success.
Carbon reaches printers through ProxyBox, an HTTP endpoint on the shop-floor network. Carbon never talks to a printer's raw port. Delivery is a POST of the rendered bytes to printerUrl. If a route also has a templateId and the deployment has a BinderyPress key, rendering is delegated to BinderyPress instead of the built-in renderer.
Assignments: which printer, in which context
A route is the printer; an assignment decides when it's used. Assignments live in the company's printing settings (companySettings.printing JSONB), one block per location. Within a location you assign a printer to each context (packages/printing/src/assignments.ts:7):
Shipment.Receipt.StockTransfer or a StorageUnit.Each context row also carries an Auto-print switch. When it's on, the matching business event prints without a click. getPrinterContextForSource maps the source document to its context, and resolveContextAssignment walks the fallback: an unassigned context uses the location's default printer.
The auto-print switch defaults on for a context that has no explicit entry, even when the location default has auto-print off. If you want a location's shipping station silent, set its shipping row explicitly, don't rely on the default row.
Printer routing uses a one-hour Redis cache at printing:{companyId}:{locationId}:{context}. Saving a route or assignment invalidates the company's cached routing data.
The print-job queue
Every print, manual or automatic, becomes a printJob row (20260326000000_print-manager.sql:80) and moves through a fixed set of statuses:
Each job records its origin, one of auto (an automatic print behind a business event), manual (someone clicked print), or reprint (re-run from the jobs list). It also keeps the sourceDocument, sourceDocumentId, a readable id, the contentType, and the printerUrl it was sent to.
You watch the queue under Settings → Printing → Print jobs (printing.jobs.tsx). The table streams in real time and shows status, description, source, type, origin, and when. Expanding a row exposes the printer URL, attempts, timestamps, and any error. From there you can View the content (ZPL previews via Labelary, PDF via an inline frame), Reprint it (a fresh job with origin: "reprint"), or Delete it.
Print jobs are pruned on a schedule: completed jobs after 30 days, failed jobs after 90. A failed job stays around long enough to diagnose.
Auto-print behavior
After a supported business event, Carbon resolves the printer configuration for the location and context, checks autoPrint (defaults to on when unset), and enqueues a job. Printing failures do not block the receipt, shipment, or job that triggered them.
The wired-in events:
| Event | Context | Prints |
|---|---|---|
| Post a receipt | receiving | Product labels for received tracked entities. |
| Post a shipment | shipping | Product labels, plus Split labels for batch splits. |
| Split a stock-transfer line | inventory | A Split label, and an Entity reprint of the original batch. |
| Complete the first operation on a job (MES) | work center | The job's product label. |
To stop a station auto-printing, turn off Auto-print on that location's context row. To trigger a print by hand instead, use the Print button on the relevant screen, or send a source document to the manual print route (/x/print), which validates the request and enqueues the same kind of job.
Automatic prints for the same source document are deduplicated within 30 seconds. Carbon drops the second event. Manual prints and reprints are not deduplicated.
Print vs download
When a location has printer routes, Print opens a printer picker with the resolved default selected and enqueues a job after confirmation. With no routes, it opens a download modal for saving the label as ZPL or PDF.
Label sizes
Media sizes are a fixed catalog (packages/utils/src/labels.ts): label2x1, label4x2, metric label100x50mm and label50x25mm, and PDF-only avery5163. Only sizes with ZPL dimensions can print to a thermal printer; a PDF-only size like Avery can't.
Two places set a size. A printer route pins its own mediaSizeId (the size loaded in that physical printer). And the company has a default product-label size (companySettings.productLabelSize), set on the document-template screen, used by the interactive label-download routes when no route dictates one. See Company settings for where that default lives, and Documents for the customizer that styles the built-in label templates.
Where the default product-label size and other company-wide printing defaults live.
Storage-unit labels and the tracked entities most print jobs describe.
The serial and batch numbers a product label carries.
Replenishment bins and the PDF cards you print for them.