Scan input
A barcode and QR code scanning input for capturing product codes, serial numbers, and other scannable identifiers.
ctx.ui.inputs.scan("name", config);Config
stringThe name of the input. This will be the key in the result object.
stringThe title displayed on the scan card. Defaults to "Scan item" (or "Scan items" in multi mode).
stringHelp text displayed below the title.
"single" | "multi"Whether to capture a single scan or multiple scans. Default is "single".
"none" | "rejectDuplicates" | "trackQuantity"How to handle duplicate scans in multi mode:
"none"- Allow duplicates as separate entries (default)"rejectDuplicates"- Reject scans that have already been captured"trackQuantity"- Group duplicates and track quantity
numberMinimum number of scans required (multi mode only).
numberMaximum number of scans allowed (multi mode only).
stringThe label for scanned items (e.g., "item", "product", "pallet"). Used in validation messages and counters. Default is "item".
booleanAutomatically continue to the next step after a successful scan (single mode only). Default is false.
Scanner support
The scan input supports two scanner types:
Keyboard emulation scanners - Most common. The scanner acts as a keyboard and "types" the barcode. Works with USB scanners and mobile devices like Zebra Android handhelds.
Serial port scanners - Connected via USB serial port through QZ Tray (opens in a new tab). Requires QZ Tray to be installed and configured.
Both scanner types work simultaneously. Scans from either source are captured by the input.
Usage
Single scan
Capture a single barcode and continue. The result is the scanned string value.
const result = await ctx.ui.page({
title: "Scan product",
content: [
ctx.ui.inputs.scan("barcode", {
title: "Scan product barcode",
description: "Scan the barcode on the product label",
}),
],
});
console.log(result.barcode);
// "5901234123457"Multiple scans
Capture multiple barcodes before continuing. The result is an array of scanned values.
const result = await ctx.ui.page({
title: "Scan items",
content: [
ctx.ui.inputs.scan("items", {
mode: "multi",
title: "Scan items to receive",
min: 1,
max: 50,
unit: "item",
}),
],
});
console.log(result.items);
// ["5901234123457", "5901234123458", "5901234123459"]Tracking quantities
When receiving multiple units of the same product, use trackQuantity to group duplicates:
const result = await ctx.ui.page({
title: "Receive goods",
content: [
ctx.ui.inputs.scan("products", {
mode: "multi",
duplicateHandling: "trackQuantity",
title: "Scan products",
unit: "product",
}),
],
});
console.log(result.products);
// [
// { value: "5901234123457", quantity: 3 },
// { value: "5901234123458", quantity: 1 }
// ]Auto-continue
For quick single-scan workflows, enable autoContinue to proceed immediately after scanning:
const result = await ctx.ui.page({
title: "Scan location",
content: [
ctx.ui.inputs.scan("locationBarcode", {
title: "Scan bin location",
autoContinue: true,
}),
],
});
// Flow continues immediately after scanResponse data
The response format depends on the mode and duplicate handling:
| Mode | Duplicate handling | Response type |
|---|---|---|
single | — | string |
multi | none | string[] |
multi | rejectDuplicates | string[] |
multi | trackQuantity | { value: string; quantity: number }[] |
Example: Stock receipt flow
A complete example scanning products for a goods receipt:
export default GoodsReceipt(async (ctx) => {
// Step 1: Scan purchase order
const poScan = await ctx.ui.page({
stage: "identify",
title: "Identify shipment",
content: [
ctx.ui.inputs.scan("purchaseOrder", {
title: "Scan purchase order",
description: "Scan the PO barcode on the delivery note",
autoContinue: true,
}),
],
});
// Look up the purchase order
const purchaseOrder = await ctx.models.purchaseOrder.findOne({
where: { barcode: poScan.purchaseOrder },
});
// Step 2: Scan received items
const itemScans = await ctx.ui.page({
stage: "receive",
title: "Scan received items",
content: [
ctx.ui.display.banner({
title: `PO: ${purchaseOrder.reference}`,
description: `Expected: ${purchaseOrder.expectedQty} items`,
mode: "info",
}),
ctx.ui.inputs.scan("items", {
mode: "multi",
duplicateHandling: "trackQuantity",
title: "Scan items",
unit: "item",
max: purchaseOrder.expectedQty,
}),
],
});
// Process the received items
for (const item of itemScans.items) {
await ctx.models.stockMovement.create({
purchaseOrderId: purchaseOrder.id,
barcode: item.value,
quantity: item.quantity,
type: "receipt",
});
}
return ctx.ui.complete({
title: "Receipt complete",
content: [
ctx.ui.display.keyValue({
data: [
{ key: "Purchase order", value: purchaseOrder.reference },
{ key: "Items received", value: itemScans.items.length },
],
}),
],
});
});