Batching

This section details the use of the standard batching capabilities available on LINQ.

Note

A Batch is a single cycle of a workflow including all labware and tasks required to execute it.

Running multiple batches involves scheduling tasks and labware for multiple instances/cycles of a workflow. LINQ’s batching feature allows users to set up workflows that can be batched (within system capacity) without requiring any custom batching logic.

Batching Basics

In this section we introduce the basic concepts used to create or modify a workflow so that it can be batched.

Defining multiple labware instances

When writing a workflow that can be batched, labware must be defined as a range, rather than individual slots. This tells the system a range of possible locations where it can retrieve a labware of a given type. The scheduler will dynamically assign labware from this range to tasks across multiple batches.

In you Labware definition, you can define the starting location as a range of slots (instead of a single specific slot) in order to achieve this behavior. A group of labware that can be batched would look something like this:

microplates = Labware(
    id="microplates",
    labware_type="96_well_microplate",
    starting_location=LabwareLocation(
        instrument=hotel, 
        slot=SlotRange(start_slot=1, end_slot=50)
    )
)

The definition of starting location slots as slot=SlotRange(start_slot=1, end_slot=50) tells the scheduler that the sequence of contiguous slots in the instrument from 1 to 50 can all store multiples of this labware that can be batched.

Note

The slot range also indicates the maximum number of batches the workflow can support. In this example, if every batch uses a single 96_well_microplate, the workflow can only be batched 50 times.

Multiple starting locations

For advanced batching scenarios, labware can have multiple starting locations across different instruments:

microplate = Labware(
    id="microplates",
    labware_type="96_well_microplate",
    starting_location=[
        LabwareLocation(instrument=hotel_a, slot=SlotRange(start_slot=1, end_slot=50)),
        LabwareLocation(instrument=hotel_b, slot=SlotRange(start_slot=1, end_slot=30)),
    ],
)

This allows the scheduler to source labware from multiple locations during batching, increasing workflow flexibility and capacity.

Defining slot filling rules

Similar to the location ranges explained above, tasks that expect multiple labware can be given slot ranges to specify where labware can be placed when batched.

For example, when storing labware in a hotel at the end of a workflow, if that labware can be batched then a range of slots at least equal to the range of labware must be available for storage.

Note

Ranges are only needed if the task takes multiple labware and have more than a single slot.

How to load slot ranges when batching is defined using a SlotFillingRule.

store_microplate_on_hotel = ActionTask(
    id="store_microplate_on_hotel",
    description="End position for the labware (in a workflow where labware can be batched)",
    ...
    labware_sources=[
        previous_task.forward(
            labware=microplates,
            destination_slot=SlotFillingRule(
                range=SlotRange(start_slot=51, end_slot=100), 
                min_count=1, 
                max_count=1, 
                distribution="unique"
            ),
        ),
    ]
)

Multiple slot ranges

SlotFillingRule can also specify multiple non-contiguous slot ranges:

store_microplate_distributed = ActionTask(
    id="store_microplate_distributed",
    description="Store labware across multiple instruments",
    ...
    labware_sources=[
        previous_task.forward(
            labware=microplate,
            destination_slot=SlotFillingRule(
                range=[
                    SlotRange(start_slot=1, end_slot=25),
                    SlotRange(start_slot=50, end_slot=75),
                ],
                min_count=1,
                max_count=1,
                distribution="unique"
            ),
        ),
    ]
)

This enables the scheduler to distribute batched labware across multiple slot ranges, useful when a single contiguous range is unavailable.

In this example, the hotel slots 51 to 100 are uniquely distributed across each microplate in each batch. The min_count and max_count argument determine the number of slots that can be filled per batch. In this example only one slot can be used for each batch cycle. Setting distribution="unique" ensures that each labware has a unique slot reserved that can not be re-used in upcoming batches.

When specifying outputs for labware using SlotFillingRule inputs, use OutputSlotFillingRule to automatically align output slots with input slots:

store_microplate_on_hotel = ActionTask(
    id="store_microplate_on_hotel",
    description="Store the microplate on hotel",
    ...
    labware_sources=[
        previous_task.forward(
            labware=microplate,
            destination_slot=SlotFillingRule(
                range=SlotRange(start_slot=51, end_slot=100),
                min_count=1,
                max_count=1,
                distribution="unique"
            ),
        ),
    ],
    labware_outputs=[
        LabwareOutput(
            labware=microplate,
            slot=OutputSlotFillingRule(behavior="match_input")  # Aligns output to input slot
        ),
    ],
)

OutputSlotFillingRule supports two behaviors:

  • "match_input": Output slot matches the input slot assigned by SlotFillingRule (default)

  • "use_range": Output is placed according to a separate slot range

Attention

When using SlotFillingRule, output slots are automatically aligned with input slots. Do not manually specify fixed output slots in labware_outputs.

In the Task sharing section we explore more advanced usage of the slot filling rule.

Task sharing

The min_count and max_count argument can be used to define slot filling rules that enable labware from different batches to share a task. In the example below, a liquid handler must be loaded with exactly three microplates before it can run. This means each set of 3 consecutive batches will share the same liquid handler task.

Attention

This also means that the workflow would only be valid if it ran with a batch number that is a multiple of three.

Setting the distribution to "non_unique" enables the same three slots to be reused by the next 3 plates from the upcoming batches.

liquid_handle = ActionTask(
    id="run_liquid_handler",
    description="Run liquid handler on plates from multiple batches",
    ...
    labware_sources=[
        previous_task.forward(
            labware=microplate,
            destination_slot=SlotFillingRule(  
                range=SlotRange(start_slot=2, end_slot=8),
                min_count=3
                max_count=3, # exactly 3 microplates must be loaded
                distribution="non_unique",
            ),
        ),
    ],
)

