diff --git a/extensions/chaeo/batch_jobs/proj0004-exp0038-fixed.py b/extensions/chaeo/batch_jobs/proj0004-exp0038-fixed.py
index 6c85309040c09637899cdd43f75846b188c7d3f9..9ad9208e1d9b8f8d31e475872db982c864c9fcc0 100644
--- a/extensions/chaeo/batch_jobs/proj0004-exp0038-fixed.py
+++ b/extensions/chaeo/batch_jobs/proj0004-exp0038-fixed.py
@@ -2,6 +2,7 @@ from pathlib import Path
 
 from model_server.util import autonumber_new_directory, get_matching_files, loop_workflow
 from extensions.chaeo.workflows import export_patches_from_multichannel_zstack
+from extensions.ilastik.models import IlastikPixelClassifierModel
 
 if __name__ == '__main__':
     where_czi = 'z:/rhodes/projects/proj0004-marine-photoactivation/data/exp0038/AutoMic/20230906-163415/Selection'
@@ -13,12 +14,11 @@ if __name__ == '__main__':
     px_ilp = Path.home() / 'model-server' / 'ilastik' / 'AF405-bodies_boundaries.ilp'
 
     params = {
-        'ilastik_project_file': px_ilp.__str__(),
         'pxmap_threshold': 0.25,
-        'pixel_class': 0,
-        'zmask_channel': 0,
+        'pxmap_foreground_channel': 0,
+        'segmentation_channel': 0,
         'patches_channel': 4,
-        'mask_type': 'boxes',
+        'zmask_type': 'boxes',
         'zmask_filters': {'area': (1e3, 1e8)},
         'zmask_expand_box_by': (128, 3),
         'export_pixel_probabilities': False,
@@ -35,7 +35,9 @@ if __name__ == '__main__':
         input_files,
         where_output,
         export_patches_from_multichannel_zstack,
+        [IlastikPixelClassifierModel(params={'project_file': Path(px_ilp)})],
         params,
+        catch_and_continue=False,
     )
 
     print('Finished')
\ No newline at end of file
diff --git a/extensions/chaeo/workflows.py b/extensions/chaeo/workflows.py
index 21880fed686bf50668d2ee805004c455c9d11870..597475f927d155e181d20f550aa76b24d43f4e35 100644
--- a/extensions/chaeo/workflows.py
+++ b/extensions/chaeo/workflows.py
@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Dict
+from typing import Dict, List
 from uuid import uuid4
 
 import numpy as np
@@ -11,22 +11,77 @@ from extensions.ilastik.models import IlastikPixelClassifierModel
 from extensions.chaeo.annotators import draw_boxes_on_3d_image
 from extensions.chaeo.products import export_patches_from_zstack, export_patch_masks_from_zstack, export_multichannel_patches_from_zstack
 from extensions.chaeo.zmask import build_zmask_from_object_mask, project_stack_from_focal_points
+from extensions.ilastik.models import IlastikPixelClassifierModel
+
 from model_server.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file
+from model_server.models import Model
 from model_server.process import rescale
 from model_server.workflows import Timer
 
