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):

FieldType
Labels for (serial or batch numbers). Printed from a Receipt, Shipment, Operation, Entity, Job, Split, or StockTransfer.
Labels for shelves, bins, and storage locations. Printed from a StorageUnit.
Replenishment cards for bins. Printed from a 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:

FieldType
What you call the printer, unique per location. "e.g. Zebra 2x1".
"ZPL (Thermal Label)" or "PDF (Document)". A CHECK constraint enforces the two values.
The label size this printer loads. ZPL printers only offer thermal (ZPL-capable) sizes; PDF printers offer all.
The ProxyBox HTTP endpoint jobs are delivered to, e.g. https://pbx-XXXX.pbxz.cloud/api/v1/print/….
Optional key sent as an X-API-Key header on delivery.
A BinderyPress template id. "Leave blank for built-in" — leave empty to use Carbon's built-in renderers.
The location the printer lives at (optional; a route can be company-wide).

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.

NOTE

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):

FieldType
The location's fallback printer. Every other context inherits this one when it has no printer of its own.
Used when the source document is a Shipment.
Used when the source document is a Receipt.
Used for a StockTransfer or a StorageUnit.
Per work center, for labels printed from a job operation. A work center with no assignment inherits the location default outright.

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.

HEADS UP

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:

Off-track
completed
Delivered, or rendered with no printer to send to (a download-only job).

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.

NOTE

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:

EventContextPrints
Post a receiptreceivingProduct labels for received tracked entities.
Post a shipmentshippingProduct labels, plus Split labels for batch splits.
Split a stock-transfer lineinventoryA Split label, and an Entity reprint of the original batch.
Complete the first operation on a job (MES)work centerThe 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.

HEADS UP

Automatic prints for the same source document are deduplicated within 30 seconds. Carbon drops the second event. Manual prints and reprints are not deduplicated.

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.