Skip to content

Label sheets

Every physical thing a floor acts on carries a printed code: locations, containers, stations, machines, and the badges people scan to act. Label sheets print those codes from the scan schemes that read them, on office printers (A4 and Letter label stock, ID card stock) and thermal printers (ZPL).

A label is a heading, up to two lines of text, and one to four codes for one target, each printed beside the act it names. A container label can print Check in and Check out side by side, and the person scanning chooses the act by choosing which code to scan. The text is whatever the target’s facts say.

Eight bin labels from a pasted list, one code each. The preview flags the one label whose line has no value.

A template says what one label shows, for one scan scheme. Templates print codes the platform mints: schemes with fixed or delimited encoding.

FieldMeaning
codes1 to 4 of the scheme’s rules, in print order. Each has a caption (default: the rule’s name) and a tone: solid (white text on a black band), outline or plain. Tones tell neighbouring codes apart on black-only stock.
text.title, text.linesA heading and up to two lines, written in label tokens
symbologycode128 (default; shelves scanned from a distance), qrcode, or datamatrix (small labels)
human_readablePrint the code as text under its symbol
mediaThe template’s default media preset
sensitivePrinting confirms first and records who and what the labels name. Always true on an identity scheme.
TokenValue
{target}The code’s target (action schemes)
{item.<facet>}A facet of the row the target came from, or a column of a pasted list
{user.display_name}, {user.name}, {user.email}, {user.roles}, {user.<property>}The badge holder (identity schemes)
{scheme.name}The scheme’s name

A token with no value prints empty, and the preview warns about it. Captions take tokens too; a blank caption prints the rule’s name. Identity credentials never print as text: {target} and {user.<identity key>} are refused on identity templates.

Manage templates on Admin > Scan Codes > (scheme) > Labels, with PUT /api/labels/templates/:name, or with the upsert_label_template MCP tool. Every write bumps the template’s revision, and an edit that names expected_revision is refused when someone else saved first.

Templates declare with their scheme and follow code-owned configuration as one unit with it:

index.ts
scanSchemes: [
{
version: 14,
name: 'Container',
target_facet: 'containerId',
encoding: 'delimited',
rules: [
{ category: '0', name: 'Check in', steps: [/* … */] },
{ category: '1', name: 'Check out', steps: [/* … */] },
],
labels: [
{
name: 'container',
title: 'Container',
codes: [{ category: '0', tone: 'solid' }, { category: '1', tone: 'outline' }],
text: { title: '{target}', lines: ['{item.subtitle}'] },
media: 'a4-l7160',
},
],
},
],

A print job is a template plus a source of targets:

SourceTargets
{ kind: 'range', prefix, from, to, pad, suffix }A numbered run, A-01 to A-40. Targets can be printed before anything else is known about them.
{ kind: 'list', text }One target per line. A header row starting with target names the other columns, which fill {item.<column>}.
{ kind: 'escalations', ids }Rows selected in a list, each read as the caller. Its metadata fills {item.*}.
{ kind: 'facets', query }Every row a faceted search returns, searched as the caller. The newest row per target wins.
{ kind: 'users', userIds, role }Users carrying the identity scheme’s key: the ids given, everyone in role, or with neither, every badge holder. The only source an identity template accepts.

A job holds at most LT_LABELS_MAX_PER_JOB labels (default 1000) and at most LT_LABELS_MAX_CREDENTIALS_PER_JOB badges (default 200).

The dashboard prints from three places: Print labels on a scheme’s page (a range or a pasted list), a selection on an operator list (the selected rows), and Accounts > Print badges.

FamilyPresets (GET /api/labels/media)
A4a4-l7160 (21 per sheet), a4-l7163 (14), a4-l7165 (8), a4-l7173 (10), a4-l7651 (65), a4-a6 (4), a4-placard (1), a4-cr80 (10 ID cards)
Letterletter-5160 (30), letter-5163 (10), letter-22805 (24 squares), letter-placard (1), letter-cr80 (8 ID cards)
Thermalthermal-4x6, thermal-4x2, thermal-2x1 at 203 dpi

A job prints on the media it names, then the template’s media, then the deployment default (LT_LABELS_DEFAULT_MEDIA, a4-l7160 unless set). A job can also pass a custom grid: page size, rows and columns, label size, margins and gutters, all in millimetres.

  • startPosition skips used labels on the first sheet, counted from 1.
  • copies prints each label several times, together.
  • onePerPage puts each label on its own page, for placards.
  • cutMarks prints trim marks for card stock.

Labels lay out to fit their box. A code that would print below the stock’s minimum bar width (minModuleMm, 0.25 mm on the presets) or without its blank margin reports a scale or quiet_zone warning.

POST /api/labels/issue delivers a job as:

outputResult
sheetThe laid-out sheet as JSON. The dashboard prints it through the browser at exact size.
pdfA PDF at the media’s exact page size
zplZPL for thermal media, using the printer’s own barcode commands
transport:<name>Sent through a registered printer transport

