Scanning

Barcode Scanning

Keel supports barcode and QR code scanning for warehouse operations, inventory management, and workflows that need fast data capture.

Scanner types

Keyboard emulation (recommended)

Most barcode scanners work as keyboard emulation devices—they "type" the barcode value as keystrokes. This includes:

  • USB barcode scanners
  • Bluetooth scanners
  • Mobile devices with built-in scanners (Zebra, Honeywell handhelds)
  • Camera-based scanning apps

These work out of the box. Just plug in and scan.

Configure your scanner to add a suffix character (usually Enter) after each scan. This helps Keel detect scan completion reliably. Check your scanner's manual for the configuration barcode.

Serial port scanners

Some industrial scanners connect via serial port (USB-to-serial). These require QZ Tray (opens in a new tab) to bridge the hardware to the browser.

To configure:

  1. Install QZ Tray from qz.io (opens in a new tab)
  2. Go to Settings > Devices in the Console
  3. Select the serial port under Scanner device

Using scanners in flows

Both scanner types feed into the same flow input. Use input.scan to capture barcodes:

Single scan

const result = await ctx.ui.page({
  title: "Scan product",
  content: [
    ctx.ui.inputs.scan("barcode", {
      title: "Scan product barcode",
    }),
  ],
});
 
// result.barcode contains the scanned value

Multiple scans with quantity

For receiving or picking workflows:

const result = await ctx.ui.page({
  title: "Receive items",
  content: [
    ctx.ui.inputs.scan("items", {
      mode: "multi",
      duplicateHandling: "trackQuantity",
      unit: "item",
    }),
  ],
});
 
// result.items = [{ value: "SKU001", quantity: 3 }, { value: "SKU002", quantity: 1 }]

See Scan input for all options.

Troubleshooting

Scanner not detected — Test by opening a text editor and scanning. If the barcode appears, the scanner works. Check it's in keyboard emulation mode (not serial).

Scans not captured in flows — Make sure no text input has focus. Click away from any input fields before scanning.

Partial scans — Configure your scanner for fast transmission, not character-by-character with delays.

Serial scanner issues — Check QZ Tray is running and the correct port is selected in Settings > Devices.

Example: Goods receiving

export default ReceiveGoods(async (ctx) => {
  // Identify the delivery
  const delivery = await ctx.ui.page({
    title: "Scan delivery",
    content: [
      ctx.ui.inputs.scan("deliveryNote", {
        title: "Scan delivery note barcode",
        autoContinue: true,
      }),
    ],
  });
 
  const purchaseOrder = await models.purchaseOrder.findOne({
    where: { deliveryNoteBarcode: delivery.deliveryNote },
  });
 
  // Scan received items
  const received = await ctx.ui.page({
    title: "Scan items",
    content: [
      ctx.ui.display.banner({
        title: `PO: ${purchaseOrder.reference}`,
        description: `Supplier: ${purchaseOrder.supplier.name}`,
        mode: "info",
      }),
      ctx.ui.inputs.scan("items", {
        mode: "multi",
        duplicateHandling: "trackQuantity",
        title: "Scan received items",
      }),
    ],
  });
 
  // Process the receipt
  for (const item of received.items) {
    await models.stockMovement.create({
      type: "receipt",
      barcode: item.value,
      quantity: item.quantity,
      purchaseOrderId: purchaseOrder.id,
    });
  }
 
  return ctx.complete({
    title: "Receipt complete",
    content: [
      ctx.ui.display.keyValue({
        data: [
          { key: "Purchase order", value: purchaseOrder.reference },
          { key: "Items received", value: received.items.length },
        ],
      }),
    ],
  });
});