diff --git a/extensions/chaeo/tests/test_zstack.py b/extensions/chaeo/tests/test_zstack.py
index 6c398d24b99dbe1d6874cc3b742e92a0a23cad16..3ce9596945ad844c7f8841e0c9ee5808485f35cb 100644
--- a/extensions/chaeo/tests/test_zstack.py
+++ b/extensions/chaeo/tests/test_zstack.py
@@ -27,7 +27,7 @@ class TestZStackDerivedDataProducts(unittest.TestCase):
         # write_accessor_data_to_file(output_path / 'obmap.tif', self.obmap)
 
     def test_zmask_makes_correct_boxes(self, mask_type='boxes', **kwargs):
-        zmask, meta = build_zmask_from_object_mask(
+        zmask, meta, df = build_zmask_from_object_mask(
             self.obmap.get_one_channel_data(0),
             self.stack.get_one_channel_data(0),
             mask_type=mask_type,
diff --git a/extensions/chaeo/workflows.py b/extensions/chaeo/workflows.py
index 9e836713cc380fce243fc8dfe4930010e9141744..997826eee276f2e8c66895903a023a0ba0ef9754 100644
--- a/extensions/chaeo/workflows.py
+++ b/extensions/chaeo/workflows.py
@@ -11,7 +11,7 @@ from extensions.chaeo.zmask import build_zmask_from_object_mask
 from model_server.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file
 from model_server.workflows import Timer
 
-def export_patches_from_multichannel_zstack(  # TODO: PyDantic model for arguments
+def export_patches_from_multichannel_zstack(
         input_zstack_path: Path,
         px_model: IlastikPixelClassifierModel,
         pxmap_threshold: float,
@@ -49,7 +49,7 @@ def export_patches_from_multichannel_zstack(  # TODO: PyDantic model for argumen
     ti.click('threshold_pixel_mask')
 
     # make zmask
-    zmask, zmask_meta = build_zmask_from_object_mask(
+    zmask, zmask_meta, df = build_zmask_from_object_mask(
         obmask.get_one_channel_data(pixel_class),
         stack.get_one_channel_data(zmask_channel),
         mask_type=mask_type,
@@ -88,4 +88,5 @@ def export_patches_from_multichannel_zstack(  # TODO: PyDantic model for argumen
         'number_of_objects': len(zmask_meta),
         'success': True,
         'timer_results': ti.events,
+        'dataframe': df,
     }
\ No newline at end of file
diff --git a/extensions/chaeo/zmask.py b/extensions/chaeo/zmask.py
index 19c1db5780027be8e734f9403d326158c436e816..8304b836f9ce57c6bc75545f72a35f8d24bbc795 100644
--- a/extensions/chaeo/zmask.py
+++ b/extensions/chaeo/zmask.py
@@ -21,13 +21,15 @@ def build_zmask_from_object_mask(
     :param mask_type: if 'boxes', zmask is True in each object's complete bounding box; otherwise 'contours'
     :param expand_box_by: (xy, z) expands bounding box by (xy, z) pixels except where this hits a boundary
     :return: tuple (zmask, meta)
-        np.ndarray zmask: boolean mask of same size as stack
-        meta: List containing one Dict per object, with keys:
+        np.ndarray:
+            boolean mask of same size as stack
+        List containing one Dict per object, with keys:
             info: object's properties from skimage.measure.regionprops_table, including bounding box (y0, y1, x0, x1)
             slice: named slice (np.s_) of (optionally) expanded bounding box
             relative_bounding_box: bounding box (y0, y1, x0, x1) in relative frame of (optionally) expanded bounding box
             contour: object's contour returned by skimage.measure.find_contours
             mask: mask of object in relative frame of (optionally) expanded bounding box
+        pd.DataFrame: objects, including bounding, box information after filtering
     """
 
     # validate inputs
@@ -131,4 +133,4 @@ def build_zmask_from_object_mask(
             sl = bb['slice']
             zi_st[sl] = 1
 
-    return zi_st, meta
\ No newline at end of file
+    return zi_st, meta, df
\ No newline at end of file
diff --git a/extensions/ilastik/examples/batch_run_patches.py b/extensions/ilastik/examples/batch_run_patches.py
index c985d818215d7497ea3766bd184b7ba49db2a07a..5b67bb3dd70e6cfdda2bc980c59481709c862aea 100644
--- a/extensions/ilastik/examples/batch_run_patches.py
+++ b/extensions/ilastik/examples/batch_run_patches.py
@@ -1,5 +1,7 @@
 from pathlib import Path
 
+import pandas as pd
+
 from extensions.chaeo.workflows import export_patches_from_multichannel_zstack
 from extensions.ilastik.models import IlastikPixelClassifierModel
 
@@ -10,16 +12,18 @@ if __name__ == '__main__':
     where_output = Path(
         'c:/Users/rhodes/projects/proj0011-plankton-seg/exp0009/batch_output'
     )
+    csv_args = {'mode': 'w', 'header': True} # when creating file
     px_ilp = Path.home() / 'model-server' / 'ilastik' / 'AF405-bodies_boundaries.ilp'
     px_model = IlastikPixelClassifierModel(
         params={'project_file': px_ilp}
     )
+
     for ff in where_czi.iterdir():
         print(ff)
         if not ff.suffix.upper() == '.CZI':
             continue
 
-        export_patches_from_multichannel_zstack(
+        result = export_patches_from_multichannel_zstack(
             input_zstack_path=where_czi/ff,
             px_model=px_model,
             pxmap_threshold=0.6,
@@ -32,3 +36,13 @@ if __name__ == '__main__':
             zmask_expand_box_by=(64, 3),
         )
 
+        # parse and record results
+        df = result['dataframe']
+        df['filename'] = ff.name
+        df.to_csv(where_output / 'df_objects.csv', **csv_args)
+        pd.DataFrame(result['timer_results'], index=[0]).to_csv(where_output / 'timer_results.csv', **csv_args)
+        csv_args = {'mode': 'a', 'header': False} # append to CSV from here on
+
+
+
+