diff --git a/extensions/chaeo/accessors.py b/extensions/chaeo/accessors.py
index f606470e971f6c354827f732e0186c3fdadab967..82f726ba86fddf817583e349027a83b134d483e2 100644
--- a/extensions/chaeo/accessors.py
+++ b/extensions/chaeo/accessors.py
@@ -16,15 +16,25 @@ class MonoPatchStack(InMemoryDataAccessor):
             assert data.ndim == 3
             self._data = np.expand_dims(data, 2)
         elif isinstance(data, list): # list of YX patches
-            nda = np.array(data).squeeze()
-            assert nda.ndim == 3
-            self._data = np.expand_dims(
-                np.moveaxis(
-                    nda,
-                    [1, 2, 0],
-                    [0, 1, 2]),
-                2
-            )
+            if len(data) == 0:
+                self._data = np.ndarray([0, 0, 0, 0], dtype='uin9')
+            elif len(data) == 1:
+                self._data = np.expand_dims(
+                    np.array(
+                        data[0].squeeze()
+                    ),
+                    (2, 3)
+                )
+            else:
+                nda = np.array(data).squeeze()
+                assert nda.ndim == 3
+                self._data = np.expand_dims(
+                    np.moveaxis(
+                        nda,
+                        [1, 2, 0],
+                        [0, 1, 2]),
+                    2
+                )
         else:
             raise InvalidDataForPatchStackError(f'Cannot create accessor from {type(data)}')
 
diff --git a/extensions/chaeo/batch_jobs/20231028_Porto_PA.py b/extensions/chaeo/batch_jobs/20231028_Porto_PA.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4afbcdd4ecfc1c9c795c01360c00e0361f4a7f1
--- /dev/null
+++ b/extensions/chaeo/batch_jobs/20231028_Porto_PA.py
@@ -0,0 +1,56 @@
+from pathlib import Path
+
+from model_server.util import autonumber_new_directory, get_matching_files, loop_workflow
+from extensions.chaeo.ecotaxa import write_ecotaxa_tsv
+from extensions.chaeo.workflows import export_patches_from_multichannel_zstack
+from extensions.ilastik.models import IlastikPixelClassifierModel
+
+
+if __name__ == '__main__':
+    sample_id = '20231028-porto-PA'
+    root = Path('c:/Users/rhodes/projects/proj0012-trec-handoff/owncloud-sync/TREC-HD/Images/')
+    where_czi = (root / 'TREC_STOP_26_Porto/Selection').__str__()
+    where_output = autonumber_new_directory(
+        'c:/Users/rhodes/projects/proj0011-plankton-seg/exp0023/output',
+        'batch-output'
+    )
+
+    px_ilp = Path('c:/Users/rhodes/projects/proj0011-plankton-seg/exp0017/pxAF405_dim8bit.ilp').__str__()
+
+    params = {
+        'pxmap_threshold': 0.25,
+        'pxmap_foreground_channel': 0,
+        'segmentation_channel': 0,
+        'zmask_zindex': None,
+        'patches_channel': 2,
+        'zmask_type': 'boxes',
+        'zmask_filters': {'area': (1e3, 1e8)},
+        'zmask_expand_box_by': (128, 3),
+        'export_pixel_probabilities': True,
+        'export_2d_patches_for_training': True,
+        'draw_bounding_box_on_2d_patch': True,
+        'export_2d_patches_for_annotation': True,
+        'export_3d_patches': False,
+        'export_annotated_zstack': True,
+        'export_patch_masks': True,
+        'zmask_clip': 0.01,
+        'rgb_overlay_channels': (1, None, None),
+        'rgb_overlay_weights': (0.2, 1.0, 1.0),
+        'draw_label_on_zstack': True,
+    }
+
+    input_files = get_matching_files(where_czi, 'czi', coord_filter={})
+
+    loop_workflow(
+        input_files,
+        where_output,
+        export_patches_from_multichannel_zstack,
+        [IlastikPixelClassifierModel(params={'project_file': Path(px_ilp)})],
+        params,
+        catch_and_continue=False,
+    )
+
+    csv_path = (Path(where_output) / 'workflow_data.csv').__str__()
+    write_ecotaxa_tsv(csv_path, where_output, sample_id=sample_id, scope_id='EMBL-MS-Zeiss-LSM900')
+
+    print('Finished')
\ No newline at end of file
diff --git a/extensions/chaeo/products.py b/extensions/chaeo/products.py
index a0291874c0ffd2f68c012ffa8f3753debb561549..0f1ac9765728c182914d59b17d87ae6598a5271c 100644
--- a/extensions/chaeo/products.py
+++ b/extensions/chaeo/products.py
@@ -120,6 +120,7 @@ def get_patches_from_zmask_meta(
         **kwargs
 ) -> MonoPatchStack:
     patches = []
