Skip to content
View as Markdown

LabelPrinter ​

Built-in label printing driver. A connector renders a label template with the data you pass in and hands the pages to the operating system's print spooler, so every printer with an installed driver works: Brother P-touch (PT-9700PC and friends), Epson TM-C3500, Zebra / TSC with their Windows drivers, "Microsoft Print to PDF", any CUPS queue on Linux or macOS. Raw printer-language data (ZPL, TSPL, ESC/P) can be sent straight to the port as well.

The Tools → Label Printing page in the UI manages printers, connectors and templates without writing any code; the connector surface below is what scripts, dashboards, the AI and that page all use. For the production-line setup — printer preferences, triggers, monitoring, runaway protection — see the Label Printing guide.

Printing is a Pro feature

Spooling a label to a real printer — print(), printBatch(), printImage(), printRaw() and the Print buttons in the UI — needs the Label Printing feature (Pro); otherwise the call fails with LICENSE_LIMIT. Listing printers, editing templates and preview() (barcodes included) work on every tier, and connectors on the null dry-run backend are never gated, so the whole pipeline can be evaluated before upgrading.

Safety gate

RequiresSafetyGates is on (printing consumes media). At the Observe safety level every action, including preview() and listPrinters(), is simulated and returns null. The driver's own caps (maxCopies, maxBatch, maxQueue, minIntervalMs) apply on every tier; a per-connector safety: block is additionally enforced on Lab.

Properties ​

PropertyTypeAccessDescription
printersstring[]RInstalled printer names as seen by the backend
printerstringR/WResolved printer; write a name or substring to switch
backendstringRwindows | cups | null | tcp
statusstringRready | printing | paper-out | offline | paused | error | unknown
statusDetailstringRHuman-readable detail from the spooler (e.g. paper out, door open)
queueLengthintRJobs waiting in the driver queue plus jobs still in the spooler
jobsPrintedintRJobs that left the spooler queue normally since start
jobsFailedintRJobs the spooler refused or that were cancelled
lastJobobjectR{ jobId, kind, template, copies, pages, printer, state, nativeId, error, queuedAt, spooledAt }
lastErrorstringRMost recent failure message (empty if none)
templatesstring[]RTemplate names found in the templates directory
templatestringR/WCurrent template — used by print() / printBatch() / preview() when only data is given; validated against the templates directory
lastPreviewstringRBase64 PNG of the most recently rendered label — see note below
sequenceintR/WNext value of {{sequence}}; increments per printed label, persists across restarts
dpiintR/WRender resolution (0 = printer resolution, else template dpi)
printingLicensedboolRWhether this connector may spool real jobs (Label Printing feature, or the null backend)

listPrinters(), printers and setting printer ask the OS at most once at a time, cached for 30 s, and wait at most 20 s for it; a slower enumeration (offline network printers on CUPS) returns the last known list and finishes in the background. status is polled from the spooler every statusIntervalMs. Put it (and statusDetail, queueLength, jobsPrinted, jobsFailed, lastError) in the connector's poll: list so dashboards and scripts see changes.

lastPreview is a large string and, like every driver property, is cached by the connector until polled. Prefer the preview stream (a data:image/png;base64,… URL pushed on every render) for a dashboard canvas widget in mode: "image".

Actions ​

ActionArgsReturnsDescription
print{ template?, data?, copies?, printer?, media? } or a bare template name{ jobId, nativeId, pages, copies, printer, template, spooled }Render a template with data and spool it
printBatch{ template?, items: [data…], copies? }job objectOne label per item as a single multi-page spooler job
preview{ template?, data?, dpi? }base64 PNGRender without printing (default previewDpi, 150)
printImage{ image, media?, copies?, printer? }job objectPrint a base64 PNG/JPEG fitted onto the label stock
printRaw{ data, encoding?, printer? }job objectSend ZPL / TSPL / EPL / ESC-P bytes straight to the printer
listPrinters—[{ name, isDefault, status, papers[], resolutions[], port, driver }]Installed printers with capabilities
listTemplates—[{ name, width, height, unit, dpi, orientation, elements, barcodes, fields, error? }]Templates on disk (broken files carry error)
validateTemplate{ template }{ valid, errors[], warnings[], name, barcodes, printingLicensed }Check a template name or inline layout
reloadTemplates—intRe-read the templates directory (also automatic on file changes)
refreshStatus—{ status, statusDetail, queueLength }Poll the spooler now
cancelAll—intCancel queued jobs in the driver queue and the spooler

Calling print() ​

The idiomatic script flow is one connector per printer, a template set once, and data-first calls:

javascript
const printer = connector("label-printer");
printer.template = "shipping";                              // or template: "shipping" in the connector config
printer.print({ product: "Widget", serial: "SN-000123" });  // the object IS the data

Every shape the driver accepts (the same rules apply to printBatch() with items and to preview()):

CallMeaning
print({ serial: 3, qty: 4 })Data form — no template / data / items key, so the whole object is the data; the template property is used; 1 copy
print()Data form with empty data
print("shipping")Template name, no data
print("shipping", { serial: 3, copies: 2 })Template name, then fields; keys that are options (copies, printer, media) are taken as options, the rest is data
print({ template: "shipping", data: {…}, copies: 2, printer, media })Options form — the object has template, data or items; template may be a name or an inline layout { size, elements }
printBatch({ items: [{…}, {…}] }), printBatch("shipping", items)One label per item; a bare array argument only works over WebSocket/MCP (scripts must use { items })

In the data form a field that happens to be called copies is data, not an option. A dashboard button bound to label-printer.print with no argument prints the current template.