+def get_zmask_meta(
+    input_file_path: str,
+    ilastik_pixel_classifier: IlastikPixelClassifierModel,
+    segmentation_channel: int,
+    pxmap_threshold: float,
+    pxmap_foreground_channel: int = 0,
+    zmask_zindex: int = None,
+    zmask_clip: int = None,
+    zmask_expand_box_by: int = None,
+    zmask_filters: Dict = None,
+    zmask_type: str = 'boxes',
+
+
+) -> Dict:
+    ti = Timer()
+    stack = generate_file_accessor(Path(input_file_path))
+    fstem = Path(input_file_path).stem
+    ti.click('file_input')
+
+    # MIP if no zmask z-index is given, then classify pixels
+    if isinstance(zmask_zindex, int):
+        assert 0 < zmask_zindex < stack.nz
+        zmask_data = stack.get_one_channel_data(channel=segmentation_channel).data[:, :, :, zmask_zindex]
+    else:
+        zmask_data = stack.get_one_channel_data(channel=segmentation_channel).data.max(axis=-1, keepdims=True)
+    if zmask_clip:
+        zmask_data = rescale(zmask_data, zmask_clip)
+    mip = InMemoryDataAccessor(
+        zmask_data,
+    )
+    pxmap, _ = ilastik_pixel_classifier.infer(mip)
+    ti.click('infer_pixel_probability')
+
+    obmask = InMemoryDataAccessor(
+        pxmap.data > pxmap_threshold
+    )
+    ti.click('threshold_pixel_mask')
+
+    # make zmask
+    zmask, zmask_meta, df, interm = build_zmask_from_object_mask(
+        obmask.get_one_channel_data(pxmap_foreground_channel),
+        stack.get_one_channel_data(segmentation_channel),
+        mask_type=zmask_type,
+        filters=zmask_filters,
+        expand_box_by=zmask_expand_box_by,
+    )
+    zmask_acc = InMemoryDataAccessor(zmask)
+    ti.click('generate_zmasks')
+
+    return ti, stack, fstem, pxmap, zmask, zmask_meta, df, interm
+
+
 # TODO: unpack and validate inputs
 def export_patches_from_multichannel_zstack(
         input_file_path: str,
         output_folder_path: str,
-        ilastik_project_file: str,
+        models: List[Model],
         pxmap_threshold: float,
-        pixel_class: int,
-        zmask_channel: int,
+        pxmap_foreground_channel: int,
+        segmentation_channel: int,
         patches_channel: int,
         zmask_zindex: int = None,  # None for MIP,
-        rescale_zmask_clip: int = None,
-        mask_type: str = 'boxes',
+        zmask_clip: int = None,
+        zmask_type: str = 'boxes',
         zmask_filters: Dict = None,
         zmask_expand_box_by: int = None,
         export_pixel_probabilities=True,
@@ -41,27 +96,20 @@ def export_patches_from_multichannel_zstack(
         rgb_overlay_channels=(None, None, None),
         rgb_overlay_weights=(1.0, 1.0, 1.0),
 ) -> Dict:
-    ti = Timer()
-    stack = generate_file_accessor(Path(input_file_path))
-    fstem = Path(input_file_path).stem
-    ti.click('file_input')
+    pixel_classifier = models[0]
 
-    # MIP and classify pixels
-    if isinstance(zmask_zindex, int):
-        assert 0 < zmask_zindex < stack.nz
-        zmask_data = stack.get_one_channel_data(channel=zmask_channel).data[:, :, :, zmask_zindex]
-    else:
-        zmask_data = stack.get_one_channel_data(channel=zmask_channel).data.max(axis=-1, keepdims=True)
-    if rescale_zmask_clip:
-        zmask_data = rescale(zmask_data, rescale_zmask_clip)
-    mip = InMemoryDataAccessor(
-        zmask_data,
-    )
-    px_model = IlastikPixelClassifierModel(
-        params={'project_file': Path(ilastik_project_file)}
+    ti, stack, fstem, pxmap, zmask, zmask_meta, df, interm = get_zmask_meta(
+        input_file_path,
+        pixel_classifier,
+        segmentation_channel,
+        pxmap_threshold,
+        pxmap_foreground_channel=pxmap_foreground_channel,
+        zmask_zindex=zmask_zindex,
+        zmask_clip=zmask_clip,
+        zmask_expand_box_by=zmask_expand_box_by,
+        zmask_filters=zmask_filters,
+        zmask_type=zmask_type,
     )
-    pxmap, _ = px_model.infer(mip)
-    ti.click('infer_pixel_probability')
 
     if export_pixel_probabilities:
         write_accessor_data_to_file(
@@ -70,22 +118,6 @@ def export_patches_from_multichannel_zstack(
         )
         ti.click('export_pixel_probability')
 
-    obmask = InMemoryDataAccessor(
-        pxmap.data > pxmap_threshold
-    )
-    ti.click('threshold_pixel_mask')
-
-    # make zmask
-    zmask, zmask_meta, df, interm = build_zmask_from_object_mask(
-        obmask.get_one_channel_data(pixel_class),
-        stack.get_one_channel_data(zmask_channel),
-        mask_type=mask_type,
-        filters=zmask_filters,
-        expand_box_by=zmask_expand_box_by,
-    )
-    zmask_acc = InMemoryDataAccessor(zmask)
-    ti.click('generate_zmasks')
-
     if export_3d_patches:
         files = export_patches_from_zstack(
             Path(output_folder_path) / '3d_patches',
@@ -168,7 +200,7 @@ def export_patches_from_multichannel_zstack(
     )
 
     return {
-        'pixel_model_id': px_model.model_id,
+        'pixel_model_id': pixel_classifier.model_id,
         'input_filepath': input_file_path,
         'number_of_objects': len(zmask_meta),
         'pixeL_scale_in_micrometers': stack.pixel_scale_in_micrometers,
diff --git a/extensions/chaeo/zmask.py b/extensions/chaeo/zmask.py
index cc04a7282dc9756561bbcd72cd6dd560d4c1fd60..e9967e70630820d876d49a0b21a2bbe39d2cf082 100644
--- a/extensions/chaeo/zmask.py
+++ b/extensions/chaeo/zmask.py
@@ -11,7 +11,7 @@ def build_zmask_from_object_mask(
         obmask: GenericImageDataAccessor,
         zstack: GenericImageDataAccessor,
         filters=None,
-        mask_type='contour',
+        mask_type='contours',
         expand_box_by=(0, 0),
 ):
     """
diff --git a/model_server/util.py b/model_server/util.py
index 8bc071ab056570a594658a0be4fa99eeb9c875dd..1cc33753dbe477e3ddff686f2a5459a93386b7bb 100644
--- a/model_server/util.py
+++ b/model_server/util.py
@@ -1,10 +1,12 @@
 from pathlib import Path
 import re
 from time import localtime, strftime
+from typing import List
 
 import pandas as pd
 
 from model_server.accessors import InMemoryDataAccessor, write_accessor_data_to_file
+from model_server.models import Model
 
 def autonumber_new_directory(where: str, prefix: str) -> str:
     """
@@ -75,6 +77,7 @@ def loop_workflow(
         files: list,
         output_folder_path: str,
         workflow_func: callable,
+        models: List[Model],
         params: dict,
         export_batch_csvs: bool = True,
         write_intermediate_products: bool = True,
@@ -95,6 +98,7 @@ def loop_workflow(
         export_kwargs = {
             'input_file_path': ff,
             'output_folder_path': output_folder_path,
+            'models': models,
             **params,
         }