Pick list
A warehouse-style picking interface for quantity confirmation. Users can pick items by scanning barcodes or manually adjusting quantities, with visual feedback as they progress toward target quantities.
ctx.ui.interactive.pickList("name", config);Config
stringThe name of the pick list. This will be the key in the result object.
T[]An array of data to display in the pick list.
(data: T) => PickListItemA function that takes an item from the data array and returns a PickListItem. See the Item structure section for details on the PickListItem type.
{ scanner?: boolean; manual?: boolean }Controls which input methods are available. When scanner is true, barcodes can be scanned to increment quantities. When manual is true, plus and minus buttons are shown for manual adjustment. Both default to true.
"increaseQuantity" | "rejectDuplicates"How to handle duplicate barcode scans. With increaseQuantity (default), scanning the same barcode multiple times increments the quantity. With rejectDuplicates, each unique barcode can only be scanned once per item.
booleanWhen true, the flow automatically advances to the next step once all items reach their target quantities. Only triggers when the final item is completed via scanning.
Item structure
The render function must return a PickListItem with the following properties:
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier for the item |
title | string | No | Display name shown in the pick list |
description | string | No | Additional details shown below the title |
targetQuantity | number | Yes | The quantity required to complete this item |
barcodes | string[] | No | Barcodes that map to this item for scanning |
image | { url: string; alt?: string; fit?: "contain" | "cover" } | No | Product image to display |
Return value
The pick list returns an object containing an items array with the picked quantities:
{
items: [
{
id: string;
quantity: number;
targetQuantity: number;
scannedBarcodes: string[];
}
]
}Each item includes the scannedBarcodes array, which tracks exactly which barcodes were scanned. This is useful for serial number tracking or audit trails.
Usage
The pick list is an interactive element that tracks progress toward target quantities. Items turn green when their target is reached.
const items = [
{
id: "sku-001",
name: "Wireless Mouse",
location: "A-12-3",
qty: 2,
barcode: "5901234123457",
imageUrl: "https://example.com/mouse.jpg",
},
{
id: "sku-002",
name: "USB-C Cable",
location: "B-04-1",
qty: 5,
barcode: "5901234123458",
},
];
const result = await ctx.ui.page({
title: "Pick items for order",
content: [
ctx.ui.interactive.pickList("picks", {
supportedInputs: {
scanner: true,
manual: true,
},
data: items,
render: (item) => ({
id: item.id,
title: item.name,
description: `Location: ${item.location}`,
targetQuantity: item.qty,
barcodes: [item.barcode],
image: item.imageUrl ? { url: item.imageUrl, alt: item.name } : undefined,
}),
}),
],
});
// Access picked quantities
for (const item of result.picks.items) {
console.log(`${item.id}: picked ${item.quantity} of ${item.targetQuantity}`);
}Warehouse picking workflow
Here's a complete example of an order fulfillment picking flow:
import { PickOrder, models } from "@teamkeel/sdk";
export default PickOrder(async (ctx, inputs) => {
// Fetch the order and its line items
const order = await models.order.findOne({
where: { id: inputs.orderId },
include: { lines: { include: { product: true } } },
});
if (!order) {
throw new Error("Order not found");
}
// Show the pick list
const pickResult = await ctx.ui.page({
title: `Pick Order ${order.reference}`,
content: [
ctx.ui.display.banner({
mode: "info",
title: `${order.lines.length} items to pick`,
}),
ctx.ui.interactive.pickList("picks", {
supportedInputs: {
scanner: true,
manual: true,
},
duplicateHandling: "rejectDuplicates",
data: order.lines,
render: (line) => ({
id: line.id,
title: line.product.name,
description: `Location: ${line.product.location} | SKU: ${line.product.sku}`,
targetQuantity: line.quantity,
barcodes: [line.product.barcode],
image: line.product.imageUrl
? { url: line.product.imageUrl }
: undefined,
}),
}),
],
});
// Check if all items were fully picked
const allComplete = pickResult.picks.items.every(
(item) => item.quantity >= item.targetQuantity
);
if (!allComplete) {
// Handle partial picks
const shortItems = pickResult.picks.items.filter(
(item) => item.quantity < item.targetQuantity
);
await ctx.ui.page({
title: "Incomplete pick",
content: [
ctx.ui.display.banner({
mode: "warning",
title: "Some items were not fully picked",
}),
ctx.ui.display.table({
data: shortItems.map((item) => {
const line = order.lines.find((l) => l.id === item.id);
return {
item: line?.product.name,
picked: item.quantity,
required: item.targetQuantity,
short: item.targetQuantity - item.quantity,
};
}),
}),
],
});
}
// Record the pick results
for (const item of pickResult.picks.items) {
await models.pickRecord.create({
orderId: order.id,
orderLineId: item.id,
quantityPicked: item.quantity,
scannedBarcodes: item.scannedBarcodes,
pickedAt: new Date(),
pickedBy: ctx.identity?.id,
});
}
return { success: allComplete };
});Scanner-only mode
For strict warehouse environments where all picks must be verified by scanning:
ctx.ui.interactive.pickList("picks", {
supportedInputs: {
scanner: true,
manual: false, // Disable manual buttons
},
duplicateHandling: "rejectDuplicates",
autoContinue: true, // Auto-advance when complete
data: items,
render: (item) => ({
id: item.id,
title: item.name,
targetQuantity: item.quantity,
barcodes: item.barcodes,
}),
});Serial number tracking
When duplicateHandling is set to "rejectDuplicates", each barcode can only be scanned once. Combined with the scannedBarcodes return value, this enables serial number tracking:
const laptopBatch = [
{
batchId: "laptop-batch",
productName: "Laptops",
instructions: "Scan each laptop's serial number",
requiredQuantity: 3,
validSerials: ["SN-001", "SN-002", "SN-003", "SN-004", "SN-005"],
},
];
const result = await ctx.ui.page({
title: "Scan serial numbers",
content: [
ctx.ui.interactive.pickList("serialPicks", {
duplicateHandling: "rejectDuplicates",
data: laptopBatch,
render: (batch) => ({
id: batch.batchId,
title: batch.productName,
description: batch.instructions,
targetQuantity: batch.requiredQuantity,
barcodes: batch.validSerials,
}),
}),
],
});
// Record which serial numbers were picked
const serialNumbers = result.serialPicks.items[0].scannedBarcodes;
console.log("Picked serial numbers:", serialNumbers);
// ["SN-002", "SN-003", "SN-001"]