PDF text uses Helvetica, which covers Latin names with accents. A deployment that prints names in other scripts registers a font with start({ labels: { font: { regular, bold } } }).

A preview (POST /api/labels/preview) lays out the job without printing or recording it, and warns about every label that would fail on the floor:

WarningThe label
unparseableIts code does not read back as its scheme, or the scheme is disabled
rule_disabledIts code’s rule is disabled, so scanning it does nothing
unresolvedIts target names no row today, or the badge holder is not an active user
missing_tokenIts heading, a line or a caption reads a value the label’s data does not have
scale, quiet_zone, overflow, unencodableIts code or text does not fit

The dashboard prints only after a preview of the same choice. In Chrome, Check codes on screen decodes every code in the preview and reports any it cannot read back.

An identity scheme’s code acts as a person, so printing one is issuing a credential:

  • Only a credential issuer may print or reissue a badge.
  • A badge preview is masked. It shows where each code goes, never the code.
  • Printing asks for confirmation (confirm: true), and the ledger row naming the requester and every holder is written before any code is composed.
  • Badge codes travel in response bodies with Cache-Control: no-store, never in URLs, logs or the ledger.
  • User reads leave identity keys out of metadata and list the ones a user carries in identity_bound.
  • Setting or removing an identity key by hand (PATCH /api/users/:id/properties) needs the same access and is recorded as a bind.
Badges for everyone in a role. The preview shows names and placement; the codes print only after confirmation.

The users API protections are on by default. start({ labels: { protectIdentityKeys: false } }) or LT_PROTECT_IDENTITY_KEYS=false keeps the earlier users API behavior, and badge printing and reissue still need credentialIssuer. Either way, a metadata replace keeps the identity keys it leaves out. Badge scans resolve on the server exactly as before.

Reissue (POST /api/labels/badges/reissue) mints a new credential, writes it and its ledger row in one statement, and prints the new badge. On a delimited identity scheme the credential is target_length random characters (8 to 64, default 10; Badge length on the scheme page), printed with its prefix, for example 14:0:7KQ2M9XW4H. On a fixed scheme it is target_length digits. Badges of any length keep scanning; the length shapes new ones. Ten characters keep a Code 128 badge on an ID card above the minimum bar width. The old code resolves to no one from that moment, and an acting grant minted from it before the reissue lasts until its grant_ttl_seconds runs out.

LabelsAccess
Item labels (action schemes)Admin access or role management (admin, superadmin, the engineer role)
Badges (identity schemes)The credentialIssuer capability: superadmin, or a holder of the credential-issuer role. Admin type alone does not grant it.
TemplatesRole management to write, any signed-in user to read

Every print, reissue and binding is a row in lt_label_jobs: who asked, when, the template and its revision (with a snapshot), the media and output, and what the labels name, targets for item labels and user ids for badges. The platform never deletes ledger rows, and the only columns it updates are a transport job’s delivery stamps.

Admin > Label History: every print, badge and binding, with Print again on each print job.

Admin > Label History (GET /api/labels/jobs) lists the ledger. Credential issuers see every row, and other readers see item jobs and their own. Print again (POST /api/labels/jobs/:id/reprint) prints a job, or chosen labels of it, again from its source with the template as it is now, and records a new job. A badge reprint confirms again.

An idempotencyKey on an issue request makes it safe to repeat, which is what a workflow activity retry does. The repeat records nothing new and prints only the labels the first request recorded:

  • A sheet, PDF or ZPL output renders those labels again.
  • A transport output is sent under a delivery lease claimed in one statement, so concurrent repeats send once. A job already delivered is not sent again. A job whose send failed, or whose sender stopped before the transport accepted, is sent by the next repeat once its lease (LT_LABELS_DELIVERY_LEASE_SECONDS, default 120) lapses.
  • A key used before for a different template or output answers 409.

Workflow activities pass their workflow id as the key.

A transport routes issued labels to a printer: a thermal printer on the network, a print server, a watched folder. Hosts register transports by name:

index.ts
import type { LTLabelTransport } from '@hotmeshio/long-tail';
const thermal: LTLabelTransport = {
formats: ['zpl'],
async send({ jobId, payload, destination }) {
await sendToPrinter(destination, payload as string);
return { accepted: true, detail: jobId };
},
};
start({ labels: { transports: { thermal }, defaultMedia: 'a4-l7160' } });

A job sends with output: 'transport:thermal'. destination on the request passes through to send as given, and the transport decides what it names. A name with no registered transport is refused before anything is recorded. GET /api/labels/transports lists the registered names, and the dashboard offers Send when one accepts the job’s format.

The examples register storage-drop, which writes each job to file storage at labels/<jobId>.<format>.

The admin MCP server carries list_label_templates, upsert_label_template, delete_label_template and render_label_sheet. render_label_sheet issues item labels as the caller, stores the PDF or ZPL in file storage, and returns the job id, the file ref, a link valid for ten minutes, and the warnings. Badges are printed by people: the tool refuses sensitive and identity templates.