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:
- Install QZ Tray from qz.io (opens in a new tab)
- Go to Settings > Devices in the Console
- 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 valueMultiple 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 },
],
}),
],
});
});