+
     for mi in zmask_meta:
 
         sl = mi['slice']
@@ -197,6 +198,7 @@ def get_patches_from_zmask_meta(
             patch = pad(patch, pad_to)
 
         patches.append(patch)
+
     if not make_3d and pc == 1:
         return MonoPatchStack(patches)
     else:
diff --git a/extensions/chaeo/workflows.py b/extensions/chaeo/workflows.py
index 57072bb835d46bf6b83dc5521a12b9693fd04018..119ee1ff2922f038ee335ca995e5ebc52f46c539 100644
--- a/extensions/chaeo/workflows.py
+++ b/extensions/chaeo/workflows.py
@@ -122,7 +122,7 @@ def export_patches_from_multichannel_zstack(
         )
         ti.click('export_pixel_probability')
 
-    if export_3d_patches:
+    if export_3d_patches and len(zmask_meta) > 0:
         files = export_patches_from_zstack(
             Path(output_folder_path) / '3d_patches',
             stack.get_one_channel_data(patches_channel),
@@ -134,7 +134,7 @@ def export_patches_from_multichannel_zstack(
         )
         ti.click('export_3d_patches')
 
-    if export_2d_patches_for_annotation:
+    if export_2d_patches_for_annotation and len(zmask_meta) > 0:
         files = export_multichannel_patches_from_zstack(
             Path(output_folder_path) / '2d_patches_annotation',
             stack,
@@ -159,7 +159,7 @@ def export_patches_from_multichannel_zstack(
         # prepopulate patch UUID
         df['patch_id'] = df.apply(lambda _: uuid4(), axis=1)
 
-    if export_2d_patches_for_training:
+    if export_2d_patches_for_training and len(zmask_meta) > 0:
         files = export_multichannel_patches_from_zstack(
             Path(output_folder_path) / '2d_patches_training',
             stack.get_one_channel_data(patches_channel),
@@ -171,7 +171,7 @@ def export_patches_from_multichannel_zstack(
         )
         ti.click('export_2d_patches')
 
-    if export_patch_masks:
+    if export_patch_masks and len(zmask_meta) > 0:
         files = export_patch_masks_from_zstack(
             Path(output_folder_path) / 'patch_masks',
             stack.get_one_channel_data(patches_channel),
@@ -195,13 +195,16 @@ def export_patches_from_multichannel_zstack(
 
     # generate multichannel projection from label centroids
     dff = df[df['keeper']]
-    interm['projected'] = project_stack_from_focal_points(
-        dff['centroid-0'].to_numpy(),
-        dff['centroid-1'].to_numpy(),
-        dff['zi'].to_numpy(),
-        stack,
-        degree=4,
-    )
+    if len(zmask_meta) > 0:
+        interm['projected'] = project_stack_from_focal_points(
+            dff['centroid-0'].to_numpy(),
+            dff['centroid-1'].to_numpy(),
+            dff['zi'].to_numpy(),
+            stack,
+            degree=4,
+        )
+    else: # else just return MIP
+        interm['projected'] = stack.data.max(axis=-1)
 
     return {
         'pixel_model_id': pixel_classifier.model_id,