data values are stringified for {{placeholders}}; nested objects are reachable as {{order.id}}. Missing required fields (declared under the template's fields) fail with VALIDATION_INVALID_VALUE before anything is spooled.

copies is capped by maxCopies, items by maxBatch; a call that would exceed maxQueue waiting jobs, or arrives within minIntervalMs of the previous one, is rejected with CONN_CALL_FAILED.

printRaw encoding is text (default, Latin-1 so control bytes 0x80–0xFF survive), utf8, base64 or hex.

Every action returns as soon as the job is accepted by the spooler, not when the label is physically out — follow the jobs stream for that.

Streams ​

StreamDescription
jobsJob life cycle: { event, jobId, kind, template, copies, pages, printer, nativeId, error, timestamp }
previewdata:image/png;base64,… of the first page after every render (previewDpi)

event is queued → printing → printed (left the spooler queue normally). Other outcomes: failed (the backend refused the job — error says why), stalled (the spooler reports paper-out / offline / error for this job; emitted once, followed by printing when it resumes), cancelled (cancelAll() or removed from the spooler).

javascript
on("stream:label-printer:jobs", (evt) => {
  if (evt.event === "failed" || evt.event === "stalled")
    connector("webhook").notify("Label job " + evt.event, evt.jobId + ": " + evt.error, "error");
});

Config Options ​

OptionTypeDefaultDescription
printerstring""Printer name or case-insensitive substring; empty = OS default printer
backendstring"auto"auto (Windows spooler / CUPS) | windows | cups | null (dry run) | tcp (raw 9100)
dryRunboolfalseForce the null backend — jobs are recorded, nothing is printed
mediaobject—Physical stock { width, height, unit }; overrides the paper size handed to the spooler and sizes printImage() pages
paperstring""Windows paper name to select instead of a custom size (exact, then substring match)
orientationstring"auto"auto (template decides) | portrait | landscape — landscape rotates the page 90°
dpiint0Render dpi; 0 = the printer's resolution, else the template's dpi
templatesDirstring"labels"Template directory inside the workspace (relative to it); a folder outside the workspace is refused
templatestring""Initial value of the template property (alias: defaultTemplate)
maxCopiesint50Per-call copy cap
maxBatchint500Items per printBatch()
maxQueueint20Jobs allowed to wait for the spooler
minIntervalMsint0Minimum gap between print calls (0 = off)
spoolTimeoutMsint30000How long print() waits for the spooler to accept the job
trackedJobMaxAgeMsint600000A spooled job still reported as printing after this long is assumed printed and released (min 30000)
previewDpiint150Resolution of preview() and the preview stream
statusIntervalMsint3000 (CUPS 5000)Spooler status polling interval
host / port / timeoutMsstring / int / int— / 9100 / 5000For backend: "tcp"
nullStatusstring—Simulated state for backend: "null" (ready, paper-out, offline, …) — used by tests

Backends ​

BackendPlatformRaster labelsprintRawStatus / job tracking
windowsWindowsGDI through the installed driver (custom paper size or a named paper)winspool RAW datatypePRINTER_STATUS flags (paper out, door open, offline, paused), spooler job list
cupsLinux / macOSPDF with a physical page size via lp -o media=Custom.WxHmmlp -o rawlpstat reasons (media-empty, offline, disabled), queue length — best effort
nullanyrecorded, not printedrecordedsimulated
tcpanynot supported yet (needs a ZPL/TSPL encoder)TCP to port 9100port reachability only

Example Connector ​

javascript
// workspace/connectors/label-printer.js
export default {
  driver: "LabelPrinter",
  config: {
    printer: "Brother",                 // substring of "Brother PT-9700PC"
    media: { width: 24, height: 90, unit: "mm" },
    paper: "24mm",                      // a size defined in the Brother driver
    template: "asset-tag",
    maxCopies: 20,
    maxBatch: 200,
  },
  poll: ["status", "statusDetail", "queueLength", "jobsPrinted", "jobsFailed", "lastError", "sequence"],
  ai: {
    instructions: "Label printer. print() consumes media — only print on request; prefer preview().",
  },
  safety: {
    rate: { print: 2, printBatch: 0.2 },
    confirm: { printBatch: "always", printRaw: "always" },
  },
};

A ready-to-copy version lives in workspace/connectors/_examples/label-printer.js, with matching templates in workspace/labels/ and a dashboard in workspace/dashboards/label-printer.dashboard.json.

Example Script ​

javascript
const printer = connector("label-printer");

// Which printers are installed, and what paper sizes do they offer?
for (const p of printer.listPrinters())
  log.info(`${p.name}${p.isDefault ? " (default)" : ""}: ${p.status}, papers: ${p.papers.join(", ")}`);

// Pick the template once, then print with data only
printer.template = "shipping";
const png = printer.preview({ product: "Widget", serial: "SN-000123" });   // check the layout first
log.info(`preview ${png.length} chars of base64 PNG`);

const job = printer.print({ product: "Widget", serial: "SN-000123", qty: 4 });
log.info(`spooled ${job.jobId} on ${job.printer}`);

// Or name the template per call
printer.print("asset-tag", { name: "Oscilloscope" });

printer.printBatch({
  template: "asset-tag",
  items: [{ name: "Oscilloscope" }, { name: "Power supply" }, { name: "Signal generator" }],
});

// Zebra / TSC printers understand their own language — no template needed
printer.printRaw({ data: "^XA^FO50,50^BCN,80,Y,N,N^FDSN-000123^FS^XZ" });

// Watch the printer
if (printer.status !== "ready") log.warn(`printer ${printer.status}: ${printer.statusDetail}`);

Muxit — Hardware Orchestration Platform