Repository: fractal-analytics-platform/fractal-tasks-core (language: python)

Issue:
Order of task arguments in the schema
We should go back to order the arguments as they appear in the function, not alphabetically. Currently, the schema has them ordered alphabetically, which leads to a web interface that orders them the same (& is not a clean user-experience)

<img width="1519" alt="Screenshot 2024-04-11 at 13 38 03" src="https://github.com/fractal-analytics-platform/fractal-web/assets/18033446/07b31177-0cf2-4bd6-82b7-b9ad24ea95c1">

Review NGFF models & Fractal OME-Zarr models
Within #671, it appears that the MIP-images zattrs do not include `wavelength_id` in the `omero` metadata.

Here is how this leads to a cellpose-task error:
```python traceback
TASK ERROR: Task name: Cellpose Segmentation, position in Workflow: 2
TRACEBACK:
2024-04-09 10:02:11,120; INFO; START cellpose_segmentation task
2024-04-09 10:02:11,120; INFO; zarr_url='/somewhere/Fractal/fractal-demos/examples/01_cardio_tiny_dataset/output_cardiac-tiny-c/20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/0'
2024-04-09 10:02:11,133; INFO; NGFF image has num_levels=5
2024-04-09 10:02:11,133; INFO; NGFF image has coarsening_xy=2
2024-04-09 10:02:11,133; INFO; NGFF image has full-res pixel sizes [1.0, 0.1625, 0.1625]
2024-04-09 10:02:11,133; INFO; NGFF image has level-2 pixel sizes [1.0, 0.65, 0.65]
2024-04-09 10:02:11,133; CRITICAL; {'color': '00FFFF', 'label': 'DAPI', 'window': {'end': 800.0, 'max': 65535.0, 'min': 0.0, 'start': 110.0}}
Traceback (most recent call last):
  File "/somewhere/Fractal/fractal-server/tests/data/example_server_startup/Tasks/.fractal/fractal-tasks-core1.0.0a0/venv/lib/python3.10/site-packages/fractal_tasks_core/tasks/cellpose_segmentation.py", line 719, in <module>
    run_fractal_task(
  File "/somewhere/Fractal/fractal-server/tests/data/example_server_startup/Tasks/.fractal/fractal-tasks-core1.0.0a0/venv/lib/python3.10/site-packages/fractal_tasks_core/tasks/_utils.py", line 79, in run_fractal_task
    metadata_update = task_function(**pars)
  File "pydantic/decorator.py", line 40, in pydantic.decorator.validate_arguments.validate.wrapper_function
  File "pydantic/decorator.py", line 134, in pydantic.decorator.ValidatedFunction.call
  File "pydantic/decorator.py", line 206, in pydantic.decorator.ValidatedFunction.execute
  File "/somewhere/Fractal/fractal-server/tests/data/example_server_startup/Tasks/.fractal/fractal-tasks-core1.0.0a0/venv/lib/python3.10/site-packages/fractal_tasks_core/tasks/cellpose_segmentation.py", line 342, in cellpose_segmentation
    tmp_channel: OmeroChannel = get_channel_from_image_zarr(
  File "/somewhere/Fractal/fractal-server/tests/data/example_server_startup/Tasks/.fractal/fractal-tasks-core1.0.0a0/venv/lib/python3.10/site-packages/fractal_tasks_core/channels.py", line 221, in get_channel_from_image_zarr
    omero_channels = get_omero_channel_list(image_zarr_path=image_zarr_path)
  File "/somewhere/Fractal/fractal-server/tests/data/example_server_startup/Tasks/.fractal/fractal-tasks-core1.0.0a0/venv/lib/python3.10/site-packages/fractal_tasks_core/channels.py", line 243, in get_omero_channel_list
    channels = [OmeroChannel(**c) for c in channels_dicts]
  File "/somewhere/Fractal/fractal-server/tests/data/example_server_startup/Tasks/.fractal/fractal-tasks-core1.0.0a0/venv/lib/python3.10/site-packages/fractal_tasks_core/channels.py", line 243, in <listcomp>
    channels = [OmeroChannel(**c) for c in channels_dicts]
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for OmeroChannel
wavelength_id
  field required (type=value_error.missing)
```
`zenodo_images_multiplex_shifted` fixture fails with Pillow error
See https://productionresultssa6.blob.core.windows.net/actions-results/7fba96e7-3c3c-4b40-a726-d48a32af5f1d/workflow-job-run-4c309e78-7174-5e1c-e1d6-a7c065a0787a/logs/job/job-logs.txt?rsct=text%2Fplain&se=2024-04-02T10%3A22%3A56Z&sig=hJTfwP2lSuZQCK8ICQaUIAgJeNxbrvZt7XdPNDLsR70%3D&sp=r&spr=https&sr=b&st=2024-04-02T10%3A12%3A51Z&sv=2021-12-02.

This was part of the PR #673, but I think the same would happen in `main` - since we are not pinning Pillow to any specific version.

The first guess here is that this is due to pillow 10.3.0, released yesterday (April 1st).

```
2024-04-02T09:18:52.0051216Z _______________ ERROR at setup of test_multiplexing_registration _______________
2024-04-02T09:18:52.0052042Z 
2024-04-02T09:18:52.0054472Z zenodo_images_multiplex = ['/home/runner/work/fractal-tasks-core/fractal-tasks-core/tests/data/fake_multiplex/cycle1', '/home/runner/work/fractal-tasks-core/fractal-tasks-core/tests/data/fake_multiplex/cycle2']
2024-04-02T09:18:52.0056293Z testdata_path = PosixPath('/home/runner/work/fractal-tasks-core/fractal-tasks-core/tests/data')
2024-04-02T09:18:52.0056868Z 
2024-04-02T09:18:52.0057008Z     @pytest.fixture(scope="function")
2024-04-02T09:18:52.0057413Z     def zenodo_images_multiplex_shifted(
2024-04-02T09:18:52.0057831Z         zenodo_images_multiplex: list[str],
2024-04-02T09:18:52.0058225Z         testdata_path: Path,
2024-04-02T09:18:52.0058566Z     ) -> list[str]:
2024-04-02T09:18:52.0058818Z         """
2024-04-02T09:18:52.0059080Z         Return a list of strings, like
2024-04-02T09:18:52.0059437Z         ```
2024-04-02T09:18:52.0059662Z         [
2024-04-02T09:18:52.0059957Z             "/some/path/fake_multiplex_shifted/cycle1",
2024-04-02T09:18:52.0060415Z             "/some/path/fake_multiplex_shifted/cycle2"
2024-04-02T09:18:52.0060810Z         ]
2024-04-02T09:18:52.0061032Z         ```
2024-04-02T09:18:52.0061253Z         """
2024-04-02T09:18:52.0061510Z         # Define old and new folders
2024-04-02T09:18:52.0061943Z         old_folder = str(testdata_path / "fake_multiplex")
2024-04-02T09:18:52.0062573Z         new_folder = old_folder.replace("fake_multiplex", "fake_multiplex_shifted")
2024-04-02T09:18:52.0063110Z     
2024-04-02T09:18:52.0063432Z         # Define output folders (one per multiplexing cycle)
2024-04-02T09:18:52.0064013Z         cycle_folders = [f"{new_folder}/cycle{ind}" for ind in (1, 2)]
2024-04-02T09:18:52.0064475Z     
2024-04-02T09:18:52.0064725Z         if os.path.isdir(new_folder):
2024-04-02T09:18:52.0065498Z             # If the shifted-images folder already exists, return immediately
2024-04-02T09:18:52.0066008Z             print(f"{new_folder} already exists")
2024-04-02T09:18:52.0066376Z             return cycle_folders
2024-04-02T09:18:52.0066660Z         else:
2024-04-02T09:18:52.0067228Z             # Copy the fake_multiplex folder into a new one
2024-04-02T09:18:52.0067991Z             shutil.copytree(old_folder, new_folder)
2024-04-02T09:18:52.0068712Z             # Loop over images of cycle2 and apply a shift
2024-04-02T09:18:52.0069541Z             for img_path in glob.glob(f"{cycle_folders[1]}/2020*.png"):
2024-04-02T09:18:52.0070317Z                 print(f"Now shifting {img_path}")
2024-04-02T09:18:52.0070950Z >               _shift_image(str(img_path))
2024-04-02T09:18:52.0071378Z 
2024-04-02T09:18:52.0071585Z tests/tasks/test_registration.py:123: 
2024-04-02T09:18:52.0072321Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
2024-04-02T09:18:52.0073168Z tests/tasks/test_registration.py:88: in _shift_image
2024-04-02T09:18:52.0073920Z     new_img = Image.fromarray(new_array, mode="I")
2024-04-02T09:18:52.0074903Z .venv/lib/python3.10/site-packages/PIL/Image.py:3154: in fromarray
2024-04-02T09:18:52.0075699Z     return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
2024-04-02T09:18:52.0076768Z .venv/lib/python3.10/site-packages/PIL/Image.py:3069: in frombuffer
2024-04-02T09:18:52.0077688Z     return frombytes(mode, size, data, decoder_name, args)
2024-04-02T09:18:52.0078691Z .venv/lib/python3.10/site-packages/PIL/Image.py:3012: in frombytes
2024-04-02T09:18:52.0079498Z     im.frombytes(data, decoder_name, args)
2024-04-02T09:18:52.0080246Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
2024-04-02T09:18:52.0081072Z 
2024-04-02T09:18:52.0081462Z self = <PIL.Image.Image image mode=I size=2560x2160 at 0x7F7221DFDE70>
2024-04-02T09:18:52.0082302Z data = array([[103, 106, 112, ..., 110, 110, 110],
2024-04-02T09:18:52.0082934Z        [105, 100, 104, ..., 110, 110, 110],
2024-04-02T09:18:52.0083535Z        [108, 103,  98, ..., 11... 110, 110, 110],
2024-04-02T09:18:52.0084121Z        [110, 110, 110, ..., 110, 110, 110],
2024-04-02T09:18:52.0084757Z        [110, 110, 110, ..., 110, 110, 110]], dtype=uint16)
2024-04-02T09:18:52.0085498Z decoder_name = 'raw', args = ('I', 0, 1)
2024-04-02T09:18:52.0086245Z d = <ImagingDecoder object at 0x7f7221e7d7d0>, s = (11059200, 0)
2024-04-02T09:18:52.0087044Z msg = 'not enough image data'
2024-04-02T09:18:52.0087382Z 
2024-04-02T09:18:52.0087937Z     def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None:
2024-04-02T09:18:52.0088749Z         """
2024-04-02T09:18:52.0089258Z         Loads this image with pixel data from a bytes object.
2024-04-02T09:18:52.0089921Z     
2024-04-02T09:18:52.0090549Z         This method is similar to the :py:func:`~PIL.Image.frombytes` function,
2024-04-02T09:18:52.0091631Z         but loads data into this image instead of creating a new image object.
2024-04-02T09:18:52.0092428Z         """
2024-04-02T09:18:52.0092770Z     
2024-04-02T09:18:52.0093193Z         if self.width == 0 or self.height == 0:
2024-04-02T09:18:52.0093778Z             return
2024-04-02T09:18:52.0094137Z     
2024-04-02T09:18:52.0094551Z         # may pass tuple instead of argument list
2024-04-02T09:18:52.0095324Z         if len(args) == 1 and isinstance(args[0], tuple):
2024-04-02T09:18:52.0095972Z             args = args[0]
2024-04-02T09:18:52.0096394Z     
2024-04-02T09:18:52.0096726Z         # default format
2024-04-02T09:18:52.0097238Z         if decoder_name == "raw" and args == ():
2024-04-02T09:18:52.0097839Z             args = self.mode
2024-04-02T09:18:52.0098277Z     
2024-04-02T09:18:52.0098612Z         # unpack data
2024-04-02T09:18:52.0099129Z         d = _getdecoder(self.mode, decoder_name, args)
2024-04-02T09:18:52.0099764Z         d.setimage(self.im)
2024-04-02T09:18:52.0100229Z         s = d.decode(data)
2024-04-02T09:18:52.0100654Z     
2024-04-02T09:18:52.0100977Z         if s[0] >= 0:
2024-04-02T09:18:52.0101618Z             msg = "not enough image data"
2024-04-02T09:18:52.0102203Z >           raise ValueError(msg)
2024-04-02T09:18:52.0102774Z E           ValueError: not enough image data
2024-04-02T09:18:52.0103214Z 
2024-04-02T09:18:52.0103695Z .venv/lib/python3.10/site-packages/PIL/Image.py:830: ValueError
```
Order of task arguments in the schema
We should go back to order the arguments as they appear in the function, not alphabetically. Currently, the schema has them ordered alphabetically, which leads to a web interface that orders them the same (& is not a clean user-experience)

<img width="1519" alt="Screenshot 2024-04-11 at 13 38 03" src="https://github.com/fractal-analytics-platform/fractal-web/assets/18033446/07b31177-0cf2-4bd6-82b7-b9ad24ea95c1">

Minor changes to v2 branch
I'm having this as a small separate PR into `V2_tasks`, so that it's easier to review its changes.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci_poetry.yml b/.github/workflows/ci_poetry.yml
index 4de1408f2..6ab65aa79 100644
--- a/.github/workflows/ci_poetry.yml
+++ b/.github/workflows/ci_poetry.yml
@@ -46,7 +46,7 @@ jobs:
         run: poetry run coverage run -m pytest tests --ignore tests/tasks
 
       - name: Upload coverage data
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v3
         with:
           name: coverage-data
           path: ".coverage*"
@@ -99,7 +99,7 @@ jobs:
         run: poetry run coverage run -m pytest tests/tasks -s --log-cli-level info
 
       - name: Upload coverage data
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v3
         with:
           name: coverage-data
           path: ".coverage*"
diff --git a/examples/01_cardio_tiny_dataset/run_workflow.py b/examples/01_cardio_tiny_dataset/run_workflow.py
index 9e808792c..70496373b 100644
--- a/examples/01_cardio_tiny_dataset/run_workflow.py
+++ b/examples/01_cardio_tiny_dataset/run_workflow.py
@@ -5,6 +5,7 @@
 Original authors:
 Marco Franzon <marco.franzon@exact-lab.it>
 Tommaso Comparin <tommaso.comparin@exact-lab.it>
+Joel Lüthi <joel.luethi@uzh.ch>
 
 This file is part of Fractal and was originally developed by eXact lab S.r.l.
 <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
@@ -18,8 +19,12 @@
 
 from fractal_tasks_core.channels import OmeroChannel
 from fractal_tasks_core.channels import Window
-from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
-from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_compute import (
+    cellvoyager_to_ome_zarr_compute,
+)
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init import (
+    cellvoyager_to_ome_zarr_init,
+)
 
 
 allowed_channels = [
@@ -53,28 +58,26 @@
         f"{Path(img_path).parent} is missing,"
         " try running ./fetch_test_data_from_zenodo.sh"
     )
-zarr_path = "tmp_out/"
-metadata: dict = {}
+zarr_dir = "tmp_out"
 
 # Create zarr structure
-metadata_update = create_ome_zarr(
-    input_paths=[img_path],
-    output_path=zarr_path,
+parallelization_list = cellvoyager_to_ome_zarr_init(
+    zarr_urls=[],
+    zarr_dir=zarr_dir,
+    image_dirs=[img_path],
     allowed_channels=allowed_channels,
     image_extension="png",
-    metadata=metadata,
     num_levels=num_levels,
     coarsening_xy=coarsening_xy,
+    overwrite=True,
 )
-metadata.update(metadata_update)
-debug(metadata)
+debug(parallelization_list)
 
-# Yokogawa to zarr
-for component in metadata["image"]:
-    yokogawa_to_ome_zarr(
-        input_paths=[zarr_path],
-        output_path=zarr_path,
-        metadata=metadata,
-        component=component,
-    )
-debug(metadata)
+image_list_updates = []
+# # Yokogawa to zarr
+for image in parallelization_list["parallelization_list"]:
+    image_list_updates += cellvoyager_to_ome_zarr_compute(
+        zarr_url=image["zarr_url"],
+        init_args=image["init_args"],
+    )["image_list_updates"]
+debug(image_list_updates)
diff --git a/examples/02_cardio_tiny_mip/run_workflow.py b/examples/02_cardio_tiny_mip/run_workflow.py
new file mode 100644
index 000000000..c54a0d949
--- /dev/null
+++ b/examples/02_cardio_tiny_mip/run_workflow.py
@@ -0,0 +1,105 @@
+"""
+Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+University of Zurich
+
+Original authors:
+Marco Franzon <marco.franzon@exact-lab.it>
+Tommaso Comparin <tommaso.comparin@exact-lab.it>
+Joel Lüthi <joel.luethi@uzh.ch>
+
+This file is part of Fractal and was originally developed by eXact lab S.r.l.
+<exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+Institute for Biomedical Research and Pelkmans Lab from the University of
+Zurich.
+"""
+import os
+from pathlib import Path
+
+from devtools import debug
+
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_compute import (
+    cellvoyager_to_ome_zarr_compute,
+)
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init import (
+    cellvoyager_to_ome_zarr_init,
+)
+from fractal_tasks_core.tasks.copy_ome_zarr_hcs_plate import (
+    copy_ome_zarr_hcs_plate,
+)
+from fractal_tasks_core.tasks.maximum_intensity_projection import (
+    maximum_intensity_projection,
+)
+
+allowed_channels = [
+    OmeroChannel(
+        label="DAPI",
+        wavelength_id="A01_C01",
+        color="00FFFF",
+        window=Window(start=0, end=700),
+    ),
+    OmeroChannel(
+        wavelength_id="A01_C02",
+        label="nanog",
+        color="FF00FF",
+        window=Window(start=0, end=180),
+    ),
+    OmeroChannel(
+        wavelength_id="A02_C03",
+        label="Lamin B1",
+        color="FFFF00",
+        window=Window(start=0, end=1500),
+    ),
+]
+num_levels = 2
+coarsening_xy = 2
+
+
+# Init
+img_path = "../images/10.5281_zenodo.7059515/"
+if not os.path.isdir(Path(img_path).parent):
+    raise FileNotFoundError(
+        f"{Path(img_path).parent} is missing,"
+        " try running ./fetch_test_data_from_zenodo.sh"
+    )
+zarr_dir = "tmp_out"
+
+# Create zarr structure
+parallelization_list = cellvoyager_to_ome_zarr_init(
+    zarr_urls=[],
+    zarr_dir=zarr_dir,
+    image_dirs=[img_path],
+    allowed_channels=allowed_channels,
+    image_extension="png",
+    num_levels=num_levels,
+    coarsening_xy=coarsening_xy,
+    overwrite=True,
+)
+debug(parallelization_list)
+
+image_list_updates = []
+# Convert image to Zarr
+for image in parallelization_list["parallelization_list"]:
+    image_list_updates += cellvoyager_to_ome_zarr_compute(
+        zarr_url=image["zarr_url"],
+        init_args=image["init_args"],
+    )["image_list_updates"]
+debug(image_list_updates)
+
+zarr_urls = [
+    "tmp_out/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr/B/03/0/"
+]
+
+# Create an MIP
+parallelization_list = copy_ome_zarr_hcs_plate(
+    zarr_urls=zarr_urls, zarr_dir="tmp_out", overwrite=True
+)
+debug(parallelization_list)
+
+for image in parallelization_list["parallelization_list"]:
+    maximum_intensity_projection(
+        zarr_url=image["zarr_url"],
+        init_args=image["init_args"],
+        # overwrite=True,
+    )
diff --git a/examples/02_cardio_tiny_dataset_sparse/.gitignore b/examples/old/_02_cardio_tiny_dataset_sparse/.gitignore
similarity index 100%
rename from examples/02_cardio_tiny_dataset_sparse/.gitignore
rename to examples/old/_02_cardio_tiny_dataset_sparse/.gitignore
diff --git a/examples/02_cardio_tiny_dataset_sparse/run_workflow.py b/examples/old/_02_cardio_tiny_dataset_sparse/run_workflow.py
similarity index 100%
rename from examples/02_cardio_tiny_dataset_sparse/run_workflow.py
rename to examples/old/_02_cardio_tiny_dataset_sparse/run_workflow.py
diff --git a/examples/03_cardio_tiny_dataset_with_illumination_correction/.gitignore b/examples/old/_03_cardio_tiny_dataset_with_illumination_correction/.gitignore
similarity index 100%
rename from examples/03_cardio_tiny_dataset_with_illumination_correction/.gitignore
rename to examples/old/_03_cardio_tiny_dataset_with_illumination_correction/.gitignore
diff --git a/examples/03_cardio_tiny_dataset_with_illumination_correction/parameters/illum_corr_matrix.png b/examples/old/_03_cardio_tiny_dataset_with_illumination_correction/parameters/illum_corr_matrix.png
similarity index 100%
rename from examples/03_cardio_tiny_dataset_with_illumination_correction/parameters/illum_corr_matrix.png
rename to examples/old/_03_cardio_tiny_dataset_with_illumination_correction/parameters/illum_corr_matrix.png
diff --git a/examples/03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py b/examples/old/_03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py
similarity index 100%
rename from examples/03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py
rename to examples/old/_03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/.gitignore b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/.gitignore
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/.gitignore
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/.gitignore
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/README.md b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/README.md
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/README.md
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/README.md
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/inspect_workflow.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/inspect_workflow.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/inspect_workflow.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/inspect_workflow.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_1.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_1.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_1.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_1.yaml
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_2.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_2.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_2.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_2.yaml
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_3.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_3.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_3.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_3.yaml
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_4.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_4.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_4.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_4.yaml
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_5.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_5.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_5.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_5.yaml
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_6.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_6.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_6.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_6.yaml
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/wf_7.yaml b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_7.yaml
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/wf_7.yaml
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/wf_7.yaml
diff --git a/examples/05_cardio_tiny_dataset_cellpose/.gitignore b/examples/old/_05_cardio_tiny_dataset_cellpose/.gitignore
similarity index 100%
rename from examples/05_cardio_tiny_dataset_cellpose/.gitignore
rename to examples/old/_05_cardio_tiny_dataset_cellpose/.gitignore
diff --git a/examples/05_cardio_tiny_dataset_cellpose/cellpose_example.submit b/examples/old/_05_cardio_tiny_dataset_cellpose/cellpose_example.submit
similarity index 100%
rename from examples/05_cardio_tiny_dataset_cellpose/cellpose_example.submit
rename to examples/old/_05_cardio_tiny_dataset_cellpose/cellpose_example.submit
diff --git a/examples/05_cardio_tiny_dataset_cellpose/run_example_cellpose.py b/examples/old/_05_cardio_tiny_dataset_cellpose/run_example_cellpose.py
similarity index 100%
rename from examples/05_cardio_tiny_dataset_cellpose/run_example_cellpose.py
rename to examples/old/_05_cardio_tiny_dataset_cellpose/run_example_cellpose.py
diff --git a/examples/06_cellpose_dev_example/run_example_cellpose_dual_channel.py b/examples/old/_06_cellpose_dev_example/run_example_cellpose_dual_channel.py
similarity index 100%
rename from examples/06_cellpose_dev_example/run_example_cellpose_dual_channel.py
rename to examples/old/_06_cellpose_dev_example/run_example_cellpose_dual_channel.py
diff --git a/examples/06_cellpose_dev_example/run_example_cellpose_normalization.py b/examples/old/_06_cellpose_dev_example/run_example_cellpose_normalization.py
similarity index 100%
rename from examples/06_cellpose_dev_example/run_example_cellpose_normalization.py
rename to examples/old/_06_cellpose_dev_example/run_example_cellpose_normalization.py
diff --git a/examples/07_use_task_from_command_line/.gitignore b/examples/old/_07_use_task_from_command_line/.gitignore
similarity index 100%
rename from examples/07_use_task_from_command_line/.gitignore
rename to examples/old/_07_use_task_from_command_line/.gitignore
diff --git a/examples/07_use_task_from_command_line/README.md b/examples/old/_07_use_task_from_command_line/README.md
similarity index 100%
rename from examples/07_use_task_from_command_line/README.md
rename to examples/old/_07_use_task_from_command_line/README.md
diff --git a/examples/07_use_task_from_command_line/cellpose.json b/examples/old/_07_use_task_from_command_line/cellpose.json
similarity index 100%
rename from examples/07_use_task_from_command_line/cellpose.json
rename to examples/old/_07_use_task_from_command_line/cellpose.json
diff --git a/examples/07_use_task_from_command_line/copy_ome_zarr_structure.args.json b/examples/old/_07_use_task_from_command_line/copy_ome_zarr_structure.args.json
similarity index 100%
rename from examples/07_use_task_from_command_line/copy_ome_zarr_structure.args.json
rename to examples/old/_07_use_task_from_command_line/copy_ome_zarr_structure.args.json
diff --git a/examples/07_use_task_from_command_line/create_ome_zarr.args.json b/examples/old/_07_use_task_from_command_line/create_ome_zarr.args.json
similarity index 100%
rename from examples/07_use_task_from_command_line/create_ome_zarr.args.json
rename to examples/old/_07_use_task_from_command_line/create_ome_zarr.args.json
diff --git a/examples/07_use_task_from_command_line/maximum_intensity_projection_B03_0.args.json b/examples/old/_07_use_task_from_command_line/maximum_intensity_projection_B03_0.args.json
similarity index 100%
rename from examples/07_use_task_from_command_line/maximum_intensity_projection_B03_0.args.json
rename to examples/old/_07_use_task_from_command_line/maximum_intensity_projection_B03_0.args.json
diff --git a/examples/07_use_task_from_command_line/yokogawa_to_ome_zarr_B03_0.args.json b/examples/old/_07_use_task_from_command_line/yokogawa_to_ome_zarr_B03_0.args.json
similarity index 100%
rename from examples/07_use_task_from_command_line/yokogawa_to_ome_zarr_B03_0.args.json
rename to examples/old/_07_use_task_from_command_line/yokogawa_to_ome_zarr_B03_0.args.json
diff --git a/examples/08_subset_of_data/run_example_single_channel.py b/examples/old/_08_subset_of_data/run_example_single_channel.py
similarity index 100%
rename from examples/08_subset_of_data/run_example_single_channel.py
rename to examples/old/_08_subset_of_data/run_example_single_channel.py
diff --git a/examples/08_subset_of_data/run_example_single_channel_few_Z_planes.py b/examples/old/_08_subset_of_data/run_example_single_channel_few_Z_planes.py
similarity index 100%
rename from examples/08_subset_of_data/run_example_single_channel_few_Z_planes.py
rename to examples/old/_08_subset_of_data/run_example_single_channel_few_Z_planes.py
diff --git a/examples/08_subset_of_data/run_example_single_channel_single_Z_plane.py b/examples/old/_08_subset_of_data/run_example_single_channel_single_Z_plane.py
similarity index 100%
rename from examples/08_subset_of_data/run_example_single_channel_single_Z_plane.py
rename to examples/old/_08_subset_of_data/run_example_single_channel_single_Z_plane.py
diff --git a/examples/08_subset_of_data/run_example_two_channel_two_Z_planes.py b/examples/old/_08_subset_of_data/run_example_two_channel_two_Z_planes.py
similarity index 100%
rename from examples/08_subset_of_data/run_example_two_channel_two_Z_planes.py
rename to examples/old/_08_subset_of_data/run_example_two_channel_two_Z_planes.py
diff --git a/examples/08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py b/examples/old/_08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py
similarity index 100%
rename from examples/08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py
rename to examples/old/_08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py
diff --git a/examples/09_secondary_labeling/.gitignore b/examples/old/_09_secondary_labeling/.gitignore
similarity index 100%
rename from examples/09_secondary_labeling/.gitignore
rename to examples/old/_09_secondary_labeling/.gitignore
diff --git a/examples/09_secondary_labeling/01_images_to_mip.py b/examples/old/_09_secondary_labeling/01_images_to_mip.py
similarity index 100%
rename from examples/09_secondary_labeling/01_images_to_mip.py
rename to examples/old/_09_secondary_labeling/01_images_to_mip.py
diff --git a/examples/09_secondary_labeling/02_cellpose_two_steps_segmentation.py b/examples/old/_09_secondary_labeling/02_cellpose_two_steps_segmentation.py
similarity index 100%
rename from examples/09_secondary_labeling/02_cellpose_two_steps_segmentation.py
rename to examples/old/_09_secondary_labeling/02_cellpose_two_steps_segmentation.py
diff --git a/examples/09_secondary_labeling/check_ROI_table.py b/examples/old/_09_secondary_labeling/check_ROI_table.py
similarity index 100%
rename from examples/09_secondary_labeling/check_ROI_table.py
rename to examples/old/_09_secondary_labeling/check_ROI_table.py
diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json
index 30b0b9e66..53bf103f0 100644
--- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json
+++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json
@@ -1,30 +1,42 @@
 {
-  "args_schema_version": "pydantic_v1",
-  "has_args_schemas": true,
-  "manifest_version": "1",
+  "manifest_version": "2",
   "task_list": [
     {
-      "args_schema": {
-        "title": "CreateOmeZarr",
+      "name": "Convert Cellvoyager to OME-Zarr",
+      "executable_non_parallel": "tasks/cellvoyager_to_ome_zarr_init.py",
+      "executable_parallel": "tasks/cellvoyager_to_ome_zarr_compute.py",
+      "meta_non_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "args_schema_non_parallel": {
+        "title": "CellvoyagerToOmeZarrInit",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
+          "zarr_urls": {
+            "title": "Zarr Urls",
             "type": "array",
             "items": {
               "type": "string"
             },
-            "description": "List of input paths where the image data from the microscope is stored (as TIF or PNG). Should point to the parent folder containing the images and the metadata files `MeasurementData.mlf` and `MeasurementDetail.mrf` (if present). Example: `[\"/some/path/\"]`. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used by the converter task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "output_path": {
-            "title": "Output Path",
+          "zarr_dir": {
+            "title": "Zarr Dir",
             "type": "string",
-            "description": "Path were the output of this task is stored. Example: \"/some/path/\" => puts the new OME-Zarr file in the \"/some/path/\". (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
+          "image_dirs": {
+            "title": "Image Dirs",
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "list of paths to the folders that contains the Cellvoyager image files. Each entry is a path to a folder that contains the image files themselves for a multiwell plate and the MeasurementData & MeasurementDetail metadata files."
           },
           "allowed_channels": {
             "title": "Allowed Channels",
@@ -73,9 +85,9 @@
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "metadata",
+          "zarr_urls",
+          "zarr_dir",
+          "image_dirs",
           "allowed_channels"
         ],
         "additionalProperties": false,
@@ -166,44 +178,19 @@
           }
         }
       },
-      "executable": "tasks/create_ome_zarr.py",
-      "input_type": "image",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000
-      },
-      "name": "Create OME-Zarr structure",
-      "output_type": "zarr",
-      "docs_info": "Create a OME-NGFF zarr folder, without reading/writing image data.\n\nFind plates (for each folder in input_paths):\n\n- glob image files,\n- parse metadata from image filename to identify plates,\n- identify populated channels.\n\nCreate a zarr folder (for each plate):\n\n- parse mlf metadata,\n- identify wells and field of view (FOV),\n- create FOV ZARR,\n- verify that channels are uniform (i.e., same channels).",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/create_ome_zarr/#fractal_tasks_core.tasks.create_ome_zarr.create_ome_zarr"
-    },
-    {
-      "args_schema": {
-        "title": "YokogawaToOmeZarr",
+      "args_schema_parallel": {
+        "title": "CellvoyagerToOmeZarrCompute",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of input paths where the OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
-            "type": "string",
-            "description": "Unclear. Should be the same as `input_path`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "component": {
-            "title": "Component",
+          "zarr_url": {
+            "title": "Zarr Url",
             "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"` (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "Dictionary containing metadata about the OME-Zarr. This task requires the following elements to be present in the metadata. `original_paths`: list of paths that correspond to the `input_paths` of the `create_ome_zarr` task (=> where the microscopy image are stored); `num_levels (int)`: number of pyramid levels in the image (this determines how many pyramid levels are built for the segmentation); `coarsening_xy (int)`: coarsening factor in XY of the downsampling when building the pyramid; `image_extension`: filename extension of images (e.g. `\"tif\"` or `\"png\"`); `image_glob_patterns`: parameter of `create_ome_zarr` task (if specified, only parse images with filenames that match with all these patterns). (standard argument for Fractal tasks, managed by Fractal server)."
+          "init_args": {
+            "$ref": "#/definitions/InitArgsCellVoyager",
+            "title": "Init_Args",
+            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
           },
           "overwrite": {
             "title": "Overwrite",
@@ -213,71 +200,126 @@
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata"
+          "zarr_url",
+          "init_args"
         ],
-        "additionalProperties": false
-      },
-      "executable": "tasks/yokogawa_to_ome_zarr.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000,
-        "parallelization_level": "image"
+        "additionalProperties": false,
+        "definitions": {
+          "InitArgsCellVoyager": {
+            "title": "InitArgsCellVoyager",
+            "description": "Arguments to be passed from cellvoyager converter init to compute",
+            "type": "object",
+            "properties": {
+              "image_dir": {
+                "title": "Image Dir",
+                "type": "string"
+              },
+              "plate_prefix": {
+                "title": "Plate Prefix",
+                "type": "string"
+              },
+              "well_ID": {
+                "title": "Well Id",
+                "type": "string"
+              },
+              "image_extension": {
+                "title": "Image Extension",
+                "type": "string"
+              },
+              "image_glob_patterns": {
+                "title": "Image Glob Patterns",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "acquisition": {
+                "title": "Acquisition",
+                "type": "integer"
+              }
+            },
+            "required": [
+              "image_dir",
+              "plate_prefix",
+              "well_ID",
+              "image_extension"
+            ]
+          }
+        }
       },
-      "name": "Convert Yokogawa to OME-Zarr",
-      "output_type": "zarr",
-      "docs_info": "Convert Yokogawa output (png, tif) to zarr file.\n\nThis task is typically run after Create OME-Zarr or\nCreate OME-Zarr Multiplexing and populates the empty OME-Zarr files that\nwere prepared.",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/yokogawa_to_ome_zarr/#fractal_tasks_core.tasks.yokogawa_to_ome_zarr.yokogawa_to_ome_zarr"
+      "docs_info": "## cellvoyager_to_ome_zarr_init\nCreate a OME-NGFF zarr folder, without reading/writing image data.\n\nFind plates (for each folder in input_paths):\n\n- glob image files,\n- parse metadata from image filename to identify plates,\n- identify populated channels.\n\nCreate a zarr folder (for each plate):\n\n- parse mlf metadata,\n- identify wells and field of view (FOV),\n- create FOV ZARR,\n- verify that channels are uniform (i.e., same channels).\n## cellvoyager_to_ome_zarr_compute\nConvert Yokogawa output (png, tif) to zarr file.\n\nThis task is typically run after Create OME-Zarr or\nCreate OME-Zarr Multiplexing and populates the empty OME-Zarr files that\nwere prepared.\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
     },
     {
-      "args_schema": {
-        "title": "CopyOmeZarr",
+      "name": "Convert Cellvoyager Multiplexing to OME-Zarr",
+      "executable_non_parallel": "tasks/cellvoyager_to_ome_zarr_init_multiplex.py",
+      "executable_parallel": "tasks/cellvoyager_to_ome_zarr_compute.py",
+      "meta_non_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "args_schema_non_parallel": {
+        "title": "CellvoyagerToOmeZarrInitMultiplex",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
+          "zarr_urls": {
+            "title": "Zarr Urls",
             "type": "array",
             "items": {
               "type": "string"
             },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used by the converter task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "output_path": {
-            "title": "Output Path",
+          "zarr_dir": {
+            "title": "Zarr Dir",
             "type": "string",
-            "description": "Path were the output of this task is stored. Example: `\"/some/path/\"` => puts the new OME-Zarr file in the same folder as the input OME-Zarr file `\"/some/new_path\"` => puts the new OME-Zarr file into a new folder at `/some/new_path`. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "metadata": {
-            "title": "Metadata",
+          "acquisitions": {
+            "title": "Acquisitions",
             "type": "object",
-            "description": "Dictionary containing metadata about the OME-Zarr. This task requires the following elements to be present in the metadata: `plate`: List of plates (e.g. `[\"MyPlate.zarr\"]`); `well`: List of wells in the OME-Zarr plate (e.g. `[\"MyPlate.zarr/B/03/MyPlate.zarr/B/05\"]`); \"image\": List of images in the OME-Zarr plate (e.g. `[\"MyPlate.zarr/B/03/0\", \"MyPlate.zarr/B/05/0\"]`). standard argument for Fractal tasks, managed by Fractal server)."
+            "additionalProperties": {
+              "$ref": "#/definitions/MultiplexingAcquisition"
+            },
+            "description": "dictionary of acquisitions. Each key is the acquisition identifier (normally 0, 1, 2, 3 etc.). Each item defines the acquisition by providing the image_dir and the allowed_channels."
           },
-          "project_to_2D": {
-            "title": "Project To 2D",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, apply a 3D->2D projection to the ROI tables that are copied to the new OME-Zarr."
+          "image_glob_patterns": {
+            "title": "Image Glob Patterns",
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "If specified, only parse images with filenames that match with all these patterns. Patterns must be defined as in https://docs.python.org/3/library/fnmatch.html, Example: `image_glob_pattern=[\"*_B03_*\"]` => only process well B03 `image_glob_pattern=[\"*_C09_*\", \"*F016*\", \"*Z[0-5][0-9]C*\"]` => only process well C09, field of view 16 and Z planes 0-59."
           },
-          "suffix": {
-            "title": "Suffix",
-            "default": "mip",
+          "num_levels": {
+            "title": "Num Levels",
+            "default": 5,
+            "type": "integer",
+            "description": "Number of resolution-pyramid levels. If set to `5`, there will be the full-resolution level and 4 levels of downsampled images."
+          },
+          "coarsening_xy": {
+            "title": "Coarsening Xy",
+            "default": 2,
+            "type": "integer",
+            "description": "Linear coarsening factor between subsequent levels. If set to `2`, level 1 is 2x downsampled, level 2 is 4x downsampled etc."
+          },
+          "image_extension": {
+            "title": "Image Extension",
+            "default": "tif",
             "type": "string",
-            "description": "The suffix that is used to transform `plate.zarr` into `plate_suffix.zarr`. Note that `None` is not currently supported."
+            "description": "Filename extension of images (e.g. `\"tif\"` or `\"png\"`)."
           },
-          "ROI_table_names": {
-            "title": "Roi Table Names",
-            "default": [
-              "FOV_ROI_table",
-              "well_ROI_table"
-            ],
-            "type": "array",
-            "items": {
+          "metadata_table_files": {
+            "title": "Metadata Table Files",
+            "type": "object",
+            "additionalProperties": {
               "type": "string"
             },
-            "description": "List of Anndata table names to be copied. Note: copying non-ROI tables may fail if `project_to_2D=True`."
+            "description": "If `None`, parse Yokogawa metadata from mrf/mlf files in the input_path folder; else, a dictionary of key-value pairs like `(acquisition, path)` with `acquisition` a string like the key of the `acquisitions` dict and `path` pointing to a csv file containing the parsed metadata table."
           },
           "overwrite": {
             "title": "Overwrite",
@@ -287,50 +329,234 @@
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "metadata"
+          "zarr_urls",
+          "zarr_dir",
+          "acquisitions"
         ],
-        "additionalProperties": false
+        "additionalProperties": false,
+        "definitions": {
+          "Window": {
+            "title": "Window",
+            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
+            "type": "object",
+            "properties": {
+              "min": {
+                "title": "Min",
+                "type": "integer",
+                "description": "Do not change. It will be set to `0` by default."
+              },
+              "max": {
+                "title": "Max",
+                "type": "integer",
+                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
+              },
+              "start": {
+                "title": "Start",
+                "type": "integer",
+                "description": "Lower-bound rescaling value for visualization."
+              },
+              "end": {
+                "title": "End",
+                "type": "integer",
+                "description": "Upper-bound rescaling value for visualization."
+              }
+            },
+            "required": [
+              "start",
+              "end"
+            ]
+          },
+          "OmeroChannel": {
+            "title": "OmeroChannel",
+            "description": "Custom class for Omero channels, based on OME-NGFF v0.4.",
+            "type": "object",
+            "properties": {
+              "wavelength_id": {
+                "title": "Wavelength Id",
+                "type": "string",
+                "description": "Unique ID for the channel wavelength, e.g. `A01_C01`."
+              },
+              "index": {
+                "title": "Index",
+                "type": "integer",
+                "description": "Do not change. For internal use only."
+              },
+              "label": {
+                "title": "Label",
+                "type": "string",
+                "description": "Name of the channel."
+              },
+              "window": {
+                "$ref": "#/definitions/Window",
+                "title": "Window",
+                "description": "Optional `Window` object to set default display settings for napari."
+              },
+              "color": {
+                "title": "Color",
+                "type": "string",
+                "description": "Optional hex colormap to display the channel in napari (it must be of length 6, e.g. `00FFFF`)."
+              },
+              "active": {
+                "title": "Active",
+                "default": true,
+                "type": "boolean",
+                "description": "Should this channel be shown in the viewer?"
+              },
+              "coefficient": {
+                "title": "Coefficient",
+                "default": 1,
+                "type": "integer",
+                "description": "Do not change. Omero-channel attribute."
+              },
+              "inverted": {
+                "title": "Inverted",
+                "default": false,
+                "type": "boolean",
+                "description": "Do not change. Omero-channel attribute."
+              }
+            },
+            "required": [
+              "wavelength_id"
+            ]
+          },
+          "MultiplexingAcquisition": {
+            "title": "MultiplexingAcquisition",
+            "description": "Input class for Multiplexing Cellvoyager converter",
+            "type": "object",
+            "properties": {
+              "image_dir": {
+                "title": "Image Dir",
+                "type": "string",
+                "description": "Path to the folder that contains the Cellvoyager image files for that acquisition and the MeasurementData & MeasurementDetail metadata files."
+              },
+              "allowed_channels": {
+                "title": "Allowed Channels",
+                "type": "array",
+                "items": {
+                  "$ref": "#/definitions/OmeroChannel"
+                },
+                "description": "A list of `OmeroChannel` objects, where each channel must include the `wavelength_id` attribute and where the `wavelength_id` values must be unique across the list."
+              }
+            },
+            "required": [
+              "image_dir",
+              "allowed_channels"
+            ]
+          }
+        }
       },
-      "executable": "tasks/copy_ome_zarr.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 1000
+      "args_schema_parallel": {
+        "title": "CellvoyagerToOmeZarrCompute",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "init_args": {
+            "$ref": "#/definitions/InitArgsCellVoyager",
+            "title": "Init_Args",
+            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
+          },
+          "overwrite": {
+            "title": "Overwrite",
+            "default": false,
+            "type": "boolean",
+            "description": "If `True`, overwrite the task output."
+          }
+        },
+        "required": [
+          "zarr_url",
+          "init_args"
+        ],
+        "additionalProperties": false,
+        "definitions": {
+          "InitArgsCellVoyager": {
+            "title": "InitArgsCellVoyager",
+            "description": "Arguments to be passed from cellvoyager converter init to compute",
+            "type": "object",
+            "properties": {
+              "image_dir": {
+                "title": "Image Dir",
+                "type": "string"
+              },
+              "plate_prefix": {
+                "title": "Plate Prefix",
+                "type": "string"
+              },
+              "well_ID": {
+                "title": "Well Id",
+                "type": "string"
+              },
+              "image_extension": {
+                "title": "Image Extension",
+                "type": "string"
+              },
+              "image_glob_patterns": {
+                "title": "Image Glob Patterns",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "acquisition": {
+                "title": "Acquisition",
+                "type": "integer"
+              }
+            },
+            "required": [
+              "image_dir",
+              "plate_prefix",
+              "well_ID",
+              "image_extension"
+            ]
+          }
+        }
       },
-      "name": "Copy OME-Zarr structure",
-      "output_type": "zarr",
-      "docs_info": "Duplicate an input zarr structure to a new path.\n\nThis task copies all the structure, but none of the image data:\n\n- For each plate, create a new zarr group with the same attributes as\n   the original one.\n- For each well (in each plate), create a new zarr subgroup with the\n   same attributes as the original one.\n- For each image (in each well), create a new zarr subgroup with the\n   same attributes as the original one.\n- For each image (in each well), copy the relevant AnnData tables from\n   the original source.\n\nNote: this task makes use of methods from the `Attributes` class, see\nhttps://zarr.readthedocs.io/en/stable/api/attrs.html.",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/copy_ome_zarr/#fractal_tasks_core.tasks.copy_ome_zarr.copy_ome_zarr"
+      "docs_info": "## cellvoyager_to_ome_zarr_init_multiplex\nCreate OME-NGFF structure and metadata to host a multiplexing dataset.\n\nThis task takes a set of image folders (i.e. different acquisition cycles)\nand build the internal structure and metadata of a OME-NGFF zarr group,\nwithout actually loading/writing the image data.\n\nEach element in input_paths should be treated as a different acquisition.\n## cellvoyager_to_ome_zarr_compute\nConvert Yokogawa output (png, tif) to zarr file.\n\nThis task is typically run after Create OME-Zarr or\nCreate OME-Zarr Multiplexing and populates the empty OME-Zarr files that\nwere prepared.\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
     },
     {
-      "args_schema": {
-        "title": "MaximumIntensityProjection",
+      "name": "Maximum Intensity Projection HCS Plate",
+      "input_types": {
+        "is_3D": true
+      },
+      "output_types": {
+        "is_3D": false
+      },
+      "executable_non_parallel": "tasks/copy_ome_zarr_hcs_plate.py",
+      "executable_parallel": "tasks/maximum_intensity_projection.py",
+      "meta_non_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "args_schema_non_parallel": {
+        "title": "CopyOmeZarrHcsPlate",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
+          "zarr_urls": {
+            "title": "Zarr Urls",
             "type": "array",
             "items": {
               "type": "string"
             },
-            "description": "This parameter is not used by this task. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "output_path": {
-            "title": "Output Path",
+          "zarr_dir": {
+            "title": "Zarr Dir",
             "type": "string",
-            "description": "Path were the output of this task is stored. Example: `\"/some/path/\"` => puts the new OME-Zarr file in that folder. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "component": {
-            "title": "Component",
+          "suffix": {
+            "title": "Suffix",
+            "default": "mip",
             "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Component is typically changed by the `copy_ome_zarr` task before, to point to a new mip Zarr file. Example: `\"some_plate_mip.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "Dictionary containing metadata about the OME-Zarr. This task requires the key `copy_ome_zarr` to be present in the metadata (as defined in `copy_ome_zarr` task). (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "The suffix that is used to transform `plate.zarr` into `plate_suffix.zarr`. Note that `None` is not currently supported."
           },
           "overwrite": {
             "title": "Overwrite",
@@ -340,55 +566,146 @@
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata"
+          "zarr_urls",
+          "zarr_dir"
         ],
         "additionalProperties": false
       },
-      "executable": "tasks/maximum_intensity_projection.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000,
-        "parallelization_level": "image"
-      },
-      "name": "Maximum Intensity Projection",
-      "output_type": "zarr",
-      "docs_info": "Perform maximum-intensity projection along Z axis.\n\nNote: this task stores the output in a new zarr file.",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/maximum_intensity_projection/#fractal_tasks_core.tasks.maximum_intensity_projection.maximum_intensity_projection"
-    },
-    {
-      "args_schema": {
-        "title": "CellposeSegmentation",
+      "args_schema_parallel": {
+        "title": "MaximumIntensityProjection",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
+          "zarr_url": {
+            "title": "Zarr Url",
             "type": "string",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "component": {
-            "title": "Component",
-            "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
+          "init_args": {
+            "$ref": "#/definitions/InitArgsMIP",
+            "title": "Init_Args",
+            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
           },
-          "metadata": {
-            "title": "Metadata",
+          "overwrite": {
+            "title": "Overwrite",
+            "default": false,
+            "type": "boolean",
+            "description": "If `True`, overwrite the task output."
+          }
+        },
+        "required": [
+          "zarr_url",
+          "init_args"
+        ],
+        "additionalProperties": false,
+        "definitions": {
+          "InitArgsMIP": {
+            "title": "InitArgsMIP",
+            "description": "Init Args for MIP task.",
             "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "level": {
-            "title": "Level",
+            "properties": {
+              "origin_url": {
+                "title": "Origin Url",
+                "type": "string"
+              }
+            },
+            "required": [
+              "origin_url"
+            ]
+          }
+        }
+      },
+      "docs_info": "## copy_ome_zarr_hcs_plate\nDuplicate the OME-Zarr HCS structure for a set of zarr_urls.\n\nThis task only processes the zarr images in the zarr_urls, not all the\nimages in the plate. It copies all the  plate & well structure, but none\nof the image metadata or the actual image data:\n\n- For each plate, create a new OME-Zarr HCS plate with the attributes for\n    all the images in zarr_urls\n- For each well (in each plate), create a new zarr subgroup with the\n   same attributes as the original one.\n\nNote: this task makes use of methods from the `Attributes` class, see\nhttps://zarr.readthedocs.io/en/stable/api/attrs.html.\n## maximum_intensity_projection\nPerform maximum-intensity projection along Z axis.\n\nNote: this task stores the output in a new zarr file.\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+    },
+    {
+      "name": "Illumination Correction",
+      "input_types": {
+        "illumination_corrected": false
+      },
+      "output_types": {
+        "illumination_corrected": true
+      },
+      "executable_parallel": "tasks/illumination_correction.py",
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "args_schema_parallel": {
+        "title": "IlluminationCorrection",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "illumination_profiles_folder": {
+            "title": "Illumination Profiles Folder",
+            "type": "string",
+            "description": "Path of folder of illumination profiles."
+          },
+          "dict_corr": {
+            "title": "Dict Corr",
+            "type": "object",
+            "additionalProperties": {
+              "type": "string"
+            },
+            "description": "Dictionary where keys match the `wavelength_id` attributes of existing channels (e.g. `A01_C01` ) and values are the filenames of the corresponding illumination profiles."
+          },
+          "background": {
+            "title": "Background",
+            "default": 110,
+            "type": "integer",
+            "description": "Background value that is subtracted from the image before the illumination correction is applied. Set it to `0` if you don't want any background subtraction."
+          },
+          "input_ROI_table": {
+            "title": "Input Roi Table",
+            "default": "FOV_ROI_table",
+            "type": "string",
+            "description": "Name of the ROI table that contains the information about the location of the individual field of views (FOVs) to which the illumination correction shall be applied. Defaults to \"FOV_ROI_table\", the default name Fractal converters give the ROI tables that list all FOVs separately. If you generated your OME-Zarr with a different converter and used Import OME-Zarr to generate the ROI tables, `image_ROI_table` is the right choice if you only have 1 FOV per Zarr image and `grid_ROI_table` if you have multiple FOVs per Zarr image and set the right grid options during import."
+          },
+          "overwrite_input": {
+            "title": "Overwrite Input",
+            "default": true,
+            "type": "boolean",
+            "description": "If `True`, the results of this task will overwrite the input image data. If false, a new image is generated and the illumination corrected data is saved there."
+          },
+          "suffix": {
+            "title": "Suffix",
+            "default": "_illum_corr",
+            "type": "string",
+            "description": "What suffix to append to the illumination corrected images. Only relevant if `overwrite_input=False`."
+          }
+        },
+        "required": [
+          "zarr_url",
+          "illumination_profiles_folder",
+          "dict_corr"
+        ],
+        "additionalProperties": false
+      },
+      "docs_info": "## illumination_correction\nApplies illumination correction to the images in the OME-Zarr.\n\nAssumes that the illumination correction profiles were generated before\nseparately and that the same background subtraction was used during\ncalculation of the illumination correction (otherwise, it will not work\nwell & the correction may only be partial).\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+    },
+    {
+      "name": "Cellpose Segmentation",
+      "executable_parallel": "tasks/cellpose_segmentation.py",
+      "meta_parallel": {
+        "cpus_per_task": 4,
+        "mem": 16000,
+        "needs_gpu": true
+      },
+      "args_schema_parallel": {
+        "title": "CellposeSegmentation",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "level": {
+            "title": "Level",
             "type": "integer",
             "description": "Pyramid level of the image to be segmented. Choose `0` to process at full resolution."
           },
@@ -571,10 +888,7 @@
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata",
+          "zarr_url",
           "level",
           "channel"
         ],
@@ -641,192 +955,397 @@
           }
         }
       },
-      "executable": "tasks/cellpose_segmentation.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 4,
-        "mem": 16000,
-        "needs_gpu": true,
-        "parallelization_level": "image"
-      },
-      "name": "Cellpose Segmentation",
-      "output_type": "zarr",
-      "docs_info": "Run cellpose segmentation on the ROIs of a single OME-Zarr image.",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/cellpose_segmentation/#fractal_tasks_core.tasks.cellpose_segmentation.cellpose_segmentation"
+      "docs_info": "## cellpose_segmentation\nRun cellpose segmentation on the ROIs of a single OME-Zarr image.\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
     },
     {
-      "args_schema": {
-        "title": "IlluminationCorrection",
+      "name": "Calculate Registration (image-based)",
+      "executable_non_parallel": "tasks/image_based_registration_hcs_init.py",
+      "executable_parallel": "tasks/calculate_registration_image_based.py",
+      "meta_non_parallel": {
+        "cpus_per_task": 1,
+        "mem": 1000
+      },
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 8000
+      },
+      "args_schema_non_parallel": {
+        "title": "ImageBasedRegistrationHcsInit",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
+          "zarr_urls": {
+            "title": "Zarr Urls",
             "type": "array",
             "items": {
               "type": "string"
             },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "output_path": {
-            "title": "Output Path",
+          "zarr_dir": {
+            "title": "Zarr Dir",
             "type": "string",
-            "description": "Path were the output of this task is stored. Examples: `\"/some/path/\"` => puts the new OME-Zarr file in the same folder as the input OME-Zarr file; `\"/some/new_path\"` => puts the new OME-Zarr file into a new folder at `/some/new_path`. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "path of the directory where the new OME-Zarrs will be created. Not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "component": {
-            "title": "Component",
+          "reference_cycle": {
+            "title": "Reference Cycle",
+            "default": 0,
+            "type": "integer",
+            "description": "Which cycle to register against. Needs to match the acquisition metadata in the OME-Zarr image."
+          }
+        },
+        "required": [
+          "zarr_urls",
+          "zarr_dir"
+        ],
+        "additionalProperties": false
+      },
+      "args_schema_parallel": {
+        "title": "CalculateRegistrationImageBased",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
             "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
+          "init_args": {
+            "$ref": "#/definitions/InitArgsRegistration",
+            "title": "Init_Args",
+            "description": "Intialization arguments provided by `image_based_registration_hcs_init`. They contain the reference_zarr_url that is used for registration. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "illumination_profiles_folder": {
-            "title": "Illumination Profiles Folder",
+          "wavelength_id": {
+            "title": "Wavelength Id",
             "type": "string",
-            "description": "Path of folder of illumination profiles."
-          },
-          "dict_corr": {
-            "title": "Dict Corr",
-            "type": "object",
-            "additionalProperties": {
-              "type": "string"
-            },
-            "description": "Dictionary where keys match the `wavelength_id` attributes of existing channels (e.g. `A01_C01` ) and values are the filenames of the corresponding illumination profiles."
-          },
-          "background": {
-            "title": "Background",
-            "default": 110,
-            "type": "integer",
-            "description": "Background value that is subtracted from the image before the illumination correction is applied. Set it to `0` if you don't want any background subtraction."
-          },
-          "overwrite_input": {
-            "title": "Overwrite Input",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, the results of this task will overwrite the input image data. In the current version, `overwrite_input=False` is not implemented."
+            "description": "Wavelength that will be used for image-based registration; e.g. `A01_C01` for Yokogawa, `C01` for MD."
           },
-          "input_ROI_table": {
-            "title": "Input Roi Table",
+          "roi_table": {
+            "title": "Roi Table",
             "default": "FOV_ROI_table",
             "type": "string",
-            "description": "Name of the ROI table that contains the information about the location of the individual field of views (FOVs) to which the illumination correction shall be applied. Defaults to \"FOV_ROI_table\", the default name Fractal converters give the ROI tables that list all FOVs separately. If you generated your OME-Zarr with a different converter and used Import OME-Zarr to generate the ROI tables, `image_ROI_table` is the right choice if you only have 1 FOV per Zarr image and `grid_ROI_table` if you have multiple FOVs per Zarr image and set the right grid options during import."
+            "description": "Name of the ROI table over which the task loops to calculate the registration. Examples: `FOV_ROI_table` => loop over the field of views, `well_ROI_table` => process the whole well as one image."
           },
-          "new_component": {
-            "title": "New Component",
-            "type": "string",
-            "description": "Not implemented yet. This is not implemented well in Fractal server at the moment, it's unclear how a user would specify fitting new components. If the results shall not overwrite the input data and the output path is the same as the input path, a new component needs to be provided. Example: `myplate_new_name.zarr/B/03/0/`."
+          "level": {
+            "title": "Level",
+            "default": 2,
+            "type": "integer",
+            "description": "Pyramid level of the image to be used for registration. Choose `0` to process at full resolution."
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata",
-          "illumination_profiles_folder",
-          "dict_corr"
+          "zarr_url",
+          "init_args",
+          "wavelength_id"
         ],
-        "additionalProperties": false
-      },
-      "executable": "tasks/illumination_correction.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000,
-        "parallelization_level": "image"
+        "additionalProperties": false,
+        "definitions": {
+          "InitArgsRegistration": {
+            "title": "InitArgsRegistration",
+            "description": "Registration init args.",
+            "type": "object",
+            "properties": {
+              "reference_zarr_url": {
+                "title": "Reference Zarr Url",
+                "type": "string"
+              }
+            },
+            "required": [
+              "reference_zarr_url"
+            ]
+          }
+        }
       },
-      "name": "Illumination correction",
-      "output_type": "zarr",
-      "docs_info": "Applies illumination correction to the images in the OME-Zarr.\n\nAssumes that the illumination correction profiles were generated before\nseparately and that the same background subtraction was used during\ncalculation of the illumination correction (otherwise, it will not work\nwell & the correction may only be partial).",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/illumination_correction/#fractal_tasks_core.tasks.illumination_correction.illumination_correction"
+      "docs_info": "## image_based_registration_hcs_init\nInitialized calculate registration task\n\nThis task prepares a parallelization list of all zarr_urls that need to be\nused to calculate the registration between cycles (all zarr_urls except\nthe reference cycle vs. the reference cycle).\nThis task only works for HCS OME-Zarrs for 2 reasons: Only HCS OME-Zarrs\ncurrently have defined acquisition metadata to determine reference cycles.\nAnd we have only implemented the grouping of images for HCS OME-Zarrs by\nwell (with the assumption that every well just has 1 image per acqusition).\n## calculate_registration_image_based\nCalculate registration based on images\n\nThis task consists of 3 parts:\n\n1. Loading the images of a given ROI (=> loop over ROIs)\n2. Calculating the transformation for that ROI\n3. Storing the calculated transformation in the ROI table\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
     },
     {
-      "args_schema": {
-        "title": "NapariWorkflowsWrapper",
+      "name": "Find Registration Consensus",
+      "executable_non_parallel": "tasks/init_group_by_well_for_multiplexing.py",
+      "executable_parallel": "tasks/find_registration_consensus.py",
+      "meta_non_parallel": {
+        "cpus_per_task": 1,
+        "mem": 1000
+      },
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 1000
+      },
+      "args_schema_non_parallel": {
+        "title": "InitGroupByWellForMultiplexing",
         "type": "object",
         "properties": {
-          "input_paths": {
-            "title": "Input Paths",
+          "zarr_urls": {
+            "title": "Zarr Urls",
             "type": "array",
             "items": {
               "type": "string"
             },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. his task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "output_path": {
-            "title": "Output Path",
+          "zarr_dir": {
+            "title": "Zarr Dir",
             "type": "string",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "path of the directory where the new OME-Zarrs will be created. Not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "component": {
-            "title": "Component",
+          "reference_cycle": {
+            "title": "Reference Cycle",
+            "default": 0,
+            "type": "integer",
+            "description": "Which cycle to register against. Defaults to 0, which is the first OME-Zarr image in the well, usually the first cycle that was provided"
+          }
+        },
+        "required": [
+          "zarr_urls",
+          "zarr_dir"
+        ],
+        "additionalProperties": false
+      },
+      "args_schema_parallel": {
+        "title": "FindRegistrationConsensus",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
             "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "Path or url to the individual OME-Zarr image to be processed. Refers to the zarr_url of the reference cycle. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
+          "init_args": {
+            "$ref": "#/definitions/InitArgsRegistrationConsensus",
+            "title": "Init_Args",
+            "description": "Intialization arguments provided by `init_group_by_well_for_multiplexing`. It contains the zarr_url_list listing all the zarr_urls in the same well as the zarr_url of the reference cycle that are being processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "workflow_file": {
-            "title": "Workflow File",
+          "roi_table": {
+            "title": "Roi Table",
+            "default": "FOV_ROI_table",
             "type": "string",
-            "description": "Absolute path to napari-workflows YAML file"
-          },
-          "input_specs": {
-            "title": "Input Specs",
-            "type": "object",
-            "additionalProperties": {
-              "$ref": "#/definitions/NapariWorkflowsInput"
-            },
-            "description": "A dictionary of `NapariWorkflowsInput` values."
+            "description": "Name of the ROI table over which the task loops to calculate the registration. Examples: `FOV_ROI_table` => loop over the field of views, `well_ROI_table` => process the whole well as one image."
           },
-          "output_specs": {
-            "title": "Output Specs",
+          "new_roi_table": {
+            "title": "New Roi Table",
+            "type": "string",
+            "description": "Optional name for the new, registered ROI table. If no name is given, it will default to \"registered_\" + `roi_table`"
+          }
+        },
+        "required": [
+          "zarr_url",
+          "init_args"
+        ],
+        "additionalProperties": false,
+        "definitions": {
+          "InitArgsRegistrationConsensus": {
+            "title": "InitArgsRegistrationConsensus",
+            "description": "Registration consensus init args.",
             "type": "object",
-            "additionalProperties": {
-              "$ref": "#/definitions/NapariWorkflowsOutput"
+            "properties": {
+              "zarr_url_list": {
+                "title": "Zarr Url List",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              }
             },
-            "description": "A dictionary of `NapariWorkflowsOutput` values."
-          },
-          "input_ROI_table": {
-            "title": "Input Roi Table",
-            "default": "FOV_ROI_table",
+            "required": [
+              "zarr_url_list"
+            ]
+          }
+        }
+      },
+      "docs_info": "## init_group_by_well_for_multiplexing\nFinds images for all acquisitions per well.\n\nReturns the parallelization_list to run `find_registration_consensus`.\n## find_registration_consensus\nApplies pre-calculated registration to ROI tables.\n\nApply pre-calculated registration such that resulting ROIs contain\nthe consensus align region between all cycles.\n\nParallelization level: well\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+    },
+    {
+      "name": "Apply Registration to Image",
+      "input_types": {
+        "registered": false
+      },
+      "output_types": {
+        "registered": true
+      },
+      "executable_parallel": "tasks/apply_registration_to_image.py",
+      "meta_parallel": {
+        "cpus_per_task": 1,
+        "mem": 4000
+      },
+      "args_schema_parallel": {
+        "title": "ApplyRegistrationToImage",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
             "type": "string",
-            "description": "Name of the ROI table over which the task loops to apply napari workflows. Examples: `FOV_ROI_table` => loop over the field of views; `organoid_ROI_table` => loop over the organoid ROI table (generated by another task); `well_ROI_table` => process the whole well as one image."
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "level": {
-            "title": "Level",
-            "default": 0,
-            "type": "integer",
-            "description": "Pyramid level of the image to be used as input for napari-workflows. Choose `0` to process at full resolution. Levels > 0 are currently only supported for workflows that only have intensity images as input and only produce a label images as output."
-          },
-          "relabeling": {
-            "title": "Relabeling",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, apply relabeling so that label values are unique across all ROIs in the well."
+          "registered_roi_table": {
+            "title": "Registered Roi Table",
+            "type": "string",
+            "description": "Name of the ROI table which has been registered and will be applied to mask and shift the images. Examples: `registered_FOV_ROI_table` => loop over the field of views, `registered_well_ROI_table` => process the whole well as one image."
           },
-          "expected_dimensions": {
-            "title": "Expected Dimensions",
-            "default": 3,
-            "type": "integer",
-            "description": "Expected dimensions (either `2` or `3`). Useful when loading 2D images that are stored in a 3D array with shape `(1, size_x, size_y)` [which is the default way Fractal stores 2D images], but you want to make sure the napari workflow gets a 2D array to process. Also useful to set to `2` when loading a 2D OME-Zarr that is saved as `(size_x, size_y)`."
+          "reference_cycle": {
+            "title": "Reference Cycle",
+            "default": "0",
+            "type": "string",
+            "description": "Which cycle to register against. Defaults to 0, which is the first OME-Zarr image in the well, usually the first cycle that was provided"
           },
-          "overwrite": {
-            "title": "Overwrite",
+          "overwrite_input": {
+            "title": "Overwrite Input",
             "default": true,
             "type": "boolean",
-            "description": "If `True`, overwrite the task output."
+            "description": "Whether the old image data should be replaced with the newly registered image data. Currently only implemented for `overwrite_input=True`."
           }
         },
         "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata",
-          "workflow_file",
+          "zarr_url",
+          "registered_roi_table"
+        ],
+        "additionalProperties": false
+      },
+      "docs_info": "## apply_registration_to_image\nApply registration to images by using a registered ROI table\n\nThis task consists of 4 parts:\n\n1. Mask all regions in images that are not available in the\nregistered ROI table and store each cycle aligned to the\nreference_cycle (by looping over ROIs).\n2. Do the same for all label images.\n3. Copy all tables from the non-aligned image to the aligned image\n(currently only works well if the only tables are well & FOV ROI tables\n(registered and original). Not implemented for measurement tables and\nother ROI tables).\n4. Clean up: Delete the old, non-aligned image and rename the new,\naligned image to take over its place.\n\nParallelization level: image\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+    },
+    {
+      "name": "Import OME-Zarr",
+      "executable_non_parallel": "tasks/import_ome_zarr.py",
+      "args_schema_non_parallel": {
+        "title": "ImportOmeZarr",
+        "type": "object",
+        "properties": {
+          "zarr_urls": {
+            "title": "Zarr Urls",
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "zarr_dir": {
+            "title": "Zarr Dir",
+            "type": "string",
+            "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "zarr_name": {
+            "title": "Zarr Name",
+            "type": "string",
+            "description": "The OME-Zarr name, without its parent folder. The parent folder is provided by zarr_dir; e.g. `zarr_name=\"array.zarr\"`, if the OME-Zarr path is in `/zarr_dir/array.zarr`."
+          },
+          "add_image_ROI_table": {
+            "title": "Add Image Roi Table",
+            "default": true,
+            "type": "boolean",
+            "description": "Whether to add a `image_ROI_table` table to each image, with a single ROI covering the whole image."
+          },
+          "add_grid_ROI_table": {
+            "title": "Add Grid Roi Table",
+            "default": true,
+            "type": "boolean",
+            "description": "Whether to add a `grid_ROI_table` table to each image, with the image split into a rectangular grid of ROIs."
+          },
+          "grid_y_shape": {
+            "title": "Grid Y Shape",
+            "default": 2,
+            "type": "integer",
+            "description": "Y shape of the ROI grid in `grid_ROI_table`."
+          },
+          "grid_x_shape": {
+            "title": "Grid X Shape",
+            "default": 2,
+            "type": "integer",
+            "description": "X shape of the ROI grid in `grid_ROI_table`."
+          },
+          "update_omero_metadata": {
+            "title": "Update Omero Metadata",
+            "default": true,
+            "type": "boolean",
+            "description": "Whether to update Omero-channels metadata, to make them Fractal-compatible."
+          },
+          "overwrite": {
+            "title": "Overwrite",
+            "default": false,
+            "type": "boolean",
+            "description": "Whether new ROI tables (added when `add_image_ROI_table` and/or `add_grid_ROI_table` are `True`) can overwite existing ones."
+          }
+        },
+        "required": [
+          "zarr_urls",
+          "zarr_dir",
+          "zarr_name"
+        ],
+        "additionalProperties": false
+      },
+      "docs_info": "## import_ome_zarr\nImport a single OME-Zarr into Fractal.\n\nThe single OME-Zarr can be a full OME-Zarr HCS plate or an individual\nOME-Zarr image. The image needs to be in the zarr_dir as specified by the\ndataset. The current version of this task:\n\n1. Creates the appropriate components-related metadata, needed for\n   processing an existing OME-Zarr through Fractal.\n2. Optionally adds new ROI tables to the existing OME-Zarr.\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+    },
+    {
+      "name": "Napari workflows wrapper",
+      "executable_parallel": "tasks/napari_workflows_wrapper.py",
+      "meta_parallel": {
+        "cpus_per_task": 8,
+        "mem": 32000
+      },
+      "args_schema_parallel": {
+        "title": "NapariWorkflowsWrapper",
+        "type": "object",
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "workflow_file": {
+            "title": "Workflow File",
+            "type": "string",
+            "description": "Absolute path to napari-workflows YAML file"
+          },
+          "input_specs": {
+            "title": "Input Specs",
+            "type": "object",
+            "additionalProperties": {
+              "$ref": "#/definitions/NapariWorkflowsInput"
+            },
+            "description": "A dictionary of `NapariWorkflowsInput` values."
+          },
+          "output_specs": {
+            "title": "Output Specs",
+            "type": "object",
+            "additionalProperties": {
+              "$ref": "#/definitions/NapariWorkflowsOutput"
+            },
+            "description": "A dictionary of `NapariWorkflowsOutput` values."
+          },
+          "input_ROI_table": {
+            "title": "Input Roi Table",
+            "default": "FOV_ROI_table",
+            "type": "string",
+            "description": "Name of the ROI table over which the task loops to apply napari workflows. Examples: `FOV_ROI_table` => loop over the field of views; `organoid_ROI_table` => loop over the organoid ROI table (generated by another task); `well_ROI_table` => process the whole well as one image."
+          },
+          "level": {
+            "title": "Level",
+            "default": 0,
+            "type": "integer",
+            "description": "Pyramid level of the image to be used as input for napari-workflows. Choose `0` to process at full resolution. Levels > 0 are currently only supported for workflows that only have intensity images as input and only produce a label images as output."
+          },
+          "relabeling": {
+            "title": "Relabeling",
+            "default": true,
+            "type": "boolean",
+            "description": "If `True`, apply relabeling so that label values are unique across all ROIs in the well."
+          },
+          "expected_dimensions": {
+            "title": "Expected Dimensions",
+            "default": 3,
+            "type": "integer",
+            "description": "Expected dimensions (either `2` or `3`). Useful when loading 2D images that are stored in a 3D array with shape `(1, size_x, size_y)` [which is the default way Fractal stores 2D images], but you want to make sure the napari workflow gets a 2D array to process. Also useful to set to `2` when loading a 2D OME-Zarr that is saved as `(size_x, size_y)`."
+          },
+          "overwrite": {
+            "title": "Overwrite",
+            "default": true,
+            "type": "boolean",
+            "description": "If `True`, overwrite the task output."
+          }
+        },
+        "required": [
+          "zarr_url",
+          "workflow_file",
           "input_specs",
           "output_specs"
         ],
@@ -910,487 +1429,10 @@
           }
         }
       },
-      "executable": "tasks/napari_workflows_wrapper.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 8,
-        "mem": 32000,
-        "parallelization_level": "image"
-      },
-      "name": "Napari workflows wrapper",
-      "output_type": "zarr",
-      "docs_info": "Run a napari-workflow on the ROIs of a single OME-NGFF image.\n\nThis task takes images and labels and runs a napari-workflow on them that\ncan produce a label and tables as output.\n\nExamples of allowed entries for `input_specs` and `output_specs`:\n\n```\ninput_specs = {\n    \"in_1\": {\"type\": \"image\", \"channel\": {\"wavelength_id\": \"A01_C02\"}},\n    \"in_2\": {\"type\": \"image\", \"channel\": {\"label\": \"DAPI\"}},\n    \"in_3\": {\"type\": \"label\", \"label_name\": \"label_DAPI\"},\n}\n\noutput_specs = {\n    \"out_1\": {\"type\": \"label\", \"label_name\": \"label_DAPI_new\"},\n    \"out_2\": {\"type\": \"dataframe\", \"table_name\": \"measurements\"},\n}\n```",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/napari_workflows_wrapper/#fractal_tasks_core.tasks.napari_workflows_wrapper.napari_workflows_wrapper"
-    },
-    {
-      "args_schema": {
-        "title": "CreateOmeZarrMultiplex",
-        "type": "object",
-        "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of input paths where the image data from the microscope is stored (as TIF or PNG). Each element of the list is treated as another cycle of the multiplexing data, the cycles are ordered by their order in this list. Should point to the parent folder containing the images and the metadata files `MeasurementData.mlf` and `MeasurementDetail.mrf` (if present). Example: `[\"/path/cycle1/\", \"/path/cycle2/\"]`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
-            "type": "string",
-            "description": "Path were the output of this task is stored. Example: `\"/some/path/\"` => puts the new OME-Zarr file in the `/some/path/`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "allowed_channels": {
-            "title": "Allowed Channels",
-            "type": "object",
-            "additionalProperties": {
-              "type": "array",
-              "items": {
-                "$ref": "#/definitions/OmeroChannel"
-              }
-            },
-            "description": "A dictionary of lists of `OmeroChannel`s, where each channel must include the `wavelength_id` attribute and where the `wavelength_id` values must be unique across each list. Dictionary keys represent channel indices (`\"0\",\"1\",..`)."
-          },
-          "image_glob_patterns": {
-            "title": "Image Glob Patterns",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "If specified, only parse images with filenames that match with all these patterns. Patterns must be defined as in https://docs.python.org/3/library/fnmatch.html, Example: `image_glob_pattern=[\"*_B03_*\"]` => only process well B03 `image_glob_pattern=[\"*_C09_*\", \"*F016*\", \"*Z[0-5][0-9]C*\"]` => only process well C09, field of view 16 and Z planes 0-59."
-          },
-          "num_levels": {
-            "title": "Num Levels",
-            "default": 5,
-            "type": "integer",
-            "description": "Number of resolution-pyramid levels. If set to `5`, there will be the full-resolution level and 4 levels of downsampled images."
-          },
-          "coarsening_xy": {
-            "title": "Coarsening Xy",
-            "default": 2,
-            "type": "integer",
-            "description": "Linear coarsening factor between subsequent levels. If set to `2`, level 1 is 2x downsampled, level 2 is 4x downsampled etc."
-          },
-          "image_extension": {
-            "title": "Image Extension",
-            "default": "tif",
-            "type": "string",
-            "description": "Filename extension of images (e.g. `\"tif\"` or `\"png\"`)."
-          },
-          "metadata_table_files": {
-            "title": "Metadata Table Files",
-            "type": "object",
-            "additionalProperties": {
-              "type": "string"
-            },
-            "description": "If `None`, parse Yokogawa metadata from mrf/mlf files in the input_path folder; else, a dictionary of key-value pairs like `(acquisition, path)` with `acquisition` a string and `path` pointing to a csv file containing the parsed metadata table."
-          },
-          "overwrite": {
-            "title": "Overwrite",
-            "default": false,
-            "type": "boolean",
-            "description": "If `True`, overwrite the task output."
-          }
-        },
-        "required": [
-          "input_paths",
-          "output_path",
-          "metadata",
-          "allowed_channels"
-        ],
-        "additionalProperties": false,
-        "definitions": {
-          "Window": {
-            "title": "Window",
-            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
-            "type": "object",
-            "properties": {
-              "min": {
-                "title": "Min",
-                "type": "integer",
-                "description": "Do not change. It will be set to `0` by default."
-              },
-              "max": {
-                "title": "Max",
-                "type": "integer",
-                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
-              },
-              "start": {
-                "title": "Start",
-                "type": "integer",
-                "description": "Lower-bound rescaling value for visualization."
-              },
-              "end": {
-                "title": "End",
-                "type": "integer",
-                "description": "Upper-bound rescaling value for visualization."
-              }
-            },
-            "required": [
-              "start",
-              "end"
-            ]
-          },
-          "OmeroChannel": {
-            "title": "OmeroChannel",
-            "description": "Custom class for Omero channels, based on OME-NGFF v0.4.",
-            "type": "object",
-            "properties": {
-              "wavelength_id": {
-                "title": "Wavelength Id",
-                "type": "string",
-                "description": "Unique ID for the channel wavelength, e.g. `A01_C01`."
-              },
-              "index": {
-                "title": "Index",
-                "type": "integer",
-                "description": "Do not change. For internal use only."
-              },
-              "label": {
-                "title": "Label",
-                "type": "string",
-                "description": "Name of the channel."
-              },
-              "window": {
-                "$ref": "#/definitions/Window",
-                "title": "Window",
-                "description": "Optional `Window` object to set default display settings for napari."
-              },
-              "color": {
-                "title": "Color",
-                "type": "string",
-                "description": "Optional hex colormap to display the channel in napari (it must be of length 6, e.g. `00FFFF`)."
-              },
-              "active": {
-                "title": "Active",
-                "default": true,
-                "type": "boolean",
-                "description": "Should this channel be shown in the viewer?"
-              },
-              "coefficient": {
-                "title": "Coefficient",
-                "default": 1,
-                "type": "integer",
-                "description": "Do not change. Omero-channel attribute."
-              },
-              "inverted": {
-                "title": "Inverted",
-                "default": false,
-                "type": "boolean",
-                "description": "Do not change. Omero-channel attribute."
-              }
-            },
-            "required": [
-              "wavelength_id"
-            ]
-          }
-        }
-      },
-      "executable": "tasks/create_ome_zarr_multiplex.py",
-      "input_type": "image",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000
-      },
-      "name": "Create OME-ZARR structure (multiplexing)",
-      "output_type": "zarr",
-      "docs_info": "Create OME-NGFF structure and metadata to host a multiplexing dataset.\n\nThis task takes a set of image folders (i.e. different acquisition cycles)\nand build the internal structure and metadata of a OME-NGFF zarr group,\nwithout actually loading/writing the image data.\n\nEach element in input_paths should be treated as a different acquisition.",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/create_ome_zarr_multiplex/#fractal_tasks_core.tasks.create_ome_zarr_multiplex.create_ome_zarr_multiplex"
-    },
-    {
-      "executable": "tasks/calculate_registration_image_based.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 8000,
-        "parallelization_level": "image"
-      },
-      "name": "Calculate registration (image-based)",
-      "output_type": "zarr",
-      "args_schema": {
-        "title": "CalculateRegistrationImageBased",
-        "type": "object",
-        "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
-            "type": "string",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "component": {
-            "title": "Component",
-            "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "wavelength_id": {
-            "title": "Wavelength Id",
-            "type": "string",
-            "description": "Wavelength that will be used for image-based registration; e.g. `A01_C01` for Yokogawa, `C01` for MD."
-          },
-          "roi_table": {
-            "title": "Roi Table",
-            "default": "FOV_ROI_table",
-            "type": "string",
-            "description": "Name of the ROI table over which the task loops to calculate the registration. Examples: `FOV_ROI_table` => loop over the field of views, `well_ROI_table` => process the whole well as one image."
-          },
-          "reference_cycle": {
-            "title": "Reference Cycle",
-            "default": 0,
-            "type": "integer",
-            "description": "Which cycle to register against. Defaults to 0, which is the first OME-Zarr image in the well (usually the first cycle that was provided)."
-          },
-          "level": {
-            "title": "Level",
-            "default": 2,
-            "type": "integer",
-            "description": "Pyramid level of the image to be segmented. Choose `0` to process at full resolution."
-          }
-        },
-        "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata",
-          "wavelength_id"
-        ],
-        "additionalProperties": false
-      },
-      "docs_info": "Calculate registration based on images\n\nThis task consists of 3 parts:\n\n1. Loading the images of a given ROI (=> loop over ROIs)\n2. Calculating the transformation for that ROI\n3. Storing the calculated transformation in the ROI table\n\nParallelization level: image",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/calculate_registration_image_based/#fractal_tasks_core.tasks.calculate_registration_image_based.calculate_registration_image_based"
-    },
-    {
-      "executable": "tasks/apply_registration_to_ROI_tables.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 1000,
-        "parallelization_level": "well"
-      },
-      "name": "Apply Registration to ROI Tables",
-      "output_type": "zarr",
-      "args_schema": {
-        "title": "ApplyRegistrationToRoiTables",
-        "type": "object",
-        "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
-            "type": "string",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "component": {
-            "title": "Component",
-            "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "roi_table": {
-            "title": "Roi Table",
-            "default": "FOV_ROI_table",
-            "type": "string",
-            "description": "Name of the ROI table over which the task loops to calculate the registration. Examples: `FOV_ROI_table` => loop over the field of views, `well_ROI_table` => process the whole well as one image."
-          },
-          "reference_cycle": {
-            "title": "Reference Cycle",
-            "default": 0,
-            "type": "integer",
-            "description": "Which cycle to register against. Defaults to 0, which is the first OME-Zarr image in the well, usually the first cycle that was provided"
-          },
-          "new_roi_table": {
-            "title": "New Roi Table",
-            "type": "string",
-            "description": "Optional name for the new, registered ROI table. If no name is given, it will default to \"registered_\" + `roi_table`"
-          }
-        },
-        "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata"
-        ],
-        "additionalProperties": false
-      },
-      "docs_info": "Applies pre-calculated registration to ROI tables.\n\nApply pre-calculated registration such that resulting ROIs contain\nthe consensus align region between all cycles.\n\nParallelization level: well",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/apply_registration_to_ROI_tables/#fractal_tasks_core.tasks.apply_registration_to_ROI_tables.apply_registration_to_ROI_tables"
-    },
-    {
-      "executable": "tasks/apply_registration_to_image.py",
-      "input_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000,
-        "parallelization_level": "image"
-      },
-      "name": "Apply Registration to Image",
-      "output_type": "zarr",
-      "args_schema": {
-        "title": "ApplyRegistrationToImage",
-        "type": "object",
-        "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of input paths where the image data is stored as OME-Zarrs. Should point to the parent folder containing one or many OME-Zarr files, not the actual OME-Zarr file. Example: `[\"/some/path/\"]`. This task only supports a single input path. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
-            "type": "string",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "component": {
-            "title": "Component",
-            "type": "string",
-            "description": "Path to the OME-Zarr image in the OME-Zarr plate that is processed. Example: `\"some_plate.zarr/B/03/0\"`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "This parameter is not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "registered_roi_table": {
-            "title": "Registered Roi Table",
-            "type": "string",
-            "description": "Name of the ROI table which has been registered and will be applied to mask and shift the images. Examples: `registered_FOV_ROI_table` => loop over the field of views, `registered_well_ROI_table` => process the whole well as one image."
-          },
-          "reference_cycle": {
-            "title": "Reference Cycle",
-            "default": "0",
-            "type": "string",
-            "description": "Which cycle to register against. Defaults to 0, which is the first OME-Zarr image in the well, usually the first cycle that was provided"
-          },
-          "overwrite_input": {
-            "title": "Overwrite Input",
-            "default": true,
-            "type": "boolean",
-            "description": "Whether the old image data should be replaced with the newly registered image data. Currently only implemented for `overwrite_input=True`."
-          }
-        },
-        "required": [
-          "input_paths",
-          "output_path",
-          "component",
-          "metadata",
-          "registered_roi_table"
-        ],
-        "additionalProperties": false
-      },
-      "docs_info": "Apply registration to images by using a registered ROI table\n\nThis task consists of 4 parts:\n\n1. Mask all regions in images that are not available in the\nregistered ROI table and store each cycle aligned to the\nreference_cycle (by looping over ROIs).\n2. Do the same for all label images.\n3. Copy all tables from the non-aligned image to the aligned image\n(currently only works well if the only tables are well & FOV ROI tables\n(registered and original). Not implemented for measurement tables and\nother ROI tables).\n4. Clean up: Delete the old, non-aligned image and rename the new,\naligned image to take over its place.\n\nParallelization level: image",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/apply_registration_to_image/#fractal_tasks_core.tasks.apply_registration_to_image.apply_registration_to_image"
-    },
-    {
-      "name": "Import OME-Zarr",
-      "executable": "tasks/import_ome_zarr.py",
-      "input_type": "zarr",
-      "output_type": "zarr",
-      "meta": {
-        "cpus_per_task": 1,
-        "mem": 4000
-      },
-      "args_schema": {
-        "title": "ImportOmeZarr",
-        "type": "object",
-        "properties": {
-          "input_paths": {
-            "title": "Input Paths",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "A length-one list with the parent folder of the OME-Zarr to be imported; e.g. `input_paths=[\"/somewhere\"]`, if the OME-Zarr path is `/somewhere/array.zarr`. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "output_path": {
-            "title": "Output Path",
-            "type": "string",
-            "description": "Not used in this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "metadata": {
-            "title": "Metadata",
-            "type": "object",
-            "description": "Not used in this task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "zarr_name": {
-            "title": "Zarr Name",
-            "type": "string",
-            "description": "The OME-Zarr name, without its parent folder; e.g. `zarr_name=\"array.zarr\"`, if the OME-Zarr path is `/somewhere/array.zarr`."
-          },
-          "add_image_ROI_table": {
-            "title": "Add Image Roi Table",
-            "default": true,
-            "type": "boolean",
-            "description": "Whether to add a `image_ROI_table` table to each image, with a single ROI covering the whole image."
-          },
-          "add_grid_ROI_table": {
-            "title": "Add Grid Roi Table",
-            "default": true,
-            "type": "boolean",
-            "description": "Whether to add a `grid_ROI_table` table to each image, with the image split into a rectangular grid of ROIs."
-          },
-          "grid_y_shape": {
-            "title": "Grid Y Shape",
-            "default": 2,
-            "type": "integer",
-            "description": "Y shape of the ROI grid in `grid_ROI_table`."
-          },
-          "grid_x_shape": {
-            "title": "Grid X Shape",
-            "default": 2,
-            "type": "integer",
-            "description": "X shape of the ROI grid in `grid_ROI_table`."
-          },
-          "update_omero_metadata": {
-            "title": "Update Omero Metadata",
-            "default": true,
-            "type": "boolean",
-            "description": "Whether to update Omero-channels metadata, to make them Fractal-compatible."
-          },
-          "overwrite": {
-            "title": "Overwrite",
-            "default": false,
-            "type": "boolean",
-            "description": "Whether new ROI tables (added when `add_image_ROI_table` and/or `add_grid_ROI_table` are `True`) can overwite existing ones."
-          }
-        },
-        "required": [
-          "input_paths",
-          "output_path",
-          "metadata",
-          "zarr_name"
-        ],
-        "additionalProperties": false
-      },
-      "docs_info": "Import an OME-Zarr into Fractal.\n\nThe current version of this task:\n\n1. Creates the appropriate components-related metadata, needed for\n   processing an existing OME-Zarr through Fractal.\n2. Optionally adds new ROI tables to the existing OME-Zarr.",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core/reference/fractal_tasks_core/tasks/import_ome_zarr/#fractal_tasks_core.tasks.import_ome_zarr.import_ome_zarr"
+      "docs_info": "## napari_workflows_wrapper\nRun a napari-workflow on the ROIs of a single OME-NGFF image.\n\nThis task takes images and labels and runs a napari-workflow on them that\ncan produce a label and tables as output.\n\nExamples of allowed entries for `input_specs` and `output_specs`:\n\n```\ninput_specs = {\n    \"in_1\": {\"type\": \"image\", \"channel\": {\"wavelength_id\": \"A01_C02\"}},\n    \"in_2\": {\"type\": \"image\", \"channel\": {\"label\": \"DAPI\"}},\n    \"in_3\": {\"type\": \"label\", \"label_name\": \"label_DAPI\"},\n}\n\noutput_specs = {\n    \"out_1\": {\"type\": \"label\", \"label_name\": \"label_DAPI_new\"},\n    \"out_2\": {\"type\": \"dataframe\", \"table_name\": \"measurements\"},\n}\n```\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
     }
-  ]
+  ],
+  "has_args_schemas": true,
+  "args_schema_version": "pydantic_v1"
 }
diff --git a/fractal_tasks_core/dev/check_manifest.py b/fractal_tasks_core/dev/check_manifest.py
index f91225218..19a3bbeb5 100644
--- a/fractal_tasks_core/dev/check_manifest.py
+++ b/fractal_tasks_core/dev/check_manifest.py
@@ -22,7 +22,6 @@
     create_schema_for_single_task,
 )
 from fractal_tasks_core.dev.lib_task_docs import create_docs_info
-from fractal_tasks_core.dev.lib_task_docs import create_docs_link
 
 
 PACKAGE = "fractal_tasks_core"
@@ -95,35 +94,44 @@ def _compare_dicts(
     task_list = manifest["task_list"]
     for ind, task in enumerate(task_list):
 
-        # Read current schema
-        current_schema = task["args_schema"]
+        for kind in ["non_paralell", "parallel"]:
+            executable = task.get(f"executable_{kind}")
+            if executable is None:
+                continue
 
-        # Create new schema
-        executable = task["executable"]
-        logging.info(f"[{executable}] START")
-        new_schema = create_schema_for_single_task(executable, package=PACKAGE)
+            # Read current schema
+            current_schema = task[f"args_schema_{kind}"]
 
-        # The following step is required because some arguments may have a
-        # default which has a non-JSON type (e.g. a tuple), which we need to
-        # convert to JSON type (i.e. an array) before comparison.
-        new_schema = json.loads(json.dumps(new_schema))
+            # Create new schema
+            logging.info(f"[{executable}] START")
+            new_schema = create_schema_for_single_task(
+                executable, package=PACKAGE
+            )
+
+            # The following step is required because some arguments may have a
+            # default which has a non-JSON type (e.g. a tuple), which we need
+            # to convert to JSON type (i.e. an array) before comparison.
+            new_schema = json.loads(json.dumps(new_schema))
+
+            # Try to provide an informative comparison of current_schema and
+            # new_schema
+            _compare_dicts(current_schema, new_schema, path=[])
 
-        # Try to provide an informative comparison of current_schema and
-        # new_schema
-        _compare_dicts(current_schema, new_schema, path=[])
+            # Also directly check the equality of current_schema and new_schema
+            # (this is redundant, in principle)
+            if current_schema != new_schema:
+                raise ValueError("Schemas are different.")
 
-        # Also directly check the equality of current_schema and new_schema
-        # (this is redundant, in principle)
-        if current_schema != new_schema:
-            raise ValueError("Schemas are different.")
+            logging.info(
+                f"[{executable}] END (task schemas are up-to-date in manifest)"
+            )
+            print()
 
         # Check docs_info and docs_link
-        docs_info = create_docs_info(executable, package=PACKAGE)
-        docs_link = create_docs_link(executable)
+        docs_info = create_docs_info(
+            executable_non_parallel=task.get("executable_non_parallel"),
+            executable_parallel=task.get("executable_parallel"),
+            package=PACKAGE,
+        )
         if docs_info != task.get("docs_info", ""):
             raise ValueError("docs_info not up-to-date")
-        if docs_link != task.get("docs_link", ""):
-            raise ValueError("docs_link not up-to-date")
-
-        logging.info(f"[{executable}] END (task is up-to-date in manifest)")
-        print()
diff --git a/fractal_tasks_core/dev/create_manifest.py b/fractal_tasks_core/dev/create_manifest.py
new file mode 100644
index 000000000..88561b647
--- /dev/null
+++ b/fractal_tasks_core/dev/create_manifest.py
@@ -0,0 +1,148 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Script to generate JSON schemas for task arguments afresh, and write them
+to the package manifest.
+"""
+import json
+import logging
+from importlib import import_module
+from pathlib import Path
+from typing import Optional
+
+from fractal_tasks_core.dev.lib_args_schemas import (
+    create_schema_for_single_task,
+)
+from fractal_tasks_core.dev.lib_task_docs import create_docs_info
+
+
+def create_manifest(
+    package: str = "fractal_tasks_core",
+    manifest_version: str = "2",
+    has_args_schemas: bool = True,
+    args_schema_version: str = "pydantic_v1",
+    docs_link: Optional[str] = None,
+    custom_pydantic_models: Optional[list[tuple[str, str, str]]] = None,
+):
+    """
+    This function creates the package manifest based on a `task_list.py`
+    Python module located in the `dev` subfolder of the package, see an
+    example of such list at ...
+
+    The manifest is then written to `__FRACTAL_MANIFEST__.json`, in the
+    main `package` directory.
+
+    Note: a valid example of `custom_pydantic_models` would be
+    ```
+    [
+        ("my_task_package", "some_module.py", "SomeModel"),
+    ]
+    ```
+
+    Arguments:
+        package: The name of the package (must be importable).
+        manifest_version: Only `"2"` is supported.
+        has_args_schemas:
+            Whether to autogenerate JSON Schemas for task arguments.
+        args_schema_version:
+            Only `"pydantic_v1"` is currently supported in `fractal-server`
+            and `fractal-web`.
+        custom_pydantic_models:
+            Custom models to be included when building JSON Schemas for task
+            arguments.
+    """
+
+    # Preliminary check
+    if manifest_version != "2":
+        raise NotImplementedError(f"{manifest_version=} is not supported")
+
+    logging.info("Start generating a new manifest")
+
+    # Prepare an empty manifest
+    manifest = dict(
+        manifest_version=manifest_version,
+        task_list=[],
+        has_args_schemas=has_args_schemas,
+    )
+    if has_args_schemas:
+        manifest["args_schema_version"] = args_schema_version
+
+    # Prepare a default value of docs_link
+    if package == "fractal_tasks_core" and docs_link is None:
+        docs_link = (
+            "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+        )
+
+    # Import the task list from `dev/task_list.py`
+    task_list_module = import_module(f"{package}.dev.task_list")
+    TASK_LIST = getattr(task_list_module, "TASK_LIST")
+
+    # Loop over TASK_LIST, and append the proper task dictionary
+    # to manifest["task_list"]
+    for task_obj in TASK_LIST:
+        # Convert Pydantic object to dictionary
+        task_dict = task_obj.dict(
+            exclude={"meta_init", "executable_init", "meta", "executable"},
+            exclude_unset=True,
+        )
+
+        # Copy some properties from `task_obj` to `task_dict`
+        if task_obj.executable_non_parallel is not None:
+            task_dict[
+                "executable_non_parallel"
+            ] = task_obj.executable_non_parallel
+        if task_obj.executable_parallel is not None:
+            task_dict["executable_parallel"] = task_obj.executable_parallel
+        if task_obj.meta_non_parallel is not None:
+            task_dict["meta_non_parallel"] = task_obj.meta_non_parallel
+        if task_obj.meta_parallel is not None:
+            task_dict["meta_parallel"] = task_obj.meta_parallel
+
+        # Autogenerate JSON Schemas for non-parallel/parallel task arguments
+        if has_args_schemas:
+            for kind in ["non_parallel", "parallel"]:
+                executable = task_dict.get(f"executable_{kind}")
+                if executable is not None:
+                    logging.info(f"[{executable}] START")
+                    schema = create_schema_for_single_task(
+                        executable,
+                        package=package,
+                        custom_pydantic_models=custom_pydantic_models,
+                    )
+                    logging.info(f"[{executable}] END (new schema)")
+                    task_dict[f"args_schema_{kind}"] = schema
+
+        # Update docs_info, based on task-function description
+        docs_info = create_docs_info(
+            executable_non_parallel=task_obj.executable_non_parallel,
+            executable_parallel=task_obj.executable_parallel,
+        )
+        if docs_info is not None:
+            task_dict["docs_info"] = docs_info
+        if docs_link is not None:
+            task_dict["docs_link"] = docs_link
+
+        manifest["task_list"].append(task_dict)
+        print()
+
+    # Write manifest
+    imported_package = import_module(package)
+    manifest_path = (
+        Path(imported_package.__file__).parent / "__FRACTAL_MANIFEST__.json"
+    )
+    with manifest_path.open("w") as f:
+        json.dump(manifest, f, indent=2)
+        f.write("\n")
+    logging.info(f"Manifest stored in {manifest_path.as_posix()}")
+
+
+if __name__ == "__main__":
+    create_manifest()
diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py
index 3b98aa539..971de66a8 100644
--- a/fractal_tasks_core/dev/lib_args_schemas.py
+++ b/fractal_tasks_core/dev/lib_args_schemas.py
@@ -52,12 +52,12 @@
     ("fractal_tasks_core", "channels.py", "ChannelInputModel"),
     (
         "fractal_tasks_core",
-        "tasks/napari_workflows_wrapper_models.py",
+        "tasks/io_models.py",
         "NapariWorkflowsInput",
     ),
     (
         "fractal_tasks_core",
-        "tasks/napari_workflows_wrapper_models.py",
+        "tasks/io_models.py",
         "NapariWorkflowsOutput",
     ),
     (
@@ -65,6 +65,11 @@
         "tasks/cellpose_transforms.py",
         "CellposeCustomNormalizer",
     ),
+    (
+        "fractal_tasks_core",
+        "tasks/io_models.py",
+        "MultiplexingAcquisition",
+    ),
 ]
 
 
diff --git a/fractal_tasks_core/dev/lib_task_docs.py b/fractal_tasks_core/dev/lib_task_docs.py
index 78b0b006b..201e82b49 100644
--- a/fractal_tasks_core/dev/lib_task_docs.py
+++ b/fractal_tasks_core/dev/lib_task_docs.py
@@ -10,6 +10,7 @@
 # Zurich.
 import logging
 from pathlib import Path
+from typing import Optional
 
 from docstring_parser import parse as docparse
 
@@ -51,42 +52,30 @@ def _get_function_description(
 
 
 def create_docs_info(
-    executable: str,
+    executable_non_parallel: Optional[str] = None,
+    executable_parallel: Optional[str] = None,
     package: str = "fractal_tasks_core",
-) -> str:
+) -> list[str]:
     """
     Return task description based on function docstring.
     """
     logging.info("[create_docs_info] START")
-    # Extract the function name. Note: this could be made more general, but for
-    # the moment we assume the function has the same name as the module)
-    function_name = Path(executable).with_suffix("").name
-    logging.info(f"[create_docs_info] {function_name=}")
-    # Get function description
-    docs_info = _get_function_description(
-        package_name=package,
-        module_relative_path=executable,
-        function_name=function_name,
-    )
+    docs_info = []
+    for executable in [executable_non_parallel, executable_parallel]:
+        if executable is None:
+            continue
+        # Extract the function name.
+        # Note: this could be made more general, but for the moment we assume
+        # that the function has the same name as the module)
+        function_name = Path(executable).with_suffix("").name
+        logging.info(f"[create_docs_info] {function_name=}")
+        # Get function description
+        description = _get_function_description(
+            package_name=package,
+            module_relative_path=executable,
+            function_name=function_name,
+        )
+        docs_info.append(f"## {function_name}\n{description}\n")
+    docs_info = "".join(docs_info)
     logging.info("[create_docs_info] END")
     return docs_info
-
-
-def create_docs_link(executable: str) -> str:
-    """
-    Return link to docs page for a fractal_tasks_core task.
-    """
-    logging.info("[create_docs_link] START")
-
-    # Extract the function name. Note: this could be made more general, but for
-    # the moment we assume the function has the same name as the module)
-    function_name = Path(executable).with_suffix("").name
-    logging.info(f"[create_docs_link] {function_name=}")
-    # Define docs_link
-    docs_link = (
-        "https://fractal-analytics-platform.github.io/fractal-tasks-core/"
-        f"reference/fractal_tasks_core/tasks/{function_name}/"
-        f"#fractal_tasks_core.tasks.{function_name}.{function_name}"
-    )
-    logging.info("[create_docs_link] END")
-    return docs_link
diff --git a/fractal_tasks_core/dev/task_list.py b/fractal_tasks_core/dev/task_list.py
new file mode 100644
index 000000000..d3a1a8780
--- /dev/null
+++ b/fractal_tasks_core/dev/task_list.py
@@ -0,0 +1,94 @@
+# Copyright 2024 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Joel Lüthi <joel.luethi@uzh.ch>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Fractal task list.
+"""
+from fractal_tasks_core.dev.task_models import CompoundTask
+from fractal_tasks_core.dev.task_models import NonParallelTask
+from fractal_tasks_core.dev.task_models import ParallelTask
+
+TASK_LIST = [
+    CompoundTask(
+        name="Convert Cellvoyager to OME-Zarr",
+        executable_init="tasks/cellvoyager_to_ome_zarr_init.py",
+        executable="tasks/cellvoyager_to_ome_zarr_compute.py",
+        meta_init={"cpus_per_task": 1, "mem": 4000},
+        meta={"cpus_per_task": 1, "mem": 4000},
+    ),
+    CompoundTask(
+        name="Convert Cellvoyager Multiplexing to OME-Zarr",
+        executable_init="tasks/cellvoyager_to_ome_zarr_init_multiplex.py",
+        executable="tasks/cellvoyager_to_ome_zarr_compute.py",
+        meta_init={"cpus_per_task": 1, "mem": 4000},
+        meta={"cpus_per_task": 1, "mem": 4000},
+    ),
+    CompoundTask(
+        name="Maximum Intensity Projection HCS Plate",
+        input_types={"is_3D": True},
+        executable_init="tasks/copy_ome_zarr_hcs_plate.py",
+        executable="tasks/maximum_intensity_projection.py",
+        output_types={"is_3D": False},
+        meta_init={"cpus_per_task": 1, "mem": 4000},
+        meta={"cpus_per_task": 1, "mem": 4000},
+    ),
+    ParallelTask(
+        name="Illumination Correction",
+        input_types=dict(illumination_corrected=False),
+        executable="tasks/illumination_correction.py",
+        output_types=dict(illumination_corrected=True),
+        meta={"cpus_per_task": 1, "mem": 4000},
+    ),
+    # CompoundTask(
+    #     name="illumination_correction_compound",
+    #     input_types=dict(illumination_correction=False),
+    #     executable_init="illumination_correction_init.py",
+    #     executable="illumination_correction_compute.py",
+    #     output_types=dict(illumination_correction=True),
+    # ),
+    ParallelTask(
+        name="Cellpose Segmentation",
+        executable="tasks/cellpose_segmentation.py",
+        meta={"cpus_per_task": 4, "mem": 16000, "needs_gpu": True},
+    ),
+    CompoundTask(
+        name="Calculate Registration (image-based)",
+        executable_init="tasks/image_based_registration_hcs_init.py",
+        executable="tasks/calculate_registration_image_based.py",
+        meta_init={"cpus_per_task": 1, "mem": 1000},
+        meta={"cpus_per_task": 1, "mem": 8000},
+    ),
+    CompoundTask(
+        name="Find Registration Consensus",
+        executable_init="tasks/init_group_by_well_for_multiplexing.py",
+        executable="tasks/find_registration_consensus.py",
+        meta_init={"cpus_per_task": 1, "mem": 1000},
+        meta={"cpus_per_task": 1, "mem": 1000},
+    ),
+    ParallelTask(
+        name="Apply Registration to Image",
+        input_types=dict(registered=False),
+        executable="tasks/apply_registration_to_image.py",
+        output_types=dict(registered=True),
+        meta={"cpus_per_task": 1, "mem": 4000},
+    ),
+    NonParallelTask(
+        name="Import OME-Zarr",
+        executable="tasks/import_ome_zarr.py",
+    ),
+    ParallelTask(
+        name="Napari workflows wrapper",
+        executable="tasks/napari_workflows_wrapper.py",
+        meta={
+            "cpus_per_task": 8,
+            "mem": 32000,
+        },
+    ),
+]
diff --git a/fractal_tasks_core/dev/task_models.py b/fractal_tasks_core/dev/task_models.py
new file mode 100644
index 000000000..89a650866
--- /dev/null
+++ b/fractal_tasks_core/dev/task_models.py
@@ -0,0 +1,106 @@
+# Copyright 2024 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Task models for Fractal tasks.
+
+These models are used in `task_list.py`, and they provide a layer that
+simplifies writing the task list of a package in a way that is compliant with
+fractal-server v2.
+"""
+from typing import Any
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class _BaseTask(BaseModel):
+    class Config:
+        arbitrary_types_allowed = True
+        extra = "forbid"
+
+    name: str
+    executable: str
+    meta: Optional[dict[str, Any]]
+    input_types: Optional[dict[str, bool]]
+    output_types: Optional[dict[str, bool]]
+
+
+class CompoundTask(_BaseTask):
+    """
+    A `CompoundTask` object must include both `executable_init` and
+    `executable` attributes, and it may include the `meta_init` and `meta`
+    attributes.
+    """
+
+    executable_init: str
+    meta_init: Optional[dict[str, Any]]
+
+    @property
+    def executable_non_parallel(self) -> str:
+        return self.executable_init
+
+    @property
+    def meta_non_parallel(self) -> Optional[dict[str, Any]]:
+        return self.meta_init
+
+    @property
+    def executable_parallel(self) -> str:
+        return self.executable
+
+    @property
+    def meta_parallel(self) -> Optional[dict[str, Any]]:
+        return self.meta
+
+
+class NonParallelTask(_BaseTask):
+    """
+    A `NonParallelTask` object must include the `executable` attribute, and it
+    may include the `meta` attribute.
+    """
+
+    @property
+    def executable_non_parallel(self) -> str:
+        return self.executable
+
+    @property
+    def meta_non_parallel(self) -> Optional[dict[str, Any]]:
+        return self.meta
+
+    @property
+    def executable_parallel(self) -> None:
+        return None
+
+    @property
+    def meta_parallel(self) -> None:
+        return None
+
+
+class ParallelTask(_BaseTask):
+    """
+    A `ParallelTask` object must include the `executable` attribute, and it may
+    include the `meta` attribute.
+    """
+
+    @property
+    def executable_non_parallel(self) -> None:
+        return None
+
+    @property
+    def meta_non_parallel(self) -> None:
+        return None
+
+    @property
+    def executable_parallel(self) -> str:
+        return self.executable
+
+    @property
+    def meta_parallel(self) -> Optional[dict[str, Any]]:
+        return self.meta
diff --git a/fractal_tasks_core/dev/update_manifest.py b/fractal_tasks_core/dev/update_manifest.py
deleted file mode 100644
index 02dd22925..000000000
--- a/fractal_tasks_core/dev/update_manifest.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Script to generate JSON schemas for task arguments afresh, and write them
-to the package manifest.
-"""
-import json
-import logging
-from importlib import import_module
-from pathlib import Path
-
-from fractal_tasks_core.dev.lib_args_schemas import (
-    create_schema_for_single_task,
-)
-from fractal_tasks_core.dev.lib_task_docs import create_docs_info
-from fractal_tasks_core.dev.lib_task_docs import create_docs_link
-
-
-PACKAGE = "fractal_tasks_core"
-
-
-if __name__ == "__main__":
-
-    # Read manifest
-    imported_package = import_module(PACKAGE)
-    manifest_path = (
-        Path(imported_package.__file__).parent / "__FRACTAL_MANIFEST__.json"
-    )
-    with manifest_path.open("r") as f:
-        manifest = json.load(f)
-
-    # Set global properties of manifest
-    manifest["has_args_schemas"] = True
-    manifest["args_schema_version"] = "pydantic_v1"
-
-    # Loop over tasks
-    task_list = manifest["task_list"]
-    for ind, task in enumerate(task_list):
-        executable = task["executable"]
-        logging.info(f"[{executable}] START")
-
-        # Create new JSON Schema for task arguments
-        schema = create_schema_for_single_task(executable, package=PACKAGE)
-        manifest["task_list"][ind]["args_schema"] = schema
-
-        # Update docs_info, based on task-function description
-        docs_info = create_docs_info(executable, package=PACKAGE)
-        docs_link = create_docs_link(executable)
-        if docs_info:
-            manifest["task_list"][ind]["docs_info"] = docs_info
-        if docs_link:
-            manifest["task_list"][ind]["docs_link"] = docs_link
-
-        logging.info(f"[{executable}] END (new schema/description/link)")
-        print()
-
-    with manifest_path.open("w") as f:
-        json.dump(manifest, f, indent=2)
-        f.write("\n")
-    logging.info(f"Up-to-date manifest stored in {manifest_path.as_posix()}")
diff --git a/fractal_tasks_core/ngff/specs.py b/fractal_tasks_core/ngff/specs.py
index 8a9692530..d27d28a80 100644
--- a/fractal_tasks_core/ngff/specs.py
+++ b/fractal_tasks_core/ngff/specs.py
@@ -376,3 +376,98 @@ def get_acquisition_paths(self) -> dict[int, str]:
                 )
             acquisition_dict[image.acquisition] = image.path
         return acquisition_dict
+
+
+######################
+# Plate models
+######################
+
+
+class AcquisitionInPlate(BaseModel):
+    """
+    Model for an element of `Plate.acquisitions`.
+
+    See https://ngff.openmicroscopy.org/0.4/#plate-md.
+    """
+
+    id: int = Field(
+        description="A unique identifier within the context of the plate"
+    )
+    maximumfieldcount: Optional[int] = Field(
+        None,
+        description=(
+            "Int indicating the maximum number of fields of view for the "
+            "acquisition"
+        ),
+    )
+    name: Optional[str] = Field(
+        None, description="a string identifying the name of the acquisition"
+    )
+    description: Optional[str] = Field(
+        None,
+        description="The description of the acquisition",
+    )
+    # TODO: Add starttime & endtime
+    # starttime: Optional[str] = Field()
+    # endtime: Optional[str] = Field()
+
+
+class WellInPlate(BaseModel):
+    """
+    Model for an element of `Plate.wells`.
+
+    See https://ngff.openmicroscopy.org/0.4/#plate-md.
+    """
+
+    path: str
+    rowIndex: int
+    columnIndex: int
+
+
+class ColumnInPlate(BaseModel):
+    """
+    Model for an element of `Plate.columns`.
+
+    See https://ngff.openmicroscopy.org/0.4/#plate-md.
+    """
+
+    name: str
+
+
+class RowInPlate(BaseModel):
+    """
+    Model for an element of `Plate.rows`.
+
+    See https://ngff.openmicroscopy.org/0.4/#plate-md.
+    """
+
+    name: str
+
+
+class Plate(BaseModel):
+    """
+    Model for `NgffPlateMeta.plate`.
+
+    See https://ngff.openmicroscopy.org/0.4/#plate-md.
+    """
+
+    acquisitions: list[AcquisitionInPlate]
+    columns: list[ColumnInPlate]
+    field_count: Optional[int]
+    name: Optional[str]
+    rows: list[RowInPlate]
+    # version will become required in 0.5
+    version: Optional[str] = Field(
+        None, description="The version of the specification"
+    )
+    wells: list[WellInPlate]
+
+
+class NgffPlateMeta(BaseModel):
+    """
+    Model for the metadata of a NGFF plate.
+
+    See https://ngff.openmicroscopy.org/0.4/#plate-md.
+    """
+
+    plate: Plate
diff --git a/fractal_tasks_core/ngff/zarr_utils.py b/fractal_tasks_core/ngff/zarr_utils.py
index ccfcb424b..8b6d11fc6 100644
--- a/fractal_tasks_core/ngff/zarr_utils.py
+++ b/fractal_tasks_core/ngff/zarr_utils.py
@@ -7,6 +7,7 @@
 from zarr.errors import GroupNotFoundError
 
 from fractal_tasks_core.ngff.specs import NgffImageMeta
+from fractal_tasks_core.ngff.specs import NgffPlateMeta
 from fractal_tasks_core.ngff.specs import NgffWellMeta
 
 logger = logging.getLogger(__name__)
@@ -37,7 +38,7 @@ def load_NgffImageMeta(zarr_path: str) -> NgffImageMeta:
     except GroupNotFoundError:
         error_msg = (
             "Could not load attributes for the requested image, "
-            f"because no Zarr image was found at {zarr_path}"
+            f"because no Zarr group was found at {zarr_path}"
         )
         logging.error(error_msg)
         raise ZarrGroupNotFoundError(error_msg)
@@ -67,7 +68,7 @@ def load_NgffWellMeta(zarr_path: str) -> NgffWellMeta:
     except GroupNotFoundError:
         error_msg = (
             "Could not load attributes for the requested well, "
-            f"because no Zarr image was found at {zarr_path}"
+            f"because no Zarr group was found at {zarr_path}"
         )
         logging.error(error_msg)
         raise ZarrGroupNotFoundError(error_msg)
@@ -82,6 +83,36 @@ def load_NgffWellMeta(zarr_path: str) -> NgffWellMeta:
         raise e
 
 
+def load_NgffPlateMeta(zarr_path: str) -> NgffPlateMeta:
+    """
+    Load the attributes of a zarr group and cast them to `NgffPlateMeta`.
+
+    Args:
+        zarr_path: Path to the zarr group.
+
+    Returns:
+        A new `NgffPlateMeta` object.
+    """
+    try:
+        zarr_group = zarr.open_group(zarr_path, mode="r")
+    except GroupNotFoundError:
+        error_msg = (
+            "Could not load attributes for the requested plate, "
+            f"because no Zarr group was found at {zarr_path}"
+        )
+        logging.error(error_msg)
+        raise ZarrGroupNotFoundError(error_msg)
+    zarr_attrs = zarr_group.attrs.asdict()
+    try:
+        return NgffPlateMeta(**zarr_attrs)
+    except Exception as e:
+        logging.error(
+            f"Contents of {zarr_path} cannot be cast to NgffPlateMeta.\n"
+            f"Original error:\n{str(e)}"
+        )
+        raise e
+
+
 def detect_ome_ngff_type(group: zarr.hierarchy.Group) -> str:
     """
     Given a Zarr group, find whether it is an OME-NGFF plate, well or image.
diff --git a/fractal_tasks_core/tables/v1.py b/fractal_tasks_core/tables/v1.py
index 9b77a5e1d..67d26f73e 100644
--- a/fractal_tasks_core/tables/v1.py
+++ b/fractal_tasks_core/tables/v1.py
@@ -264,3 +264,62 @@ def _write_table_v1(
     table_group.attrs.update(**table_attrs)
 
     return table_group
+
+
+def get_tables_list_v1(
+    zarr_url: str, table_type: str = None, strict: bool = False
+) -> list[str]:
+    """
+    Find the list of tables in the Zarr file
+
+    Optionally match a table type and only return the names of those tables.
+
+    Args:
+        zarr_url: Path to the OME-Zarr image
+        table_type: The type of table to look for. Special handling for
+            "ROIs" => matches both "roi_table" & "masking_roi_table".
+        strict: If `True`, only return tables that have a type attribute.
+            If `False`, also include tables without a type attribute.
+
+    Returns:
+        List of the names of available tables
+    """
+    with zarr.open(zarr_url, mode="r") as zarr_group:
+        zarr_subgroups = list(zarr_group.group_keys())
+    if "tables" not in zarr_subgroups:
+        return []
+    with zarr.open(zarr_url, mode="r") as zarr_group:
+        all_tables = list(zarr_group.tables.group_keys())
+
+    if not table_type:
+        return all_tables
+    else:
+        return _filter_tables_by_type_v1(
+            zarr_url, all_tables, table_type, strict
+        )
+
+
+def _filter_tables_by_type_v1(
+    zarr_url: str,
+    all_tables: list[str],
+    table_type: Optional[str] = None,
+    strict: bool = False,
+) -> list[str]:
+    tables_list = []
+    for table_name in all_tables:
+        with zarr.open(zarr_url, mode="r").tables[table_name] as table:
+            table_attrs = table.attrs.asdict()
+            if "type" in table_attrs:
+                if table_type == "ROIs":
+                    roi_table_types = ["roi_table", "masking_roi_table"]
+                    if table_attrs["type"] in roi_table_types:
+                        tables_list.append(table_name)
+                elif table_attrs["type"] == table_type:
+                    tables_list.append(table_name)
+            else:
+                # If there are tables without types, let the users choose
+                # from all tables
+                logger.warning(f"Table {table_name} had no type attribute.")
+                if not strict:
+                    tables_list.append(table_name)
+    return tables_list
diff --git a/fractal_tasks_core/tasks/_registration_utils.py b/fractal_tasks_core/tasks/_registration_utils.py
new file mode 100644
index 000000000..d74744fd7
--- /dev/null
+++ b/fractal_tasks_core/tasks/_registration_utils.py
@@ -0,0 +1,246 @@
+# Copyright 2024 (C) BioVisionCenter
+#
+# Original authors:
+# Joel Lüthi <joel.luethi@uzh.ch>
+#
+# This file is part of Fractal
+"""Utils functions for registration"""
+import copy
+
+import anndata as ad
+import numpy as np
+import pandas as pd
+
+from fractal_tasks_core.ngff.zarr_utils import load_NgffWellMeta
+
+
+def _split_well_path_image_path(zarr_url: str) -> tuple[str, str]:
+    """
+    Returns path to well folder for HCS OME-Zarr `zarr_url`.
+    """
+    zarr_url = zarr_url.rstrip("/")
+    well_path = "/".join(zarr_url.split("/")[:-1])
+    img_path = zarr_url.split("/")[-1]
+    return well_path, img_path
+
+
+def create_well_acquisition_dict(
+    zarr_urls: list[str],
+) -> dict[str, dict[int, str]]:
+    """
+    Parses zarr_urls & groups them by HCS wells & acquisition
+
+    Generates a dict with keys a unique description of the acquisition
+    (e.g. plate + well for HCS plates). The values are dictionaries. The keys
+    of the secondary dictionary are the acqusitions, its values the `zarr_url`
+    for a given acquisition.
+
+    Args:
+        zarr_urls: List of zarr_urls
+
+    Returns:
+        image_groups
+    """
+    image_groups = dict()
+
+    # Dict to cache well-level metadata
+    well_metadata = dict()
+    for zarr_url in zarr_urls:
+        well_path, img_sub_path = _split_well_path_image_path(zarr_url)
+        # For the first zarr_url of a well, load the well metadata and
+        # initialize the image_groups dict
+        if well_path not in image_groups:
+            well_meta = load_NgffWellMeta(well_path)
+            well_metadata[well_path] = well_meta.well
+            image_groups[well_path] = {}
+
+        # For every zarr_url, add it under the well_path & acquisition keys to
+        # the image_groups dict
+        for image in well_metadata[well_path].images:
+            if image.path == img_sub_path:
+                if image.acquisition in image_groups[well_path]:
+                    raise ValueError(
+                        "This task has not been built for OME-Zarr HCS plates"
+                        "with multiple images of the same acquisition per well"
+                        f". {image.acquisition} is the acquisition for "
+                        f"multiple images in {well_path=}."
+                    )
+
+                image_groups[well_path][image.acquisition] = zarr_url
+    return image_groups
+
+
+def calculate_physical_shifts(
+    shifts: np.array,
+    level: int,
+    coarsening_xy: int,
+    full_res_pxl_sizes_zyx: list[float],
+) -> list[float]:
+    """
+    Calculates shifts in physical units based on pixel shifts
+
+    Args:
+        shifts: array of shifts, zyx or yx
+        level: resolution level
+        coarsening_xy: coarsening factor between levels
+        full_res_pxl_sizes_zyx: pixel sizes in physical units as zyx
+
+    Returns:
+        shifts in physical units as zyx
+    """
+
+    curr_pixel_size = np.array(full_res_pxl_sizes_zyx) * coarsening_xy**level
+    if len(shifts) == 3:
+        shifts_physical = shifts * curr_pixel_size
+    elif len(shifts) == 2:
+        shifts_physical = [
+            0,
+            shifts[0] * curr_pixel_size[1],
+            shifts[1] * curr_pixel_size[2],
+        ]
+    else:
+        raise ValueError(
+            f"Wrong input for calculate_physical_shifts ({shifts=})"
+        )
+    return shifts_physical
+
+
+def get_ROI_table_with_translation(
+    ROI_table: ad.AnnData,
+    new_shifts: dict[str, list[float]],
+) -> ad.AnnData:
+    """
+    Adds translation columns to a ROI table
+
+    Args:
+        ROI_table: Fractal ROI table
+        new_shifts: zyx list of shifts
+
+    Returns:
+        Fractal ROI table with 3 additional columns for calculated translations
+    """
+
+    shift_table = pd.DataFrame(new_shifts).T
+    shift_table.columns = ["translation_z", "translation_y", "translation_x"]
+    shift_table = shift_table.rename_axis("FieldIndex")
+    new_roi_table = ROI_table.to_df().merge(
+        shift_table, left_index=True, right_index=True
+    )
+    if len(new_roi_table) != len(ROI_table):
+        raise ValueError(
+            "New ROI table with registration info has a "
+            f"different length ({len(new_roi_table)=}) "
+            f"from the original ROI table ({len(ROI_table)=})"
+        )
+
+    adata = ad.AnnData(X=new_roi_table.astype(np.float32))
+    adata.obs_names = new_roi_table.index
+    adata.var_names = list(map(str, new_roi_table.columns))
+    return adata
+
+
+# Helper functions
+def add_zero_translation_columns(ad_table: ad.AnnData):
+    """
+    Add three zero-filled columns (`translation_{x,y,z}`) to an AnnData table.
+    """
+    columns = ["translation_z", "translation_y", "translation_x"]
+    if ad_table.var.index.isin(columns).any().any():
+        raise ValueError(
+            "The roi table already contains translation columns. Did you "
+            "enter a wrong reference cycle?"
+        )
+    df = pd.DataFrame(np.zeros([len(ad_table), 3]), columns=columns)
+    df.index = ad_table.obs.index
+    ad_new = ad.concat([ad_table, ad.AnnData(df)], axis=1)
+    return ad_new
+
+
+def calculate_min_max_across_dfs(tables_list):
+    # Initialize dataframes to store the max and min values
+    max_df = pd.DataFrame(
+        index=tables_list[0].index, columns=tables_list[0].columns
+    )
+    min_df = pd.DataFrame(
+        index=tables_list[0].index, columns=tables_list[0].columns
+    )
+
+    # Loop through the tables and calculate max and min values
+    for table in tables_list:
+        max_df = pd.DataFrame(
+            np.maximum(max_df.values, table.values),
+            columns=max_df.columns,
+            index=max_df.index,
+        )
+        min_df = pd.DataFrame(
+            np.minimum(min_df.values, table.values),
+            columns=min_df.columns,
+            index=min_df.index,
+        )
+
+    return max_df, min_df
+
+
+def apply_registration_to_single_ROI_table(
+    roi_table: ad.AnnData,
+    max_df: pd.DataFrame,
+    min_df: pd.DataFrame,
+) -> ad.AnnData:
+    """
+    Applies the registration to a ROI table
+
+    Calculates the new position as: p = position + max(shift, 0) - own_shift
+    Calculates the new len as: l = len - max(shift, 0) + min(shift, 0)
+
+    Args:
+        roi_table: AnnData table which contains a Fractal ROI table.
+            Rows are ROIs
+        max_df: Max translation shift in z, y, x for each ROI. Rows are ROIs,
+            columns are translation_z, translation_y, translation_x
+        min_df: Min translation shift in z, y, x for each ROI. Rows are ROIs,
+            columns are translation_z, translation_y, translation_x
+    Returns:
+        ROI table where all ROIs are registered to the smallest common area
+        across all cycles.
+    """
+    roi_table = copy.deepcopy(roi_table)
+    rois = roi_table.obs.index
+    if (rois != max_df.index).all() or (rois != min_df.index).all():
+        raise ValueError(
+            "ROI table and max & min translation need to contain the same "
+            f"ROIS, but they were {rois=}, {max_df.index=}, {min_df.index=}"
+        )
+
+    for roi in rois:
+        roi_table[[roi], ["z_micrometer"]] = (
+            roi_table[[roi], ["z_micrometer"]].X
+            + float(max_df.loc[roi, "translation_z"])
+            - roi_table[[roi], ["translation_z"]].X
+        )
+        roi_table[[roi], ["y_micrometer"]] = (
+            roi_table[[roi], ["y_micrometer"]].X
+            + float(max_df.loc[roi, "translation_y"])
+            - roi_table[[roi], ["translation_y"]].X
+        )
+        roi_table[[roi], ["x_micrometer"]] = (
+            roi_table[[roi], ["x_micrometer"]].X
+            + float(max_df.loc[roi, "translation_x"])
+            - roi_table[[roi], ["translation_x"]].X
+        )
+        # This calculation only works if all ROIs are the same size initially!
+        roi_table[[roi], ["len_z_micrometer"]] = (
+            roi_table[[roi], ["len_z_micrometer"]].X
+            - float(max_df.loc[roi, "translation_z"])
+            + float(min_df.loc[roi, "translation_z"])
+        )
+        roi_table[[roi], ["len_y_micrometer"]] = (
+            roi_table[[roi], ["len_y_micrometer"]].X
+            - float(max_df.loc[roi, "translation_y"])
+            + float(min_df.loc[roi, "translation_y"])
+        )
+        roi_table[[roi], ["len_x_micrometer"]] = (
+            roi_table[[roi], ["len_x_micrometer"]].X
+            - float(max_df.loc[roi, "translation_x"])
+            + float(min_df.loc[roi, "translation_x"])
+        )
+    return roi_table
diff --git a/fractal_tasks_core/tasks/_utils.py b/fractal_tasks_core/tasks/_utils.py
index e262f35f1..a7c0a4b89 100644
--- a/fractal_tasks_core/tasks/_utils.py
+++ b/fractal_tasks_core/tasks/_utils.py
@@ -51,27 +51,27 @@ def run_fractal_task(
     # Parse `-j` and `--metadata-out` arguments
     parser = ArgumentParser()
     parser.add_argument(
-        "-j", "--json", help="Read parameters from json file", required=True
+        "--args-json", help="Read parameters from json file", required=True
     )
     parser.add_argument(
-        "--metadata-out",
+        "--out-json",
         help="Output file to redirect serialised returned data",
         required=True,
     )
-    args = parser.parse_args()
+    parsed_args = parser.parse_args()
 
     # Set logger
     logger = logging.getLogger(logger_name)
 
     # Preliminary check
-    if Path(args.metadata_out).exists():
+    if Path(parsed_args.out_json).exists():
         logger.error(
-            f"Output file {args.metadata_out} already exists. Terminating"
+            f"Output file {parsed_args.out_json} already exists. Terminating"
         )
         exit(1)
 
     # Read parameters dictionary
-    with open(args.json, "r") as f:
+    with open(parsed_args.args_json, "r") as f:
         pars = json.load(f)
 
     # Run task
@@ -80,5 +80,5 @@ def run_fractal_task(
     logger.info(f"END {task_function.__name__} task")
 
     # Write output metadata to file, with custom JSON encoder
-    with open(args.metadata_out, "w") as fout:
+    with open(parsed_args.out_json, "w") as fout:
         json.dump(metadata_update, fout, cls=TaskParameterEncoder, indent=2)
diff --git a/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py b/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py
deleted file mode 100644
index b9268f802..000000000
--- a/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py
+++ /dev/null
@@ -1,296 +0,0 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-# Joel Lüthi <joel.luethi@uzh.ch>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Applies the multiplexing translation to all ROI tables
-"""
-import copy
-import logging
-from typing import Any
-from typing import Optional
-from typing import Sequence
-
-import anndata as ad
-import numpy as np
-import pandas as pd
-import zarr
-from pydantic.decorator import validate_arguments
-
-from fractal_tasks_core.ngff import load_NgffWellMeta
-from fractal_tasks_core.roi import (
-    are_ROI_table_columns_valid,
-)
-from fractal_tasks_core.tables import write_table
-
-logger = logging.getLogger(__name__)
-
-
-@validate_arguments
-def apply_registration_to_ROI_tables(
-    *,
-    # Fractal arguments
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
-    # Task-specific arguments
-    roi_table: str = "FOV_ROI_table",
-    reference_cycle: int = 0,
-    new_roi_table: Optional[str] = None,
-) -> dict[str, Any]:
-    """
-    Applies pre-calculated registration to ROI tables.
-
-    Apply pre-calculated registration such that resulting ROIs contain
-    the consensus align region between all cycles.
-
-    Parallelization level: well
-
-    Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file. Example:
-            `["/some/path/"]`. This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: This parameter is not used by this task.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed. Example: `"some_plate.zarr/B/03/0"`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        roi_table: Name of the ROI table over which the task loops to
-            calculate the registration. Examples: `FOV_ROI_table` => loop over
-            the field of views, `well_ROI_table` => process the whole well as
-            one image.
-        reference_cycle: Which cycle to register against. Defaults to 0,
-            which is the first OME-Zarr image in the well, usually the first
-            cycle that was provided
-        new_roi_table: Optional name for the new, registered ROI table. If no
-            name is given, it will default to "registered_" + `roi_table`
-
-    """
-    if not new_roi_table:
-        new_roi_table = "registered_" + roi_table
-    logger.info(
-        f"Running for {input_paths=}, {component=}. \n"
-        f"Applyg translation registration to {roi_table=} and storing it as "
-        f"{new_roi_table=}."
-    )
-
-    well_zarr = f"{input_paths[0]}/{component}"
-    ngff_well_meta = load_NgffWellMeta(well_zarr)
-    acquisition_dict = ngff_well_meta.get_acquisition_paths()
-    logger.info(
-        "Calculating common registration for the following cycles: "
-        f"{acquisition_dict}"
-    )
-
-    # TODO: Allow a filter on which acquisitions should get processed?
-
-    # Collect all the ROI tables
-    roi_tables = {}
-    roi_tables_attrs = {}
-    for acq in acquisition_dict.keys():
-        acq_path = acquisition_dict[acq]
-        curr_ROI_table = ad.read_zarr(
-            f"{well_zarr}/{acq_path}/tables/{roi_table}"
-        )
-        curr_ROI_table_group = zarr.open_group(
-            f"{well_zarr}/{acq_path}/tables/{roi_table}", mode="r"
-        )
-        curr_ROI_table_attrs = curr_ROI_table_group.attrs.asdict()
-
-        # For reference_cycle acquisition, handle the fact that it doesn't
-        # have the shifts
-        if acq == reference_cycle:
-            curr_ROI_table = add_zero_translation_columns(curr_ROI_table)
-        # Check for valid ROI tables
-        are_ROI_table_columns_valid(table=curr_ROI_table)
-        translation_columns = [
-            "translation_z",
-            "translation_y",
-            "translation_x",
-        ]
-        if curr_ROI_table.var.index.isin(translation_columns).sum() != 3:
-            raise ValueError(
-                f"Cycle {acq}'s {roi_table} does not contain the "
-                f"translation columns {translation_columns} necessary to use "
-                "this task."
-            )
-        roi_tables[acq] = curr_ROI_table
-        roi_tables_attrs[acq] = curr_ROI_table_attrs
-
-    # Check that all acquisitions have the same ROIs
-    rois = roi_tables[reference_cycle].obs.index
-    for acq, acq_roi_table in roi_tables.items():
-        if not (acq_roi_table.obs.index == rois).all():
-            raise ValueError(
-                f"Acquisition {acq} does not contain the same ROIs as the "
-                f"reference acquisition {reference_cycle}:\n"
-                f"{acq}: {acq_roi_table.obs.index}\n"
-                f"{reference_cycle}: {rois}"
-            )
-
-    roi_table_dfs = [
-        roi_table.to_df().loc[:, translation_columns]
-        for roi_table in roi_tables.values()
-    ]
-    logger.info("Calculating min & max translation across cycles.")
-    max_df, min_df = calculate_min_max_across_dfs(roi_table_dfs)
-    shifted_rois = {}
-    # Loop over acquisitions
-    for acq in acquisition_dict.keys():
-        shifted_rois[acq] = apply_registration_to_single_ROI_table(
-            roi_tables[acq], max_df, min_df
-        )
-
-        # TODO: Drop translation columns from this table?
-
-        logger.info(
-            f"Write the registered ROI table {new_roi_table} for {acq=}"
-        )
-        # Save the shifted ROI table as a new table
-        image_group = zarr.group(f"{well_zarr}/{acq}")
-        write_table(
-            image_group,
-            new_roi_table,
-            shifted_rois[acq],
-            table_attrs=roi_tables_attrs[acq],
-        )
-
-    # TODO: Optionally apply registration to other tables as well?
-    # e.g. to well_ROI_table based on FOV_ROI_table
-    # => out of scope for the initial task, apply registration separately
-    # to each table
-    # Easiest implementation: Apply average shift calculcated here to other
-    # ROIs. From many to 1 (e.g. FOV => well) => average shift, but crop len
-    # From well to many (e.g. well to FOVs) => average shift, crop len by that
-    # amount
-    # Many to many (FOVs to organoids) => tricky because of matching
-
-    return {}
-
-
-# Helper functions
-def add_zero_translation_columns(ad_table: ad.AnnData):
-    """
-    Add three zero-filled columns (`translation_{x,y,z}`) to an AnnData table.
-    """
-    columns = ["translation_z", "translation_y", "translation_x"]
-    if ad_table.var.index.isin(columns).any().any():
-        raise ValueError(
-            "The roi table already contains translation columns. Did you "
-            "enter a wrong reference cycle?"
-        )
-    df = pd.DataFrame(np.zeros([len(ad_table), 3]), columns=columns)
-    df.index = ad_table.obs.index
-    ad_new = ad.concat([ad_table, ad.AnnData(df)], axis=1)
-    return ad_new
-
-
-def calculate_min_max_across_dfs(tables_list):
-    # Initialize dataframes to store the max and min values
-    max_df = pd.DataFrame(
-        index=tables_list[0].index, columns=tables_list[0].columns
-    )
-    min_df = pd.DataFrame(
-        index=tables_list[0].index, columns=tables_list[0].columns
-    )
-
-    # Loop through the tables and calculate max and min values
-    for table in tables_list:
-        max_df = pd.DataFrame(
-            np.maximum(max_df.values, table.values),
-            columns=max_df.columns,
-            index=max_df.index,
-        )
-        min_df = pd.DataFrame(
-            np.minimum(min_df.values, table.values),
-            columns=min_df.columns,
-            index=min_df.index,
-        )
-
-    return max_df, min_df
-
-
-def apply_registration_to_single_ROI_table(
-    roi_table: ad.AnnData,
-    max_df: pd.DataFrame,
-    min_df: pd.DataFrame,
-) -> ad.AnnData:
-    """
-    Applies the registration to a ROI table
-
-    Calculates the new position as: p = position + max(shift, 0) - own_shift
-    Calculates the new len as: l = len - max(shift, 0) + min(shift, 0)
-
-    Args:
-        roi_table: AnnData table which contains a Fractal ROI table.
-            Rows are ROIs
-        max_df: Max translation shift in z, y, x for each ROI. Rows are ROIs,
-            columns are translation_z, translation_y, translation_x
-        min_df: Min translation shift in z, y, x for each ROI. Rows are ROIs,
-            columns are translation_z, translation_y, translation_x
-    Returns:
-        ROI table where all ROIs are registered to the smallest common area
-        across all cycles.
-    """
-    roi_table = copy.deepcopy(roi_table)
-    rois = roi_table.obs.index
-    if (rois != max_df.index).all() or (rois != min_df.index).all():
-        raise ValueError(
-            "ROI table and max & min translation need to contain the same "
-            f"ROIS, but they were {rois=}, {max_df.index=}, {min_df.index=}"
-        )
-
-    for roi in rois:
-        roi_table[[roi], ["z_micrometer"]] = (
-            roi_table[[roi], ["z_micrometer"]].X
-            + float(max_df.loc[roi, "translation_z"])
-            - roi_table[[roi], ["translation_z"]].X
-        )
-        roi_table[[roi], ["y_micrometer"]] = (
-            roi_table[[roi], ["y_micrometer"]].X
-            + float(max_df.loc[roi, "translation_y"])
-            - roi_table[[roi], ["translation_y"]].X
-        )
-        roi_table[[roi], ["x_micrometer"]] = (
-            roi_table[[roi], ["x_micrometer"]].X
-            + float(max_df.loc[roi, "translation_x"])
-            - roi_table[[roi], ["translation_x"]].X
-        )
-        # This calculation only works if all ROIs are the same size initially!
-        roi_table[[roi], ["len_z_micrometer"]] = (
-            roi_table[[roi], ["len_z_micrometer"]].X
-            - float(max_df.loc[roi, "translation_z"])
-            + float(min_df.loc[roi, "translation_z"])
-        )
-        roi_table[[roi], ["len_y_micrometer"]] = (
-            roi_table[[roi], ["len_y_micrometer"]].X
-            - float(max_df.loc[roi, "translation_y"])
-            + float(min_df.loc[roi, "translation_y"])
-        )
-        roi_table[[roi], ["len_x_micrometer"]] = (
-            roi_table[[roi], ["len_x_micrometer"]].X
-            - float(max_df.loc[roi, "translation_x"])
-            + float(min_df.loc[roi, "translation_x"])
-        )
-    return roi_table
-
-
-if __name__ == "__main__":
-    from fractal_tasks_core.tasks._utils import run_fractal_task
-
-    run_fractal_task(
-        task_function=apply_registration_to_ROI_tables,
-        logger_name=logger.name,
-    )
diff --git a/fractal_tasks_core/tasks/apply_registration_to_image.py b/fractal_tasks_core/tasks/apply_registration_to_image.py
index 0faf21935..2c84b4313 100644
--- a/fractal_tasks_core/tasks/apply_registration_to_image.py
+++ b/fractal_tasks_core/tasks/apply_registration_to_image.py
@@ -15,10 +15,7 @@
 import os
 import shutil
 import time
-from pathlib import Path
-from typing import Any
 from typing import Callable
-from typing import Sequence
 
 import anndata as ad
 import dask.array as da
@@ -37,7 +34,7 @@
 from fractal_tasks_core.roi import is_standard_roi_table
 from fractal_tasks_core.roi import load_region
 from fractal_tasks_core.tables import write_table
-from fractal_tasks_core.utils import get_table_path_dict
+from fractal_tasks_core.utils import _get_table_path_dict
 
 logger = logging.getLogger(__name__)
 
@@ -45,11 +42,8 @@
 @validate_arguments
 def apply_registration_to_image(
     *,
-    # Fractal arguments
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    # Fractal argument
+    zarr_url: str,
     # Task-specific arguments
     registered_roi_table: str,
     reference_cycle: str = "0",
@@ -74,17 +68,7 @@ def apply_registration_to_image(
     Parallelization level: image
 
     Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file. Example:
-            `["/some/path/"]`. This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: This parameter is not used by this task.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed. Example: `"some_plate.zarr/B/03/0"`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
         registered_roi_table: Name of the ROI table which has been registered
             and will be applied to mask and shift the images.
@@ -99,33 +83,26 @@ def apply_registration_to_image(
             `overwrite_input=True`.
 
     """
-    logger.info(component)
-    if not overwrite_input:
-        raise NotImplementedError(
-            "This task is only implemented for the overwrite_input version"
-        )
+    logger.info(zarr_url)
     logger.info(
-        f"Running `apply_registration_to_image` on {input_paths=}, "
-        f"{component=}, {registered_roi_table=} and {reference_cycle=}. "
+        f"Running `apply_registration_to_image` on {zarr_url=}, "
+        f"{registered_roi_table=} and {reference_cycle=}. "
         f"Using {overwrite_input=}"
     )
 
-    input_path = Path(input_paths[0])
-    new_component = "/".join(
-        component.split("/")[:-1] + [component.split("/")[-1] + "_registered"]
-    )
-    reference_component = "/".join(
-        component.split("/")[:-1] + [reference_cycle]
+    new_zarr_url = "/".join(
+        zarr_url.split("/")[:-1] + [zarr_url.split("/")[-1] + "_registered"]
     )
+    # TODO: Make this more robust by actually checking for acquisition
+    # metadata
+    reference_zarr_url = "/".join(zarr_url.split("/")[:-1] + [reference_cycle])
 
     ROI_table_ref = ad.read_zarr(
-        f"{input_path / reference_component}/tables/{registered_roi_table}"
-    )
-    ROI_table_cycle = ad.read_zarr(
-        f"{input_path / component}/tables/{registered_roi_table}"
+        f"{reference_zarr_url}/tables/{registered_roi_table}"
     )
+    ROI_table_cycle = ad.read_zarr(f"{zarr_url}/tables/{registered_roi_table}")
 
-    ngff_image_meta = load_NgffImageMeta(str(input_path / component))
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     coarsening_xy = ngff_image_meta.coarsening_xy
     num_levels = ngff_image_meta.num_levels
 
@@ -134,9 +111,8 @@ def apply_registration_to_image(
     ####################
     logger.info("Write the registered Zarr image to disk")
     write_registered_zarr(
-        input_path=input_path,
-        component=component,
-        new_component=new_component,
+        zarr_url=zarr_url,
+        new_zarr_url=new_zarr_url,
         ROI_table=ROI_table_cycle,
         ROI_table_ref=ROI_table_ref,
         num_levels=num_levels,
@@ -148,23 +124,20 @@ def apply_registration_to_image(
     # Process labels
     ####################
     try:
-        labels_group = zarr.open_group(f"{input_path / component}/labels", "r")
+        labels_group = zarr.open_group(f"{zarr_url}/labels", "r")
         label_list = labels_group.attrs["labels"]
     except (zarr.errors.GroupNotFoundError, KeyError):
         label_list = []
 
     if label_list:
         logger.info(f"Processing the label images: {label_list}")
-        labels_group = zarr.group(f"{input_path / new_component}/labels")
+        labels_group = zarr.group(f"{new_zarr_url}/labels")
         labels_group.attrs["labels"] = label_list
 
         for label in label_list:
-            label_component = f"{component}/labels/{label}"
-            label_component_new = f"{new_component}/labels/{label}"
             write_registered_zarr(
-                input_path=input_path,
-                component=label_component,
-                new_component=label_component_new,
+                zarr_url=f"{zarr_url}/labels/{label}",
+                new_zarr_url=f"{new_zarr_url}/labels/{label}",
                 ROI_table=ROI_table_cycle,
                 ROI_table_ref=ROI_table_ref,
                 num_levels=num_levels,
@@ -177,8 +150,8 @@ def apply_registration_to_image(
     # 1. Copy all standard ROI tables from cycle 0.
     # 2. Copy all tables that aren't standard ROI tables from the given cycle
     ####################
-    table_dict_reference = get_table_path_dict(input_path, reference_component)
-    table_dict_component = get_table_path_dict(input_path, component)
+    table_dict_reference = _get_table_path_dict(reference_zarr_url)
+    table_dict_component = _get_table_path_dict(zarr_url)
 
     table_dict = {}
     # Define which table should get copied:
@@ -187,9 +160,9 @@ def apply_registration_to_image(
             table_dict[table] = table_dict_reference[table]
     for table in table_dict_component:
         if not is_standard_roi_table(table):
-            if reference_component != component:
+            if reference_zarr_url != zarr_url:
                 logger.warning(
-                    f"{component} contained a table that is not a standard "
+                    f"{zarr_url} contained a table that is not a standard "
                     "ROI table. The `Apply Registration To Image task` is "
                     "best used before additional tables are generated. It "
                     f"will copy the {table} from this cycle without applying "
@@ -203,7 +176,7 @@ def apply_registration_to_image(
 
     if table_dict:
         logger.info(f"Processing the tables: {table_dict}")
-        new_image_group = zarr.group(f"{input_path / new_component}")
+        new_image_group = zarr.group(new_zarr_url)
 
         for table in table_dict.keys():
             logger.info(f"Copying table: {table}")
@@ -232,6 +205,7 @@ def apply_registration_to_image(
                 table,
                 curr_table,
                 table_attrs=old_table_group.attrs.asdict(),
+                overwrite=True,
             )
 
     ####################
@@ -244,20 +218,21 @@ def apply_registration_to_image(
         # Potential for race conditions: Every cycle reads the
         # reference cycle, but the reference cycle also gets modified
         # See issue #516 for the details
-        os.rename(f"{input_path / component}", f"{input_path / component}_tmp")
-        os.rename(f"{input_path / new_component}", f"{input_path / component}")
-        shutil.rmtree(f"{input_path / component}_tmp")
+        os.rename(zarr_url, f"{zarr_url}_tmp")
+        os.rename(new_zarr_url, zarr_url)
+        shutil.rmtree(f"{zarr_url}_tmp")
+        image_list_updates = dict(image_list_updates=[dict(zarr_url=zarr_url)])
     else:
-        raise NotImplementedError
-        # The thing that would be missing in this branch is that Fractal
-        # isn't aware of the new component. If there's a way to add it back,
-        # that's the only thing that would be required here
+        image_list_updates = dict(
+            image_list_updates=[dict(zarr_url=new_zarr_url, origin=zarr_url)]
+        )
+
+    return image_list_updates
 
 
 def write_registered_zarr(
-    input_path: Path,
-    component: str,
-    new_component: str,
+    zarr_url: str,
+    new_zarr_url: str,
     ROI_table: ad.AnnData,
     ROI_table_ref: ad.AnnData,
     num_levels: int,
@@ -276,13 +251,9 @@ def write_registered_zarr(
     the two lists of ROI indices vary.
 
     Args:
-        input_path: Base folder where the Zarr is stored
-            (does not contain the Zarr file itself)
-        component: Path to the OME-Zarr image that is processed. For example:
-            `"20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/1"`
-        new_component: Path to the new Zarr image that will be written
-            (also in the input_path folder). For example:
-            `"20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/1_registered"`
+        zarr_url: Path or url to the individual OME-Zarr image to be used as
+            the basis for the new OME-Zarr image.
+        new_zarr_url: Path or url to the new OME-Zarr image to be written
         ROI_table: Fractal ROI table for the component
         ROI_table_ref: Fractal ROI table for the reference cycle
         num_levels: Number of pyramid layers to be created (argument of
@@ -293,7 +264,7 @@ def write_registered_zarr(
 
     """
     # Read pixel sizes from Zarr attributes
-    ngff_image_meta = load_NgffImageMeta(str(input_path / component))
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(level=0)
 
     # Create list of indices for 3D ROIs
@@ -310,9 +281,9 @@ def write_registered_zarr(
         full_res_pxl_sizes_zyx=pxl_sizes_zyx,
     )
 
-    old_image_group = zarr.open_group(f"{input_path / component}", mode="r")
-    old_ngff_image_meta = load_NgffImageMeta(str(input_path / component))
-    new_image_group = zarr.group(f"{input_path / new_component}")
+    old_image_group = zarr.open_group(zarr_url, mode="r")
+    old_ngff_image_meta = load_NgffImageMeta(zarr_url)
+    new_image_group = zarr.group(new_zarr_url)
     new_image_group.attrs.put(old_image_group.attrs.asdict())
 
     # Loop over all channels. For each channel, write full-res image data.
@@ -364,7 +335,7 @@ def write_registered_zarr(
             )
 
     new_array.to_zarr(
-        f"{input_path / new_component}/0",
+        f"{new_zarr_url}/0",
         overwrite=True,
         dimension_separator="/",
         write_empty_chunks=False,
@@ -373,7 +344,7 @@ def write_registered_zarr(
     # Starting from on-disk highest-resolution data, build and write to
     # disk a pyramid of coarser levels
     build_pyramid(
-        zarrurl=f"{input_path / new_component}",
+        zarrurl=new_zarr_url,
         overwrite=True,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
diff --git a/fractal_tasks_core/tasks/calculate_registration_image_based.py b/fractal_tasks_core/tasks/calculate_registration_image_based.py
index 015dcc4fd..86c1ad4f0 100644
--- a/fractal_tasks_core/tasks/calculate_registration_image_based.py
+++ b/fractal_tasks_core/tasks/calculate_registration_image_based.py
@@ -13,14 +13,10 @@
 Calculates translation for image-based registration
 """
 import logging
-from pathlib import Path
-from typing import Any
-from typing import Sequence
 
 import anndata as ad
 import dask.array as da
 import numpy as np
-import pandas as pd
 import zarr
 from pydantic.decorator import validate_arguments
 from skimage.registration import phase_cross_correlation
@@ -37,6 +33,13 @@
 )
 from fractal_tasks_core.roi import load_region
 from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tasks._registration_utils import (
+    calculate_physical_shifts,
+)
+from fractal_tasks_core.tasks._registration_utils import (
+    get_ROI_table_with_translation,
+)
+from fractal_tasks_core.tasks.io_models import InitArgsRegistration
 
 logger = logging.getLogger(__name__)
 
@@ -45,16 +48,13 @@
 def calculate_registration_image_based(
     *,
     # Fractal arguments
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    zarr_url: str,
+    init_args: InitArgsRegistration,
     # Task-specific arguments
     wavelength_id: str,
     roi_table: str = "FOV_ROI_table",
-    reference_cycle: int = 0,
     level: int = 2,
-) -> dict[str, Any]:
+) -> None:
     """
     Calculate registration based on images
 
@@ -64,20 +64,12 @@ def calculate_registration_image_based(
     2. Calculating the transformation for that ROI
     3. Storing the calculated transformation in the ROI table
 
-    Parallelization level: image
-
     Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file. Example:
-            `["/some/path/"]`. This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: This parameter is not used by this task.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed. Example: `"some_plate.zarr/B/03/0"`.
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        init_args: Intialization arguments provided by
+            `image_based_registration_hcs_init`. They contain the
+            reference_zarr_url that is used for registration.
             (standard argument for Fractal tasks, managed by Fractal server).
         wavelength_id: Wavelength that will be used for image-based
             registration; e.g. `A01_C01` for Yokogawa, `C01` for MD.
@@ -85,69 +77,49 @@ def calculate_registration_image_based(
             calculate the registration. Examples: `FOV_ROI_table` => loop over
             the field of views, `well_ROI_table` => process the whole well as
             one image.
-        reference_cycle: Which cycle to register against. Defaults to 0,
-            which is the first OME-Zarr image in the well (usually the first
-            cycle that was provided).
-        level: Pyramid level of the image to be segmented. Choose `0` to
-            process at full resolution.
+        level: Pyramid level of the image to be used for registration.
+            Choose `0` to process at full resolution.
 
     """
     logger.info(
-        f"Running for {input_paths=}, {component=}. \n"
+        f"Running for {zarr_url=}.\n"
         f"Calculating translation registration per {roi_table=} for "
         f"{wavelength_id=}."
     )
-    # Set OME-Zarr paths
-    zarr_img_cycle_x = Path(input_paths[0]) / component
-
-    # If the task is run for the reference cycle, exit
-    # TODO: Improve the input for this: Can we filter components to not
-    # run for itself?
-    alignment_cycle = zarr_img_cycle_x.name
-    if alignment_cycle == str(reference_cycle):
-        logger.info(
-            "Calculate registration image-based is running for "
-            f"cycle {alignment_cycle}, which is the reference_cycle."
-            "Thus, exiting the task."
-        )
-        return {}
-    else:
-        logger.info(
-            "Calculate registration image-based is running for "
-            f"cycle {alignment_cycle}"
-        )
 
-    zarr_img_ref_cycle = zarr_img_cycle_x.parent / str(reference_cycle)
+    init_args.reference_zarr_url = init_args.reference_zarr_url
 
     # Read some parameters from Zarr metadata
-    ngff_image_meta = load_NgffImageMeta(str(zarr_img_ref_cycle))
+    ngff_image_meta = load_NgffImageMeta(str(init_args.reference_zarr_url))
     coarsening_xy = ngff_image_meta.coarsening_xy
 
     # Get channel_index via wavelength_id.
     # Intially only allow registration of the same wavelength
     channel_ref: OmeroChannel = get_channel_from_image_zarr(
-        image_zarr_path=str(zarr_img_ref_cycle),
+        image_zarr_path=init_args.reference_zarr_url,
         wavelength_id=wavelength_id,
     )
     channel_index_ref = channel_ref.index
 
     channel_align: OmeroChannel = get_channel_from_image_zarr(
-        image_zarr_path=str(zarr_img_cycle_x),
+        image_zarr_path=zarr_url,
         wavelength_id=wavelength_id,
     )
     channel_index_align = channel_align.index
 
     # Lazily load zarr array
-    data_reference_zyx = da.from_zarr(f"{zarr_img_ref_cycle}/{level}")[
-        channel_index_ref
-    ]
-    data_alignment_zyx = da.from_zarr(f"{zarr_img_cycle_x}/{level}")[
+    data_reference_zyx = da.from_zarr(
+        f"{init_args.reference_zarr_url}/{level}"
+    )[channel_index_ref]
+    data_alignment_zyx = da.from_zarr(f"{zarr_url}/{level}")[
         channel_index_align
     ]
 
     # Read ROIs
-    ROI_table_ref = ad.read_zarr(f"{zarr_img_ref_cycle}/tables/{roi_table}")
-    ROI_table_x = ad.read_zarr(f"{zarr_img_cycle_x}/tables/{roi_table}")
+    ROI_table_ref = ad.read_zarr(
+        f"{init_args.reference_zarr_url}/tables/{roi_table}"
+    )
+    ROI_table_x = ad.read_zarr(f"{zarr_url}/tables/{roi_table}")
     logger.info(
         f"Found {len(ROI_table_x)} ROIs in {roi_table=} to be processed."
     )
@@ -161,7 +133,7 @@ def calculate_registration_image_based(
         None,
     ]
     ROI_table_ref_group = zarr.open_group(
-        f"{zarr_img_ref_cycle}/tables/{roi_table}",
+        f"{init_args.reference_zarr_url}/tables/{roi_table}",
         mode="r",
     )
     ref_table_attrs = ROI_table_ref_group.attrs.asdict()
@@ -189,7 +161,7 @@ def calculate_registration_image_based(
     # in the list will break.
 
     # Read pixel sizes from zarr attributes
-    ngff_image_meta_cycle_x = load_NgffImageMeta(str(zarr_img_cycle_x))
+    ngff_image_meta_cycle_x = load_NgffImageMeta(zarr_url)
     pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(level=0)
     pxl_sizes_zyx_cycle_x = ngff_image_meta_cycle_x.get_pixel_sizes_zyx(
         level=0
@@ -274,7 +246,7 @@ def calculate_registration_image_based(
 
     # Write physical shifts to disk (as part of the ROI table)
     logger.info(f"Updating the {roi_table=} with translation columns")
-    image_group = zarr.group(zarr_img_cycle_x)
+    image_group = zarr.group(zarr_url)
     new_ROI_table = get_ROI_table_with_translation(ROI_table_x, new_shifts)
     write_table(
         image_group,
@@ -284,77 +256,6 @@ def calculate_registration_image_based(
         table_attrs=ref_table_attrs,
     )
 
-    return {}
-
-
-def calculate_physical_shifts(
-    shifts: np.array,
-    level: int,
-    coarsening_xy: int,
-    full_res_pxl_sizes_zyx: list[float],
-) -> list[float]:
-    """
-    Calculates shifts in physical units based on pixel shifts
-
-    Args:
-        shifts: array of shifts, zyx or yx
-        level: resolution level
-        coarsening_xy: coarsening factor between levels
-        full_res_pxl_sizes_zyx: pixel sizes in physical units as zyx
-
-    Returns:
-        shifts_physical: shifts in physical units as zyx
-    """
-
-    curr_pixel_size = np.array(full_res_pxl_sizes_zyx) * coarsening_xy**level
-    if len(shifts) == 3:
-        shifts_physical = shifts * curr_pixel_size
-    elif len(shifts) == 2:
-        shifts_physical = [
-            0,
-            shifts[0] * curr_pixel_size[1],
-            shifts[1] * curr_pixel_size[2],
-        ]
-    else:
-        raise ValueError(
-            f"Wrong input for calculate_physical_shifts ({shifts=})"
-        )
-    return shifts_physical
-
-
-def get_ROI_table_with_translation(
-    ROI_table: ad.AnnData,
-    new_shifts: dict[str, list[float]],
-) -> ad.AnnData:
-    """
-    Adds translation columns to a ROI table
-
-    Args:
-        ROI_table: Fractal ROI table
-        new_shifts: zyx list of shifts
-
-    Returns:
-        Fractal ROI table with 3 additional columns for calculated translations
-    """
-
-    shift_table = pd.DataFrame(new_shifts).T
-    shift_table.columns = ["translation_z", "translation_y", "translation_x"]
-    shift_table = shift_table.rename_axis("FieldIndex")
-    new_roi_table = ROI_table.to_df().merge(
-        shift_table, left_index=True, right_index=True
-    )
-    if len(new_roi_table) != len(ROI_table):
-        raise ValueError(
-            "New ROI table with registration info has a "
-            f"different length ({len(new_roi_table)=}) "
-            f"from the original ROI table ({len(ROI_table)=})"
-        )
-
-    adata = ad.AnnData(X=new_roi_table.astype(np.float32))
-    adata.obs_names = new_roi_table.index
-    adata.var_names = list(map(str, new_roi_table.columns))
-    return adata
-
 
 if __name__ == "__main__":
     from fractal_tasks_core.tasks._utils import run_fractal_task
diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py
index 1053c5806..46f359818 100644
--- a/fractal_tasks_core/tasks/cellpose_segmentation.py
+++ b/fractal_tasks_core/tasks/cellpose_segmentation.py
@@ -16,11 +16,8 @@
 import logging
 import os
 import time
-from pathlib import Path
-from typing import Any
 from typing import Literal
 from typing import Optional
-from typing import Sequence
 
 import anndata as ad
 import cellpose
@@ -197,11 +194,8 @@ def segment_ROI(
 @validate_arguments
 def cellpose_segmentation(
     *,
-    # Fractal arguments
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    # Fractal argument
+    zarr_url: str,
     # Task-specific arguments
     level: int,
     channel: ChannelInputModel,
@@ -233,22 +227,12 @@ def cellpose_segmentation(
     stitch_threshold: float = 0.0,
     # Overwrite option
     overwrite: bool = True,
-) -> dict[str, Any]:
+) -> None:
     """
     Run cellpose segmentation on the ROIs of a single OME-Zarr image.
 
     Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file. Example:
-            `["/some/path/"]`. This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: This parameter is not used by this task.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed. Example: `"some_plate.zarr/B/03/0"`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
         level: Pyramid level of the image to be segmented. Choose `0` to
             process at full resolution.
@@ -330,11 +314,7 @@ def cellpose_segmentation(
     """
 
     # Set input path
-    if len(input_paths) > 1:
-        raise NotImplementedError
-    in_path = Path(input_paths[0])
-    zarrurl = (in_path.resolve() / component).as_posix()
-    logger.info(f"{zarrurl=}")
+    logger.info(f"{zarr_url=}")
 
     # Preliminary checks on Cellpose model
     if pretrained_model:
@@ -342,7 +322,7 @@ def cellpose_segmentation(
             raise ValueError(f"{pretrained_model=} does not exist.")
 
     # Read attributes from NGFF metadata
-    ngff_image_meta = load_NgffImageMeta(zarrurl)
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     num_levels = ngff_image_meta.num_levels
     coarsening_xy = ngff_image_meta.coarsening_xy
     full_res_pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(level=0)
@@ -360,7 +340,7 @@ def cellpose_segmentation(
     # Find channel index
     try:
         tmp_channel: OmeroChannel = get_channel_from_image_zarr(
-            image_zarr_path=zarrurl,
+            image_zarr_path=zarr_url,
             wavelength_id=channel.wavelength_id,
             label=channel.label,
         )
@@ -369,14 +349,14 @@ def cellpose_segmentation(
             "Channel not found, exit from the task.\n"
             f"Original error: {str(e)}"
         )
-        return {}
+        return None
     ind_channel = tmp_channel.index
 
     # Find channel index for second channel, if one is provided
     if channel2:
         try:
             tmp_channel_c2: OmeroChannel = get_channel_from_image_zarr(
-                image_zarr_path=zarrurl,
+                image_zarr_path=zarr_url,
                 wavelength_id=channel2.wavelength_id,
                 label=channel2.label,
             )
@@ -386,7 +366,7 @@ def cellpose_segmentation(
                 f"and label: {channel2.label} not found, exit from the task.\n"
                 f"Original error: {str(e)}"
             )
-            return {}
+            return None
         ind_channel_c2 = tmp_channel_c2.index
 
     # Set channel label
@@ -398,14 +378,14 @@ def cellpose_segmentation(
             output_label_name = f"label_{ind_channel}"
 
     # Load ZYX data
-    data_zyx = da.from_zarr(f"{zarrurl}/{level}")[ind_channel]
+    data_zyx = da.from_zarr(f"{zarr_url}/{level}")[ind_channel]
     logger.info(f"{data_zyx.shape=}")
     if channel2:
-        data_zyx_c2 = da.from_zarr(f"{zarrurl}/{level}")[ind_channel_c2]
+        data_zyx_c2 = da.from_zarr(f"{zarr_url}/{level}")[ind_channel_c2]
         logger.info(f"Second channel: {data_zyx_c2.shape=}")
 
     # Read ROI table
-    ROI_table_path = f"{zarrurl}/tables/{input_ROI_table}"
+    ROI_table_path = f"{zarr_url}/tables/{input_ROI_table}"
     ROI_table = ad.read_zarr(ROI_table_path)
 
     # Perform some checks on the ROI table
@@ -481,7 +461,7 @@ def cellpose_segmentation(
         ],
     }
 
-    image_group = zarr.group(zarrurl)
+    image_group = zarr.group(zarr_url)
     label_group = prepare_label_group(
         image_group,
         output_label_name,
@@ -493,8 +473,8 @@ def cellpose_segmentation(
     logger.info(
         f"Helper function `prepare_label_group` returned {label_group=}"
     )
-    logger.info(f"Output label path: {zarrurl}/labels/{output_label_name}/0")
-    store = zarr.storage.FSStore(f"{zarrurl}/labels/{output_label_name}/0")
+    logger.info(f"Output label path: {zarr_url}/labels/{output_label_name}/0")
+    store = zarr.storage.FSStore(f"{zarr_url}/labels/{output_label_name}/0")
     label_dtype = np.uint32
 
     # Ensure that all output shapes & chunks are 3D (for 2D data: (1, y, x))
@@ -529,7 +509,7 @@ def cellpose_segmentation(
         model = models.CellposeModel(gpu=gpu, model_type=model_type)
 
     # Initialize other things
-    logger.info(f"Start cellpose_segmentation task for {zarrurl}")
+    logger.info(f"Start cellpose_segmentation task for {zarr_url}")
     logger.info(f"relabeling: {relabeling}")
     logger.info(f"do_3D: {do_3D}")
     logger.info(f"use_gpu: {gpu}")
@@ -619,7 +599,7 @@ def cellpose_segmentation(
         if use_masks:
             preprocessing_kwargs = dict(
                 region=region,
-                current_label_path=f"{zarrurl}/labels/{output_label_name}/0",
+                current_label_path=f"{zarr_url}/labels/{output_label_name}/0",
                 ROI_table_path=ROI_table_path,
                 ROI_positional_index=i_ROI,
             )
@@ -678,14 +658,14 @@ def cellpose_segmentation(
         )
 
     logger.info(
-        f"End cellpose_segmentation task for {zarrurl}, "
+        f"End cellpose_segmentation task for {zarr_url}, "
         "now building pyramids."
     )
 
     # Starting from on-disk highest-resolution data, build and write to disk a
     # pyramid of coarser levels
     build_pyramid(
-        zarrurl=f"{zarrurl}/labels/{output_label_name}",
+        zarrurl=f"{zarr_url}/labels/{output_label_name}",
         overwrite=overwrite,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
@@ -714,10 +694,10 @@ def cellpose_segmentation(
         bbox_table.obs = labels
 
         # Write to zarr group
-        image_group = zarr.group(f"{in_path}/{component}")
+        image_group = zarr.group(zarr_url)
         logger.info(
             "Now writing bounding-box ROI table to "
-            f"{in_path}/{component}/tables/{output_ROI_table}"
+            f"{zarr_url}/tables/{output_ROI_table}"
         )
         table_attrs = {
             "type": "masking_roi_table",
@@ -732,8 +712,6 @@ def cellpose_segmentation(
             table_attrs=table_attrs,
         )
 
-    return {}
-
 
 if __name__ == "__main__":
     from fractal_tasks_core.tasks._utils import run_fractal_task
diff --git a/fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py
similarity index 58%
rename from fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py
rename to fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py
index 4e3b4d623..b880a3800 100644
--- a/fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py
+++ b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py
@@ -13,9 +13,6 @@
 Task that writes image data to an existing OME-NGFF zarr array.
 """
 import logging
-from pathlib import Path
-from typing import Any
-from typing import Sequence
 
 import dask.array as da
 import zarr
@@ -36,9 +33,7 @@
 from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.utils import (
-    get_parameters_from_metadata,
-)
+from fractal_tasks_core.tasks.io_models import InitArgsCellVoyager
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
@@ -61,12 +56,10 @@ def sort_fun(filename: str) -> list[int]:
 
 
 @validate_arguments
-def yokogawa_to_ome_zarr(
+def cellvoyager_to_ome_zarr_compute(
     *,
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    zarr_url: str,
+    init_args: InitArgsCellVoyager,
     overwrite: bool = False,
 ):
     """
@@ -77,44 +70,15 @@ def yokogawa_to_ome_zarr(
     were prepared.
 
     Args:
-        input_paths: List of input paths where the OME-Zarrs. Should point to
-            the parent folder containing one or many OME-Zarr files, not the
-            actual OME-Zarr file. Example: `["/some/path/"]`.
-            This task only supports a single input path.
-            (standard argument for Fractal tasks,
-            managed by Fractal server).
-        output_path: Unclear. Should be the same as `input_path`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed. Example: `"some_plate.zarr/B/03/0"`
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: Dictionary containing metadata about the OME-Zarr. This task
-            requires the following elements to be present in the metadata.
-            `original_paths`:
-            list of paths that correspond to the `input_paths` of the
-            `create_ome_zarr` task (=> where the microscopy image are stored);
-            `num_levels (int)`:
-            number of pyramid levels in the image (this determines how many
-            pyramid levels are built for the segmentation);
-            `coarsening_xy (int)`:
-            coarsening factor in XY of the downsampling when building the
-            pyramid;
-            `image_extension`:
-            filename extension of images (e.g. `"tif"` or `"png"`);
-            `image_glob_patterns`:
-            parameter of `create_ome_zarr` task (if specified, only parse
-            images with filenames that match with all these patterns).
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
+        init_args: Intialization arguments provided by
+            `create_cellvoyager_ome_zarr_init`.
         overwrite: If `True`, overwrite the task output.
     """
 
-    # Preliminary checks
-    if len(input_paths) > 1:
-        raise NotImplementedError
-    zarrurl = Path(input_paths[0]).as_posix() + f"/{component}"
-
     # Read attributes from NGFF metadata
-    ngff_image_meta = load_NgffImageMeta(zarrurl)
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     num_levels = ngff_image_meta.num_levels
     coarsening_xy = ngff_image_meta.coarsening_xy
     full_res_pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(level=0)
@@ -124,43 +88,19 @@ def yokogawa_to_ome_zarr(
         f"NGFF image has full-res pixel sizes {full_res_pxl_sizes_zyx}"
     )
 
-    parameters = get_parameters_from_metadata(
-        keys=[
-            "original_paths",
-            "image_extension",
-            "image_glob_patterns",
-        ],
-        metadata=metadata,
-        # FIXME: Why rely on output_path here, when we use the input path for
-        # the zarr_url? That just means that different input & output paths
-        # don't work, no?
-        image_zarr_path=(Path(output_path) / component),
-    )
-    original_path_list = parameters["original_paths"]
-    image_extension = parameters["image_extension"]
-    image_glob_patterns = parameters["image_glob_patterns"]
-
     channels: list[OmeroChannel] = get_omero_channel_list(
-        image_zarr_path=zarrurl
+        image_zarr_path=zarr_url
     )
     wavelength_ids = [c.wavelength_id for c in channels]
 
-    in_path = Path(original_path_list[0])
-
-    # Define well
-    component_split = component.split("/")
-    well_row = component_split[1]
-    well_column = component_split[2]
-    well_ID = well_row + well_column
-
     # Read useful information from ROI table
-    adata = read_zarr(f"{zarrurl}/tables/FOV_ROI_table")
+    adata = read_zarr(f"{zarr_url}/tables/FOV_ROI_table")
     fov_indices = convert_ROI_table_to_indices(
         adata,
         full_res_pxl_sizes_zyx=full_res_pxl_sizes_zyx,
     )
     check_valid_ROI_indices(fov_indices, "FOV_ROI_table")
-    adata_well = read_zarr(f"{zarrurl}/tables/well_ROI_table")
+    adata_well = read_zarr(f"{zarr_url}/tables/well_ROI_table")
     well_indices = convert_ROI_table_to_indices(
         adata_well,
         full_res_pxl_sizes_zyx=full_res_pxl_sizes_zyx,
@@ -169,18 +109,20 @@ def yokogawa_to_ome_zarr(
     if len(well_indices) > 1:
         raise ValueError(f"Something wrong with {well_indices=}")
 
-    # FIXME: Put back the choice of columns by name? Not here..
-
     max_z = well_indices[0][1]
     max_y = well_indices[0][3]
     max_x = well_indices[0][5]
 
     # Load a single image, to retrieve useful information
-    patterns = [f"*_{well_ID}_*.{image_extension}"]
-    if image_glob_patterns:
-        patterns.extend(image_glob_patterns)
+    patterns = [
+        f"{init_args.plate_prefix}_{init_args.well_ID}_*."
+        f"{init_args.image_extension}"
+    ]
+    if init_args.image_glob_patterns:
+        patterns.extend(init_args.image_glob_patterns)
+
     tmp_images = glob_with_multiple_patterns(
-        folder=str(in_path),
+        folder=init_args.image_dir,
         patterns=patterns,
     )
     sample = imread(tmp_images.pop())
@@ -192,13 +134,13 @@ def yokogawa_to_ome_zarr(
             shape=(len(wavelength_ids), max_z, max_y, max_x),
             chunks=chunksize,
             dtype=sample.dtype,
-            store=zarr.storage.FSStore(zarrurl + "/0"),
+            store=zarr.storage.FSStore(zarr_url + "/0"),
             overwrite=overwrite,
             dimension_separator="/",
         )
     except ContainsArrayError as e:
         error_msg = (
-            f"Cannot create a zarr group at '{zarrurl}/0', "
+            f"Cannot create a zarr group at '{zarr_url}/0', "
             f"with {overwrite=} (original error: {str(e)}).\n"
             "Hint: try setting overwrite=True."
         )
@@ -209,20 +151,21 @@ def yokogawa_to_ome_zarr(
     for i_c, wavelength_id in enumerate(wavelength_ids):
         A, C = wavelength_id.split("_")
 
-        patterns = [f"*_{well_ID}_*{A}*{C}*.{image_extension}"]
-        if image_glob_patterns:
-            patterns.extend(image_glob_patterns)
+        patterns = [
+            f"{init_args.plate_prefix}_{init_args.well_ID}_*{A}*{C}*."
+            f"{init_args.image_extension}"
+        ]
+        if init_args.image_glob_patterns:
+            patterns.extend(init_args.image_glob_patterns)
         filenames_set = glob_with_multiple_patterns(
-            folder=str(in_path),
+            folder=init_args.image_dir,
             patterns=patterns,
         )
         filenames = sorted(list(filenames_set), key=sort_fun)
         if len(filenames) == 0:
             raise ValueError(
                 "Error in yokogawa_to_ome_zarr: len(filenames)=0.\n"
-                f"  in_path: {in_path}\n"
-                f"  image_extension: {image_extension}\n"
-                f"  well_ID: {well_ID}\n"
+                f"  image_dir: {init_args.image_dir}\n"
                 f"  wavelength_id: {wavelength_id},\n"
                 f"  patterns: {patterns}"
             )
@@ -249,28 +192,44 @@ def yokogawa_to_ome_zarr(
     # Starting from on-disk highest-resolution data, build and write to disk a
     # pyramid of coarser levels
     build_pyramid(
-        zarrurl=zarrurl,
+        zarrurl=zarr_url,
         overwrite=overwrite,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
         chunksize=chunksize,
     )
 
-    # Deprecated: Delete images (optional)
-    # if delete_input:
-    #     for f in filenames:
-    #         try:
-    #             os.remove(f)
-    #         except OSError as e:
-    #             logging.info("Error: %s : %s" % (f, e.strerror))
+    # Generate image list updates
+    # TODO: Can we check for dimensionality more robustly? Just checks for the
+    # last FOV of the last wavelength now
+    if FOV_4D.shape[-3] > 1:
+        is_3D = True
+    else:
+        is_3D = False
+    attributes = {
+        "plate": f"{init_args.plate_prefix}.zarr",
+        "well": init_args.well_ID,
+    }
+    if init_args.acquisition is not None:
+        attributes["acquisition"] = init_args.acquisition
+
+    image_list_updates = dict(
+        image_list_updates=[
+            dict(
+                zarr_url=zarr_url,
+                attributes=attributes,
+                types={"is_3D": is_3D},
+            )
+        ]
+    )
 
-    return {}
+    return image_list_updates
 
 
 if __name__ == "__main__":
     from fractal_tasks_core.tasks._utils import run_fractal_task
 
     run_fractal_task(
-        task_function=yokogawa_to_ome_zarr,
+        task_function=cellvoyager_to_ome_zarr_compute,
         logger_name=logger.name,
     )
diff --git a/fractal_tasks_core/tasks/create_ome_zarr.py b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py
similarity index 87%
rename from fractal_tasks_core/tasks/create_ome_zarr.py
rename to fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py
index 3cfe90287..e3cd80924 100644
--- a/fractal_tasks_core/tasks/create_ome_zarr.py
+++ b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py
@@ -15,7 +15,6 @@
 from pathlib import Path
 from typing import Any
 from typing import Optional
-from typing import Sequence
 
 import pandas as pd
 from pydantic.decorator import validate_arguments
@@ -29,16 +28,18 @@
     parse_yokogawa_metadata,
 )
 from fractal_tasks_core.channels import check_unique_wavelength_ids
-from fractal_tasks_core.channels import check_well_channel_labels
 from fractal_tasks_core.channels import define_omero_channels
 from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.ngff.specs import NgffImageMeta
+from fractal_tasks_core.ngff.specs import Plate
+from fractal_tasks_core.ngff.specs import Well
 from fractal_tasks_core.roi import prepare_FOV_ROI_table
 from fractal_tasks_core.roi import prepare_well_ROI_table
 from fractal_tasks_core.roi import remove_FOV_overlaps
 from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tasks.io_models import InitArgsCellVoyager
 from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
 
-
 __OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
 
 import logging
@@ -47,11 +48,11 @@
 
 
 @validate_arguments
-def create_ome_zarr(
+def cellvoyager_to_ome_zarr_init(
     *,
-    input_paths: Sequence[str],
-    output_path: str,
-    metadata: dict[str, Any],
+    zarr_urls: list[str],
+    zarr_dir: str,
+    image_dirs: list[str],
     allowed_channels: list[OmeroChannel],
     image_glob_patterns: Optional[list[str]] = None,
     num_levels: int = 5,
@@ -77,18 +78,16 @@ def create_ome_zarr(
     - verify that channels are uniform (i.e., same channels).
 
     Args:
-        input_paths: List of input paths where the image data from
-            the microscope is stored (as TIF or PNG).  Should point to the
-            parent folder containing the images and the metadata files
-            `MeasurementData.mlf` and `MeasurementDetail.mrf` (if present).
-            Example: `["/some/path/"]`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: Path were the output of this task is stored.
-            Example: "/some/path/" => puts the new OME-Zarr file in the
-            "/some/path/".
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed. Not used by the converter task.
             (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        zarr_dir: path of the directory where the new OME-Zarrs will be
+            created.
             (standard argument for Fractal tasks, managed by Fractal server).
+        image_dirs: list of paths to the folders that contains the Cellvoyager
+            image files. Each entry is a path to a folder that contains the
+            image files themselves for a multiwell plate and the
+            MeasurementData & MeasurementDetail metadata files.
         allowed_channels: A list of `OmeroChannel` s, where each channel must
             include the `wavelength_id` attribute and where the
             `wavelength_id` values must be unique across the list.
@@ -132,15 +131,13 @@ def create_ome_zarr(
     # Preliminary checks on allowed_channels argument
     check_unique_wavelength_ids(allowed_channels)
 
-    for in_path_str in input_paths:
-        in_path = Path(in_path_str)
-
+    for image_dir in image_dirs:
         # Glob image filenames
         patterns = [f"*.{image_extension}"]
         if image_glob_patterns:
             patterns.extend(image_glob_patterns)
         input_filenames = glob_with_multiple_patterns(
-            folder=in_path_str,
+            folder=image_dir,
             patterns=patterns,
         )
 
@@ -166,7 +163,7 @@ def create_ome_zarr(
 
         info = (
             "Listing plates/channels:\n"
-            f"Folder:   {in_path_str}\n"
+            f"Folder:   {image_dir}\n"
             f"Patterns: {patterns}\n"
             f"Plates:   {tmp_plates}\n"
             f"Channels: {tmp_wavelength_ids}\n"
@@ -206,7 +203,7 @@ def create_ome_zarr(
                 )
 
         # Update dict_plate_paths
-        dict_plate_paths[plate] = in_path
+        dict_plate_paths[plate] = image_dir
 
     # Check that all channels are in the allowed_channels
     allowed_wavelength_ids = [
@@ -226,23 +223,23 @@ def create_ome_zarr(
         if channel.wavelength_id in actual_wavelength_ids
     ]
 
-    zarrurls: dict[str, list[str]] = {"plate": [], "well": [], "image": []}
-
     ################################################################
+    # Create well/image OME-Zarr folders on disk, and prepare output
+    # metadata
+    parallelization_list = []
+
     for plate in plates:
         # Define plate zarr
-        zarrurl = f"{plate}.zarr"
+        relative_zarrurl = f"{plate}.zarr"
         in_path = dict_plate_paths[plate]
-        logger.info(f"Creating {zarrurl}")
+        logger.info(f"Creating {relative_zarrurl}")
         # Call zarr.open_group wrapper, which handles overwrite=True/False
         group_plate = open_zarr_group_with_overwrite(
-            str(Path(output_path) / zarrurl),
+            str(Path(zarr_dir) / relative_zarrurl),
             overwrite=overwrite,
         )
-        zarrurls["plate"].append(zarrurl)
 
         # Obtain FOV-metadata dataframe
-
         if metadata_table_file is None:
             mrf_path = f"{in_path}/MeasurementDetail.mrf"
             mlf_path = f"{in_path}/MeasurementData.mlf"
@@ -340,10 +337,11 @@ def create_ome_zarr(
         row_list = sorted(list(set(row_list)))
         col_list = sorted(list(set(col_list)))
 
-        group_plate.attrs["plate"] = {
+        plate_attrs = {
             "acquisitions": [{"id": 0, "name": plate}],
             "columns": [{"name": col} for col in col_list],
             "rows": [{"name": row} for row in row_list],
+            "version": __OME_NGFF_VERSION__,
             "wells": [
                 {
                     "path": well_row_column[0] + "/" + well_row_column[1],
@@ -354,19 +352,36 @@ def create_ome_zarr(
             ],
         }
 
-        for row, column in well_rows_columns:
+        # Validate plate attrs:
+        Plate(**plate_attrs)
 
+        group_plate.attrs["plate"] = plate_attrs
+
+        for row, column in well_rows_columns:
+            parallelization_list.append(
+                {
+                    "zarr_url": f"{zarr_dir}/{plate}.zarr/{row}/{column}/0/",
+                    "init_args": InitArgsCellVoyager(
+                        image_dir=in_path,
+                        plate_prefix=plate_prefix,
+                        well_ID=f"{row}{column}",
+                        image_extension=image_extension,
+                        image_glob_patterns=image_glob_patterns,
+                    ).dict(),
+                }
+            )
             group_well = group_plate.create_group(f"{row}/{column}/")
 
-            group_well.attrs["well"] = {
+            well_attrs = {
                 "images": [{"path": "0"}],
                 "version": __OME_NGFF_VERSION__,
             }
 
-            group_image = group_well.create_group("0/")  # noqa: F841
-            zarrurls["well"].append(f"{plate}.zarr/{row}/{column}/")
-            zarrurls["image"].append(f"{plate}.zarr/{row}/{column}/0/")
+            # Validate well attrs:
+            Well(**well_attrs)
+            group_well.attrs["well"] = well_attrs
 
+            group_image = group_well.create_group("0/")  # noqa: F841
             group_image.attrs["multiscales"] = [
                 {
                     "version": __OME_NGFF_VERSION__,
@@ -411,7 +426,7 @@ def create_ome_zarr(
             ]
 
             group_image.attrs["omero"] = {
-                "id": 1,  # FIXME does this depend on the plate number?
+                "id": 1,  # TODO does this depend on the plate number?
                 "name": "TBD",
                 "version": __OME_NGFF_VERSION__,
                 "channels": define_omero_channels(
@@ -419,6 +434,9 @@ def create_ome_zarr(
                 ),
             }
 
+            # Validate Image attrs
+            NgffImageMeta(**group_image.attrs)
+
             # Prepare AnnData tables for FOV/well ROIs
             well_id = row + column
             FOV_ROIs_table = prepare_FOV_ROI_table(site_metadata.loc[well_id])
@@ -442,32 +460,13 @@ def create_ome_zarr(
                 table_attrs={"type": "roi_table"},
             )
 
-    # Check that the different images in each well have unique channel labels.
-    # Since we currently merge all fields of view in the same image, this check
-    # is useless. It should remain there to catch an error in case we switch
-    # back to one-image-per-field-of-view mode
-    for well_path in zarrurls["well"]:
-        check_well_channel_labels(
-            well_zarr_path=str(Path(output_path) / well_path)
-        )
-
-    metadata_update = dict(
-        plate=zarrurls["plate"],
-        well=zarrurls["well"],
-        image=zarrurls["image"],
-        num_levels=num_levels,
-        coarsening_xy=coarsening_xy,
-        image_extension=image_extension,
-        image_glob_patterns=image_glob_patterns,
-        original_paths=input_paths[:],
-    )
-    return metadata_update
+    return dict(parallelization_list=parallelization_list)
 
 
 if __name__ == "__main__":
     from fractal_tasks_core.tasks._utils import run_fractal_task
 
     run_fractal_task(
-        task_function=create_ome_zarr,
+        task_function=cellvoyager_to_ome_zarr_init,
         logger_name=logger.name,
     )
diff --git a/fractal_tasks_core/tasks/create_ome_zarr_multiplex.py b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py
similarity index 83%
rename from fractal_tasks_core/tasks/create_ome_zarr_multiplex.py
rename to fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py
index 5dd37a9c8..734802dbc 100644
--- a/fractal_tasks_core/tasks/create_ome_zarr_multiplex.py
+++ b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py
@@ -15,7 +15,6 @@
 from pathlib import Path
 from typing import Any
 from typing import Optional
-from typing import Sequence
 
 import pandas as pd
 import zarr
@@ -33,14 +32,17 @@
 from fractal_tasks_core.channels import check_unique_wavelength_ids
 from fractal_tasks_core.channels import check_well_channel_labels
 from fractal_tasks_core.channels import define_omero_channels
-from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.ngff.specs import NgffImageMeta
+from fractal_tasks_core.ngff.specs import Plate
+from fractal_tasks_core.ngff.specs import Well
 from fractal_tasks_core.roi import prepare_FOV_ROI_table
 from fractal_tasks_core.roi import prepare_well_ROI_table
 from fractal_tasks_core.roi import remove_FOV_overlaps
 from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tasks.io_models import InitArgsCellVoyager
+from fractal_tasks_core.tasks.io_models import MultiplexingAcquisition
 from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
 
-
 __OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
 
 import logging
@@ -49,12 +51,11 @@
 
 
 @validate_arguments
-def create_ome_zarr_multiplex(
+def cellvoyager_to_ome_zarr_init_multiplex(
     *,
-    input_paths: Sequence[str],
-    output_path: str,
-    metadata: dict[str, Any],
-    allowed_channels: dict[str, list[OmeroChannel]],
+    zarr_urls: list[str],
+    zarr_dir: str,
+    acquisitions: dict[str, MultiplexingAcquisition],
     image_glob_patterns: Optional[list[str]] = None,
     num_levels: int = 5,
     coarsening_xy: int = 2,
@@ -72,24 +73,15 @@ def create_ome_zarr_multiplex(
     Each element in input_paths should be treated as a different acquisition.
 
     Args:
-        input_paths: List of input paths where the image data from the
-            microscope is stored (as TIF or PNG).  Each element of the list is
-            treated as another cycle of the multiplexing data, the cycles are
-            ordered by their order in this list.  Should point to the parent
-            folder containing the images and the metadata files
-            `MeasurementData.mlf` and `MeasurementDetail.mrf` (if present).
-            Example: `["/path/cycle1/", "/path/cycle2/"]`. (standard argument
-            for Fractal tasks, managed by Fractal server).
-        output_path: Path were the output of this task is stored.
-            Example: `"/some/path/"` => puts the new OME-Zarr file in the
-            `/some/path/`.
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed. Not used by the converter task.
             (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        zarr_dir: path of the directory where the new OME-Zarrs will be
+            created.
             (standard argument for Fractal tasks, managed by Fractal server).
-        allowed_channels: A dictionary of lists of `OmeroChannel`s, where
-            each channel must include the `wavelength_id` attribute and where
-            the `wavelength_id` values must be unique across each list.
-            Dictionary keys represent channel indices (`"0","1",..`).
+        acquisitions: dictionary of acquisitions. Each key is the acquisition
+            identifier (normally 0, 1, 2, 3 etc.). Each item defines the
+            acquisition by providing the image_dir and the allowed_channels.
         image_glob_patterns: If specified, only parse images with filenames
             that match with all these patterns. Patterns must be defined as in
             https://docs.python.org/3/library/fnmatch.html, Example:
@@ -106,9 +98,9 @@ def create_ome_zarr_multiplex(
             (e.g. `"tif"` or `"png"`).
         metadata_table_files: If `None`, parse Yokogawa metadata from mrf/mlf
             files in the input_path folder; else, a dictionary of key-value
-            pairs like `(acquisition, path)` with `acquisition` a string
-            and `path` pointing to a csv file containing the parsed metadata
-            table.
+            pairs like `(acquisition, path)` with `acquisition` a string like
+            the key of the `acquisitions` dict and `path` pointing to a csv
+            file containing the parsed metadata table.
         overwrite: If `True`, overwrite the task output.
 
     Returns:
@@ -120,13 +112,13 @@ def create_ome_zarr_multiplex(
     if metadata_table_files:
 
         # Checks on the dict:
-        # 1. Acquisitions as keys (same as keys of allowed_channels)
+        # 1. Acquisitions in acquisitions dict and metadata_table_files match
         # 2. Files end with ".csv"
         # 3. Files exist.
-        if set(allowed_channels.keys()) != set(metadata_table_files.keys()):
+        if set(acquisitions.keys()) != set(metadata_table_files.keys()):
             raise ValueError(
                 "Mismatch in acquisition keys between "
-                f"{allowed_channels.keys()=} and "
+                f"{acquisitions.keys()=} and "
                 f"{metadata_table_files.keys()=}"
             )
         for f in metadata_table_files.values():
@@ -139,20 +131,17 @@ def create_ome_zarr_multiplex(
                     f"{f} (in metadata_table_file) does not exist."
                 )
 
-    # Preliminary checks on allowed_channels
+    # Preliminary checks on acquisitions
     # Note that in metadata the keys of dictionary arguments should be
     # strings (and not integers), so that they can be read from a JSON file
-    for key, _channels in allowed_channels.items():
+    for key, values in acquisitions.items():
         if not isinstance(key, str):
-            raise ValueError(f"{allowed_channels=} has non-string keys")
-        check_unique_wavelength_ids(_channels)
+            raise ValueError(f"{acquisitions=} has non-string keys")
+        check_unique_wavelength_ids(values.allowed_channels)
 
     # Identify all plates and all channels, per input folders
     dict_acquisitions: dict = {}
-
-    for ind_in_path, in_path_str in enumerate(input_paths):
-        acquisition = str(ind_in_path)
-        in_path = Path(in_path_str)
+    for acquisition, acq_input in acquisitions.items():
         dict_acquisitions[acquisition] = {}
 
         actual_wavelength_ids = []
@@ -164,7 +153,7 @@ def create_ome_zarr_multiplex(
         if image_glob_patterns:
             patterns.extend(image_glob_patterns)
         input_filenames = glob_with_multiple_patterns(
-            folder=in_path_str,
+            folder=acq_input.image_dir,
             patterns=patterns,
         )
         for fn in input_filenames:
@@ -209,7 +198,7 @@ def create_ome_zarr_multiplex(
 
         # Check that all channels are in the allowed_channels
         allowed_wavelength_ids = [
-            c.wavelength_id for c in allowed_channels[acquisition]
+            c.wavelength_id for c in acq_input.allowed_channels
         ]
         if not set(actual_wavelength_ids).issubset(
             set(allowed_wavelength_ids)
@@ -223,7 +212,7 @@ def create_ome_zarr_multiplex(
         # are present
         actual_channels = [
             channel
-            for channel in allowed_channels[acquisition]
+            for channel in acq_input.allowed_channels
             if channel.wavelength_id in actual_wavelength_ids
         ]
 
@@ -234,21 +223,24 @@ def create_ome_zarr_multiplex(
         dict_acquisitions[acquisition]["plate"] = plate
         dict_acquisitions[acquisition]["original_plate"] = original_plate
         dict_acquisitions[acquisition]["plate_prefix"] = plate_prefix
-        dict_acquisitions[acquisition]["image_folder"] = in_path
-        dict_acquisitions[acquisition]["original_paths"] = [in_path]
+        dict_acquisitions[acquisition]["image_folder"] = acq_input.image_dir
+        dict_acquisitions[acquisition]["original_paths"] = [
+            acq_input.image_dir
+        ]
         dict_acquisitions[acquisition]["actual_channels"] = actual_channels
         dict_acquisitions[acquisition][
             "actual_wavelength_ids"
         ] = actual_wavelength_ids
 
-    acquisitions = sorted(list(dict_acquisitions.keys()))
+    parallelization_list = []
+    acquisitions_sorted = sorted(list(acquisitions.keys()))
     current_plates = [item["plate"] for item in dict_acquisitions.values()]
     if len(set(current_plates)) > 1:
         raise ValueError(f"{current_plates=}")
     plate = current_plates[0]
 
-    zarrurl = dict_acquisitions[acquisitions[0]]["plate"] + ".zarr"
-    full_zarrurl = str(Path(output_path) / zarrurl)
+    zarrurl = dict_acquisitions[acquisitions_sorted[0]]["plate"] + ".zarr"
+    full_zarrurl = str(Path(zarr_dir) / zarrurl)
     logger.info(f"Creating {full_zarrurl=}")
     # Call zarr.open_group wrapper, which handles overwrite=True/False
     group_plate = open_zarr_group_with_overwrite(
@@ -260,7 +252,7 @@ def create_ome_zarr_multiplex(
                 "id": int(acquisition),
                 "name": dict_acquisitions[acquisition]["original_plate"],
             }
-            for acquisition in acquisitions
+            for acquisition in acquisitions_sorted
         ]
     }
 
@@ -268,9 +260,9 @@ def create_ome_zarr_multiplex(
     zarrurls["plate"] = [f"{plate}.zarr"]
 
     ################################################################
-    logging.info(f"{acquisitions=}")
+    logging.info(f"{acquisitions_sorted=}")
 
-    for acquisition in acquisitions:
+    for acquisition in acquisitions_sorted:
 
         # Define plate zarr
         image_folder = dict_acquisitions[acquisition]["image_folder"]
@@ -368,14 +360,32 @@ def create_ome_zarr_multiplex(
             }
             for well_row_column in well_rows_columns
         ]
+        plate_attrs["version"] = __OME_NGFF_VERSION__
+        # Validate plate attrs
+        Plate(**plate_attrs)
         group_plate.attrs["plate"] = plate_attrs
 
         for row, column in well_rows_columns:
-
+            parallelization_list.append(
+                {
+                    "zarr_url": (
+                        f"{zarr_dir}/{plate}.zarr/{row}/{column}/"
+                        f"{acquisition}/"
+                    ),
+                    "init_args": InitArgsCellVoyager(
+                        image_dir=acquisitions[acquisition].image_dir,
+                        plate_prefix=plate_prefix,
+                        well_ID=f"{row}{column}",
+                        image_extension=image_extension,
+                        image_glob_patterns=image_glob_patterns,
+                        acquisition=acquisition,
+                    ).dict(),
+                }
+            )
             try:
                 group_well = group_plate.create_group(f"{row}/{column}/")
                 logging.info(f"Created new group_well at {row}/{column}/")
-                group_well.attrs["well"] = {
+                well_attrs = {
                     "images": [
                         {
                             "path": f"{acquisition}",
@@ -384,6 +394,9 @@ def create_ome_zarr_multiplex(
                     ],
                     "version": __OME_NGFF_VERSION__,
                 }
+                # Validate well attrs:
+                Well(**well_attrs)
+                group_well.attrs["well"] = well_attrs
                 zarrurls["well"].append(f"{plate}.zarr/{row}/{column}")
             except ContainsGroupError:
                 group_well = zarr.open_group(
@@ -395,10 +408,13 @@ def create_ome_zarr_multiplex(
                 current_images = group_well.attrs["well"]["images"] + [
                     {"path": f"{acquisition}", "acquisition": int(acquisition)}
                 ]
-                group_well.attrs["well"] = dict(
+                well_attrs = dict(
                     images=current_images,
                     version=group_well.attrs["well"]["version"],
                 )
+                # Validate well attrs:
+                Well(**well_attrs)
+                group_well.attrs["well"] = well_attrs
 
             group_image = group_well.create_group(
                 f"{acquisition}/"
@@ -460,6 +476,8 @@ def create_ome_zarr_multiplex(
                     label_prefix=acquisition,
                 ),
             }
+            # Validate Image attrs
+            NgffImageMeta(**group_image.attrs)
 
             # Prepare AnnData tables for FOV/well ROIs
             well_id = row + column
@@ -488,31 +506,16 @@ def create_ome_zarr_multiplex(
     # have unique labels
     for well_path in zarrurls["well"]:
         check_well_channel_labels(
-            well_zarr_path=str(Path(output_path) / well_path)
+            well_zarr_path=str(Path(zarr_dir) / well_path)
         )
 
-    original_paths = {
-        acquisition: dict_acquisitions[acquisition]["original_paths"]
-        for acquisition in acquisitions
-    }
-
-    metadata_update = dict(
-        plate=zarrurls["plate"],
-        well=zarrurls["well"],
-        image=zarrurls["image"],
-        num_levels=num_levels,
-        coarsening_xy=coarsening_xy,
-        original_paths=original_paths,
-        image_extension=image_extension,
-        image_glob_patterns=image_glob_patterns,
-    )
-    return metadata_update
+    return dict(parallelization_list=parallelization_list)
 
 
 if __name__ == "__main__":
     from fractal_tasks_core.tasks._utils import run_fractal_task
 
     run_fractal_task(
-        task_function=create_ome_zarr_multiplex,
+        task_function=cellvoyager_to_ome_zarr_init_multiplex,
         logger_name=logger.name,
     )
diff --git a/fractal_tasks_core/tasks/copy_ome_zarr.py b/fractal_tasks_core/tasks/copy_ome_zarr.py
deleted file mode 100644
index bf05ffca1..000000000
--- a/fractal_tasks_core/tasks/copy_ome_zarr.py
+++ /dev/null
@@ -1,225 +0,0 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-# Marco Franzon <marco.franzon@exact-lab.it>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Task that copies the structure of an OME-NGFF zarr array to a new one.
-"""
-import logging
-from pathlib import Path
-from typing import Any
-from typing import Sequence
-
-import anndata as ad
-import zarr
-from pydantic.decorator import validate_arguments
-
-import fractal_tasks_core
-from fractal_tasks_core.ngff import load_NgffImageMeta
-from fractal_tasks_core.roi import (
-    convert_ROIs_from_3D_to_2D,
-)
-from fractal_tasks_core.tables import write_table
-from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
-
-logger = logging.getLogger(__name__)
-
-
-__OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
-
-
-@validate_arguments
-def copy_ome_zarr(
-    *,
-    input_paths: Sequence[str],
-    output_path: str,
-    metadata: dict[str, Any],
-    project_to_2D: bool = True,
-    suffix: str = "mip",
-    ROI_table_names: tuple[str, ...] = ("FOV_ROI_table", "well_ROI_table"),
-    overwrite: bool = False,
-) -> dict[str, Any]:
-
-    """
-    Duplicate an input zarr structure to a new path.
-
-    This task copies all the structure, but none of the image data:
-
-    - For each plate, create a new zarr group with the same attributes as
-       the original one.
-    - For each well (in each plate), create a new zarr subgroup with the
-       same attributes as the original one.
-    - For each image (in each well), create a new zarr subgroup with the
-       same attributes as the original one.
-    - For each image (in each well), copy the relevant AnnData tables from
-       the original source.
-
-    Note: this task makes use of methods from the `Attributes` class, see
-    https://zarr.readthedocs.io/en/stable/api/attrs.html.
-
-    Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file. Example:
-            `["/some/path/"]`. This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: Path were the output of this task is stored. Example:
-            `"/some/path/"` => puts the new OME-Zarr file in the same folder as
-            the input OME-Zarr file `"/some/new_path"` => puts the new OME-Zarr
-            file into a new folder at `/some/new_path`. (standard argument for
-            Fractal tasks, managed by Fractal server).
-        metadata: Dictionary containing metadata about the OME-Zarr. This task
-            requires the following elements to be present in the metadata:
-            `plate`: List of plates
-            (e.g. `["MyPlate.zarr"]`);
-            `well`: List of wells in the OME-Zarr plate
-            (e.g. `["MyPlate.zarr/B/03/MyPlate.zarr/B/05"]`);
-            "image": List of images in the OME-Zarr plate
-            (e.g. `["MyPlate.zarr/B/03/0", "MyPlate.zarr/B/05/0"]`).
-            standard argument for Fractal tasks, managed by Fractal server).
-        project_to_2D: If `True`, apply a 3D->2D projection to the ROI tables
-            that are copied to the new OME-Zarr.
-        suffix: The suffix that is used to transform `plate.zarr` into
-            `plate_suffix.zarr`. Note that `None` is not currently supported.
-        ROI_table_names: List of Anndata table names to be copied. Note:
-            copying non-ROI tables may fail if `project_to_2D=True`.
-        overwrite: If `True`, overwrite the task output.
-
-    Returns:
-        An update to the metadata table with new `plate`, `well`, `image`
-            entries (now with the suffix in the plate name).
-    """
-
-    # Preliminary check
-    if len(input_paths) > 1:
-        raise NotImplementedError
-    if suffix is None:
-        # FIXME create a standard suffix (with timestamp)
-        raise NotImplementedError
-
-    # List all plates
-    in_path = Path(input_paths[0])
-    list_plates = [
-        p.as_posix()
-        for p in Path(in_path).glob("*.zarr")
-        if p.name in metadata["plate"]
-    ]
-    logger.info(f"{list_plates=}")
-
-    meta_update: dict[str, Any] = {"copy_ome_zarr": {}}
-    meta_update["copy_ome_zarr"]["suffix"] = suffix
-    meta_update["copy_ome_zarr"]["sources"] = {}
-
-    # Loop over all plates
-    for zarrurl_old in list_plates:
-        zarrfile = zarrurl_old.split("/")[-1]
-        old_plate_name = zarrfile.split(".zarr")[0]
-        new_plate_name = f"{old_plate_name}_{suffix}"
-        new_plate_dir = Path(output_path).resolve()
-        zarrurl_new = f"{(new_plate_dir / new_plate_name).as_posix()}.zarr"
-        meta_update["copy_ome_zarr"]["sources"][new_plate_name] = zarrurl_old
-
-        logger.info(f"{zarrurl_old=}")
-        logger.info(f"{zarrurl_new=}")
-        logger.info(f"{meta_update=}")
-
-        # Replicate plate attrs
-        old_plate_group = zarr.open_group(zarrurl_old, mode="r")
-        new_plate_group = open_zarr_group_with_overwrite(
-            zarrurl_new, overwrite=overwrite
-        )
-        new_plate_group.attrs.put(old_plate_group.attrs.asdict())
-
-        well_paths = [
-            well["path"] for well in new_plate_group.attrs["plate"]["wells"]
-        ]
-        logger.info(f"{well_paths=}")
-        for well_path in well_paths:
-
-            # Replicate well attrs
-            old_well_group = zarr.open_group(
-                f"{zarrurl_old}/{well_path}", mode="r"
-            )
-            new_well_group = zarr.group(f"{zarrurl_new}/{well_path}")
-            new_well_group.attrs.put(old_well_group.attrs.asdict())
-
-            image_paths = [
-                image["path"]
-                for image in new_well_group.attrs["well"]["images"]
-            ]
-            logger.info(f"{image_paths=}")
-
-            for image_path in image_paths:
-
-                # Replicate image attrs
-                old_image_group = zarr.open_group(
-                    f"{zarrurl_old}/{well_path}/{image_path}", mode="r"
-                )
-                new_image_group = zarr.group(
-                    f"{zarrurl_new}/{well_path}/{image_path}"
-                )
-                new_image_group.attrs.put(old_image_group.attrs.asdict())
-
-                # Extract pixel sizes, if needed
-                if ROI_table_names:
-
-                    if project_to_2D:
-                        path_image = f"{zarrurl_old}/{well_path}/{image_path}"
-                        ngff_image_meta = load_NgffImageMeta(path_image)
-                        pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(
-                            level=0
-                        )
-                        pxl_size_z = pxl_sizes_zyx[0]
-
-                    # Copy the tables in ROI_table_names
-                    for ROI_table_name in ROI_table_names:
-
-                        logger.info(
-                            f"I will now read {ROI_table_name} from "
-                            f"{zarrurl_old=}, convert it to 2D, and "
-                            "write it back to the new zarr file."
-                        )
-                        new_ROI_table = ad.read_zarr(
-                            f"{zarrurl_old}/{well_path}/{image_path}/"
-                            f"tables/{ROI_table_name}"
-                        )
-                        old_ROI_table_attrs = zarr.open_group(
-                            f"{zarrurl_old}/{well_path}/{image_path}/"
-                            f"tables/{ROI_table_name}"
-                        ).attrs.asdict()
-                        # Convert 3D ROIs to 2D
-                        if project_to_2D:
-                            new_ROI_table = convert_ROIs_from_3D_to_2D(
-                                new_ROI_table, pxl_size_z
-                            )
-                        # Write new table
-                        write_table(
-                            new_image_group,
-                            ROI_table_name,
-                            new_ROI_table,
-                            table_attrs=old_ROI_table_attrs,
-                        )
-
-    for key in ["plate", "well", "image"]:
-        meta_update[key] = [
-            component.replace(".zarr", f"_{suffix}.zarr")
-            for component in metadata[key]
-        ]
-
-    return meta_update
-
-
-if __name__ == "__main__":
-    from fractal_tasks_core.tasks._utils import run_fractal_task
-
-    run_fractal_task(
-        task_function=copy_ome_zarr,
-        logger_name=logger.name,
-    )
diff --git a/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py b/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py
new file mode 100644
index 000000000..b6ada3367
--- /dev/null
+++ b/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py
@@ -0,0 +1,300 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+# Marco Franzon <marco.franzon@exact-lab.it>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Task that copies the structure of an OME-NGFF zarr array to a new one.
+"""
+import logging
+from typing import Any
+
+import zarr
+from pydantic.decorator import validate_arguments
+
+import fractal_tasks_core
+from fractal_tasks_core.ngff.specs import NgffPlateMeta
+from fractal_tasks_core.ngff.specs import WellInPlate
+from fractal_tasks_core.ngff.zarr_utils import load_NgffPlateMeta
+from fractal_tasks_core.ngff.zarr_utils import load_NgffWellMeta
+from fractal_tasks_core.tasks.io_models import InitArgsMIP
+from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
+
+logger = logging.getLogger(__name__)
+
+
+__OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
+
+
+def _get_plate_url_from_image_url(zarr_url: str) -> str:
+    """
+    Given the absolute `zarr_url` for an OME-Zarr image within an HCS plate,
+    return the path to the plate zarr group.
+    """
+    zarr_url = zarr_url.rstrip("/")
+    plate_path = "/".join(zarr_url.split("/")[:-3])
+    return plate_path
+
+
+def _get_well_sub_url(zarr_url: str) -> str:
+    """
+    Given the absolute `zarr_url` for an OME-Zarr image within an HCS plate,
+    return the path to the image zarr group.
+    """
+    zarr_url = zarr_url.rstrip("/")
+    well_url = "/".join(zarr_url.split("/")[-3:-1])
+    return well_url
+
+
+def _get_image_sub_url(zarr_url: str) -> str:
+    """
+    Given the absolute `zarr_url` for an OME-Zarr image, return the image
+    zarr-group name.
+    """
+    zarr_url = zarr_url.rstrip("/")
+    image_sub_url = zarr_url.split("/")[-1]
+    return image_sub_url
+
+
+def _generate_wells_rows_columns(
+    well_list: list[str],
+) -> tuple[list[WellInPlate], list[str], list[str]]:
+    """
+    Generate the plate well metadata based on the list of wells.
+    """
+    rows = []
+    columns = []
+    wells = []
+    for well in well_list:
+        rows.append(well.split("/")[0])
+        columns.append(well.split("/")[1])
+    rows = sorted(list(set(rows)))
+    columns = sorted(list(set(columns)))
+    for well in well_list:
+        wells.append(
+            WellInPlate(
+                path=well,
+                rowIndex=rows.index(well.split("/")[0]),
+                columnIndex=columns.index(well.split("/")[1]),
+            )
+        )
+
+    return wells, rows, columns
+
+
+def _generate_plate_well_metadata(
+    zarr_urls: list[str],
+) -> tuple[dict[str, dict], dict[str, dict[str, dict]], dict[str, dict]]:
+    """
+    Generate metadata for OME-Zarr HCS plates & wells.
+
+    Based on the list of zarr_urls, generate metadata for all plates and all
+    their wells.
+
+    Args:
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed.
+
+    Returns:
+        plate_metadata_dicts: Dictionary of plate plate metadata. The structure
+            is: {"old_plate_name": NgffPlateMeta (as dict)}.
+        new_well_image_attrs: Dictionary of image lists for the new wells.
+            The structure is: {"old_plate_name": {"old_well_name":
+            [ImageInWell(as dict)]}}
+        well_image_attrs: Dictionary of Image attributes of the existing wells.
+    """
+    # TODO: Simplify this block. Currently complicated, because we need to loop
+    # through all potential plates, all their wells & their images to build up
+    # the metadata for the plate & well.
+    plate_metadata_dicts = {}
+    plate_wells = {}
+    well_image_attrs = {}
+    new_well_image_attrs = {}
+    for zarr_url in zarr_urls:
+        # Extract plate/well/image parts of `zarr_url`
+        old_plate_url = _get_plate_url_from_image_url(zarr_url)
+        well_sub_url = _get_well_sub_url(zarr_url)
+        curr_img_sub_url = _get_image_sub_url(zarr_url)
+
+        # The first time a plate is found, create its metadata
+        if old_plate_url not in plate_metadata_dicts:
+            logger.info(f"Reading plate metadata of {old_plate_url=}")
+            old_plate_meta = load_NgffPlateMeta(old_plate_url)
+            plate_metadata = dict(
+                plate=dict(
+                    acquisitions=old_plate_meta.plate.acquisitions,
+                    field_count=old_plate_meta.plate.field_count,
+                    name=old_plate_meta.plate.name,
+                    # The new field count could be different from the old
+                    # field count
+                    version=old_plate_meta.plate.version,
+                )
+            )
+            plate_metadata_dicts[old_plate_url] = plate_metadata
+            plate_wells[old_plate_url] = []
+            well_image_attrs[old_plate_url] = {}
+            new_well_image_attrs[old_plate_url] = {}
+
+        # The first time a plate/well pair is found, create the well metadata
+        if well_sub_url not in plate_wells[old_plate_url]:
+            plate_wells[old_plate_url].append(well_sub_url)
+            old_well_url = f"{old_plate_url}/{well_sub_url}"
+            logger.info(f"Reading well metadata of {old_well_url}")
+            well_attrs = load_NgffWellMeta(old_well_url)
+            well_image_attrs[old_plate_url][well_sub_url] = well_attrs.well
+            new_well_image_attrs[old_plate_url][well_sub_url] = []
+
+        # Find images of the current well with name matching the current image
+        # TODO: clarify whether this list must always have length 1
+        curr_well_image_list = [
+            img
+            for img in well_image_attrs[old_plate_url][well_sub_url].images
+            if img.path == curr_img_sub_url
+        ]
+        new_well_image_attrs[old_plate_url][
+            well_sub_url
+        ] += curr_well_image_list
+
+    # Fill in the plate metadata based on all available wells
+    for old_plate_url in plate_metadata_dicts:
+        well_list, column_list, row_list = _generate_wells_rows_columns(
+            plate_wells[old_plate_url]
+        )
+        plate_metadata_dicts[old_plate_url]["plate"]["columns"] = []
+        for column in column_list:
+            plate_metadata_dicts[old_plate_url]["plate"]["columns"].append(
+                {"name": column}
+            )
+
+        plate_metadata_dicts[old_plate_url]["plate"]["rows"] = []
+        for row in row_list:
+            plate_metadata_dicts[old_plate_url]["plate"]["rows"].append(
+                {"name": row}
+            )
+        plate_metadata_dicts[old_plate_url]["plate"]["wells"] = well_list
+
+        # Validate with NgffPlateMeta model
+        plate_metadata_dicts[old_plate_url] = NgffPlateMeta(
+            **plate_metadata_dicts[old_plate_url]
+        ).dict(exclude_none=True)
+
+    return plate_metadata_dicts, new_well_image_attrs, well_image_attrs
+
+
+@validate_arguments
+def copy_ome_zarr_hcs_plate(
+    *,
+    zarr_urls: list[str],
+    zarr_dir: str,
+    suffix: str = "mip",
+    overwrite: bool = False,
+) -> dict[str, Any]:
+    """
+    Duplicate the OME-Zarr HCS structure for a set of zarr_urls.
+
+    This task only processes the zarr images in the zarr_urls, not all the
+    images in the plate. It copies all the  plate & well structure, but none
+    of the image metadata or the actual image data:
+
+    - For each plate, create a new OME-Zarr HCS plate with the attributes for
+        all the images in zarr_urls
+    - For each well (in each plate), create a new zarr subgroup with the
+       same attributes as the original one.
+
+    Note: this task makes use of methods from the `Attributes` class, see
+    https://zarr.readthedocs.io/en/stable/api/attrs.html.
+
+    Args:
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        zarr_dir: path of the directory where the new OME-Zarrs will be
+            created.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        suffix: The suffix that is used to transform `plate.zarr` into
+            `plate_suffix.zarr`. Note that `None` is not currently supported.
+        overwrite: If `True`, overwrite the task output.
+
+    Returns:
+        A parallelization list to be used in a compute task to fill the wells
+        with OME-Zarr images.
+    """
+
+    # Preliminary check
+    if suffix is None or suffix == "":
+        raise ValueError(
+            "Running copy_ome_zarr_hcs_plate without a suffix would lead to"
+            "overwriting of the existing HCS plates."
+        )
+
+    parallelization_list = []
+
+    # Generate parallelization list
+    for zarr_url in zarr_urls:
+        old_plate_url = _get_plate_url_from_image_url(zarr_url)
+        well_sub_url = _get_well_sub_url(zarr_url)
+        old_plate_name = old_plate_url.split(".zarr")[-2].split("/")[-1]
+        new_plate_name = f"{old_plate_name}_{suffix}"
+        zarrurl_plate_new = f"{zarr_dir}/{new_plate_name}.zarr"
+        curr_img_sub_url = _get_image_sub_url(zarr_url)
+        new_zarr_url = f"{zarrurl_plate_new}/{well_sub_url}/{curr_img_sub_url}"
+        parallelization_item = dict(
+            zarr_url=new_zarr_url,
+            init_args=dict(origin_url=zarr_url),
+        )
+        InitArgsMIP(**parallelization_item["init_args"])
+        parallelization_list.append(parallelization_item)
+
+    # Generate the plate metadata & parallelization list
+    (
+        plate_attrs_dicts,
+        new_well_image_attrs,
+        well_image_attrs,
+    ) = _generate_plate_well_metadata(zarr_urls=zarr_urls)
+
+    # Create the new OME-Zarr HCS plate
+    for old_plate_url, plate_attrs in plate_attrs_dicts.items():
+        old_plate_name = old_plate_url.split(".zarr")[-2].split("/")[-1]
+        new_plate_name = f"{old_plate_name}_{suffix}"
+        zarrurl_new = f"{zarr_dir}/{new_plate_name}.zarr"
+        logger.info(f"{old_plate_url=}")
+        logger.info(f"{zarrurl_new=}")
+        new_plate_group = open_zarr_group_with_overwrite(
+            zarrurl_new, overwrite=overwrite
+        )
+        new_plate_group.attrs.put(plate_attrs)
+
+        # Write well groups:
+        for well_sub_url in new_well_image_attrs[old_plate_url]:
+            new_well_group = zarr.group(f"{zarrurl_new}/{well_sub_url}")
+            well_attrs = dict(
+                well=dict(
+                    images=[
+                        img.dict(exclude_none=True)
+                        for img in new_well_image_attrs[old_plate_url][
+                            well_sub_url
+                        ]
+                    ],
+                    version=well_image_attrs[old_plate_url][
+                        well_sub_url
+                    ].version,
+                )
+            )
+            new_well_group.attrs.put(well_attrs)
+
+    return dict(parallelization_list=parallelization_list)
+
+
+if __name__ == "__main__":
+    from fractal_tasks_core.tasks._utils import run_fractal_task
+
+    run_fractal_task(
+        task_function=copy_ome_zarr_hcs_plate,
+        logger_name=logger.name,
+    )
diff --git a/fractal_tasks_core/tasks/find_registration_consensus.py b/fractal_tasks_core/tasks/find_registration_consensus.py
new file mode 100644
index 000000000..f26a2d47b
--- /dev/null
+++ b/fractal_tasks_core/tasks/find_registration_consensus.py
@@ -0,0 +1,171 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+# Joel Lüthi <joel.luethi@uzh.ch>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Applies the multiplexing translation to all ROI tables
+"""
+import logging
+from typing import Optional
+
+import anndata as ad
+import zarr
+from pydantic.decorator import validate_arguments
+
+from fractal_tasks_core.roi import (
+    are_ROI_table_columns_valid,
+)
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tasks._registration_utils import (
+    add_zero_translation_columns,
+)
+from fractal_tasks_core.tasks._registration_utils import (
+    apply_registration_to_single_ROI_table,
+)
+from fractal_tasks_core.tasks._registration_utils import (
+    calculate_min_max_across_dfs,
+)
+from fractal_tasks_core.tasks.io_models import InitArgsRegistrationConsensus
+
+
+logger = logging.getLogger(__name__)
+
+
+@validate_arguments
+def find_registration_consensus(
+    *,
+    # Fractal arguments
+    zarr_url: str,
+    init_args: InitArgsRegistrationConsensus,
+    # Task-specific arguments
+    roi_table: str = "FOV_ROI_table",
+    new_roi_table: Optional[str] = None,
+):
+    """
+    Applies pre-calculated registration to ROI tables.
+
+    Apply pre-calculated registration such that resulting ROIs contain
+    the consensus align region between all cycles.
+
+    Parallelization level: well
+
+    Args:
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
+            Refers to the zarr_url of the reference cycle.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        init_args: Intialization arguments provided by
+            `init_group_by_well_for_multiplexing`. It contains the
+            zarr_url_list listing all the zarr_urls in the same well as the
+            zarr_url of the reference cycle that are being processed.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        roi_table: Name of the ROI table over which the task loops to
+            calculate the registration. Examples: `FOV_ROI_table` => loop over
+            the field of views, `well_ROI_table` => process the whole well as
+            one image.
+        new_roi_table: Optional name for the new, registered ROI table. If no
+            name is given, it will default to "registered_" + `roi_table`
+
+    """
+    if not new_roi_table:
+        new_roi_table = "registered_" + roi_table
+    logger.info(
+        f"Running for {zarr_url=} & the other acquisitions in that well. \n"
+        f"Applying translation registration to {roi_table=} and storing it as "
+        f"{new_roi_table=}."
+    )
+
+    # Collect all the ROI tables
+    roi_tables = {}
+    roi_tables_attrs = {}
+    for acq_zarr_url in init_args.zarr_url_list:
+        curr_ROI_table = ad.read_zarr(f"{acq_zarr_url}/tables/{roi_table}")
+        curr_ROI_table_group = zarr.open_group(
+            f"{acq_zarr_url}/tables/{roi_table}", mode="r"
+        )
+        curr_ROI_table_attrs = curr_ROI_table_group.attrs.asdict()
+
+        # For reference_cycle acquisition, handle the fact that it doesn't
+        # have the shifts
+        if acq_zarr_url == zarr_url:
+            curr_ROI_table = add_zero_translation_columns(curr_ROI_table)
+        # Check for valid ROI tables
+        are_ROI_table_columns_valid(table=curr_ROI_table)
+        translation_columns = [
+            "translation_z",
+            "translation_y",
+            "translation_x",
+        ]
+        if curr_ROI_table.var.index.isin(translation_columns).sum() != 3:
+            raise ValueError(
+                f"{roi_table=} in {acq_zarr_url} does not contain the "
+                f"translation columns {translation_columns} necessary to use "
+                "this task."
+            )
+        roi_tables[acq_zarr_url] = curr_ROI_table
+        roi_tables_attrs[acq_zarr_url] = curr_ROI_table_attrs
+
+    # Check that all acquisitions have the same ROIs
+    rois = roi_tables[list(roi_tables.keys())[0]].obs.index
+    for acq_zarr_url, acq_roi_table in roi_tables.items():
+        if not (acq_roi_table.obs.index == rois).all():
+            raise ValueError(
+                f"Acquisition {acq_zarr_url} does not contain the same ROIs "
+                f"as the reference acquisition {zarr_url}:\n"
+                f"{acq_zarr_url}: {acq_roi_table.obs.index}\n"
+                f"{zarr_url}: {rois}"
+            )
+
+    roi_table_dfs = [
+        roi_table.to_df().loc[:, translation_columns]
+        for roi_table in roi_tables.values()
+    ]
+    logger.info("Calculating min & max translation across cycles.")
+    max_df, min_df = calculate_min_max_across_dfs(roi_table_dfs)
+    shifted_rois = {}
+
+    # Loop over acquisitions
+    for acq_zarr_url in init_args.zarr_url_list:
+        shifted_rois[acq_zarr_url] = apply_registration_to_single_ROI_table(
+            roi_tables[acq_zarr_url], max_df, min_df
+        )
+
+        # TODO: Drop translation columns from this table?
+
+        logger.info(
+            f"Write the registered ROI table {new_roi_table} for "
+            "{acq_zarr_url=}"
+        )
+        # Save the shifted ROI table as a new table
+        image_group = zarr.group(acq_zarr_url)
+        write_table(
+            image_group,
+            new_roi_table,
+            shifted_rois[acq_zarr_url],
+            table_attrs=roi_tables_attrs[acq_zarr_url],
+        )
+
+    # TODO: Optionally apply registration to other tables as well?
+    # e.g. to well_ROI_table based on FOV_ROI_table
+    # => out of scope for the initial task, apply registration separately
+    # to each table
+    # Easiest implementation: Apply average shift calculcated here to other
+    # ROIs. From many to 1 (e.g. FOV => well) => average shift, but crop len
+    # From well to many (e.g. well to FOVs) => average shift, crop len by that
+    # amount
+    # Many to many (FOVs to organoids) => tricky because of matching
+
+
+if __name__ == "__main__":
+    from fractal_tasks_core.tasks._utils import run_fractal_task
+
+    run_fractal_task(
+        task_function=find_registration_consensus,
+        logger_name=logger.name,
+    )
diff --git a/fractal_tasks_core/tasks/illumination_correction.py b/fractal_tasks_core/tasks/illumination_correction.py
index c5aa1dfdb..7f8df027b 100644
--- a/fractal_tasks_core/tasks/illumination_correction.py
+++ b/fractal_tasks_core/tasks/illumination_correction.py
@@ -17,8 +17,6 @@
 import warnings
 from pathlib import Path
 from typing import Any
-from typing import Optional
-from typing import Sequence
 
 import anndata as ad
 import dask.array as da
@@ -95,18 +93,15 @@ def correct(
 @validate_arguments
 def illumination_correction(
     *,
-    # Standard arguments
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    # Fractal argument
+    zarr_url: str,
     # Task-specific arguments
     illumination_profiles_folder: str,
     dict_corr: dict[str, str],
     background: int = 110,
-    overwrite_input: bool = True,
     input_ROI_table: str = "FOV_ROI_table",
-    new_component: Optional[str] = None,
+    overwrite_input: bool = True,
+    suffix: str = "_illum_corr",
 ) -> dict[str, Any]:
 
     """
@@ -118,20 +113,7 @@ def illumination_correction(
     well & the correction may only be partial).
 
     Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file. Example:
-            `["/some/path/"]`. This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: Path were the output of this task is stored. Examples:
-            `"/some/path/"` => puts the new OME-Zarr file in the same folder as
-            the input OME-Zarr file; `"/some/new_path"` => puts the new
-            OME-Zarr file into a new folder at `/some/new_path`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed. Example: `"some_plate.zarr/B/03/0"`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
         illumination_profiles_folder: Path of folder of illumination profiles.
         dict_corr: Dictionary where keys match the `wavelength_id` attributes
@@ -140,10 +122,6 @@ def illumination_correction(
         background: Background value that is subtracted from the image before
             the illumination correction is applied. Set it to `0` if you don't
             want any background subtraction.
-        overwrite_input:
-            If `True`, the results of this task will overwrite the input image
-            data. In the current version, `overwrite_input=False` is not
-            implemented.
         input_ROI_table: Name of the ROI table that contains the information
             about the location of the individual field of views (FOVs) to
             which the illumination correction shall be applied. Defaults to
@@ -154,50 +132,27 @@ def illumination_correction(
             you only have 1 FOV per Zarr image and `grid_ROI_table` if you
             have multiple FOVs per Zarr image and set the right grid options
             during import.
-        new_component: Not implemented yet. This is not implemented well in
-            Fractal server at the moment, it's unclear how a user would specify
-            fitting new components. If the results shall not overwrite the
-            input data and the output path is the same as the input path, a new
-            component needs to be provided.
-            Example: `myplate_new_name.zarr/B/03/0/`.
+        overwrite_input: If `True`, the results of this task will overwrite
+            the input image data. If false, a new image is generated and the
+            illumination corrected data is saved there.
+        suffix: What suffix to append to the illumination corrected images.
+            Only relevant if `overwrite_input=False`.
     """
 
-    # Preliminary checks
-    if len(input_paths) > 1:
-        raise NotImplementedError
-    if (overwrite_input and new_component is not None) or (
-        new_component is None and not overwrite_input
-    ):
-        raise ValueError(f"{overwrite_input=}, but {new_component=}")
-
-    if not overwrite_input:
-        msg = (
-            "We still have to harmonize illumination_correction("
-            "overwrite_input=False) with replicate_zarr_structure(..., "
-            "suffix=..)"
-        )
-        raise NotImplementedError(msg)
-
     # Defione old/new zarrurls
-    plate, well = component.split(".zarr/")
-    in_path = Path(input_paths[0])
-    zarrurl_old = (in_path / component).as_posix()
     if overwrite_input:
-        zarrurl_new = zarrurl_old
+        zarr_url_new = zarr_url
     else:
-        new_plate, new_well = new_component.split(".zarr/")
-        if new_well != well:
-            raise ValueError(f"{well=}, {new_well=}")
-        zarrurl_new = (Path(output_path) / new_component).as_posix()
+        zarr_url_new = zarr_url + suffix
 
     t_start = time.perf_counter()
     logger.info("Start illumination_correction")
     logger.info(f"  {overwrite_input=}")
-    logger.info(f"  {zarrurl_old=}")
-    logger.info(f"  {zarrurl_new=}")
+    logger.info(f"  {zarr_url=}")
+    logger.info(f"  {zarr_url_new=}")
 
     # Read attributes from NGFF metadata
-    ngff_image_meta = load_NgffImageMeta(zarrurl_old)
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     num_levels = ngff_image_meta.num_levels
     coarsening_xy = ngff_image_meta.coarsening_xy
     full_res_pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(level=0)
@@ -209,12 +164,12 @@ def illumination_correction(
 
     # Read channels from .zattrs
     channels: list[OmeroChannel] = get_omero_channel_list(
-        image_zarr_path=zarrurl_old
+        image_zarr_path=zarr_url
     )
     num_channels = len(channels)
 
     # Read FOV ROIs
-    FOV_ROI_table = ad.read_zarr(f"{zarrurl_old}/tables/{input_ROI_table}")
+    FOV_ROI_table = ad.read_zarr(f"{zarr_url}/tables/{input_ROI_table}")
 
     # Create list of indices for 3D FOVs spanning the entire Z direction
     list_indices = convert_ROI_table_to_indices(
@@ -255,19 +210,17 @@ def illumination_correction(
             )
 
     # Lazily load highest-res level from original zarr array
-    data_czyx = da.from_zarr(f"{zarrurl_old}/0")
+    data_czyx = da.from_zarr(f"{zarr_url}/0")
 
     # Create zarr for output
     if overwrite_input:
-        fov_path = zarrurl_old
-        new_zarr = zarr.open(f"{zarrurl_old}/0")
+        new_zarr = zarr.open(f"{zarr_url_new}/0")
     else:
-        fov_path = zarrurl_new
         new_zarr = zarr.create(
             shape=data_czyx.shape,
             chunks=data_czyx.chunksize,
             dtype=data_czyx.dtype,
-            store=zarr.storage.FSStore(f"{zarrurl_new}/0"),
+            store=zarr.storage.FSStore(f"{zarr_url_new}/0"),
             overwrite=False,
             dimension_separator="/",
         )
@@ -304,7 +257,7 @@ def illumination_correction(
     # Starting from on-disk highest-resolution data, build and write to disk a
     # pyramid of coarser levels
     build_pyramid(
-        zarrurl=fov_path,
+        zarrurl=zarr_url_new,
         overwrite=True,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
@@ -314,7 +267,13 @@ def illumination_correction(
     t_end = time.perf_counter()
     logger.info(f"End illumination_correction, elapsed: {t_end-t_start}")
 
-    return {}
+    if overwrite_input:
+        image_list_updates = dict(image_list_updates=[dict(zarr_url=zarr_url)])
+    else:
+        image_list_updates = dict(
+            image_list_updates=[dict(zarr_url=zarr_url_new, origin=zarr_url)]
+        )
+    return image_list_updates
 
 
 if __name__ == "__main__":
diff --git a/fractal_tasks_core/tasks/image_based_registration_hcs_init.py b/fractal_tasks_core/tasks/image_based_registration_hcs_init.py
new file mode 100644
index 000000000..3c8b8254e
--- /dev/null
+++ b/fractal_tasks_core/tasks/image_based_registration_hcs_init.py
@@ -0,0 +1,97 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+# Joel Lüthi <joel.luethi@uzh.ch>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Initializes the parallelization list for registration in HCS plates.
+"""
+import logging
+from typing import Any
+
+from pydantic.decorator import validate_arguments
+
+from fractal_tasks_core.tasks._registration_utils import (
+    create_well_acquisition_dict,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@validate_arguments
+def image_based_registration_hcs_init(
+    *,
+    # Fractal arguments
+    zarr_urls: list[str],
+    zarr_dir: str,
+    # Task-specific arguments
+    reference_cycle: int = 0,
+) -> dict[str, list[dict[str, Any]]]:
+    """
+    Initialized calculate registration task
+
+    This task prepares a parallelization list of all zarr_urls that need to be
+    used to calculate the registration between cycles (all zarr_urls except
+    the reference cycle vs. the reference cycle).
+    This task only works for HCS OME-Zarrs for 2 reasons: Only HCS OME-Zarrs
+    currently have defined acquisition metadata to determine reference cycles.
+    And we have only implemented the grouping of images for HCS OME-Zarrs by
+    well (with the assumption that every well just has 1 image per acqusition).
+
+    Args:
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        zarr_dir: path of the directory where the new OME-Zarrs will be
+            created. Not used by this task.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        reference_cycle: Which cycle to register against. Needs to match the
+            acquisition metadata in the OME-Zarr image.
+
+    Returns:
+        task_output: Dictionary for Fractal server that contains a
+            parallelization list.
+    """
+    logger.info(
+        f"Running `image_based_registration_hcs_init` for {zarr_urls=}"
+    )
+    image_groups = create_well_acquisition_dict(zarr_urls)
+
+    # Create the parallelization list
+    parallelization_list = []
+    for key, image_group in image_groups.items():
+        # Assert that all image groups have the reference cycle present
+        if reference_cycle not in image_group.keys():
+            raise ValueError(
+                f"Registration with {reference_cycle=} can only work if all"
+                "wells have the reference cycle present. It was not found"
+                f"for well {key}."
+            )
+        # Add all zarr_urls except the reference cycle to the
+        # parallelization list
+        for acquisition, zarr_url in image_group.items():
+            if acquisition != reference_cycle:
+                reference_zarr_url = image_group[reference_cycle]
+                parallelization_list.append(
+                    dict(
+                        zarr_url=zarr_url,
+                        init_args=dict(reference_zarr_url=reference_zarr_url),
+                    )
+                )
+
+    return dict(parallelization_list=parallelization_list)
+
+
+if __name__ == "__main__":
+    from fractal_tasks_core.tasks._utils import run_fractal_task
+
+    run_fractal_task(
+        task_function=image_based_registration_hcs_init,
+        logger_name=logger.name,
+    )
diff --git a/fractal_tasks_core/tasks/import_ome_zarr.py b/fractal_tasks_core/tasks/import_ome_zarr.py
index ca27ac1a8..95216ef6f 100644
--- a/fractal_tasks_core/tasks/import_ome_zarr.py
+++ b/fractal_tasks_core/tasks/import_ome_zarr.py
@@ -12,10 +12,8 @@
 Task to import an existing OME-Zarr.
 """
 import logging
-from pathlib import Path
 from typing import Any
 from typing import Optional
-from typing import Sequence
 
 import dask.array as da
 import zarr
@@ -39,7 +37,7 @@ def _process_single_image(
     *,
     grid_YX_shape: Optional[tuple[int, int]] = None,
     overwrite: bool = False,
-) -> None:
+) -> dict[str, str]:
     """
     Validate OME-NGFF metadata and optionally generate ROI tables.
 
@@ -48,6 +46,7 @@ def _process_single_image(
     1. Validates OME-NGFF image metadata, via `NgffImageMeta`;
     2. Optionally generates and writes two ROI tables;
     3. Optionally update OME-NGFF omero metadata.
+    4. Returns dataset types
 
     Args:
         image_path: Absolute path to the image Zarr group.
@@ -151,13 +150,22 @@ def _process_single_image(
         new_omero["channels"] = new_channels
         image_group.attrs.update(omero=new_omero)
 
+    # Determine image types:
+    # Later: also provide a has_T flag.
+    # TODO: Potentially also load acquisition metadata if available in a Zarr
+    is_3D = False
+    if "z" in image_meta.axes_names:
+        if array.shape[-3] > 1:
+            is_3D = True
+    types = dict(is_3D=is_3D)
+    return types
+
 
 @validate_arguments
 def import_ome_zarr(
     *,
-    input_paths: Sequence[str],
-    output_path: str,
-    metadata: dict[str, Any],
+    zarr_urls: list[str],
+    zarr_dir: str,
     zarr_name: str,
     add_image_ROI_table: bool = True,
     add_grid_ROI_table: bool = True,
@@ -167,26 +175,26 @@ def import_ome_zarr(
     overwrite: bool = False,
 ) -> dict[str, Any]:
     """
-    Import an OME-Zarr into Fractal.
+    Import a single OME-Zarr into Fractal.
 
-    The current version of this task:
+    The single OME-Zarr can be a full OME-Zarr HCS plate or an individual
+    OME-Zarr image. The image needs to be in the zarr_dir as specified by the
+    dataset. The current version of this task:
 
     1. Creates the appropriate components-related metadata, needed for
        processing an existing OME-Zarr through Fractal.
     2. Optionally adds new ROI tables to the existing OME-Zarr.
 
     Args:
-        input_paths: A length-one list with the parent folder of the OME-Zarr
-            to be imported; e.g. `input_paths=["/somewhere"]`, if the OME-Zarr
-            path is `/somewhere/array.zarr`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: Not used in this task.
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed. Not used.
             (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: Not used in this task.
+        zarr_dir: path of the directory where the new OME-Zarrs will be
+            created.
             (standard argument for Fractal tasks, managed by Fractal server).
-        zarr_name: The OME-Zarr name, without its parent folder; e.g.
-            `zarr_name="array.zarr"`, if the OME-Zarr path is
-            `/somewhere/array.zarr`.
+        zarr_name: The OME-Zarr name, without its parent folder. The parent
+            folder is provided by zarr_dir; e.g. `zarr_name="array.zarr"`,
+            if the OME-Zarr path is in `/zarr_dir/array.zarr`.
         add_image_ROI_table: Whether to add a `image_ROI_table` table to each
             image, with a single ROI covering the whole image.
         add_grid_ROI_table: Whether to add a `grid_ROI_table` table to each
@@ -199,41 +207,51 @@ def import_ome_zarr(
             and/or `add_grid_ROI_table` are `True`) can overwite existing ones.
     """
 
-    # Preliminary checks
-    if len(input_paths) > 1:
-        raise NotImplementedError
+    # Is this based on the Zarr_dir or the zarr_urls?
+    if len(zarr_urls) > 0:
+        logger.warning(
+            "Running import while there are already items from the image list "
+            "provided to the task. The following inputs were provided: "
+            f"{zarr_urls=}"
+            "This task will not process the existing images, but look for "
+            f"zarr files named {zarr_name=} in the {zarr_dir=} instead."
+        )
 
-    zarr_path = str(Path(input_paths[0]) / zarr_name)
+    zarr_path = f"{zarr_dir.rstrip('/')}/{zarr_name}"
     logger.info(f"Zarr path: {zarr_path}")
 
-    zarrurls: dict = dict(plate=[], well=[], image=[])
-
     root_group = zarr.open_group(zarr_path, mode="r")
     ngff_type = detect_ome_ngff_type(root_group)
     grid_YX_shape = (grid_y_shape, grid_x_shape)
 
+    image_list_updates = []
     if ngff_type == "plate":
-        zarrurls["plate"].append(zarr_name)
         for well in root_group.attrs["plate"]["wells"]:
             well_path = well["path"]
-            zarrurls["well"].append(f"{zarr_name}/{well_path}")
 
             well_group = zarr.open_group(zarr_path, path=well_path, mode="r")
             for image in well_group.attrs["well"]["images"]:
                 image_path = image["path"]
-                zarrurls["image"].append(
-                    f"{zarr_name}/{well_path}/{image_path}"
-                )
-                _process_single_image(
-                    f"{zarr_path}/{well_path}/{image_path}",
+                zarr_url = f"{zarr_path}/{well_path}/{image_path}"
+                types = _process_single_image(
+                    zarr_url,
                     add_image_ROI_table,
                     add_grid_ROI_table,
                     update_omero_metadata,
                     grid_YX_shape=grid_YX_shape,
                     overwrite=overwrite,
                 )
+                image_list_updates.append(
+                    dict(
+                        zarr_url=zarr_url,
+                        attributes=dict(
+                            plate=zarr_name,
+                            well=well_path.replace("/", ""),
+                        ),
+                        types=types,
+                    )
+                )
     elif ngff_type == "well":
-        zarrurls["well"].append(zarr_name)
         logger.warning(
             "Only OME-Zarr for plates are fully supported in Fractal; "
             f"e.g. the current one ({ngff_type=}) cannot be "
@@ -241,37 +259,48 @@ def import_ome_zarr(
         )
         for image in root_group.attrs["well"]["images"]:
             image_path = image["path"]
-            zarrurls["image"].append(f"{zarr_name}/{image_path}")
-            _process_single_image(
-                f"{zarr_path}/{image_path}",
+            zarr_url = f"{zarr_path}/{image_path}"
+            types = _process_single_image(
+                zarr_url,
                 add_image_ROI_table,
                 add_grid_ROI_table,
                 update_omero_metadata,
                 grid_YX_shape=grid_YX_shape,
                 overwrite=overwrite,
             )
+            image_list_updates.append(
+                dict(
+                    zarr_url=zarr_url,
+                    attributes=dict(
+                        well=zarr_name,
+                    ),
+                    types=types,
+                )
+            )
     elif ngff_type == "image":
-        zarrurls["image"].append(zarr_name)
         logger.warning(
             "Only OME-Zarr for plates are fully supported in Fractal; "
             f"e.g. the current one ({ngff_type=}) cannot be "
             "processed via the `maximum_intensity_projection` task."
         )
-        _process_single_image(
-            zarr_path,
+        zarr_url = zarr_path
+        types = _process_single_image(
+            zarr_url,
             add_image_ROI_table,
             add_grid_ROI_table,
             update_omero_metadata,
             grid_YX_shape=grid_YX_shape,
             overwrite=overwrite,
         )
+        image_list_updates.append(
+            dict(
+                zarr_url=zarr_url,
+                types=types,
+            )
+        )
 
-    # Remove zarrurls keys pointing to empty lists
-    clean_zarrurls = {
-        key: value for key, value in zarrurls.items() if len(value) > 0
-    }
-
-    return clean_zarrurls
+    image_list_changes = dict(image_list_updates=image_list_updates)
+    return image_list_changes
 
 
 if __name__ == "__main__":
diff --git a/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py b/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py
new file mode 100644
index 000000000..defb4461c
--- /dev/null
+++ b/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py
@@ -0,0 +1,91 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+# Joel Lüthi <joel.luethi@uzh.ch>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Applies the multiplexing translation to all ROI tables
+"""
+import logging
+
+from pydantic.decorator import validate_arguments
+
+from fractal_tasks_core.tasks._registration_utils import (
+    create_well_acquisition_dict,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@validate_arguments
+def init_group_by_well_for_multiplexing(
+    *,
+    # Fractal arguments
+    zarr_urls: list[str],
+    zarr_dir: str,
+    # Task-specific arguments
+    reference_cycle: int = 0,
+) -> dict[str, list[str]]:
+    """
+    Finds images for all acquisitions per well.
+
+    Returns the parallelization_list to run `find_registration_consensus`.
+
+    Args:
+        zarr_urls: List of paths or urls to the individual OME-Zarr image to
+            be processed.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        zarr_dir: path of the directory where the new OME-Zarrs will be
+            created. Not used by this task.
+            (standard argument for Fractal tasks, managed by Fractal server).
+        reference_cycle: Which cycle to register against. Defaults to 0,
+            which is the first OME-Zarr image in the well, usually the first
+            cycle that was provided
+    """
+    logger.info(
+        f"Running `init_group_by_well_for_multiplexing` for {zarr_urls=}"
+    )
+    image_groups = create_well_acquisition_dict(zarr_urls)
+
+    # Create the parallelization list
+    parallelization_list = []
+    for key, image_group in image_groups.items():
+        # Assert that all image groups have the reference cycle present
+        if reference_cycle not in image_group.keys():
+            raise ValueError(
+                f"Registration with {reference_cycle=} can only work if all"
+                "wells have the reference cycle present. It was not found"
+                f"for well {key}."
+            )
+
+        # Create a parallelization list entry for each image group
+        zarr_url_list = []
+        for acquisition, zarr_url in image_group.items():
+            if acquisition == reference_cycle:
+                reference_zarr_url = zarr_url
+
+            zarr_url_list.append(zarr_url)
+
+        parallelization_list.append(
+            dict(
+                zarr_url=reference_zarr_url,
+                init_args=dict(zarr_url_list=zarr_url_list),
+            )
+        )
+
+    return dict(parallelization_list=parallelization_list)
+
+
+if __name__ == "__main__":
+    from fractal_tasks_core.tasks._utils import run_fractal_task
+
+    run_fractal_task(
+        task_function=init_group_by_well_for_multiplexing,
+        logger_name=logger.name,
+    )
diff --git a/fractal_tasks_core/tasks/io_models.py b/fractal_tasks_core/tasks/io_models.py
new file mode 100644
index 000000000..75c7c9cfe
--- /dev/null
+++ b/fractal_tasks_core/tasks/io_models.py
@@ -0,0 +1,169 @@
+from typing import Literal
+from typing import Optional
+
+from pydantic import BaseModel
+from pydantic import Field
+from pydantic import validator
+
+from fractal_tasks_core.channels import ChannelInputModel
+from fractal_tasks_core.channels import OmeroChannel
+
+
+class InitArgsRegistration(BaseModel):
+    """
+    Registration init args.
+
+    Passed from `image_based_registration_hcs_init` to
+    `calculate_registration_image_based`.
+
+    Attributes:
+        reference_zarr_url: zarr_url for the reference image
+    """
+
+    reference_zarr_url: str
+
+
+class InitArgsRegistrationConsensus(BaseModel):
+    """
+    Registration consensus init args.
+
+    Provides the list of zarr_urls for all acquisitions for a given well
+
+    Attributes:
+        zarr_url_list: List of zarr_urls for all the OME-Zarr images in the
+            well.
+    """
+
+    zarr_url_list: list[str]
+
+
+class InitArgsCellVoyager(BaseModel):
+    """
+    Arguments to be passed from cellvoyager converter init to compute
+
+    Attributes:
+        image_dir: Directory where the raw images are found
+        plate_prefix: part of the image filename needed for finding the
+            right subset of image files
+        well_ID: part of the image filename needed for finding the
+            right subset of image files
+        image_extension: part of the image filename needed for finding the
+            right subset of image files
+        image_glob_patterns: Additional glob patterns to filter the available
+            images with
+        acquisition: Acquisition metadata needed for multiplexing
+    """
+
+    image_dir: str
+    plate_prefix: str
+    well_ID: str
+    image_extension: str
+    image_glob_patterns: Optional[list[str]]
+    acquisition: Optional[int]
+
+
+class InitArgsIllumination(BaseModel):
+    """
+    Dummy model description.
+
+    Attributes:
+        raw_path: dummy attribute description.
+        subsets: dummy attribute description.
+    """
+
+    raw_path: str
+    subsets: dict[Literal["C_index"], int] = Field(default_factory=dict)
+
+
+class InitArgsMIP(BaseModel):
+    """
+    Init Args for MIP task.
+
+    Attributes:
+        origin_url: Path to the zarr_url with the 3D data
+    """
+
+    origin_url: str
+
+
+class MultiplexingAcquisition(BaseModel):
+    """
+    Input class for Multiplexing Cellvoyager converter
+
+    Attributes:
+        image_dir: Path to the folder that contains the Cellvoyager image
+            files for that acquisition and the MeasurementData &
+            MeasurementDetail metadata files.
+        allowed_channels: A list of `OmeroChannel` objects, where each channel
+            must include the `wavelength_id` attribute and where the
+            `wavelength_id` values must be unique across the list.
+    """
+
+    image_dir: str
+    allowed_channels: list[OmeroChannel]
+
+
+class NapariWorkflowsOutput(BaseModel):
+    """
+    A value of the `output_specs` argument in `napari_workflows_wrapper`.
+
+    Attributes:
+        type: Output type (either `label` or `dataframe`).
+        label_name: Label name (for label outputs, it is used as the name of
+            the label; for dataframe outputs, it is used to fill the
+            `region["path"]` field).
+        table_name: Table name (for dataframe outputs only).
+    """
+
+    type: Literal["label", "dataframe"]
+    label_name: str
+    table_name: Optional[str] = None
+
+    @validator("table_name", always=True)
+    def table_name_only_for_dataframe_type(cls, v, values):
+        """
+        Check that table_name is set only for dataframe outputs.
+        """
+        _type = values.get("type")
+        if (_type == "dataframe" and (not v)) or (_type != "dataframe" and v):
+            raise ValueError(
+                f"Output item has type={_type} but table_name={v}."
+            )
+        return v
+
+
+class NapariWorkflowsInput(BaseModel):
+    """
+    A value of the `input_specs` argument in `napari_workflows_wrapper`.
+
+    Attributes:
+        type: Input type (either `image` or `label`).
+        label_name: Label name (for label inputs only).
+        channel: `ChannelInputModel` object (for image inputs only).
+    """
+
+    type: Literal["image", "label"]
+    label_name: Optional[str]
+    channel: Optional[ChannelInputModel]
+
+    @validator("label_name", always=True)
+    def label_name_is_present(cls, v, values):
+        """
+        Check that label inputs have `label_name` set.
+        """
+        _type = values.get("type")
+        if _type == "label" and not v:
+            raise ValueError(
+                f"Input item has type={_type} but label_name={v}."
+            )
+        return v
+
+    @validator("channel", always=True)
+    def channel_is_present(cls, v, values):
+        """
+        Check that image inputs have `channel` set.
+        """
+        _type = values.get("type")
+        if _type == "image" and not v:
+            raise ValueError(f"Input item has type={_type} but channel={v}.")
+        return v
diff --git a/fractal_tasks_core/tasks/maximum_intensity_projection.py b/fractal_tasks_core/tasks/maximum_intensity_projection.py
index ba75e8b4a..437242a8d 100644
--- a/fractal_tasks_core/tasks/maximum_intensity_projection.py
+++ b/fractal_tasks_core/tasks/maximum_intensity_projection.py
@@ -13,28 +13,33 @@
 Task for 3D->2D maximum-intensity projection.
 """
 import logging
-from pathlib import Path
 from typing import Any
-from typing import Sequence
 
+import anndata as ad
 import dask.array as da
+import zarr
 from pydantic.decorator import validate_arguments
 from zarr.errors import ContainsArrayError
 
 from fractal_tasks_core.ngff import load_NgffImageMeta
 from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.roi import (
+    convert_ROIs_from_3D_to_2D,
+)
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tables.v1 import get_tables_list_v1
+from fractal_tasks_core.tasks.io_models import InitArgsMIP
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
+
 logger = logging.getLogger(__name__)
 
 
 @validate_arguments
 def maximum_intensity_projection(
     *,
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    zarr_url: str,
+    init_args: InitArgsMIP,
     overwrite: bool = False,
 ) -> dict[str, Any]:
     """
@@ -43,49 +48,37 @@ def maximum_intensity_projection(
     Note: this task stores the output in a new zarr file.
 
     Args:
-        input_paths: This parameter is not used by this task.
-            This task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: Path were the output of this task is stored.
-            Example: `"/some/path/"` => puts the new OME-Zarr file in that
-            folder.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that
-            is processed. Component is typically changed by the `copy_ome_zarr`
-            task before, to point to a new mip Zarr file.
-            Example: `"some_plate_mip.zarr/B/03/0"`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: Dictionary containing metadata about the OME-Zarr.
-            This task requires the key `copy_ome_zarr` to be present in the
-            metadata (as defined in `copy_ome_zarr` task).
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
+        init_args: Intialization arguments provided by
+            `create_cellvoyager_ome_zarr_init`.
         overwrite: If `True`, overwrite the task output.
     """
-
-    # Preliminary checks
-    if len(input_paths) > 1:
-        raise NotImplementedError
-
-    plate, well = component.split(".zarr/")
-    zarrurl_old = metadata["copy_ome_zarr"]["sources"][plate] + "/" + well
-    clean_output_path = Path(output_path).resolve()
-    zarrurl_new = (clean_output_path / component).as_posix()
-    logger.info(f"{zarrurl_old=}")
-    logger.info(f"{zarrurl_new=}")
-
-    # Read some parameters from metadata
-    ngff_image = load_NgffImageMeta(zarrurl_old)
-    num_levels = ngff_image.num_levels
-    coarsening_xy = ngff_image.coarsening_xy
+    logger.info(f"{init_args.origin_url=}")
+    logger.info(f"{zarr_url=}")
+
+    # Read image metadata
+    ngff_image = load_NgffImageMeta(init_args.origin_url)
+    # Currently not using the validation models due to wavelength_id issue
+    # See #681 for discussion
+    # new_attrs = ngff_image.dict(exclude_none=True)
+    # Current way to get the necessary metadata for MIP
+    group = zarr.open_group(init_args.origin_url, mode="r")
+    new_attrs = group.attrs.asdict()
+
+    # Create the zarr image with correct
+    new_image_group = zarr.group(zarr_url)
+    new_image_group.attrs.put(new_attrs)
 
     # Load 0-th level
-    data_czyx = da.from_zarr(zarrurl_old + "/0")
+    data_czyx = da.from_zarr(init_args.origin_url + "/0")
     num_channels = data_czyx.shape[0]
     chunksize_y = data_czyx.chunksize[-2]
     chunksize_x = data_czyx.chunksize[-1]
     logger.info(f"{num_channels=}")
     logger.info(f"{chunksize_y=}")
     logger.info(f"{chunksize_x=}")
+
     # Loop over channels
     accumulate_chl = []
     for ind_ch in range(num_channels):
@@ -97,14 +90,14 @@ def maximum_intensity_projection(
     # Write to disk (triggering execution)
     try:
         accumulated_array.to_zarr(
-            f"{zarrurl_new}/0",
+            f"{zarr_url}/0",
             overwrite=overwrite,
             dimension_separator="/",
             write_empty_chunks=False,
         )
     except ContainsArrayError as e:
         error_msg = (
-            f"Cannot write array to zarr group at '{zarrurl_new}/0', "
+            f"Cannot write array to zarr group at '{zarr_url}/0', "
             f"with {overwrite=} (original error: {str(e)}).\n"
             "Hint: try setting overwrite=True."
         )
@@ -114,14 +107,77 @@ def maximum_intensity_projection(
     # Starting from on-disk highest-resolution data, build and write to disk a
     # pyramid of coarser levels
     build_pyramid(
-        zarrurl=zarrurl_new,
+        zarrurl=zarr_url,
         overwrite=overwrite,
-        num_levels=num_levels,
-        coarsening_xy=coarsening_xy,
+        num_levels=ngff_image.num_levels,
+        coarsening_xy=ngff_image.coarsening_xy,
         chunksize=(1, 1, chunksize_y, chunksize_x),
     )
 
-    return {}
+    # Copy over any tables from the original zarr
+    # Generate the list of tables:
+    tables = get_tables_list_v1(init_args.origin_url)
+    roi_tables = get_tables_list_v1(init_args.origin_url, table_type="ROIs")
+    non_roi_tables = [table for table in tables if table not in roi_tables]
+
+    for table in roi_tables:
+        logger.info(
+            f"Reading {table} from "
+            f"{init_args.origin_url=}, convert it to 2D, and "
+            "write it back to the new zarr file."
+        )
+        new_ROI_table = ad.read_zarr(f"{init_args.origin_url}/tables/{table}")
+        old_ROI_table_attrs = zarr.open_group(
+            f"{init_args.origin_url}/tables/{table}"
+        ).attrs.asdict()
+
+        # Convert 3D ROIs to 2D
+        pxl_sizes_zyx = ngff_image.get_pixel_sizes_zyx(level=0)
+        new_ROI_table = convert_ROIs_from_3D_to_2D(
+            new_ROI_table, pixel_size_z=pxl_sizes_zyx[0]
+        )
+        # Write new table
+        write_table(
+            new_image_group,
+            table,
+            new_ROI_table,
+            table_attrs=old_ROI_table_attrs,
+            overwrite=overwrite,
+        )
+
+    for table in non_roi_tables:
+        logger.info(
+            f"Reading {table} from "
+            f"{init_args.origin_url=}, and "
+            "write it back to the new zarr file."
+        )
+        new_non_ROI_table = ad.read_zarr(
+            f"{init_args.origin_url}/tables/{table}"
+        )
+        old_non_ROI_table_attrs = zarr.open_group(
+            f"{init_args.origin_url}/tables/{table}"
+        ).attrs.asdict()
+
+        # Write new table
+        write_table(
+            new_image_group,
+            table,
+            new_non_ROI_table,
+            table_attrs=old_non_ROI_table_attrs,
+            overwrite=overwrite,
+        )
+
+    # Generate image_list_updates
+    image_list_update_dict = dict(
+        image_list_updates=[
+            dict(
+                zarr_url=zarr_url,
+                origin=init_args.origin_url,
+                types=dict(is_3D=False),
+            )
+        ]
+    )
+    return image_list_update_dict
 
 
 if __name__ == "__main__":
diff --git a/fractal_tasks_core/tasks/napari_workflows_wrapper.py b/fractal_tasks_core/tasks/napari_workflows_wrapper.py
index 9b4405485..9281a22dd 100644
--- a/fractal_tasks_core/tasks/napari_workflows_wrapper.py
+++ b/fractal_tasks_core/tasks/napari_workflows_wrapper.py
@@ -13,9 +13,7 @@
 Wrapper of napari-workflows.
 """
 import logging
-from pathlib import Path
 from typing import Any
-from typing import Sequence
 
 import anndata as ad
 import dask.array as da
@@ -37,10 +35,10 @@
 )
 from fractal_tasks_core.roi import load_region
 from fractal_tasks_core.tables import write_table
-from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+from fractal_tasks_core.tasks.io_models import (
     NapariWorkflowsInput,
 )
-from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+from fractal_tasks_core.tasks.io_models import (
     NapariWorkflowsOutput,
 )
 from fractal_tasks_core.upscale_array import upscale_array
@@ -64,11 +62,8 @@ class OutOfTaskScopeError(NotImplementedError):
 @validate_arguments
 def napari_workflows_wrapper(
     *,
-    # Default arguments for fractal tasks:
-    input_paths: Sequence[str],
-    output_path: str,
-    component: str,
-    metadata: dict[str, Any],
+    # Fractal argument
+    zarr_url: str,
     # Task-specific arguments:
     workflow_file: str,
     input_specs: dict[str, NapariWorkflowsInput],
@@ -101,19 +96,7 @@ def napari_workflows_wrapper(
     ```
 
     Args:
-        input_paths: List of input paths where the image data is stored as
-            OME-Zarrs. Should point to the parent folder containing one or many
-            OME-Zarr files, not the actual OME-Zarr file.
-            Example: `["/some/path/"]`.
-            his task only supports a single input path.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        output_path: This parameter is not used by this task.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        component: Path to the OME-Zarr image in the OME-Zarr plate that is
-            processed.
-            Example: `"some_plate.zarr/B/03/0"`.
-            (standard argument for Fractal tasks, managed by Fractal server).
-        metadata: This parameter is not used by this task.
+        zarr_url: Path or url to the individual OME-Zarr image to be processed.
             (standard argument for Fractal tasks, managed by Fractal server).
         workflow_file: Absolute path to napari-workflows YAML file
         input_specs: A dictionary of `NapariWorkflowsInput` values.
@@ -187,20 +170,13 @@ def napari_workflows_wrapper(
     if relabeling:
         max_label_for_relabeling = 0
 
-    # Pre-processing of task inputs
-    if len(input_paths) > 1:
-        raise NotImplementedError(
-            "We currently only support a single input path"
-        )
-    in_path = Path(input_paths[0]).as_posix()
     label_dtype = np.uint32
 
     # Read ROI table
-    zarrurl = f"{in_path}/{component}"
-    ROI_table = ad.read_zarr(f"{in_path}/{component}/tables/{input_ROI_table}")
+    ROI_table = ad.read_zarr(f"{zarr_url}/tables/{input_ROI_table}")
 
     # Load image metadata
-    ngff_image_meta = load_NgffImageMeta(zarrurl)
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     num_levels = ngff_image_meta.num_levels
     coarsening_xy = ngff_image_meta.coarsening_xy
 
@@ -229,11 +205,11 @@ def napari_workflows_wrapper(
     ]
     input_image_arrays = {}
     if image_inputs:
-        img_array = da.from_zarr(f"{in_path}/{component}/{level}")
+        img_array = da.from_zarr(f"{zarr_url}/{level}")
         # Loop over image inputs and assign corresponding channel of the image
         for (name, params) in image_inputs:
             channel = get_channel_from_image_zarr(
-                image_zarr_path=f"{in_path}/{component}",
+                image_zarr_path=zarr_url,
                 wavelength_id=params.channel.wavelength_id,
                 label=params.channel.label,
             )
@@ -287,7 +263,7 @@ def napari_workflows_wrapper(
         for (name, params) in label_inputs:
             label_name = params.label_name
             label_array_raw = da.from_zarr(
-                f"{in_path}/{component}/labels/{label_name}/{level}"
+                f"{zarr_url}/labels/{label_name}/{level}"
             )
             input_label_arrays[name] = label_array_raw
 
@@ -371,7 +347,7 @@ def napari_workflows_wrapper(
             # Re-load pixel size, matching to the correct level
             input_label_name = label_inputs[0][1].label_name
             ngff_label_image_meta = load_NgffImageMeta(
-                f"{in_path}/{component}/labels/{input_label_name}"
+                f"{zarr_url}/labels/{input_label_name}"
             )
             full_res_pxl_sizes_zyx = ngff_label_image_meta.get_pixel_sizes_zyx(
                 level=0
@@ -456,8 +432,7 @@ def napari_workflows_wrapper(
             }
 
             # (2) Prepare label group
-            zarrurl = f"{in_path}/{component}"
-            image_group = zarr.group(zarrurl)
+            image_group = zarr.group(zarr_url)
             label_group = prepare_label_group(
                 image_group,
                 label_name,
@@ -471,9 +446,7 @@ def napari_workflows_wrapper(
             )
 
             # (3) Create zarr group at level=0
-            store = zarr.storage.FSStore(
-                f"{in_path}/{component}/labels/{label_name}/0"
-            )
+            store = zarr.storage.FSStore(f"{zarr_url}/labels/{label_name}/0")
             mask_zarr = zarr.create(
                 shape=label_shape,
                 chunks=label_chunksize,
@@ -627,7 +600,7 @@ def napari_workflows_wrapper(
             measurement_table.obs = labels
 
         # Write to zarr group
-        image_group = zarr.group(f"{in_path}/{component}")
+        image_group = zarr.group(zarr_url)
         table_attrs = dict(
             type="feature_table",
             region=dict(path=f"../labels/{out_params.label_name}"),
@@ -646,7 +619,7 @@ def napari_workflows_wrapper(
     for (name, out_params) in label_outputs:
         label_name = out_params.label_name
         build_pyramid(
-            zarrurl=f"{zarrurl}/labels/{label_name}",
+            zarrurl=f"{zarr_url}/labels/{label_name}",
             overwrite=overwrite,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
@@ -654,8 +627,6 @@ def napari_workflows_wrapper(
             aggregation_function=np.max,
         )
 
-    return {}
-
 
 if __name__ == "__main__":
     from fractal_tasks_core.tasks._utils import run_fractal_task
diff --git a/fractal_tasks_core/tasks/napari_workflows_wrapper_models.py b/fractal_tasks_core/tasks/napari_workflows_wrapper_models.py
deleted file mode 100644
index 7e74ef666..000000000
--- a/fractal_tasks_core/tasks/napari_workflows_wrapper_models.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from typing import Literal
-from typing import Optional
-
-from pydantic import BaseModel
-from pydantic import validator
-
-from fractal_tasks_core.channels import ChannelInputModel
-
-
-class NapariWorkflowsOutput(BaseModel):
-    """
-    A value of the `output_specs` argument in `napari_workflows_wrapper`.
-
-    Attributes:
-        type: Output type (either `label` or `dataframe`).
-        label_name: Label name (for label outputs, it is used as the name of
-            the label; for dataframe outputs, it is used to fill the
-            `region["path"]` field).
-        table_name: Table name (for dataframe outputs only).
-    """
-
-    type: Literal["label", "dataframe"]
-    label_name: str
-    table_name: Optional[str] = None
-
-    @validator("table_name", always=True)
-    def table_name_only_for_dataframe_type(cls, v, values):
-        """
-        Check that table_name is set only for dataframe outputs.
-        """
-        _type = values.get("type")
-        if (_type == "dataframe" and (not v)) or (_type != "dataframe" and v):
-            raise ValueError(
-                f"Output item has type={_type} but table_name={v}."
-            )
-        return v
-
-
-class NapariWorkflowsInput(BaseModel):
-    """
-    A value of the `input_specs` argument in `napari_workflows_wrapper`.
-
-    Attributes:
-        type: Input type (either `image` or `label`).
-        label_name: Label name (for label inputs only).
-        channel: `ChannelInputModel` object (for image inputs only).
-    """
-
-    type: Literal["image", "label"]
-    label_name: Optional[str]
-    channel: Optional[ChannelInputModel]
-
-    @validator("label_name", always=True)
-    def label_name_is_present(cls, v, values):
-        """
-        Check that label inputs have `label_name` set.
-        """
-        _type = values.get("type")
-        if _type == "label" and not v:
-            raise ValueError(
-                f"Input item has type={_type} but label_name={v}."
-            )
-        return v
-
-    @validator("channel", always=True)
-    def channel_is_present(cls, v, values):
-        """
-        Check that image inputs have `channel` set.
-        """
-        _type = values.get("type")
-        if _type == "image" and not v:
-            raise ValueError(f"Input item has type={_type} but channel={v}.")
-        return v
diff --git a/fractal_tasks_core/utils.py b/fractal_tasks_core/utils.py
index aa6cbb833..4530109a6 100644
--- a/fractal_tasks_core/utils.py
+++ b/fractal_tasks_core/utils.py
@@ -75,18 +75,14 @@ def rescale_datasets(
     return new_datasets
 
 
-def get_table_path_dict(input_path: Path, component: str) -> dict[str, str]:
+def _get_table_path_dict(zarr_url: str) -> dict[str, str]:
     """
     Compile dictionary of (table name, table path) key/value pairs.
 
 
     Args:
-        input_path:
-            Path to the parent folder of a plate zarr group (e.g.
-            `/some/path/`).
-        component:
-            Path (relative to `input_path`) to an image zarr group (e.g.
-            `plate.zarr/B/03/0`).
+        zarr_url:
+            Path or url to the individual OME-Zarr image to be processed.
 
     Returns:
         Dictionary with table names as keys and table paths as values. If
@@ -95,14 +91,14 @@ def get_table_path_dict(input_path: Path, component: str) -> dict[str, str]:
     """
 
     try:
-        tables_group = zarr.open_group(f"{input_path / component}/tables", "r")
+        tables_group = zarr.open_group(f"{zarr_url}/tables", "r")
         table_list = tables_group.attrs["tables"]
     except (zarr.errors.GroupNotFoundError, KeyError):
         table_list = []
 
     table_path_dict = {}
     for table in table_list:
-        table_path_dict[table] = f"{input_path / component}/tables/{table}"
+        table_path_dict[table] = f"{zarr_url}/tables/{table}"
 
     return table_path_dict
 
diff --git a/poetry.lock b/poetry.lock
index 83c9c1257..6353417c2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -3831,79 +3831,80 @@ ptyprocess = ">=0.5"
 
 [[package]]
 name = "pillow"
-version = "10.2.0"
+version = "10.3.0"
 description = "Python Imaging Library (Fork)"
 optional = true
 python-versions = ">=3.8"
 files = [
-    {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
-    {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
-    {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
-    {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
-    {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
-    {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
-    {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
-    {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
-    {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
-    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
-    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
-    {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
-    {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
-    {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
-    {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
-    {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
-    {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
-    {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
-    {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
-    {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
-    {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
-    {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
-    {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
-    {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
-    {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
-    {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
-    {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
-    {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
-    {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
-    {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
-    {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
-    {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
-    {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
-    {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
-    {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
-    {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
+    {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
+    {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
+    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
+    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
+    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
+    {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
+    {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
+    {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
+    {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
+    {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
+    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
+    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
+    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
+    {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
+    {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
+    {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
+    {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
+    {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
+    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
+    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
+    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
+    {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
+    {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
+    {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
+    {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
+    {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
+    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
+    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
+    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
+    {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
+    {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
+    {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
+    {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
+    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
+    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
+    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
+    {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
+    {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
+    {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
+    {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
+    {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
+    {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
 ]
 
 [package.extras]
@@ -4457,7 +4458,6 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
     {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
     {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
     {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
     {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
     {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},


Test patch:
diff --git a/examples/03_cardio_tiny_dataset_with_illumination_correction/fetch_test_data_from_zenodo.sh b/examples/02_cardio_tiny_mip/fetch_test_data_from_zenodo.sh
similarity index 100%
rename from examples/03_cardio_tiny_dataset_with_illumination_correction/fetch_test_data_from_zenodo.sh
rename to examples/02_cardio_tiny_mip/fetch_test_data_from_zenodo.sh
diff --git a/examples/02_cardio_tiny_dataset_sparse/fetch_test_data_from_zenodo.sh b/examples/old/_02_cardio_tiny_dataset_sparse/fetch_test_data_from_zenodo.sh
similarity index 100%
rename from examples/02_cardio_tiny_dataset_sparse/fetch_test_data_from_zenodo.sh
rename to examples/old/_02_cardio_tiny_dataset_sparse/fetch_test_data_from_zenodo.sh
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/fetch_test_data_from_zenodo.sh b/examples/old/_03_cardio_tiny_dataset_with_illumination_correction/fetch_test_data_from_zenodo.sh
similarity index 100%
rename from examples/04_cardio_tiny_dataset_napari_worfklows/fetch_test_data_from_zenodo.sh
rename to examples/old/_03_cardio_tiny_dataset_with_illumination_correction/fetch_test_data_from_zenodo.sh
diff --git a/examples/05_cardio_tiny_dataset_cellpose/fetch_test_data_from_zenodo.sh b/examples/old/_04_cardio_tiny_dataset_napari_worfklows/fetch_test_data_from_zenodo.sh
similarity index 100%
rename from examples/05_cardio_tiny_dataset_cellpose/fetch_test_data_from_zenodo.sh
rename to examples/old/_04_cardio_tiny_dataset_napari_worfklows/fetch_test_data_from_zenodo.sh
diff --git a/examples/06_cellpose_dev_example/fetch_test_data_from_zenodo.sh b/examples/old/_05_cardio_tiny_dataset_cellpose/fetch_test_data_from_zenodo.sh
similarity index 100%
rename from examples/06_cellpose_dev_example/fetch_test_data_from_zenodo.sh
rename to examples/old/_05_cardio_tiny_dataset_cellpose/fetch_test_data_from_zenodo.sh
diff --git a/examples/old/_06_cellpose_dev_example/fetch_test_data_from_zenodo.sh b/examples/old/_06_cellpose_dev_example/fetch_test_data_from_zenodo.sh
new file mode 100755
index 000000000..fcda5db76
--- /dev/null
+++ b/examples/old/_06_cellpose_dev_example/fetch_test_data_from_zenodo.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+DOI="10.5281/zenodo.7059515"
+CLEAN_DOI=${DOI/\//_}
+zenodo_get $DOI -o ../images/${CLEAN_DOI}
diff --git a/examples/06_cellpose_dev_example/fetch_test_data_from_zenodo_2x2.sh b/examples/old/_06_cellpose_dev_example/fetch_test_data_from_zenodo_2x2.sh
similarity index 100%
rename from examples/06_cellpose_dev_example/fetch_test_data_from_zenodo_2x2.sh
rename to examples/old/_06_cellpose_dev_example/fetch_test_data_from_zenodo_2x2.sh
diff --git a/examples/07_use_task_from_command_line/fetch_test_data_from_zenodo_2x2.sh b/examples/old/_07_use_task_from_command_line/fetch_test_data_from_zenodo_2x2.sh
similarity index 100%
rename from examples/07_use_task_from_command_line/fetch_test_data_from_zenodo_2x2.sh
rename to examples/old/_07_use_task_from_command_line/fetch_test_data_from_zenodo_2x2.sh
diff --git a/examples/08_subset_of_data/fetch_test_data_from_zenodo_2x2.sh b/examples/old/_08_subset_of_data/fetch_test_data_from_zenodo_2x2.sh
similarity index 100%
rename from examples/08_subset_of_data/fetch_test_data_from_zenodo_2x2.sh
rename to examples/old/_08_subset_of_data/fetch_test_data_from_zenodo_2x2.sh
diff --git a/tests/_zenodo_ome_zarrs.py b/tests/_zenodo_ome_zarrs.py
index f0976879f..662a394a2 100644
--- a/tests/_zenodo_ome_zarrs.py
+++ b/tests/_zenodo_ome_zarrs.py
@@ -15,7 +15,6 @@
 import logging
 import shutil
 from pathlib import Path
-from typing import Any
 
 import dask.array as da
 import zarr
@@ -25,12 +24,10 @@
 def prepare_3D_zarr(
     zarr_path: str,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     remove_tables: bool = False,
     remove_omero: bool = False,
-):
+) -> list[str]:
     zenodo_zarr_3D, zenodo_zarr_2D = zenodo_zarr[:]
-    metadata_3D, metadata_2D = zenodo_zarr_metadata[:]
     shutil.copytree(
         zenodo_zarr_3D, str(Path(zarr_path) / Path(zenodo_zarr_3D).name)
     )
@@ -48,19 +45,17 @@ def prepare_3D_zarr(
         image_attrs.pop("omero")
         image_group.attrs.put(image_attrs)
         logging.warning("Removing omero attributes from 3D Zenodo zarr")
-    metadata = metadata_3D.copy()
-    return metadata
+
+    return [str(Path(zarr_path) / Path(zenodo_zarr_3D).name / "B/03/0")]
 
 
 def prepare_2D_zarr(
     zarr_path: str,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     remove_labels: bool = False,
     make_CYX: bool = False,
-):
+) -> list[str]:
     zenodo_zarr_3D, zenodo_zarr_2D = zenodo_zarr[:]
-    metadata_3D, metadata_2D = zenodo_zarr_metadata[:]
     shutil.copytree(
         zenodo_zarr_2D, str(Path(zarr_path) / Path(zenodo_zarr_2D).name)
     )
@@ -103,5 +98,4 @@ def prepare_2D_zarr(
             shutil.rmtree(zarr_path)
             da.array(data_cyx).to_zarr(zarr_path, dimension_separator="/")
 
-    metadata = metadata_2D.copy()
-    return metadata
+    return [FOV_path.as_posix()]
diff --git a/tests/tasks/conftest.py b/tests/tasks/conftest.py
index adf31d3ca..0db752253 100644
--- a/tests/tasks/conftest.py
+++ b/tests/tasks/conftest.py
@@ -118,32 +118,3 @@ def zenodo_zarr(testdata_path: Path) -> list[str]:
             shutil.rmtree(str(folder))
         shutil.copytree(Path(zarr_full_path) / file_name, folder)
     return [str(f) for f in folders]
-
-
-@pytest.fixture(scope="function")
-def zenodo_zarr_metadata(testdata_path: Path):
-    metadata_3D = {
-        "plate": ["plate.zarr"],
-        "well": ["plate.zarr/B/03"],
-        "image": ["plate.zarr/B/03/0/"],
-        "num_levels": 6,
-        "coarsening_xy": 2,
-        "original_paths": [str(testdata_path / "10_5281_zenodo_7059515/")],
-        "image_extension": "png",
-    }
-
-    metadata_2D = {
-        "plate": ["plate.zarr"],
-        "well": ["plate_mip.zarr/B/03/"],
-        "image": ["plate_mip.zarr/B/03/0/"],
-        "num_levels": 6,
-        "coarsening_xy": 2,
-        "original_paths": [str(testdata_path / "10_5281_zenodo_7059515/")],
-        "image_extension": "png",
-        "replicate_zarr": {
-            "suffix": "mip",
-            "sources": {"plate_mip": "/this/should/not/be/used/"},
-        },
-    }
-
-    return [metadata_3D, metadata_2D]
diff --git a/tests/tasks/test_executable_workflow.py b/tests/tasks/test_executable_workflow.py
index 4f550065a..1cd5d5b6a 100644
--- a/tests/tasks/test_executable_workflow.py
+++ b/tests/tasks/test_executable_workflow.py
@@ -54,16 +54,15 @@ def test_workflow_yokogawa_to_ome_zarr(tmp_path: Path, zenodo_images: str):
 
     # Init
     img_path = zenodo_images
-    zarr_path = str(tmp_path / "tmp_out/")
-    metadata = {}
+    zarr_dir = str(tmp_path / "tmp_out/")
     tasks_path = str(Path(fractal_tasks_core.tasks.__file__).parent)
 
     # Create zarr structure
     args_create_zarr = dict(
-        input_paths=[str(img_path)],
-        output_path=str(zarr_path),
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
+        image_dirs=[img_path],
         allowed_channels=allowed_channels,
-        metadata={},
         image_extension="png",
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
@@ -72,39 +71,34 @@ def test_workflow_yokogawa_to_ome_zarr(tmp_path: Path, zenodo_images: str):
 
     # Run task as executable
     input_json_path = f"{str(tmp_path)}/args_create_zarr.json"
-    output_json_path = f"{str(tmp_path)}/metadata_create_zarr.json"
+    output_json_path = f"{str(tmp_path)}/out_create_zarr.json"
     with open(input_json_path, "w") as js:
         json.dump(args_create_zarr, js, cls=TaskParameterEncoder)
     cmd = (
-        f"python {tasks_path}/create_ome_zarr.py "
-        f"-j {input_json_path} "
-        f"--metadata-out {output_json_path}"
+        f"python {tasks_path}/cellvoyager_to_ome_zarr_init.py "
+        f"--args-json {input_json_path} "
+        f"--out-json {output_json_path}"
     )
     run_command(cmd)
     with open(output_json_path, "r") as js:
-        diff_metadata = json.load(js)
-
-    # Update metadata
-    metadata.update(diff_metadata)
-    debug(metadata)
+        parallelization_list = json.load(js)["parallelization_list"]
+    debug(parallelization_list)
 
     # Yokogawa to zarr
-    for component in metadata["image"]:
+    for image in parallelization_list:
         args_yokogawa = dict(
-            input_paths=[zarr_path],
-            output_path=zarr_path,
-            metadata=metadata,
-            component=component,
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
         )
 
         # Run task as executable
         input_json_path = f"{str(tmp_path)}/args_yokogawa.json"
-        output_json_path = f"{str(tmp_path)}/metadata_yokogawa.json"
+        output_json_path = f"{str(tmp_path)}/out_yokogawa.json"
         with open(input_json_path, "w") as js:
             json.dump(args_yokogawa, js, cls=TaskParameterEncoder)
         cmd = (
-            f"python {tasks_path}/yokogawa_to_ome_zarr.py "
-            f"-j {input_json_path} "
-            f"--metadata-out {output_json_path}"
+            f"python {tasks_path}/cellvoyager_to_ome_zarr_compute.py "
+            f"--args-json {input_json_path} "
+            f"--out-json {output_json_path}"
         )
         run_command(cmd)
diff --git a/tests/tasks/test_import_ome_zarr.py b/tests/tasks/test_import_ome_zarr.py
index d408b737f..687719d2a 100644
--- a/tests/tasks/test_import_ome_zarr.py
+++ b/tests/tasks/test_import_ome_zarr.py
@@ -2,14 +2,16 @@
 import zarr
 from devtools import debug
 
-import fractal_tasks_core.tasks  # noqa
 from .._zenodo_ome_zarrs import prepare_3D_zarr
-from fractal_tasks_core.channels import ChannelInputModel
-from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
+from fractal_tasks_core.tasks.copy_ome_zarr_hcs_plate import (
+    copy_ome_zarr_hcs_plate,
+)
 from fractal_tasks_core.tasks.import_ome_zarr import import_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
     maximum_intensity_projection,
-)  # noqa
+)
+
+# from fractal_tasks_core.channels import ChannelInputModel
 
 
 def _check_ROI_tables(_image_path):
@@ -20,126 +22,143 @@ def _check_ROI_tables(_image_path):
     zarr.open_group(f"{_image_path}/tables/grid_ROI_table", mode="r")
 
 
-def test_import_ome_zarr_plate(tmp_path, zenodo_zarr, zenodo_zarr_metadata):
+def test_import_ome_zarr_plate(tmp_path, zenodo_zarr):
 
     # Prepare an on-disk OME-Zarr at the plate level
-    root_path = tmp_path
-    prepare_3D_zarr(
-        root_path, zenodo_zarr, zenodo_zarr_metadata, remove_tables=True
-    )
+    zarr_dir = str(tmp_path)
+    prepare_3D_zarr(zarr_dir, zenodo_zarr, remove_tables=True)
     zarr_name = "plate.zarr"
 
     # Run import_ome_zarr
-    metadiff = import_ome_zarr(
-        input_paths=[str(root_path)],
+    image_list_changes = import_ome_zarr(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
         zarr_name=zarr_name,
-        output_path="null",
-        metadata={},
         grid_y_shape=3,
         grid_x_shape=3,
     )
-    metadata = metadiff.copy()
-
-    # Check metadata
-    EXPECTED_METADATA = dict(
-        plate=["plate.zarr"],
-        well=["plate.zarr/B/03"],
-        image=["plate.zarr/B/03/0"],
-    )
-    assert metadata == EXPECTED_METADATA
+    debug(image_list_changes)
+    zarr_urls = [
+        x["zarr_url"] for x in image_list_changes["image_list_updates"]
+    ]
+
+    expected_image_list_changes = {
+        "image_list_updates": [
+            {
+                "zarr_url": zarr_urls[0],
+                "attributes": {
+                    "plate": "plate.zarr",
+                    "well": "B03",
+                },
+                "types": {
+                    "is_3D": True,
+                },
+            },
+        ],
+    }
+    assert expected_image_list_changes == image_list_changes
 
     # Check that table were created
-    _check_ROI_tables(f"{root_path}/{zarr_name}/B/03/0")
+    _check_ROI_tables(f"{zarr_dir}/{zarr_name}/B/03/0")
 
     # Run copy_ome_zarr and maximum_intensity_projection
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(root_path)],
-        output_path=str(root_path),
-        metadata=metadata,
-        project_to_2D=True,
-        suffix="mip",
-        ROI_table_names=("image_ROI_table", "grid_ROI_table"),
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
-    for component in metadata["image"]:
+    parallelization_list = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls,
+        zarr_dir="tmp_out",
+        overwrite=True,
+    )["parallelization_list"]
+    debug(parallelization_list)
+
+    for image in parallelization_list:
         maximum_intensity_projection(
-            input_paths=[str(tmp_path)],
-            output_path=str(tmp_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+            overwrite=True,
         )
 
 
-def test_import_ome_zarr_well(tmp_path, zenodo_zarr, zenodo_zarr_metadata):
+def test_import_ome_zarr_well(tmp_path, zenodo_zarr):
 
     # Prepare an on-disk OME-Zarr at the plate level
-    root_path = tmp_path
-    prepare_3D_zarr(
-        root_path, zenodo_zarr, zenodo_zarr_metadata, remove_tables=True
-    )
+    zarr_dir = str(tmp_path)
+    prepare_3D_zarr(zarr_dir, zenodo_zarr, remove_tables=True)
     zarr_name = "plate.zarr/B/03"
 
     # Run import_ome_zarr
-    metadiff = import_ome_zarr(
-        input_paths=[str(root_path)],
+    image_list_changes = import_ome_zarr(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
         zarr_name=zarr_name,
-        output_path="null",
-        metadata={},
         grid_y_shape=3,
         grid_x_shape=3,
     )
-    metadata = metadiff.copy()
-
-    # Check metadata
-    EXPECTED_METADATA = dict(
-        well=["plate.zarr/B/03"],
-        image=["plate.zarr/B/03/0"],
-    )
-    assert metadata == EXPECTED_METADATA
+    debug(image_list_changes)
+    zarr_urls = [
+        x["zarr_url"] for x in image_list_changes["image_list_updates"]
+    ]
+
+    expected_image_list_changes = {
+        "image_list_updates": [
+            {
+                "zarr_url": zarr_urls[0],
+                "attributes": {
+                    "well": "plate.zarr/B/03",
+                },
+                "types": {
+                    "is_3D": True,
+                },
+            },
+        ],
+    }
+    assert expected_image_list_changes == image_list_changes
 
     # Check that table were created
-    _check_ROI_tables(f"{root_path}/{zarr_name}/0")
+    _check_ROI_tables(f"{zarr_dir}/{zarr_name}/0")
 
 
 @pytest.mark.parametrize("reset_omero", [True, False])
-def test_import_ome_zarr_image(
-    tmp_path, zenodo_zarr, zenodo_zarr_metadata, reset_omero
-):
+def test_import_ome_zarr_image(tmp_path, zenodo_zarr, reset_omero):
 
     # Prepare an on-disk OME-Zarr at the plate level
-    root_path = tmp_path
+    zarr_dir = str(tmp_path)
     prepare_3D_zarr(
-        root_path,
+        zarr_dir,
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_tables=True,
         remove_omero=reset_omero,
     )
     zarr_name = "plate.zarr/B/03/0"
 
     # Run import_ome_zarr
-    metadiff = import_ome_zarr(
-        input_paths=[str(root_path)],
+    image_list_changes = import_ome_zarr(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
         zarr_name=zarr_name,
-        output_path="null",
-        metadata={},
         grid_y_shape=3,
         grid_x_shape=3,
     )
-    metadata = metadiff.copy()
-
-    # Check metadata
-    EXPECTED_METADATA = dict(
-        image=["plate.zarr/B/03/0"],
-    )
-    assert metadata == EXPECTED_METADATA
+    debug(image_list_changes)
+    zarr_urls = [
+        x["zarr_url"] for x in image_list_changes["image_list_updates"]
+    ]
+
+    expected_image_list_changes = {
+        "image_list_updates": [
+            {
+                "zarr_url": zarr_urls[0],
+                "types": {
+                    "is_3D": True,
+                },
+            },
+        ],
+    }
+    assert expected_image_list_changes == image_list_changes
 
     # Check that table were created
-    _check_ROI_tables(f"{root_path}/{zarr_name}")
+    _check_ROI_tables(f"{zarr_dir}/{zarr_name}")
 
     # Check that omero attributes were filled correctly
-    g = zarr.open_group(str(root_path / zarr_name), mode="r")
+    g = zarr.open_group(f"{zarr_dir}/{zarr_name}", mode="r")
     debug(g.attrs["omero"]["channels"])
     if reset_omero:
         EXPECTED_CHANNELS = [
@@ -156,22 +175,19 @@ def test_import_ome_zarr_image(
         )
 
 
-def test_import_ome_zarr_image_wrong_channels(
-    tmp_path, zenodo_zarr, zenodo_zarr_metadata
-):
+def test_import_ome_zarr_image_wrong_channels(tmp_path, zenodo_zarr):
     # Prepare an on-disk OME-Zarr at the plate level
-    root_path = tmp_path
+    zarr_dir = str(tmp_path)
     prepare_3D_zarr(
-        root_path,
+        zarr_dir,
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_tables=True,
         remove_omero=True,
     )
     zarr_name = "plate.zarr/B/03/0"
     # Modify NGFF omero metadata, adding two channels (even if the Zarr array
     # has only one)
-    g = zarr.open_group(str(root_path / zarr_name), mode="r+")
+    g = zarr.open_group(f"{zarr_dir}/{zarr_name}", mode="r+")
     new_omero = dict(
         channels=[
             dict(color="asd"),
@@ -181,17 +197,18 @@ def test_import_ome_zarr_image_wrong_channels(
     g.attrs.update(omero=new_omero)
     # Run import_ome_zarr and catch the error
     with pytest.raises(ValueError) as e:
-        import_ome_zarr(
-            input_paths=[str(root_path)],
+        _ = import_ome_zarr(
+            zarr_urls=[],
+            zarr_dir=zarr_dir,
             zarr_name=zarr_name,
-            output_path="null",
-            metadata={},
+            grid_y_shape=3,
+            grid_x_shape=3,
         )
     debug(e.value)
     assert "Channels-number mismatch" in str(e.value)
 
 
-@pytest.mark.skip
+# @pytest.mark.skip
 def test_import_ome_zarr_image_BIA(tmp_path, monkeypatch):
     """
     This test imports one of the BIA OME-Zarr listed in
@@ -220,24 +237,27 @@ def test_import_ome_zarr_image_BIA(tmp_path, monkeypatch):
     with zipfile.ZipFile(tmp_path / fname, "r") as zip_ref:
         zip_ref.extractall(tmp_path)
 
-    root_path = str(tmp_path)
+    zarr_dir = str(tmp_path)
     zarr_name = "WD1_15-02_WT_confocalonly.zarr/0"
 
     # Run import_ome_zarr
-    metadiff = import_ome_zarr(
-        input_paths=[str(root_path)],
+    image_list_changes = import_ome_zarr(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
         zarr_name=zarr_name,
-        output_path="null",
-        metadata={},
+        grid_y_shape=3,
+        grid_x_shape=3,
     )
-    metadata = metadiff.copy()
-    debug(metadata)
+    debug(image_list_changes)
+    # zarr_urls = [
+    #     x["zarr_url"] for x in image_list_changes["image_list_updates"]
+    # ]
 
     # Check that table were created
-    _check_ROI_tables(f"{root_path}/{zarr_name}")
+    _check_ROI_tables(f"{zarr_dir}/{zarr_name}")
 
     # Check image_ROI_table
-    g = zarr.open(f"{root_path}/{zarr_name}", mode="r")
+    g = zarr.open(f"{zarr_dir}/{zarr_name}", mode="r")
     debug(g.attrs.asdict())
     pixel_size_x = g.attrs["multiscales"][0]["datasets"][0][
         "coordinateTransformations"
@@ -245,12 +265,12 @@ def test_import_ome_zarr_image_BIA(tmp_path, monkeypatch):
         -1
     ]  # noqa
     debug(pixel_size_x)
-    g = zarr.open(f"{root_path}/{zarr_name}/0", mode="r")
+    g = zarr.open(f"{zarr_dir}/{zarr_name}/0", mode="r")
     array_shape_x = g.shape[-1]
     debug(array_shape_x)
     EXPECTED_X_LENGTH = array_shape_x * pixel_size_x
     image_ROI_table = ad.read_zarr(
-        f"{root_path}/{zarr_name}/tables/image_ROI_table"
+        f"{zarr_dir}/{zarr_name}/tables/image_ROI_table"
     )
     debug(image_ROI_table.X)
     assert np.allclose(
@@ -258,7 +278,7 @@ def test_import_ome_zarr_image_BIA(tmp_path, monkeypatch):
         EXPECTED_X_LENGTH,
     )
 
-    g = zarr.open(f"{root_path}/{zarr_name}", mode="r")
+    g = zarr.open(f"{zarr_dir}/{zarr_name}", mode="r")
     omero_channels = g.attrs["omero"]["channels"]
     debug(omero_channels)
     assert len(omero_channels) == 1
@@ -268,35 +288,35 @@ def test_import_ome_zarr_image_BIA(tmp_path, monkeypatch):
 
     # Part 2: run Cellpose on the imported OME-Zarr.
 
-    from fractal_tasks_core.cellpose_segmentation import cellpose_segmentation
-    from .test_workflows_cellpose_segmentation import (
-        patched_cellpose_core_use_gpu,
-        patched_segment_ROI,
-    )
-
-    monkeypatch.setattr(
-        "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
-        patched_cellpose_core_use_gpu,
-    )
-
-    monkeypatch.setattr(
-        "fractal_tasks_core.tasks.cellpose_segmentation.segment_ROI",
-        patched_segment_ROI,
-    )
-
-    # Per-FOV labeling
-    for component in metadata["image"]:
-        cellpose_segmentation(
-            input_paths=[str(root_path)],
-            output_path=str(root_path),
-            input_ROI_table="grid_ROI_table",
-            metadata=metadata,
-            component=component,
-            channel=ChannelInputModel(wavelength_id="Channel 0"),
-            level=0,
-            relabeling=True,
-            diameter_level0=80.0,
-            augment=True,
-            net_avg=True,
-            min_size=30,
-        )
+    # Cellpose task deactivated, as it cannot handle t axis of this dataset yet
+    # from fractal_tasks_core.tasks.cellpose_segmentation import (
+    #     cellpose_segmentation
+    # )
+    # from .test_workflows_cellpose_segmentation import (
+    #     patched_cellpose_core_use_gpu,
+    #     patched_segment_ROI,
+    # )
+
+    # monkeypatch.setattr(
+    #     "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
+    #     patched_cellpose_core_use_gpu,
+    # )
+
+    # monkeypatch.setattr(
+    #     "fractal_tasks_core.tasks.cellpose_segmentation.segment_ROI",
+    #     patched_segment_ROI,
+    # )
+
+    # # Per-FOV labeling
+    # for zarr_url in zarr_urls:
+    #     cellpose_segmentation(
+    #         zarr_url=zarr_url,
+    #         input_ROI_table="grid_ROI_table",
+    #         channel=ChannelInputModel(wavelength_id="Channel 0"),
+    #         level=0,
+    #         relabeling=True,
+    #         diameter_level0=80.0,
+    #         augment=True,
+    #         net_avg=True,
+    #         min_size=30,
+    #     )
diff --git a/tests/tasks/test_registration.py b/tests/tasks/test_registration.py
index ea1c98749..3a7d3c646 100644
--- a/tests/tasks/test_registration.py
+++ b/tests/tasks/test_registration.py
@@ -6,11 +6,11 @@
 
 import anndata as ad
 import dask.array as da
+import imageio
 import numpy as np
 import pandas as pd
 import pytest
 from devtools import debug
-from PIL import Image
 from pytest import MonkeyPatch
 
 from fractal_tasks_core.channels import ChannelInputModel
@@ -25,25 +25,34 @@
 from fractal_tasks_core.tasks.apply_registration_to_image import (
     apply_registration_to_image,
 )
-from fractal_tasks_core.tasks.apply_registration_to_ROI_tables import (
-    apply_registration_to_ROI_tables,
-)
 from fractal_tasks_core.tasks.calculate_registration_image_based import (
     calculate_registration_image_based,
 )
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
-from fractal_tasks_core.tasks.copy_ome_zarr import (
-    copy_ome_zarr,
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_compute import (
+    cellvoyager_to_ome_zarr_compute,
+)
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init_multiplex import (
+    cellvoyager_to_ome_zarr_init_multiplex,
+)
+from fractal_tasks_core.tasks.copy_ome_zarr_hcs_plate import (
+    copy_ome_zarr_hcs_plate,
 )
-from fractal_tasks_core.tasks.create_ome_zarr_multiplex import (
-    create_ome_zarr_multiplex,
+from fractal_tasks_core.tasks.find_registration_consensus import (
+    find_registration_consensus,
 )
+from fractal_tasks_core.tasks.image_based_registration_hcs_init import (
+    image_based_registration_hcs_init,
+)
+from fractal_tasks_core.tasks.init_group_by_well_for_multiplexing import (
+    init_group_by_well_for_multiplexing,
+)
+from fractal_tasks_core.tasks.io_models import MultiplexingAcquisition
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
     maximum_intensity_projection,
 )
-from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
 
 
 single_cycle_allowed_channels_no_label = [
@@ -70,23 +79,20 @@ def _shift_image(
     """
 
     # Open old image as array
-    old_img = Image.open(img_path)
-    new_array = np.asarray(old_img).copy()
+    img = imageio.v2.imread(img_path)
 
     # Shift image by (shift_y_pxl, shift_x_pxl)
-    new_array[:, :-shift_x_pxl] = new_array[:, shift_x_pxl:]
-    new_array[:-shift_y_pxl] = new_array[shift_y_pxl:, :]
+    img[:, :-shift_x_pxl] = img[:, shift_x_pxl:]
+    img[:-shift_y_pxl] = img[shift_y_pxl:, :]
 
-    new_array[:, -shift_x_pxl:] = (
-        np.ones(new_array[:, -shift_x_pxl:].shape) * 110
-    )
-    new_array[-shift_y_pxl:, :] = (
-        np.ones(new_array[-shift_y_pxl:, :].shape) * 110
-    )
+    img[:, -shift_x_pxl:] = np.ones(img[:, -shift_x_pxl:].shape) * 110
+    img[-shift_y_pxl:, :] = np.ones(img[-shift_y_pxl:, :].shape) * 110
 
     # Save new image
-    new_img = Image.fromarray(new_array, mode="I")
-    new_img.save(img_path, mode="png")
+    imageio.v2.imwrite(
+        uri=img_path,
+        im=img,
+    )
 
 
 @pytest.fixture(scope="function")
@@ -137,8 +143,8 @@ def zenodo_images_multiplex_shifted(
 ]
 
 expected_registered_table = {
-    "FOV_ROI_table": {
-        "20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/0": pd.DataFrame(  # noqa: E501
+    "FOV_ROI_table": [
+        pd.DataFrame(
             {
                 "x_micrometer": [32.5, 448.5],
                 "y_micrometer": [7.8, 7.8],
@@ -148,10 +154,8 @@ def zenodo_images_multiplex_shifted(
                 "len_z_micrometer": [1.0, 1.0],
             },
             index=["FOV_1", "FOV_2"],
-        ).rename_axis(
-            "FieldIndex"
-        ),
-        "20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/1": pd.DataFrame(  # noqa: E501
+        ).rename_axis("FieldIndex"),
+        pd.DataFrame(
             {
                 "x_micrometer": [0.0, 416.0],
                 "y_micrometer": [0, 0],
@@ -161,10 +165,8 @@ def zenodo_images_multiplex_shifted(
                 "len_z_micrometer": [1.0, 1.0],
             },
             index=["FOV_1", "FOV_2"],
-        ).rename_axis(
-            "FieldIndex"
-        ),
-    }
+        ).rename_axis("FieldIndex"),
+    ]
 }
 
 
@@ -199,131 +201,210 @@ def test_multiplexing_registration(
     zenodo_images_multiplex_shifted: list[str],
     tmp_path,
     monkeypatch: MonkeyPatch,
-    roi_table="FOV_ROI_table",  # Given the test data, only implemented per FOV
+    # Given the test data, only implemented per FOV
+    roi_table="FOV_ROI_table",
 ):
 
+    zarr_dir = str(tmp_path / "registration_output/")
+    num_levels = 3
+
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.segment_ROI",
         patched_segment_ROI,
     )
 
-    zarr_path = tmp_path / "registration_output/"
-    zarr_path_mip = tmp_path / "registration_output_mip/"
-    metadata = {}
-    # Create the multiplexed OME-Zarr
-    metadata_update = create_ome_zarr_multiplex(
-        input_paths=zenodo_images_multiplex_shifted,
-        output_path=str(zarr_path),
-        metadata={},
-        image_extension="png",
-        allowed_channels=allowed_channels,
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
-
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
-        )
-    debug(metadata)
-
-    # Replicate
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path_mip),
-        metadata=metadata,
-        project_to_2D=True,
-        suffix="mip",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
-
-    # MIP
-    for component in metadata["image"]:
-        maximum_intensity_projection(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
-        )
+    acquisitions = {
+        "0": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex_shifted[0],
+            allowed_channels=single_cycle_allowed_channels_no_label,
+        ),
+        "1": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex_shifted[1],
+            allowed_channels=single_cycle_allowed_channels_no_label,
+        ),
+    }
 
-    # Cellpose segmentation (so that we test handling of label images)
-    for component in metadata["image"]:
+    # # Create zarr structure
+    parallelization_list = cellvoyager_to_ome_zarr_init_multiplex(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
+        acquisitions=acquisitions,
+        num_levels=num_levels,
+        coarsening_xy=2,
+        image_extension="png",
+        metadata_table_files=None,
+    )["parallelization_list"]
+    debug(parallelization_list)
+
+    # # Convert to OME-Zarr
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+        )["image_list_updates"]
+    debug(image_list_updates)
+
+    zarr_urls_3D = [image["zarr_url"] for image in image_list_updates]
+    debug(zarr_urls_3D)
+
+    parallelization_list = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls_3D,
+        zarr_dir=zarr_dir,
+        overwrite=True,
+    )["parallelization_list"]
+    debug(parallelization_list)
+
+    # # MIP
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += maximum_intensity_projection(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+            overwrite=True,
+        )["image_list_updates"]
+
+    zarr_urls_2D = [image["zarr_url"] for image in image_list_updates]
+    debug(zarr_urls_2D)
+
+    # Run cellpose to create a label image that needs to be handled in
+    # registration
+    # Per-FOV labeling
+    for zarr_url in zarr_urls_2D:
         cellpose_segmentation(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
-            level=1,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
+            level=1,
+        )
+
+    # Test non-available reference cycle:
+    with pytest.raises(ValueError):
+        image_based_registration_hcs_init(
+            zarr_urls=zarr_urls_2D,
+            zarr_dir=zarr_dir,
+            reference_cycle=2,
         )
 
-    # Calculate registration
-    for component in metadata["image"]:
+    parallelization_list = image_based_registration_hcs_init(
+        zarr_urls=zarr_urls_2D,
+        zarr_dir=zarr_dir,
+    )["parallelization_list"]
+    debug(parallelization_list)
+
+    expected_par_list = [
+        {
+            "zarr_url": zarr_urls_2D[1],
+            "init_args": {
+                "reference_zarr_url": zarr_urls_2D[0],
+            },
+        },
+    ]
+    assert expected_par_list == parallelization_list
+    debug(zarr_urls_2D)
+
+    for image in parallelization_list:
         calculate_registration_image_based(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
             wavelength_id="A01_C01",
             roi_table=roi_table,
         )
 
-    # Check the table for the second component (the image of the second cycle)
-    component = metadata["image"][1]
+    # Check the table for the second acquisition
+    # (the image of the second cycle)
     curr_table = ad.read_zarr(
-        f"{zarr_path_mip / component}/tables/{roi_table}"
+        f"{parallelization_list[0]['zarr_url']}/tables/{roi_table}"
     )
     assert curr_table.shape == (2, 11)
     np.testing.assert_almost_equal(
         curr_table.X[0, 8:11], np.array(expected_shift[roi_table]), decimal=5
     )
-    # Apply registration to ROI table
-    for component in metadata["well"]:
-        apply_registration_to_ROI_tables(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+
+    # Initialize consensus finding
+    parallelization_list_well = init_group_by_well_for_multiplexing(
+        zarr_urls=zarr_urls_2D,
+        zarr_dir=zarr_dir,
+        reference_cycle=0,
+    )["parallelization_list"]
+    debug(parallelization_list_well)
+    well_paral_list = {
+        "zarr_url": zarr_urls_2D[0],
+        "init_args": {"zarr_url_list": [zarr_urls_2D[0], zarr_urls_2D[1]]},
+    }
+    assert parallelization_list_well[0] == well_paral_list
+
+    for image in parallelization_list_well:
+        find_registration_consensus(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
             roi_table=roi_table,
         )
 
     # Validate the aligned tables
-    for component in metadata["image"]:
+    for i, zarr_url in enumerate(zarr_urls_2D):
         registered_table = ad.read_zarr(
-            f"{zarr_path_mip / component}/tables/registered_{roi_table}"
+            f"{zarr_url}/tables/registered_{roi_table}"
         )
         pd.testing.assert_frame_equal(
             registered_table.to_df()[registered_columns].astype("float32"),
-            expected_registered_table[roi_table][component].astype("float32"),
+            expected_registered_table[roi_table][i].astype("float32"),
             check_column_type=False,
         )
 
-    # Apply registration to image
-    for component in metadata["image"]:
-        apply_registration_to_image(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+    # Apply registration to image without overwrite_input and validate the
+    # output
+    zarr_list = []
+    for zarr_url in zarr_urls_2D:
+        zarr_list.append(f"{zarr_url}_registered")
+        image_list_update = apply_registration_to_image(
+            zarr_url=zarr_url,
+            registered_roi_table="registered_" + roi_table,
+            overwrite_input=False,
+        )
+        assert image_list_update == dict(
+            image_list_updates=[
+                dict(zarr_url=f"{zarr_url}_registered", origin=zarr_url)
+            ]
+        )
+
+    validate_assumptions_after_image_registration(
+        zarr_list=zarr_list,
+        roi_table=roi_table,
+    )
+
+    # # Apply registration to image with overwrite_input and validate the
+    # # output
+    zarr_list = []
+    for zarr_url in zarr_urls_2D:
+        zarr_list.append(zarr_url)
+        image_list_update = apply_registration_to_image(
+            zarr_url=zarr_url,
             registered_roi_table="registered_" + roi_table,
+            overwrite_input=True,
+        )
+        assert image_list_update == dict(
+            image_list_updates=[dict(zarr_url=zarr_url)]
         )
+    validate_assumptions_after_image_registration(
+        zarr_list=zarr_list,
+        roi_table=roi_table,
+    )
+
 
+def validate_assumptions_after_image_registration(
+    zarr_list,
+    roi_table,
+):
     # Load the Zarr image
     # How many padded pixels are expected => number of pixels that are 0
     # a) many when loading the original ROI
     # b) none when loading the registered ROI
-    for component in metadata["image"]:
+    for zarr_url in zarr_list:
         # Read pixel sizes from zattrs file
-        ngff_image_meta = load_NgffImageMeta(str(zarr_path_mip / component))
+        ngff_image_meta = load_NgffImageMeta(zarr_url)
         pxl_sizes_zyx = ngff_image_meta.get_pixel_sizes_zyx(level=0)
 
-        original_table = ad.read_zarr(
-            f"{zarr_path_mip / component}/tables/{roi_table}"
-        )
+        original_table = ad.read_zarr(f"{zarr_url}/tables/{roi_table}")
         # Create list of indices for 3D ROIs
         list_indices = convert_ROI_table_to_indices(
             original_table,
@@ -332,14 +413,14 @@ def test_multiplexing_registration(
             full_res_pxl_sizes_zyx=pxl_sizes_zyx,
         )
         region = convert_indices_to_regions(list_indices[0])
-        data_array = da.from_zarr(f"{zarr_path_mip / component / str(0)}")[0]
+        data_array = da.from_zarr(f"{zarr_url}/0")[0]
         img_array = load_region(
             data_zyx=data_array, region=region, compute=True
         )
         assert np.sum(img_array == 0) == 545280
 
         registered_table = ad.read_zarr(
-            f"{zarr_path_mip / component}/tables/registered_{roi_table}"
+            f"{zarr_url}/tables/registered_{roi_table}"
         )
         # Create list of indices for 3D ROIs
         list_indices = convert_ROI_table_to_indices(
@@ -349,18 +430,16 @@ def test_multiplexing_registration(
             full_res_pxl_sizes_zyx=pxl_sizes_zyx,
         )
         region = convert_indices_to_regions(list_indices[0])
-        data_array = da.from_zarr(f"{zarr_path_mip / component / str(0)}")[0]
+        data_array = da.from_zarr(f"{zarr_url}/0")[0]
         img_array_reg = load_region(
             data_zyx=data_array, region=region, compute=True
         )
         assert np.sum(img_array_reg == 0) == 0
 
         # Check that the Zarr files contains the relevant label channels:
-        with open(
-            f"{str(zarr_path_mip / component)}/labels/.zattrs", "r"
-        ) as jsonfile:
+        with open(f"{zarr_url}/labels/.zattrs", "r") as jsonfile:
             zattrs = json.load(jsonfile)
         assert len(zattrs["labels"]) == 1
-        assert (
-            zattrs["labels"][0] == f"label_{component.split('/')[-1]}_A01_C01"
-        )
+        print(zarr_url.split("/")[-1].split("_")[0])
+        label_name = f"label_{zarr_url.split('/')[-1].split('_')[0]}_A01_C01"
+        assert zattrs["labels"][0] == label_name
diff --git a/tests/tasks/test_unit_illumination_correction.py b/tests/tasks/test_unit_illumination_correction.py
index a64e93045..7821f316d 100644
--- a/tests/tasks/test_unit_illumination_correction.py
+++ b/tests/tasks/test_unit_illumination_correction.py
@@ -6,7 +6,6 @@
 import anndata as ad
 import dask.array as da
 import numpy as np
-import pytest
 from pytest import LogCaptureFixture
 from pytest import MonkeyPatch
 
@@ -20,34 +19,6 @@
 )
 
 
-def test_illumination_correction_fail(tmp_path):
-
-    with pytest.raises(ValueError):
-        illumination_correction(
-            input_paths=["/tmp"],
-            output_path="/tmp",
-            metadata={},
-            component="something",
-            overwrite_input=False,
-            illumination_profiles_folder="/tmp",
-            dict_corr={},
-            background=0,
-        )
-
-    with pytest.raises(NotImplementedError):
-        illumination_correction(
-            input_paths=["/tmp"],
-            output_path="/tmp",
-            metadata={},
-            component="something",
-            overwrite_input=False,
-            new_component="something_else",
-            illumination_profiles_folder="/tmp",
-            dict_corr={},
-            background=0,
-        )
-
-
 def test_illumination_correction(
     tmp_path: Path,
     testdata_path: Path,
@@ -64,20 +35,18 @@ def test_illumination_correction(
 
     # Copy a reference zarr into a temporary folder
     raw_zarrurl = (testdata_path / "plate_ones.zarr").as_posix()
-    zarrurl = (tmp_path / "plate.zarr").resolve().as_posix()
-    shutil.copytree(raw_zarrurl, zarrurl)
-    zarrurl += "/B/03/0/"
-    component = "plate.zarr/B/03/0"
+    zarr_url = (tmp_path / "plate.zarr").resolve().as_posix()
+    shutil.copytree(raw_zarrurl, zarr_url)
+    zarr_url += "/B/03/0/"
 
     # Prepare arguments for illumination_correction function
-    zarr_path = str(tmp_path)
     testdata_str = testdata_path.as_posix()
     illum_params = {
         "A01_C01": "illum_corr_matrix.png",
         "A01_C02": "illum_corr_matrix.png",
     }
     illumination_profiles_folder = f"{testdata_str}/illumination_correction/"
-    with open(zarrurl + ".zattrs") as fin:
+    with open(zarr_url + ".zattrs") as fin:
         zattrs = json.load(fin)
         num_levels = len(zattrs["multiscales"][0]["datasets"])
     metadata: dict = {
@@ -88,9 +57,9 @@ def test_illumination_correction(
     num_levels = metadata["num_levels"]
 
     # Read FOV ROIs and create corresponding indices
-    ngff_image_meta = load_NgffImageMeta(zarrurl)
+    ngff_image_meta = load_NgffImageMeta(zarr_url)
     pixels = ngff_image_meta.get_pixel_sizes_zyx(level=0)
-    ROIs = ad.read_zarr(zarrurl + "tables/FOV_ROI_table/")
+    ROIs = ad.read_zarr(zarr_url + "tables/FOV_ROI_table/")
     list_indices = convert_ROI_table_to_indices(
         ROIs, level=0, full_res_pxl_sizes_zyx=pixels
     )
@@ -116,10 +85,7 @@ def patched_correct(*args, **kwargs):
 
     # Call illumination correction task, with patched correct()
     illumination_correction(
-        input_paths=[zarr_path],
-        output_path=zarr_path,
-        metadata=metadata,
-        component=component,
+        zarr_url=zarr_url,
         overwrite_input=True,
         illumination_profiles_folder=illumination_profiles_folder,
         dict_corr=illum_params,
@@ -139,7 +105,7 @@ def patched_correct(*args, **kwargs):
         old = da.from_zarr(
             testdata_path / f"plate_ones.zarr/B/03/0/{ind_level}"
         )
-        new = da.from_zarr(f"{zarrurl}{ind_level}")
+        new = da.from_zarr(f"{zarr_url}{ind_level}")
         assert old.shape == new.shape
         assert old.chunks == new.chunks
         assert new.compute()[0, 0, 0, 0] == 1
diff --git a/tests/tasks/test_unit_napari_workflows_wrapper.py b/tests/tasks/test_unit_napari_workflows_wrapper.py
index 2ee075cd1..93e2e8b46 100644
--- a/tests/tasks/test_unit_napari_workflows_wrapper.py
+++ b/tests/tasks/test_unit_napari_workflows_wrapper.py
@@ -23,12 +23,10 @@ def test_input_specs(tmp_path, testdata_path):
     output_specs = {
         "output_label": {"type": "label", "label_name": "label_DAPI"}
     }
+    zarr_url = str(tmp_path / "component")
     with pytest.raises(ValidationError):
         napari_workflows_wrapper(
-            input_paths=[str(tmp_path)],
-            output_path=str(tmp_path),
-            metadata={},
-            component="component",
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
@@ -56,13 +54,11 @@ def test_output_specs(tmp_path, testdata_path, caplog):
         }
     }
     output_specs = {"some_output": {"type": "label", "label_name": "xxx"}}
+    zarr_url = str(tmp_path / "component")
 
     try:
         napari_workflows_wrapper(
-            input_paths=[str(tmp_path)],
-            output_path=str(tmp_path),
-            metadata={},
-            component="component",
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
@@ -109,13 +105,10 @@ def test_level_setting_in_non_labeling_worfklow(tmp_path, testdata_path):
             "label_name": "label_DAPI",
         },
     }
-
+    zarr_url = str(tmp_path / "component")
     with pytest.raises(NotImplementedError):
         napari_workflows_wrapper(
-            input_paths=[str(tmp_path)],
-            output_path=str(tmp_path),
-            metadata={},
-            component="component",
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
diff --git a/tests/tasks/test_unit_registration_helper_functions.py b/tests/tasks/test_unit_registration_helper_functions.py
index cb6b0bf3d..9d5257767 100644
--- a/tests/tasks/test_unit_registration_helper_functions.py
+++ b/tests/tasks/test_unit_registration_helper_functions.py
@@ -4,13 +4,13 @@
 import pytest
 from devtools import debug
 
-from fractal_tasks_core.tasks.apply_registration_to_ROI_tables import (
+from fractal_tasks_core.tasks._registration_utils import (
     add_zero_translation_columns,
 )
-from fractal_tasks_core.tasks.apply_registration_to_ROI_tables import (
+from fractal_tasks_core.tasks._registration_utils import (
     apply_registration_to_single_ROI_table,
 )
-from fractal_tasks_core.tasks.apply_registration_to_ROI_tables import (
+from fractal_tasks_core.tasks._registration_utils import (
     calculate_min_max_across_dfs,
 )
 from fractal_tasks_core.tasks.calculate_registration_image_based import (
diff --git a/tests/tasks/test_unit_task.py b/tests/tasks/test_unit_task.py
index 5897bffa1..6287b50f8 100644
--- a/tests/tasks/test_unit_task.py
+++ b/tests/tasks/test_unit_task.py
@@ -3,42 +3,30 @@
 
 from devtools import debug
 
-import fractal_tasks_core
-from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
-
-
-# Load manifest
-module_dir = Path(fractal_tasks_core.__file__).parent
-with (module_dir / "__FRACTAL_MANIFEST__.json").open("r") as fin:
-    __FRACTAL_MANIFEST__ = json.load(fin)
-
-# Select a task
-create_ome_zarr_manifest = next(
-    item
-    for item in __FRACTAL_MANIFEST__["task_list"]
-    if item["name"] == "Create OME-Zarr structure"
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init import (
+    cellvoyager_to_ome_zarr_init,
 )
 
 
 def test_create_ome_zarr(tmp_path, testdata_path):
-    input_paths = [str(testdata_path / "png/")]
-    output_path = str(tmp_path)
+    img_path = str(testdata_path / "png/")
+    zarr_dir = str(tmp_path)
     args = {}
     args["allowed_channels"] = [
         {"wavelength_id": "A01_C01", "window": dict(start=0, end=1000)}
     ]
     args["image_extension"] = "png"
 
-    debug(input_paths)
-    debug(output_path)
+    debug(img_path)
+    debug(zarr_dir)
     debug(args)
 
-    dummy = create_ome_zarr(
-        input_paths=input_paths, output_path=output_path, metadata={}, **args
+    dummy = cellvoyager_to_ome_zarr_init(
+        zarr_urls=[], zarr_dir=zarr_dir, image_dirs=[img_path], **args
     )
     debug(dummy)
 
-    zattrs = Path(output_path) / "myplate.zarr/.zattrs"
+    zattrs = Path(zarr_dir) / "myplate.zarr/.zattrs"
     with open(zattrs) as f:
         data = json.load(f)
         debug(data)
@@ -55,13 +43,13 @@ def test_run_fractal_tasks(tmp_path, testdata_path, monkeypatch):
 
     # Write arguments to a file
     args = {}
-    args["input_paths"] = [str(testdata_path / "png/")]
-    args["output_path"] = str(tmp_path)
+    args["zarr_urls"] = []
+    args["image_dirs"] = [str(testdata_path / "png/")]
+    args["zarr_dir"] = str(tmp_path)
     args["allowed_channels"] = [
         {"wavelength_id": "A01_C01", "window": dict(start=0, end=1000)}
     ]
     args["image_extension"] = "png"
-    args["metadata"] = {}
     debug(args)
     args_path = tmp_path / "args.json"
     with args_path.open("w") as f:
@@ -76,8 +64,8 @@ def parse_args(self, *args, **kwargs):
             class Args(object):
                 def __init__(self):
                     debug("INIT")
-                    self.metadata_out = str(tmp_path / "metadiff.json")
-                    self.json = str(args_path)
+                    self.out_json = str(tmp_path / "metadiff.json")
+                    self.args_json = str(args_path)
 
             return Args()
 
@@ -88,7 +76,7 @@ def __init__(self):
 
     # Run the task
     out = fractal_tasks_core.tasks._utils.run_fractal_task(
-        task_function=create_ome_zarr
+        task_function=cellvoyager_to_ome_zarr_init
     )
 
     # Check that the task wrote some output to args.metadata_out
@@ -98,7 +86,7 @@ def __init__(self):
     assert out
 
     # Check that the output zarr exists and includes a well
-    zattrs = Path(args["output_path"]) / "myplate.zarr/.zattrs"
+    zattrs = Path(args["zarr_dir"]) / "myplate.zarr/.zattrs"
     with open(zattrs) as f:
         data = json.load(f)
         debug(data)
diff --git a/tests/tasks/test_valid_args_schemas.py b/tests/tasks/test_valid_args_schemas.py
index 533ffad2d..0bbd7a298 100644
--- a/tests/tasks/test_valid_args_schemas.py
+++ b/tests/tasks/test_valid_args_schemas.py
@@ -102,10 +102,13 @@ def test_task_functions_have_valid_signatures():
     """
     Test that task functions have valid signatures.
     """
-    for ind_task, task in enumerate(TASK_LIST):
-        function_name = Path(task["executable"]).with_suffix("").name
-        task_function = _extract_function(task["executable"], function_name)
-        _validate_function_signature(task_function)
+    for task in TASK_LIST:
+        for key in ["executable_non_parallel", "executable_parallel"]:
+            value = task.get(key, None)
+            if value is not None:
+                function_name = Path(task[key]).with_suffix("").name
+                task_function = _extract_function(task[key], function_name)
+                _validate_function_signature(task_function)
 
 
 def test_args_schemas_are_up_to_date():
@@ -113,14 +116,22 @@ def test_args_schemas_are_up_to_date():
     Test that args_schema attributes in the manifest are up-to-date
     """
     for ind_task, task in enumerate(TASK_LIST):
-        print(f"Now handling {task['executable']}")
-        old_schema = TASK_LIST[ind_task]["args_schema"]
-        new_schema = create_schema_for_single_task(task["executable"])
-        # The following step is required because some arguments may have a
-        # default which has a non-JSON type (e.g. a tuple), which we need to
-        # convert to JSON type (i.e. an array) before comparison.
-        new_schema = json.loads(json.dumps(new_schema))
-        assert new_schema == old_schema
+        for kind in ["non_parallel", "parallel"]:
+            key = f"executable_{kind}"
+            value = task.get(key, None)
+            if value is not None:
+                print(f"Now handling {task[key]}")
+                old_schema = TASK_LIST[ind_task].get(
+                    f"args_schema_{kind}", None
+                )
+                assert old_schema is not None
+                new_schema = create_schema_for_single_task(task[key])
+                # The following step is required because some arguments may
+                # have a default which has a non-JSON type (e.g. a tuple),
+                # which we need to convert to JSON type (i.e. an array) before
+                # comparison.
+                new_schema = json.loads(json.dumps(new_schema))
+                assert new_schema == old_schema
 
 
 @pytest.mark.parametrize(
@@ -132,14 +143,18 @@ def test_args_schema_comply_with_jsonschema_specs(jsonschema_validator):
     This test is actually useful, see
     https://github.com/fractal-analytics-platform/fractal-tasks-core/issues/564.
     """
-    for ind_task, task in enumerate(TASK_LIST):
-        schema = TASK_LIST[ind_task]["args_schema"]
-        my_validator = jsonschema_validator(schema=schema)
-        my_validator.check_schema(my_validator.schema)
-        print(
-            f"Schema for task {task['executable']} is valid for "
-            f"{jsonschema_validator}."
-        )
+    for task in TASK_LIST:
+        for kind in ["non_parallel", "parallel"]:
+            key = f"executable_{kind}"
+            value = task.get(key, None)
+            if value is not None:
+                schema = task[f"args_schema_{kind}"]
+                my_validator = jsonschema_validator(schema=schema)
+                my_validator.check_schema(my_validator.schema)
+                print(
+                    f"Schema for task {task[key]} is valid for "
+                    f"{jsonschema_validator}."
+                )
 
 
 def test_args_title():
@@ -155,7 +170,9 @@ def test_args_title():
     cellpose_task = next(
         task for task in TASK_LIST if task["name"] == "Cellpose Segmentation"
     )
-    new_schema = create_schema_for_single_task(cellpose_task["executable"])
+    new_schema = create_schema_for_single_task(
+        cellpose_task["executable_parallel"]
+    )
     properties = new_schema["properties"]
     # Standard task argument
     level_prop = properties["level"]
@@ -166,13 +183,13 @@ def test_args_title():
     debug(channel2_prop)
     assert channel2_prop["title"] == "Channel2"
 
-    create_ome_zarr_task = next(
+    cellvoyager_task = next(
         task
         for task in TASK_LIST
-        if task["name"] == "Create OME-Zarr structure"
+        if task["name"] == "Convert Cellvoyager to OME-Zarr"
     )
     new_schema = create_schema_for_single_task(
-        create_ome_zarr_task["executable"]
+        cellvoyager_task["executable_non_parallel"]
     )
     definitions = new_schema["definitions"]
     omero_channel_def = definitions["OmeroChannel"]
diff --git a/tests/tasks/test_valid_task_interface.py b/tests/tasks/test_valid_task_interface.py
index 2d964a817..260b6b726 100644
--- a/tests/tasks/test_valid_task_interface.py
+++ b/tests/tasks/test_valid_task_interface.py
@@ -48,10 +48,14 @@ def test_task_interface(task, tmp_path):
         args = dict(wrong_arg_1=123, wrong_arg_2=[1, 2, 3])
         json.dump(args, fout, indent=4)
 
-    task_path = (module_dir / task["executable"]).as_posix()
-    cmd = (
-        f"python {task_path} "
-        f"-j {tmp_file_args} "
-        f"--metadata-out {tmp_file_metadiff}"
-    )
-    validate_command(cmd)
+    for key in ["executable_non_parallel", "executable_parallel"]:
+        value = task.get(key, None)
+        if value is None:
+            continue
+        task_path = (module_dir / value).as_posix()
+        cmd = (
+            f"python {task_path} "
+            f"--args-json {tmp_file_args} "
+            f"--out-json {tmp_file_metadiff}"
+        )
+        validate_command(cmd)
diff --git a/tests/tasks/test_workflows.py b/tests/tasks/test_workflows.py
index 86175dd1b..193d3e961 100644
--- a/tests/tasks/test_workflows.py
+++ b/tests/tasks/test_workflows.py
@@ -14,7 +14,6 @@
 import logging
 import shutil
 from pathlib import Path
-from typing import Any
 
 import pytest
 import zarr
@@ -22,15 +21,21 @@
 
 from ._validation import check_file_number
 from ._validation import validate_schema
-from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
-from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_compute import (
+    cellvoyager_to_ome_zarr_compute,
+)
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init import (
+    cellvoyager_to_ome_zarr_init,
+)
+from fractal_tasks_core.tasks.copy_ome_zarr_hcs_plate import (
+    copy_ome_zarr_hcs_plate,
+)
 from fractal_tasks_core.tasks.illumination_correction import (
     illumination_correction,
 )
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
     maximum_intensity_projection,
-)  # noqa
-from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
+)
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
@@ -69,15 +74,15 @@ def test_create_ome_zarr_fail(tmp_path: Path, zenodo_images: str):
     ]
 
     # Init
-    img_path = zenodo_images
-    zarr_path = str(tmp_path / "tmp_out/")
+    image_dir = zenodo_images
+    zarr_dir = str(tmp_path / "tmp_out/")
 
     # Create zarr structure
     with pytest.raises(ValueError):
-        _ = create_ome_zarr(
-            input_paths=[img_path],
-            metadata={},
-            output_path=zarr_path,
+        _ = cellvoyager_to_ome_zarr_init(
+            zarr_urls=[],
+            zarr_dir=zarr_dir,
+            image_dirs=[image_dir],
             allowed_channels=tmp_allowed_channels,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
@@ -95,10 +100,10 @@ def test_create_ome_zarr_no_images(
     create_ome_zarr must fail.
     """
     with pytest.raises(ValueError):
-        create_ome_zarr(
-            input_paths=[zenodo_images],
-            output_path=str(tmp_path / "output"),
-            metadata={},
+        cellvoyager_to_ome_zarr_init(
+            zarr_urls=[],
+            zarr_dir=str(tmp_path / "output"),
+            image_dirs=[zenodo_images],
             allowed_channels=allowed_channels,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
@@ -106,10 +111,10 @@ def test_create_ome_zarr_no_images(
             image_extension="xyz",
         )
     with pytest.raises(ValueError):
-        create_ome_zarr(
-            input_paths=[zenodo_images],
-            output_path=str(tmp_path / "output"),
-            metadata={},
+        cellvoyager_to_ome_zarr_init(
+            zarr_urls=[],
+            zarr_dir=str(tmp_path / "output"),
+            image_dirs=[zenodo_images],
             allowed_channels=allowed_channels,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
@@ -146,27 +151,24 @@ def test_yokogawa_to_ome_zarr(
     output_path = tmp_path / "output"
 
     # Create zarr structure
-    metadata: dict = {}
-    _original_metadata: dict = {}
-    metadata_update = create_ome_zarr(
-        input_paths=[str(img_path)],
-        output_path=str(output_path),
-        metadata=metadata,
+    parallelization_list = cellvoyager_to_ome_zarr_init(
+        zarr_urls=[],
+        zarr_dir=str(output_path),
+        image_dirs=[str(img_path)],
         allowed_channels=allowed_channels,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
         metadata_table_file=metadata_table_file,
         image_extension="png",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    )["parallelization_list"]
+    debug(parallelization_list)
 
     # Re-run (with overwrite=False) and fail
     with pytest.raises(OverwriteNotAllowedError):
-        create_ome_zarr(
-            input_paths=[str(img_path)],
-            output_path=str(output_path),
-            metadata=_original_metadata,
+        cellvoyager_to_ome_zarr_init(
+            zarr_urls=[],
+            zarr_dir=str(output_path),
+            image_dirs=[str(img_path)],
             allowed_channels=allowed_channels,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
@@ -176,53 +178,64 @@ def test_yokogawa_to_ome_zarr(
         )
 
     # Re-run (with overwrite=True)
-    metadata_update = create_ome_zarr(
-        input_paths=[str(img_path)],
-        output_path=str(output_path),
-        metadata=_original_metadata,
+    parallelization_list = cellvoyager_to_ome_zarr_init(
+        zarr_urls=[],
+        zarr_dir=str(output_path),
+        image_dirs=[str(img_path)],
         allowed_channels=allowed_channels,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
         metadata_table_file=metadata_table_file,
         image_extension="png",
         overwrite=True,
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    )["parallelization_list"]
+    debug(parallelization_list)
 
+    image_list_updates = []
     # Yokogawa to zarr
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(output_path)],
-            output_path=str(output_path),
-            metadata=metadata,
-            component=component,
-        )
-    debug(metadata)
+    for image in parallelization_list:
+        image_list_updates += cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+        )["image_list_updates"]
+    debug(image_list_updates)
+
+    # Validate image_list_updates contents
+    expected_image_list_update = {
+        "zarr_url": (
+            f"{output_path}/20200812-CardiomyocyteDifferentiation14"
+            "-Cycle1.zarr/B/03/0/"
+        ),
+        "attributes": {
+            "plate": "20200812-CardiomyocyteDifferentiation14-Cycle1.zarr",
+            "well": "B03",
+        },
+        "types": {
+            "is_3D": True,
+        },
+    }
+
+    assert image_list_updates[0] == expected_image_list_update
 
     # Re-run (with overwrite=True)
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(output_path)],
-            output_path=str(output_path),
-            metadata=metadata,
-            component=component,
+    for image in parallelization_list:
+        cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
             overwrite=True,
         )
 
     # Re-run (with overwrite=False)
-    with pytest.raises(OverwriteNotAllowedError):
-        for component in metadata["image"]:
-            yokogawa_to_ome_zarr(
-                input_paths=[str(output_path)],
-                output_path=str(output_path),
-                metadata=metadata,
-                component=component,
+    for image in parallelization_list:
+        with pytest.raises(OverwriteNotAllowedError):
+            cellvoyager_to_ome_zarr_compute(
+                zarr_url=image["zarr_url"],
+                init_args=image["init_args"],
                 overwrite=False,
             )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(output_path / metadata["image"][0])
+    image_zarr = Path(parallelization_list[0]["zarr_url"])
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
     validate_schema(path=str(image_zarr), type="image")
@@ -243,85 +256,81 @@ def test_yokogawa_to_ome_zarr(
 def test_MIP(
     tmp_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Init
     zarr_path = tmp_path / "tmp_out/"
-    zarr_path_mip = tmp_path / "tmp_out_mip/"
 
     # Load zarr array from zenodo
     zenodo_zarr_3D, zenodo_zarr_2D = zenodo_zarr[:]
-    metadata_3D, metadata_2D = zenodo_zarr_metadata[:]
     shutil.copytree(zenodo_zarr_3D, str(zarr_path / Path(zenodo_zarr_3D).name))
-    metadata = metadata_3D.copy()
 
-    # Replicate
-    _original_metadata = metadata.copy()
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path_mip),
-        metadata=metadata,
-        project_to_2D=True,
-        suffix="mip",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    zarr_urls = []
+    zarr_dir = "/".join(zenodo_zarr_3D.split("/")[:-1])
+    zarr_urls = [Path(zarr_dir, "plate.zarr/B/03/0").as_posix()]
+
+    parallelization_list = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls,
+        zarr_dir="tmp_out",
+        overwrite=True,
+    )["parallelization_list"]
+    debug(parallelization_list)
 
     # Run again, with overwrite=True
-    metadata_update_second_try = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path_mip),
-        metadata=_original_metadata,
-        project_to_2D=True,
-        suffix="mip",
+    parallelization_list_2 = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls,
+        zarr_dir="tmp_out",
         overwrite=True,
-    )
-    assert metadata_update_second_try == metadata_update
+    )["parallelization_list"]
+    assert parallelization_list_2 == parallelization_list
 
     # Run again, with overwrite=False
     with pytest.raises(OverwriteNotAllowedError):
-        copy_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path_mip),
-            metadata=_original_metadata,
-            project_to_2D=True,
-            suffix="mip",
+        _ = copy_ome_zarr_hcs_plate(
+            zarr_urls=zarr_urls,
+            zarr_dir="tmp_out",
             overwrite=False,
         )
 
     # MIP
-    for component in metadata["image"]:
-        maximum_intensity_projection(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
-        )
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += maximum_intensity_projection(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+            overwrite=True,
+        )["image_list_updates"]
+
+    debug(image_list_updates[0])
+    expected_image_list_updates = {
+        "zarr_url": (parallelization_list[0]["zarr_url"]),
+        "origin": f"{zarr_dir}/plate.zarr/B/03/0",
+        "types": {
+            "is_3D": False,
+        },
+    }
+    debug(expected_image_list_updates)
+    assert image_list_updates[0] == expected_image_list_updates
 
     # Re-run with overwrite=True
-    for component in metadata["image"]:
+    for image in parallelization_list:
         maximum_intensity_projection(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
             overwrite=True,
         )
 
     # Re-run with overwrite=False
     with pytest.raises(OverwriteNotAllowedError):
-        for component in metadata["image"]:
+        for image in parallelization_list:
             maximum_intensity_projection(
-                input_paths=[str(zarr_path_mip)],
-                output_path=str(zarr_path_mip),
-                metadata=metadata,
-                component=component,
+                zarr_url=image["zarr_url"],
+                init_args=image["init_args"],
                 overwrite=False,
             )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path_mip / metadata["image"][0])
+    image_zarr = Path(parallelization_list[0]["zarr_url"])
     debug(image_zarr)
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
@@ -348,57 +357,52 @@ def test_MIP_subset_of_images(
     """
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    zarr_path_mip = tmp_path / "tmp_out_mip/"
+    zarr_dir = tmp_path / "tmp_out/"
 
     # Create zarr structure
-    metadata: dict = {}
-    metadata_update = create_ome_zarr(
-        input_paths=[zenodo_images],
-        output_path=str(zarr_path),
-        metadata=metadata,
+    parallelization_list = cellvoyager_to_ome_zarr_init(
+        zarr_urls=[],
+        zarr_dir=str(zarr_dir),
+        image_dirs=[zenodo_images],
         allowed_channels=allowed_channels,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
         metadata_table_file=None,
         image_extension="png",
         image_glob_patterns=["*F001*"],
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    )["parallelization_list"]
+    debug(parallelization_list)
 
     # Yokogawa to zarr
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
-        )
-    debug(metadata)
-
+    image_list_updates = []
+    # Yokogawa to zarr
+    for image in parallelization_list:
+        image_list_updates += cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+        )["image_list_updates"]
+    debug(image_list_updates)
+
+    zarr_urls = [a["zarr_url"] for a in image_list_updates]
+    debug(zarr_urls)
     # Replicate
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path_mip),
-        metadata=metadata,
-        project_to_2D=True,
-        suffix="mip",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    parallelization_list = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls,
+        zarr_dir="tmp_out",
+        overwrite=True,
+    )["parallelization_list"]
+    debug(parallelization_list)
 
     # MIP
-    for component in metadata["image"]:
+    for image in parallelization_list:
         maximum_intensity_projection(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+            overwrite=True,
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path_mip / metadata["image"][0])
+    image_zarr = Path(parallelization_list[0]["zarr_url"])
     debug(image_zarr)
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
@@ -420,46 +424,39 @@ def test_illumination_correction(
 
     # Init
     img_path = Path(zenodo_images)
-    zarr_path = tmp_path / "tmp_out"
-    metadata: dict = {}
+    zarr_dir = tmp_path / "tmp_out"
 
     testdata_str = testdata_path.as_posix()
     illum_params = {"A01_C01": "illum_corr_matrix.png"}
     illumination_profiles_folder = f"{testdata_str}/illumination_correction/"
 
     # Create zarr structure
-    metadata_update = create_ome_zarr(
-        input_paths=[str(img_path)],
-        output_path=str(zarr_path),
-        metadata=metadata,
+    parallelization_list = cellvoyager_to_ome_zarr_init(
+        zarr_urls=[],
+        zarr_dir=str(zarr_dir),
+        image_dirs=[str(img_path)],
         image_extension="png",
         allowed_channels=allowed_channels,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
         metadata_table_file=None,
-    )
-    metadata.update(metadata_update)
+    )["parallelization_list"]
     print(caplog.text)
     caplog.clear()
 
     # Yokogawa to zarr
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+    for image in parallelization_list:
+        cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
         )
     print(caplog.text)
     caplog.clear()
 
     # Illumination correction
-    for component in metadata["image"]:
+    for image in parallelization_list:
         illumination_correction(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=image["zarr_url"],
             overwrite_input=True,
             illumination_profiles_folder=illumination_profiles_folder,
             dict_corr=illum_params,
@@ -468,7 +465,7 @@ def test_illumination_correction(
     caplog.clear()
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path / metadata["image"][0])
+    image_zarr = Path(parallelization_list[0]["zarr_url"])
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
     validate_schema(path=str(image_zarr), type="image")
@@ -476,52 +473,3 @@ def test_illumination_correction(
     validate_schema(path=str(plate_zarr), type="plate")
 
     check_file_number(zarr_path=image_zarr)
-
-
-def test_copy_ome_zarr_twice(
-    tmp_path: Path,
-    zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
-):
-    """
-    See
-    https://github.com/fractal-analytics-platform/fractal-tasks-core/issues/510
-    """
-
-    # Init and load zarr array from zenodo
-    zarr_path = tmp_path / "data"
-    zenodo_zarr_3D, zenodo_zarr_2D = zenodo_zarr[:]
-    metadata_3D, metadata_2D = zenodo_zarr_metadata[:]
-    shutil.copytree(zenodo_zarr_3D, str(zarr_path / Path(zenodo_zarr_3D).name))
-    metadata = metadata_3D.copy()
-
-    # Run first time
-    _original_metadata = metadata.copy()
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path),
-        metadata=_original_metadata,
-        project_to_2D=True,
-        suffix="mip",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
-    assert len(metadata["copy_ome_zarr"]["sources"].keys()) == 1
-    assert "plate_mip_mip" not in metadata["copy_ome_zarr"]["sources"].keys()
-
-    # Run second time, with overwrite=True
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path),
-        metadata=_original_metadata,
-        project_to_2D=True,
-        suffix="mip",
-        overwrite=True,
-    )
-    debug(metadata_update)
-    metadata.update(metadata_update)
-    assert (
-        "plate_mip_mip"
-        not in metadata_update["copy_ome_zarr"]["sources"].keys()
-    )
-    assert len(metadata_update["copy_ome_zarr"]["sources"].keys()) == 1
diff --git a/tests/tasks/test_workflows_cellpose_segmentation.py b/tests/tasks/test_workflows_cellpose_segmentation.py
index 48daba1ab..8cf24e4af 100644
--- a/tests/tasks/test_workflows_cellpose_segmentation.py
+++ b/tests/tasks/test_workflows_cellpose_segmentation.py
@@ -17,7 +17,6 @@
 import subprocess
 import sys
 from pathlib import Path
-from typing import Any
 
 import anndata as ad
 import numpy as np
@@ -37,17 +36,20 @@
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
-from fractal_tasks_core.tasks.copy_ome_zarr import (
-    copy_ome_zarr,
-)  # noqa
-from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_compute import (
+    cellvoyager_to_ome_zarr_compute,
+)
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init import (
+    cellvoyager_to_ome_zarr_init,
+)
+from fractal_tasks_core.tasks.copy_ome_zarr_hcs_plate import (
+    copy_ome_zarr_hcs_plate,
+)
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
     maximum_intensity_projection,
-)  # noqa
-from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
+)
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
-
 allowed_channels = [
     {
         "label": "DAPI",
@@ -146,9 +148,7 @@ def patched_cellpose_core_use_gpu(*args, **kwargs):
 
 def test_failures(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
@@ -166,21 +166,19 @@ def test_failures(
     caplog.set_level(logging.WARNING)
 
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(
+        str(zarr_dir),
+        zenodo_zarr,
     )
-    debug(zarr_path)
-    debug(metadata)
+    debug(zarr_dir)
+    debug(zarr_urls)
+    caplog.clear()
 
     # A sequence of invalid attempts
-    for component in metadata["image"]:
-
+    for zarr_url in zarr_urls:
         kwargs = dict(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             level=3,
         )
         # Attempt 1
@@ -211,9 +209,7 @@ def test_failures(
 
 def test_workflow_with_per_FOV_labeling(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
@@ -233,20 +229,18 @@ def test_workflow_with_per_FOV_labeling(
     caplog.set_level(logging.INFO)
 
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(
+        str(zarr_dir),
+        zenodo_zarr,
     )
-    debug(zarr_path)
-    debug(metadata)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
@@ -257,7 +251,7 @@ def test_workflow_with_per_FOV_labeling(
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     label_zarr = image_zarr / "labels/label_DAPI"
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
@@ -271,9 +265,7 @@ def test_workflow_with_per_FOV_labeling(
 
 def test_workflow_with_multi_channel_input(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[Path],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
@@ -295,20 +287,18 @@ def test_workflow_with_multi_channel_input(
     caplog.set_level(logging.INFO)
 
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), [str(x) for x in zenodo_zarr], zenodo_zarr_metadata
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(
+        str(zarr_dir),
+        [str(x) for x in zenodo_zarr],
     )
-    debug(zarr_path)
-    debug(metadata)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             channel2=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
@@ -318,7 +308,7 @@ def test_workflow_with_multi_channel_input(
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     label_zarr = image_zarr / "labels/label_DAPI"
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
@@ -332,10 +322,7 @@ def test_workflow_with_multi_channel_input(
 
 def test_workflow_with_per_FOV_labeling_2D(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
-    caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
 
@@ -351,21 +338,17 @@ def test_workflow_with_per_FOV_labeling_2D(
     )
 
     # Load pre-made 2D zarr array
-    zarr_path_mip = tmp_path / "tmp_out_mip/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path_mip),
+    zarr_dir_mip = tmp_path / "tmp_out_mip/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir_mip),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
     )
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=2,
             relabeling=True,
@@ -373,7 +356,7 @@ def test_workflow_with_per_FOV_labeling_2D(
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path_mip / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     debug(image_zarr)
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
@@ -386,9 +369,7 @@ def test_workflow_with_per_FOV_labeling_2D(
 
 def test_workflow_with_per_well_labeling_2D(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_images: str,
-    caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
 
@@ -405,59 +386,53 @@ def test_workflow_with_per_well_labeling_2D(
 
     # Init
     img_path = Path(zenodo_images)
-    zarr_path = tmp_path / "tmp_out/"
-    zarr_path_mip = tmp_path / "tmp_out_mip/"
-    metadata: dict[str, Any] = {}
+    zarr_dir = tmp_path / "tmp_out/"
 
     # Create zarr structure
-    metadata_update = create_ome_zarr(
-        input_paths=[str(img_path)],
-        output_path=str(zarr_path),
-        metadata=metadata,
+    parallelization_list = cellvoyager_to_ome_zarr_init(
+        zarr_urls=[],
+        zarr_dir=str(zarr_dir),
+        image_dirs=[str(img_path)],
         image_extension="png",
         allowed_channels=allowed_channels,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
         metadata_table_file=None,
-    )
-    metadata.update(metadata_update)
+    )["parallelization_list"]
 
     # Yokogawa to zarr
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
-        )
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+        )["image_list_updates"]
+    debug(image_list_updates)
+
+    zarr_urls = []
+    for image in image_list_updates:
+        zarr_urls.append(image["zarr_url"])
 
     # Replicate
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path_mip),
-        metadata=metadata,
-        project_to_2D=True,
-        suffix="mip",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    parallelization_list = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls,
+        zarr_dir=str(zarr_dir),
+    )["parallelization_list"]
+    debug(parallelization_list)
 
     # MIP
-    for component in metadata["image"]:
-        maximum_intensity_projection(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
-        )
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += maximum_intensity_projection(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+            overwrite=True,
+        )["image_list_updates"]
 
     # Whole-well labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=2,
             input_ROI_table="well_ROI_table",
@@ -466,7 +441,7 @@ def test_workflow_with_per_well_labeling_2D(
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path_mip / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     label_zarr = image_zarr / "labels/label_DAPI"
     debug(image_zarr)
     well_zarr = image_zarr.parent
@@ -482,9 +457,7 @@ def test_workflow_with_per_well_labeling_2D(
 
 def test_workflow_bounding_box(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
@@ -505,20 +478,15 @@ def test_workflow_bounding_box(
     caplog.set_level(logging.INFO)
 
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
@@ -527,12 +495,9 @@ def test_workflow_bounding_box(
         )
 
     # Re-run with overwrite=True
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
@@ -543,12 +508,9 @@ def test_workflow_bounding_box(
 
     # Re-run with overwrite=False
     with pytest.raises(OverwriteNotAllowedError):
-        for component in metadata["image"]:
+        for zarr_url in zarr_urls:
             cellpose_segmentation(
-                input_paths=[str(zarr_path)],
-                output_path=str(zarr_path),
-                metadata=metadata,
-                component=component,
+                zarr_url=zarr_url,
                 channel=ChannelInputModel(wavelength_id="A01_C01"),
                 level=3,
                 relabeling=True,
@@ -557,9 +519,7 @@ def test_workflow_bounding_box(
                 overwrite=False,
             )
 
-    bbox_ROIs_table_path = str(
-        zarr_path / metadata["image"][0] / "tables/bbox_table/"
-    )
+    bbox_ROIs_table_path = str(Path(zarr_urls[0]) / "tables/bbox_table/")
     bbox_ROIs = ad.read_zarr(bbox_ROIs_table_path)
     debug(bbox_ROIs)
     debug(bbox_ROIs.X)
@@ -579,9 +539,7 @@ def test_workflow_bounding_box(
 
 def test_workflow_bounding_box_with_overlap(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
@@ -601,20 +559,15 @@ def test_workflow_bounding_box_with_overlap(
     caplog.set_level(logging.WARNING)
 
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
@@ -627,17 +580,13 @@ def test_workflow_bounding_box_with_overlap(
 
 def test_workflow_with_per_FOV_labeling_via_script(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     python_path = sys.executable
     task_path = (
@@ -648,15 +597,13 @@ def test_workflow_with_per_FOV_labeling_via_script(
     out_path = tmp_path / "out.json"
     command = (
         f"{str(python_path)} {str(task_path)} "
-        f"-j {str(args_path)} --metadata-out {str(out_path)}"
+        f"--args-json {str(args_path)} --out-json {str(out_path)}"
     )
     debug(command)
 
+    zarr_url = str(zarr_urls[0])
     task_args = dict(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path),
-        metadata=metadata,
-        component=metadata["image"][0],
+        zarr_url=zarr_url,
         channel=dict(wavelength_id="A01_C01"),
         level=4,
         relabeling=True,
@@ -709,38 +656,29 @@ def test_workflow_with_per_FOV_labeling_via_script(
 
 def test_workflow_with_per_FOV_labeling_with_empty_FOV_table(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
-    caplog: pytest.LogCaptureFixture,
-    monkeypatch: MonkeyPatch,
 ):
     """
     Run the cellpose task iterating over an empty table of ROIs
     """
 
     # Use pre-made 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Prepare empty ROI table
     TABLE_NAME = "empty_ROI_table"
     _add_empty_ROI_table(
-        image_zarr_path=Path(zarr_path / metadata["image"][0]),
+        image_zarr_path=Path(zarr_urls[0]),
         table_name=TABLE_NAME,
     )
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_ROI_table=TABLE_NAME,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
@@ -752,7 +690,7 @@ def test_workflow_with_per_FOV_labeling_with_empty_FOV_table(
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     label_zarr = image_zarr / "labels/label_DAPI"
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
@@ -766,10 +704,7 @@ def test_workflow_with_per_FOV_labeling_with_empty_FOV_table(
 
 def test_CYX_input(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
-    caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
     """
@@ -789,22 +724,18 @@ def test_CYX_input(
     )
 
     # Load pre-made 2D zarr array
-    zarr_path_mip = tmp_path / "tmp_out_mip/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path_mip),
+    zarr_dir_mip = tmp_path / "tmp_out_mip/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir_mip),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
         make_CYX=True,
     )
 
     # Per-FOV labeling
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
@@ -812,7 +743,7 @@ def test_CYX_input(
         )
 
     # OME-NGFF JSON validation
-    image_zarr = Path(zarr_path_mip / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     label_zarr = image_zarr / "labels/label_DAPI"
     debug(image_zarr)
     well_zarr = image_zarr.parent
@@ -826,9 +757,7 @@ def test_CYX_input(
 
 def test_workflow_secondary_labeling(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     monkeypatch: MonkeyPatch,
 ):
 
@@ -843,22 +772,18 @@ def test_workflow_secondary_labeling(
     )
 
     # Load pre-made 2D zarr array
-    zarr_path = tmp_path / "tmp_out_mip/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path),
+    zarr_dir = tmp_path / "tmp_out_mip/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
         make_CYX=False,
     )
 
     # Primary segmentation (organoid)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
@@ -868,7 +793,7 @@ def test_workflow_secondary_labeling(
         )
 
     organoid_ROI_table_path = str(
-        zarr_path / metadata["image"][0] / "tables/organoid_ROI_table"
+        Path(zarr_urls[0]) / "tables/organoid_ROI_table"
     )
     organoid_ROI_table = ad.read_zarr(organoid_ROI_table_path)
     organoid_ROI_table_zarr_group = zarr.open(organoid_ROI_table_path)
@@ -881,15 +806,12 @@ def test_workflow_secondary_labeling(
     assert len(organoid_ROI_table) > 0
     assert np.max(organoid_ROI_table.X) == float(728)
 
-    debug(zarr_path / metadata["image"][0])
+    debug(zarr_urls[0])
 
     # Secondary segmentation (nuclei)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
@@ -902,9 +824,7 @@ def test_workflow_secondary_labeling(
 
 def test_workflow_secondary_labeling_no_labels(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     monkeypatch: MonkeyPatch,
 ):
     """
@@ -929,22 +849,18 @@ def test_workflow_secondary_labeling_no_labels(
     )
 
     # Load pre-made 2D zarr array
-    zarr_path = tmp_path / "tmp_out_mip/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path),
+    zarr_dir = tmp_path / "tmp_out_mip/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
         make_CYX=False,
     )
 
     # Primary segmentation (organoid)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
@@ -954,7 +870,7 @@ def test_workflow_secondary_labeling_no_labels(
         )
 
     organoid_ROI_table_path = str(
-        zarr_path / metadata["image"][0] / "tables/organoid_ROI_table"
+        Path(zarr_urls[0]) / "tables/organoid_ROI_table"
     )
     organoid_ROI_table = ad.read_zarr(organoid_ROI_table_path)
     organoid_ROI_table_zarr_group = zarr.open(organoid_ROI_table_path)
@@ -964,15 +880,12 @@ def test_workflow_secondary_labeling_no_labels(
     debug(organoid_ROI_table.shape)
     assert len(organoid_ROI_table) == 0
 
-    debug(zarr_path / metadata["image"][0])
+    debug(zarr_urls[0])
 
     # Secondary segmentation (nuclei)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
@@ -985,9 +898,7 @@ def test_workflow_secondary_labeling_no_labels(
 
 def test_workflow_secondary_labeling_two_channels(
     tmp_path: Path,
-    testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
     monkeypatch: MonkeyPatch,
 ):
     """
@@ -1008,22 +919,18 @@ def test_workflow_secondary_labeling_two_channels(
     )
 
     # Load pre-made 2D zarr array
-    zarr_path = tmp_path / "tmp_out_mip/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path),
+    zarr_dir = tmp_path / "tmp_out_mip/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
         make_CYX=False,
     )
 
     # Primary segmentation (organoid)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
@@ -1033,7 +940,7 @@ def test_workflow_secondary_labeling_two_channels(
         )
 
     organoid_ROI_table_path = str(
-        zarr_path / metadata["image"][0] / "tables/organoid_ROI_table"
+        Path(zarr_urls[0]) / "tables/organoid_ROI_table"
     )
     organoid_ROI_table = ad.read_zarr(organoid_ROI_table_path)
     organoid_ROI_table_zarr_group = zarr.open(organoid_ROI_table_path)
@@ -1046,15 +953,12 @@ def test_workflow_secondary_labeling_two_channels(
     assert len(organoid_ROI_table) > 0
     assert np.max(organoid_ROI_table.X) == float(728)
 
-    debug(zarr_path / metadata["image"][0])
+    debug(zarr_urls[0])
 
     # Secondary segmentation (nuclei)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         cellpose_segmentation(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             channel=ChannelInputModel(wavelength_id="A01_C01"),
             channel2=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
diff --git a/tests/tasks/test_workflows_multiplexing.py b/tests/tasks/test_workflows_multiplexing.py
index c139cbfc8..299f62154 100644
--- a/tests/tasks/test_workflows_multiplexing.py
+++ b/tests/tasks/test_workflows_multiplexing.py
@@ -19,16 +19,19 @@
 
 from ._validation import check_file_number
 from ._validation import validate_schema
-from fractal_tasks_core.tasks.copy_ome_zarr import (
-    copy_ome_zarr,
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_compute import (
+    cellvoyager_to_ome_zarr_compute,
 )
-from fractal_tasks_core.tasks.create_ome_zarr_multiplex import (
-    create_ome_zarr_multiplex,
+from fractal_tasks_core.tasks.cellvoyager_to_ome_zarr_init_multiplex import (
+    cellvoyager_to_ome_zarr_init_multiplex,
 )
+from fractal_tasks_core.tasks.copy_ome_zarr_hcs_plate import (
+    copy_ome_zarr_hcs_plate,
+)
+from fractal_tasks_core.tasks.io_models import MultiplexingAcquisition
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
     maximum_intensity_projection,
 )
-from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
@@ -50,11 +53,6 @@
     },
 ]
 
-allowed_channels = {
-    "0": single_cycle_allowed_channels_no_label,
-    "1": single_cycle_allowed_channels_no_label,
-}
-
 num_levels = 6
 coarsening_xy = 2
 
@@ -62,26 +60,30 @@
 def test_multiplexing_create_ome_zarr_fail(
     tmp_path: Path, zenodo_images_multiplex: Sequence[str]
 ):
-
     single_cycle_allowed_channels = [
         {"wavelength_id": "A01_C01", "label": "my label"}
     ]
-    allowed_channels = {
-        "0": single_cycle_allowed_channels,
-        "1": single_cycle_allowed_channels,
+    acquisitions = {
+        "0": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex[0],
+            allowed_channels=single_cycle_allowed_channels,
+        ),
+        "1": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex[1],
+            allowed_channels=single_cycle_allowed_channels,
+        ),
     }
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
+    zarr_dir = tmp_path / "tmp_out/"
 
     # Create zarr structure
     debug(zenodo_images_multiplex)
     with pytest.raises(ValueError):
-        _ = create_ome_zarr_multiplex(
-            input_paths=zenodo_images_multiplex,
-            output_path=str(zarr_path),
-            metadata={},
-            allowed_channels=allowed_channels,
+        _ = cellvoyager_to_ome_zarr_init_multiplex(
+            zarr_urls=[],
+            zarr_dir=zarr_dir,
+            acquisitions=acquisitions,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
             metadata_table_files=None,
@@ -92,7 +94,7 @@ def test_multiplexing_create_ome_zarr_fail(
 
 
 @pytest.mark.parametrize("metadata_input", metadata_inputs)
-def test_multiplexing_yokogawa_to_ome_zarr(
+def test_multiplexing_compute(
     tmp_path: Path,
     zenodo_images_multiplex: Sequence[str],
     metadata_input: str,
@@ -113,65 +115,104 @@ def test_multiplexing_yokogawa_to_ome_zarr(
 
     debug(metadata_table_files)
 
+    acquisitions = {
+        "0": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex[0],
+            allowed_channels=single_cycle_allowed_channels_no_label,
+        ),
+        "1": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex[1],
+            allowed_channels=single_cycle_allowed_channels_no_label,
+        ),
+    }
+
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    metadata: dict = {}
+    zarr_dir = str(tmp_path / "tmp_out/")
 
     # Create zarr structure
-    metadata_update = create_ome_zarr_multiplex(
-        input_paths=zenodo_images_multiplex,
-        output_path=str(zarr_path),
-        metadata=metadata,
-        image_extension="png",
-        allowed_channels=allowed_channels,
+    parallelization_list = cellvoyager_to_ome_zarr_init_multiplex(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
+        acquisitions=acquisitions,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
+        image_extension="png",
         metadata_table_files=metadata_table_files,
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    )["parallelization_list"]
+
+    debug(parallelization_list)
 
     # Run again, with overwrite=True
-    metadata_update_second_time = create_ome_zarr_multiplex(
-        input_paths=zenodo_images_multiplex,
-        output_path=str(zarr_path),
-        metadata=metadata,
-        image_extension="png",
-        allowed_channels=allowed_channels,
+    parallelization_list_2 = cellvoyager_to_ome_zarr_init_multiplex(
+        zarr_urls=[],
+        zarr_dir=zarr_dir,
+        acquisitions=acquisitions,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
+        image_extension="png",
         metadata_table_files=metadata_table_files,
         overwrite=True,
-    )
-    assert metadata_update_second_time == metadata_update
+    )["parallelization_list"]
+    assert parallelization_list_2 == parallelization_list
 
     # Run again, with overwrite=False
     with pytest.raises(OverwriteNotAllowedError):
-        create_ome_zarr_multiplex(
-            input_paths=zenodo_images_multiplex,
-            output_path=str(zarr_path),
-            metadata=metadata,
-            image_extension="png",
-            allowed_channels=allowed_channels,
+        _ = cellvoyager_to_ome_zarr_init_multiplex(
+            zarr_urls=[],
+            zarr_dir=zarr_dir,
+            acquisitions=acquisitions,
             num_levels=num_levels,
             coarsening_xy=coarsening_xy,
+            image_extension="png",
             metadata_table_files=metadata_table_files,
             overwrite=False,
         )
 
-    # Yokogawa to zarr
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
-        )
-    debug(metadata)
+    # Convert to OME-Zarr
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+        )["image_list_updates"]
+    debug(image_list_updates)
+
+    # Check image_list_updates
+    expected_image_list_update = [
+        {
+            "zarr_url": (
+                f"{zarr_dir}/20200812-CardiomyocyteDifferentiation14"
+                "-Cycle1.zarr/B/03/0/"
+            ),
+            "attributes": {
+                "plate": "20200812-CardiomyocyteDifferentiation14-Cycle1.zarr",
+                "well": "B03",
+                "acquisition": 0,
+            },
+            "types": {
+                "is_3D": True,
+            },
+        },
+        {
+            "zarr_url": (
+                f"{zarr_dir}/20200812-CardiomyocyteDifferentiation14"
+                "-Cycle1.zarr/B/03/1/"
+            ),
+            "attributes": {
+                "plate": "20200812-CardiomyocyteDifferentiation14-Cycle1.zarr",
+                "well": "B03",
+                "acquisition": 1,
+            },
+            "types": {
+                "is_3D": True,
+            },
+        },
+    ]
+    assert image_list_updates == expected_image_list_update
 
     # OME-NGFF JSON validation
-    image_zarr_0 = zarr_path / metadata["image"][0]
-    image_zarr_1 = zarr_path / metadata["image"][1]
+    image_zarr_0 = Path(zarr_dir) / parallelization_list[0]["zarr_url"]
+    image_zarr_1 = Path(zarr_dir) / parallelization_list[1]["zarr_url"]
     well_zarr = image_zarr_0.parent
     plate_zarr = image_zarr_0.parents[2]
     validate_schema(path=str(image_zarr_0), type="image")
@@ -188,57 +229,64 @@ def test_multiplexing_MIP(
 ):
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    zarr_path_mip = tmp_path / "tmp_out_mip/"
-    metadata: dict = {}
+    zarr_dir = tmp_path / "tmp_out/"
+
+    acquisitions = {
+        "0": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex[0],
+            allowed_channels=single_cycle_allowed_channels_no_label,
+        ),
+        "1": MultiplexingAcquisition(
+            image_dir=zenodo_images_multiplex[1],
+            allowed_channels=single_cycle_allowed_channels_no_label,
+        ),
+    }
 
     # Create zarr structure
-    metadata_update = create_ome_zarr_multiplex(
-        input_paths=zenodo_images_multiplex,
-        output_path=str(zarr_path),
-        metadata=metadata,
-        allowed_channels=allowed_channels,
-        image_extension="png",
+    parallelization_list = cellvoyager_to_ome_zarr_init_multiplex(
+        zarr_urls=[],
+        zarr_dir=str(zarr_dir),
+        acquisitions=acquisitions,
         num_levels=num_levels,
         coarsening_xy=coarsening_xy,
+        image_extension="png",
         metadata_table_files=None,
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
-
-    # Yokogawa to zarr
-    for component in metadata["image"]:
-        yokogawa_to_ome_zarr(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
-        )
-    debug(metadata)
+    )["parallelization_list"]
+    debug(parallelization_list)
+
+    # Convert to OME-Zarr
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += cellvoyager_to_ome_zarr_compute(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+        )["image_list_updates"]
+    debug(image_list_updates)
+
+    zarr_urls = []
+    for image in image_list_updates:
+        zarr_urls.append(image["zarr_url"])
 
     # Replicate
-    metadata_update = copy_ome_zarr(
-        input_paths=[str(zarr_path)],
-        output_path=str(zarr_path_mip),
-        metadata=metadata,
-        project_to_2D=True,
-        suffix="mip",
-    )
-    metadata.update(metadata_update)
-    debug(metadata)
+    parallelization_list = copy_ome_zarr_hcs_plate(
+        zarr_urls=zarr_urls,
+        zarr_dir=str(zarr_dir),
+        overwrite=True,
+    )["parallelization_list"]
+    debug(parallelization_list)
 
     # MIP
-    for component in metadata["image"]:
-        maximum_intensity_projection(
-            input_paths=[str(zarr_path_mip)],
-            output_path=str(zarr_path_mip),
-            metadata=metadata,
-            component=component,
-        )
+    image_list_updates = []
+    for image in parallelization_list:
+        image_list_updates += maximum_intensity_projection(
+            zarr_url=image["zarr_url"],
+            init_args=image["init_args"],
+            overwrite=True,
+        )["image_list_updates"]
 
     # OME-NGFF JSON validation
-    image_zarr_0 = zarr_path_mip / metadata["image"][0]
-    image_zarr_1 = zarr_path_mip / metadata["image"][1]
+    image_zarr_0 = Path(zarr_dir) / parallelization_list[0]["zarr_url"]
+    image_zarr_1 = Path(zarr_dir) / parallelization_list[1]["zarr_url"]
     well_zarr = image_zarr_0.parent
     plate_zarr = image_zarr_0.parents[2]
     validate_schema(path=str(image_zarr_0), type="image")
diff --git a/tests/tasks/test_workflows_napari_workflows.py b/tests/tasks/test_workflows_napari_workflows.py
index e9f108d8a..9314fb5c6 100644
--- a/tests/tasks/test_workflows_napari_workflows.py
+++ b/tests/tasks/test_workflows_napari_workflows.py
@@ -12,7 +12,6 @@
 Zurich.
 """
 from pathlib import Path
-from typing import Any
 
 import anndata as ad
 import pytest
@@ -25,15 +24,15 @@
 from ._validation import validate_labels_and_measurements
 from ._validation import validate_schema
 from .lib_empty_ROI_table import _add_empty_ROI_table
-from fractal_tasks_core.tasks.napari_workflows_wrapper import (
-    napari_workflows_wrapper,
-)
-from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+from fractal_tasks_core.tasks.io_models import (
     NapariWorkflowsInput,
 )
-from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+from fractal_tasks_core.tasks.io_models import (
     NapariWorkflowsOutput,
 )
+from fractal_tasks_core.tasks.napari_workflows_wrapper import (
+    napari_workflows_wrapper,
+)
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 try:
@@ -49,16 +48,13 @@ def test_napari_workflow(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Prepare parameters for first napari-workflows task (labeling)
     workflow_file = str(testdata_path / "napari_workflows/wf_1.yaml")
@@ -73,19 +69,15 @@ def test_napari_workflow(
     }
 
     # Run once
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
             level=2,
         )
-    debug(metadata)
 
     # Prepare parameters for second napari-workflows task (measurement)
     workflow_file = str(testdata_path / "napari_workflows/wf_4.yaml")
@@ -102,21 +94,17 @@ def test_napari_workflow(
     }
 
     # Run once
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
         )
-    debug(metadata)
 
     # OME-NGFF JSON validation
-    image_zarr = zarr_path / metadata["image"][0]
+    image_zarr = Path(zarr_urls[0])
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
     label_zarr = image_zarr / "labels/label_DAPI"
@@ -134,9 +122,7 @@ def test_napari_workflow(
     validate_axes_and_coordinateTransformations(label_zarr)
 
     # Load measurements
-    meas = ad.read_zarr(
-        str(zarr_path / metadata["image"][0] / "tables/regionprops_DAPI/")
-    )
+    meas = ad.read_zarr(str(Path(zarr_urls[0]) / "tables/regionprops_DAPI/"))
     debug(meas.var_names)
     assert "area" in meas.var_names
     assert "bbox_area" in meas.var_names
@@ -146,16 +132,13 @@ def test_napari_worfklow_label_input_only(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Prepare 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # First napari-workflows task (labeling)
     workflow_file = str(testdata_path / "napari_workflows/wf_1.yaml")
@@ -168,19 +151,15 @@ def test_napari_worfklow_label_input_only(
             "label_name": "label_DAPI",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
             level=2,
         )
-    debug(metadata)
 
     # Second napari-workflows task (measurement)
     workflow_file = str(
@@ -195,21 +174,17 @@ def test_napari_worfklow_label_input_only(
             "label_name": "label_DAPI_expanded",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
         )
-    debug(metadata)
 
     # OME-NGFF JSON validation
-    image_zarr = zarr_path / metadata["image"][0]
+    image_zarr = Path(zarr_urls[0])
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
     label_zarr = image_zarr / "labels/label_DAPI"
@@ -278,28 +253,22 @@ def test_relabeling(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Prepare 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # If needed, produce some labels before the actual test
     if needs_labels:
         workflow_file = str(
             testdata_path / "napari_workflows" / RELABELING_CASE_1[0]
         )
-        for component in metadata["image"]:
+        for zarr_url in zarr_urls:
             napari_workflows_wrapper(
-                input_paths=[str(zarr_path)],
-                output_path=str(zarr_path),
-                metadata=metadata,
-                component=component,
+                zarr_url=zarr_url,
                 input_specs=RELABELING_CASE_1[1],
                 output_specs=RELABELING_CASE_1[2],
                 workflow_file=workflow_file,
@@ -313,21 +282,17 @@ def test_relabeling(
     debug(workflow_file)
     debug(input_specs)
     debug(output_specs)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
         )
-    debug(metadata)
 
     # Check output
-    image_zarr = Path(zarr_path / metadata["image"][0])
+    image_zarr = Path(zarr_urls[0])
     validate_labels_and_measurements(
         image_zarr, label_name=LABEL_NAME, table_name=TABLE_NAME
     )
@@ -337,7 +302,7 @@ def test_relabeling(
     ]
     if dataframe_outputs:
         meas = ad.read_zarr(
-            zarr_path / metadata["image"][0] / f"tables/{TABLE_NAME}/"
+            Path(zarr_urls[0], f"tables/{TABLE_NAME}/").as_posix()
         )
         debug(meas.var_names)
         assert "area" in meas.var_names
@@ -348,16 +313,13 @@ def test_fail_if_no_relabeling(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Prepare 3D zarr
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Run napari-workflow RELABELING_CASE_1, but with relabeling=False
     workflow_file_name, input_specs, output_specs = RELABELING_CASE_1
@@ -367,21 +329,17 @@ def test_fail_if_no_relabeling(
     debug(workflow_file)
     debug(input_specs)
     debug(output_specs)
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
             relabeling=False,
         )
-    debug(metadata)
 
-    image_zarr = zarr_path / metadata["image"][0]
+    image_zarr = Path(zarr_urls[0])
     with pytest.raises(AssertionError):
         validate_labels_and_measurements(
             image_zarr, label_name=LABEL_NAME, table_name=TABLE_NAME
@@ -409,27 +367,23 @@ def test_expected_dimensions(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Prepare zarr
-    zarr_path = tmp_path / "tmp_out/"
+    zarr_dir = tmp_path / "tmp_out/"
     if zarr_dimensions == 2:
-        metadata = prepare_2D_zarr(
-            str(zarr_path),
+        zarr_urls = prepare_2D_zarr(
+            str(zarr_dir),
             zenodo_zarr,
-            zenodo_zarr_metadata,
             remove_labels=True,
             make_CYX=make_CYX,
         )
     else:
         if make_CYX:
             raise ValueError(f"{make_CYX=} and {zarr_dimensions=}")
-        metadata = prepare_3D_zarr(
-            str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-        )
-    debug(zarr_path)
-    debug(metadata)
+        zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # First napari-workflows task (labeling)
     workflow_file = str(
@@ -448,12 +402,9 @@ def test_expected_dimensions(
         },
     }
 
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         arguments = dict(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
@@ -473,24 +424,21 @@ def test_napari_workflow_empty_input_ROI_table(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
     """
     Run the napari_workflows task, iterating over an empty table of ROIs
     """
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Prepare empty ROI table
     TABLE_NAME = "empty_ROI_table"
     _add_empty_ROI_table(
-        image_zarr_path=Path(zarr_path / metadata["image"][0]),
+        image_zarr_path=Path(zarr_urls[0]),
         table_name=TABLE_NAME,
     )
 
@@ -505,19 +453,15 @@ def test_napari_workflow_empty_input_ROI_table(
             "label_name": "label_DAPI",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_ROI_table=TABLE_NAME,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             level=2,
         )
-    debug(metadata)
 
     # Second napari-workflows task (measurement)
     workflow_file = str(testdata_path / "napari_workflows/wf_4.yaml")
@@ -532,21 +476,17 @@ def test_napari_workflow_empty_input_ROI_table(
             "label_name": "label_DAPI",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_ROI_table=TABLE_NAME,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
         )
-    debug(metadata)
 
     # OME-NGFF JSON validation
-    image_zarr = zarr_path / metadata["image"][0]
+    image_zarr = Path(zarr_urls[0])
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
     label_zarr = image_zarr / "labels/label_DAPI"
@@ -563,7 +503,7 @@ def test_napari_workflow_empty_input_ROI_table(
 
     # Load measurements
     meas = ad.read_zarr(
-        str(zarr_path / metadata["image"][0] / "tables/regionprops_DAPI/")
+        Path(zarr_urls[0], "tables/regionprops_DAPI/").as_posix()
     )
     debug(meas.var_names)
 
@@ -572,20 +512,18 @@ def test_napari_workflow_CYX(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path),
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
         make_CYX=True,
     )
-    debug(zarr_path)
-    debug(metadata)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # First napari-workflows task (labeling)
     workflow_file = str(testdata_path / "napari_workflows/wf_1.yaml")
@@ -598,12 +536,9 @@ def test_napari_workflow_CYX(
             "label_name": "label_DAPI",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
@@ -611,7 +546,7 @@ def test_napari_workflow_CYX(
             expected_dimensions=2,
             level=2,
         )
-    debug(metadata)
+    debug(zarr_urls)
 
     # Second napari-workflows task (measurement)
     workflow_file = str(testdata_path / "napari_workflows/wf_4.yaml")
@@ -626,22 +561,19 @@ def test_napari_workflow_CYX(
             "label_name": "label_DAPI",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
             expected_dimensions=2,
         )
-    debug(metadata)
+    debug(zarr_urls)
 
     # OME-NGFF JSON validation
-    image_zarr = zarr_path / metadata["image"][0]
+    image_zarr = Path(zarr_urls[0])
     well_zarr = image_zarr.parent
     plate_zarr = image_zarr.parents[2]
     label_zarr = image_zarr / "labels/label_DAPI"
@@ -659,9 +591,7 @@ def test_napari_workflow_CYX(
     validate_axes_and_coordinateTransformations(label_zarr)
 
     # Load measurements
-    meas = ad.read_zarr(
-        str(zarr_path / metadata["image"][0] / "tables/regionprops_DAPI/")
-    )
+    meas = ad.read_zarr(str(Path(zarr_urls[0]) / "tables/regionprops_DAPI/"))
     debug(meas.var_names)
     assert "area" in meas.var_names
     assert "bbox_area" in meas.var_names
@@ -671,23 +601,21 @@ def test_napari_workflow_CYX_wrong_dimensions(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
     """
     This will fail because of wrong expected_dimensions
     """
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_2D_zarr(
-        str(zarr_path),
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_2D_zarr(
+        str(zarr_dir),
         zenodo_zarr,
-        zenodo_zarr_metadata,
         remove_labels=True,
         make_CYX=True,
     )
-    debug(zarr_path)
-    debug(metadata)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # First napari-workflows task (labeling)
     workflow_file = str(testdata_path / "napari_workflows/wf_1.yaml")
@@ -700,13 +628,10 @@ def test_napari_workflow_CYX_wrong_dimensions(
             "label_name": "label_DAPI",
         },
     }
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         with pytest.raises(ValueError) as e:
             napari_workflows_wrapper(
-                input_paths=[str(zarr_path)],
-                output_path=str(zarr_path),
-                metadata=metadata,
-                component=component,
+                zarr_url=zarr_url,
                 input_specs=input_specs,
                 output_specs=output_specs,
                 workflow_file=workflow_file,
@@ -725,16 +650,13 @@ def test_napari_workflow_mock(
     tmp_path: Path,
     testdata_path: Path,
     zenodo_zarr: list[str],
-    zenodo_zarr_metadata: list[dict[str, Any]],
 ):
 
     # Init
-    zarr_path = tmp_path / "tmp_out/"
-    metadata = prepare_3D_zarr(
-        str(zarr_path), zenodo_zarr, zenodo_zarr_metadata
-    )
-    debug(zarr_path)
-    debug(metadata)
+    zarr_dir = tmp_path / "tmp_out/"
+    zarr_urls = prepare_3D_zarr(str(zarr_dir), zenodo_zarr)
+    debug(zarr_dir)
+    debug(zarr_urls)
 
     # Prepare parameters for first napari-workflows task (labeling)
     workflow_file = str(testdata_path / "napari_workflows/wf_1.yaml")
@@ -749,27 +671,20 @@ def test_napari_workflow_mock(
     }
 
     # Run once
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
             level=2,
         )
-    debug(metadata)
 
     # Re-run with overwrite=True
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
@@ -779,13 +694,10 @@ def test_napari_workflow_mock(
         )
 
     # Re-run with overwrite=False
-    with pytest.raises(OverwriteNotAllowedError):
-        for component in metadata["image"]:
+    for zarr_url in zarr_urls:
+        with pytest.raises(OverwriteNotAllowedError):
             napari_workflows_wrapper(
-                input_paths=[str(zarr_path)],
-                output_path=str(zarr_path),
-                metadata=metadata,
-                component=component,
+                zarr_url=zarr_url,
                 input_specs=input_specs,
                 output_specs=output_specs,
                 workflow_file=workflow_file,
@@ -809,26 +721,19 @@ def test_napari_workflow_mock(
     }
 
     # Run once
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
             input_ROI_table="FOV_ROI_table",
         )
-    debug(metadata)
 
     # Re-run with overwrite=True
-    for component in metadata["image"]:
+    for zarr_url in zarr_urls:
         napari_workflows_wrapper(
-            input_paths=[str(zarr_path)],
-            output_path=str(zarr_path),
-            metadata=metadata,
-            component=component,
+            zarr_url=zarr_url,
             input_specs=input_specs,
             output_specs=output_specs,
             workflow_file=workflow_file,
@@ -837,13 +742,10 @@ def test_napari_workflow_mock(
         )
 
     # Re-run with overwrite=False
-    with pytest.raises(OverwriteNotAllowedError):
-        for component in metadata["image"]:
+    for zarr_url in zarr_urls:
+        with pytest.raises(OverwriteNotAllowedError):
             napari_workflows_wrapper(
-                input_paths=[str(zarr_path)],
-                output_path=str(zarr_path),
-                metadata=metadata,
-                component=component,
+                zarr_url=zarr_url,
                 input_specs=input_specs,
                 output_specs=output_specs,
                 workflow_file=workflow_file,
diff --git a/tests/test_unit_input_models.py b/tests/test_unit_input_models.py
index ae5a321d0..b74ba4251 100644
--- a/tests/test_unit_input_models.py
+++ b/tests/test_unit_input_models.py
@@ -2,10 +2,10 @@
 from devtools import debug
 
 from fractal_tasks_core.channels import ChannelInputModel
-from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+from fractal_tasks_core.tasks.io_models import (
     NapariWorkflowsInput,
 )
-from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+from fractal_tasks_core.tasks.io_models import (
     NapariWorkflowsOutput,
 )
 
diff --git a/tests/test_unit_masked_loading.py b/tests/test_unit_masked_loading.py
index ac06f6e48..349dde137 100644
--- a/tests/test_unit_masked_loading.py
+++ b/tests/test_unit_masked_loading.py
@@ -3,8 +3,6 @@
 import numpy as np
 from devtools import debug
 
-from ._zenodo_ome_zarrs import prepare_2D_zarr  # noqa
-from ._zenodo_ome_zarrs import prepare_3D_zarr  # noqa
 from fractal_tasks_core.masked_loading import (
     masked_loading_wrapper,
 )  # noqa
diff --git a/tests/test_unit_zattrs_utils.py b/tests/test_unit_zattrs_utils.py
index 653dd8bc5..27ef8fcdf 100644
--- a/tests/test_unit_zattrs_utils.py
+++ b/tests/test_unit_zattrs_utils.py
@@ -2,7 +2,7 @@
 from devtools import debug
 
 from fractal_tasks_core.utils import (
-    get_table_path_dict,
+    _get_table_path_dict,
 )
 from fractal_tasks_core.utils import rescale_datasets
 
@@ -47,26 +47,21 @@ def test_rescale_datasets(tmp_path):
 
 def test_get_table_path_dict(tmp_path):
 
-    input_path = tmp_path
-    component = "plate.zarr/B/03/0"
-    img_group = zarr.open_group(str(input_path / component))
+    zarr_url = str(tmp_path / "plate.zarr/B/03/0")
+    img_group = zarr.open_group(zarr_url)
 
     # Missing tables sub-group
-    table_path_dict = get_table_path_dict(input_path, component)
+    table_path_dict = _get_table_path_dict(zarr_url)
     debug(table_path_dict)
     assert table_path_dict == {}
 
     tables_group = img_group.create_group("tables")
-    table_path_dict = get_table_path_dict(input_path, component)
+    table_path_dict = _get_table_path_dict(zarr_url)
     debug(table_path_dict)
     assert table_path_dict == {}
 
     tables_group.attrs.update({"tables": ["table1", "table2"]})
-    table_path_dict = get_table_path_dict(input_path, component)
+    table_path_dict = _get_table_path_dict(zarr_url)
     debug(table_path_dict)
-    assert table_path_dict.pop("table1") == str(
-        input_path / component / "tables/table1"
-    )
-    assert table_path_dict.pop("table2") == str(
-        input_path / component / "tables/table2"
-    )
+    assert table_path_dict.pop("table1") == str(f"{zarr_url}/tables/table1")
+    assert table_path_dict.pop("table2") == str(f"{zarr_url}/tables/table2")
diff --git a/tests/test_valid_manifest.py b/tests/test_valid_manifest.py
index d76aa79f9..238874581 100644
--- a/tests/test_valid_manifest.py
+++ b/tests/test_valid_manifest.py
@@ -2,7 +2,6 @@
 from pathlib import Path
 
 import requests
-from devtools import debug
 from jsonschema import validate
 
 import fractal_tasks_core
@@ -13,21 +12,22 @@ def test_valid_manifest(tmp_path):
     NOTE: to avoid adding a fractal-server dependency, we simply download the
     relevant file.
     """
-    # Download JSON Schema for ManifestV1
+    # Download JSON Schema for ManifestV2
     url = (
         "https://raw.githubusercontent.com/fractal-analytics-platform/"
         "fractal-server/main/"
-        "fractal_server/app/schemas/json_schemas/manifest.json"
+        "fractal_server/json_schemas/manifest_v2.json"
     )
     r = requests.get(url)
-    with (tmp_path / "manifest_schema.json").open("wb") as f:
+    with (tmp_path / "manifest_schema_v2.json").open("wb") as f:
         f.write(r.content)
-    with (tmp_path / "manifest_schema.json").open("r") as f:
+    with (tmp_path / "manifest_schema_v2.json").open("r") as f:
         manifest_schema = json.load(f)
 
+    # Load tasks-package manifest
     module_dir = Path(fractal_tasks_core.__file__).parent
     with (module_dir / "__FRACTAL_MANIFEST__.json").open("r") as fin:
         manifest_dict = json.load(fin)
 
-    debug(manifest_dict)
+    # Validate manifest against JSON Schema
     validate(instance=manifest_dict, schema=manifest_schema)