diff --git a/extensions/chaeo/batch_jobs/20231023_Porto_4ch.py b/extensions/chaeo/batch_jobs/20231023_Porto_4ch.py
new file mode 100644
index 0000000000000000000000000000000000000000..c252fd8fa748c407919518bcaca6cd444c59593f
--- /dev/null
+++ b/extensions/chaeo/batch_jobs/20231023_Porto_4ch.py
@@ -0,0 +1,76 @@
+from pathlib import Path
+
+import czifile
+
+from model_server.accessors import write_accessor_data_to_file
+from model_server.czi_util import get_accessor_from_multiposition_czi
+from model_server.util import autonumber_new_directory, get_matching_files, loop_workflow
+from extensions.chaeo.ecotaxa import write_ecotaxa_tsv_chunked_subdirectories
+from extensions.chaeo.workflows import export_patches_from_multichannel_zstack
+from extensions.ilastik.models import IlastikPixelClassifierModel
+
+
+if __name__ == '__main__':
+    sample_id = '20231023-porto-4ch'
+    root = Path('y:/TREC_STOP_26_Porto/MobileLab/LSM900/231023')
+    czi_filepath = root / 'PK2_LM_BioO5D_231023_am_bottle_8_5_1_2_7.czi'
+    where_output = autonumber_new_directory(
+        'c:/Users/rhodes/projects/proj0017-ehcfm-analysis/exp0001/output',
+        'batch-output'
+    )
+    cf = czifile.CziFile(czi_filepath.__str__())
+
+    # write single-position TIF color stacks
+    where_proc = Path(where_output) / 'proc'
+    for pos in range(0, cf.shape[cf.axes.index('S')]):
+        fname = f'{czi_filepath.stem}_p{pos:02d}.tif'
+        write_accessor_data_to_file(
+            where_proc / fname,
+            get_accessor_from_multiposition_czi(cf, pos)
+        )
+        print(f'Wrote file {fname}')
+
+    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_proc, 'tif', 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,
+        chunk_size=25,
+    )
+
+    write_ecotaxa_tsv_chunked_subdirectories(
+        where_output,
+        'workflow_data.csv',
+        sample_id=sample_id,
+        scope_id='EMBL-MS-Zeiss-LSM900'
+    )
+
+    print('Finished')
\ No newline at end of file