The min_count and max_count arguments can also enable more lax rules such as this:

SlotFillingRule(  
    range=SlotRange(start_slot=2, end_slot=8),
    min_count=1,
    max_count=3, # up to 3 microplates can be loaded
    distribution="non_unique",
),

In this case, the instrument does not require exactly three plates to be loaded, but can also be loaded with one or two plates. This allows the workflow to support batch numbers that are not multiples of three, as the last instance of the task could be loaded with only one or two plates.

class linq.task.SlotFillingRule(range: linq.labware.SlotRange | list[linq.labware.SlotRange], min_count: int | None = None, max_count: int | None = None, distribution: Literal['unique', 'non_unique'] = 'unique')
range: SlotRange | list[SlotRange]

The range of slots that the labware can be placed in.

min_count: int | None

The minimum number of labware items that must be placed in the slots.

max_count: int | None

The maximum number of labware items that can be placed in the slots.

distribution: Literal['unique', 'non_unique']

How the labware will be distributed across the slots. unique ensure each labware is placed in a unique slot. non_unique allows multiple labware to be placed in the same slot over many instances of the task

Setting a batch number

Finally, if your labware is defined with ranges in starting locations, and your task sequence forms a feasible and valid journey with slots correctly defined, you can then set the batch number as an argument of your workflow definition.

workflow = Workflow(
    workcell=workcell,
    name="Batchable Workflow",
    author="John Doe",
    description="A workflow that can be batched",
    ....
    batches=8, # set the batch number here. Deafult is 1 (i.e. no batching)
)

Although LINQ SDK does validate your workflow definition, as a user you should also ensure that your workflow design and definition enables batching correctly. Ensure that any batched labware has the required starting location range definitions and that these are supported by the instruments used. Additionally, ensure any slot filling rules have the require slot ranges and that those instrument also support that capacity. Finally, make sure the batch number you set your workflow to is feasible given your instrument capacity and defined slot ranges.

Task-Level Batch Assignment

In addition to defining batching at the workflow level using the batches argument on a Workflow, you can optionally assign a batch value directly to individual tasks.

The batch parameter is supported on:

  • ActionTask

  • CodeTask

  • Conditional

action = ActionTask(
    id="store_samples",
    batch=2,
    ...
)

code = CodeTask(
    id="process_data",
    batch=2,
    ...
)

conditional = Conditional(
    id="validate_results",
    batch=2,
    ...
)

Although LINQ SDK does validate your workflow definition, as a user you should also ensure that your workflow design and definition enables batching correctly. Ensure that any batched labware has the required starting location range definitions and that these are supported by the instruments used. Additionally, ensure any slot filling rules have the required slot ranges and that those instruments also support that capacity. Finally, make sure the batch number you set your workflow to is feasible given your instrument capacity and defined slot ranges.

Task-Level Batch Assignment

In addition to defining batching at the workflow level using the batches argument on a Workflow, you can optionally assign a batch value directly to individual tasks.

The batch parameter is supported on:

  • ActionTask

  • CodeTask

  • Conditional

action = ActionTask(
    id="store_samples",
    batch=2,
    ...
)

code = CodeTask(
    id="process_data",
    batch=2,
    ...
)

conditional = Conditional(
    id="validate_results",
    batch=2,
    ...
)

When provided, batch explicitly assigns the task to the specified batch.

Attention

Task-level batch must not be used together with workflow-level batching (for example, setting batches on the Workflow or defining slot ranges intended for multi-batch execution). A workflow must use one batching strategy exclusively.

Labware-Level Batch Assignment

Similar to task-level batching, individual labware can be assigned to specific batches using the batch parameter:

microplate_batch_1 = Labware(
    id="microplate_batch_1",
    labware_type="96_well_microplate",
    starting_location=LabwareLocation(instrument=hotel, slot=1),
    batch=1,  # Assign to first batch only
)

microplate_batch_2 = Labware(
    id="microplate_batch_2",
    labware_type="96_well_microplate",
    starting_location=LabwareLocation(instrument=hotel, slot=2),
    batch=2,  # Assign to second batch only
)

This allows explicit control over which labware is used in specific batches, useful for workflows with different labware in different batch cycles.

Attention

Labware-level batch must not be used together with workflow-level batching (for example, setting batches on the Workflow or using SlotRange for multi-batch execution). A workflow must use one batching strategy exclusively.

Reusable labware

You may not always require new labware iteration for every batch, this is typically the case with consumables like tip boxes. LINQ SDK handles reusable labware using the concept of unit consumption.

Labware definitions can include a total_units argument that defines a measure of how much capacity the labware has (default is 1). Tasks that use the labware can then contain a units_consumed argument that defines how many units that task consumes from the labware. The labware will only be replaced in the next batch when the total units have been exhausted.

Lets look at an example; consider a workflow that uses the following set of tip boxes, with total_units=3:

tips = Labware(
    id="tip_boxes",
    labware_type="tip_box_200",
    starting_location=LabwareLocation(
        instrument=hotel, 
        slot=SlotRange(start_slot=51, end_slot=100)
    ),
    total_units=3,
)

Now consider that the tip boxes are used in a liquid handling task, with the tip boxes as a labware source. The tip box source has a unit consumption of one for this task: (assume this is the only task in the workflow using the tip box)

liquid_handle = ActionTask(
    id="run_liquid_handler",
    description="Run liquid handler",
    ...
    labware_sources=[
        LabwareSource(labware=tip_boxes, destination_slot=1, units_consumed=1),
    ],
)

In this case the tip box would only be replaced after every third batch once all 3 units are consumed. While it still has units, the tip box will be left on the liquid handler to be reused in the following batch.