Input.scan

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

name*
string

The name of the input. This will be the key in the result object.


title
string

The title displayed on the scan card. Defaults to "Scan item" (or "Scan items" in multi mode).


description
string

Help text displayed below the title.


mode
"single" | "multi"

Whether to capture a single scan or multiple scans. Default is "single".


duplicateHandling
"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

min
number

Minimum number of scans required (multi mode only).


max
number

Maximum number of scans allowed (multi mode only).


unit
string

The label for scanned items (e.g., "item", "product", "pallet"). Used in validation messages and counters. Default is "item".

autoContinue
boolean

Automatically 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 scan

Response data

The response format depends on the mode and duplicate handling:

ModeDuplicate handlingResponse type
singlestring
multinonestring[]
multirejectDuplicatesstring[]
multitrackQuantity{ 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 },
        ],
      }),
    ],
  });
});