diff --git a/channel-transport/README.md b/channel-transport/README.md
new file mode 100644
index 000000000..7a7d8e5ff
--- /dev/null
+++ b/channel-transport/README.md
@@ -0,0 +1,53 @@
+---
+title: Channel transport
+permalink: tutorials-channel-transport.html
+keywords: volume coupling, chemistry, OpenFOAM, Nutils, species, transport
+summary: A CFD problem is coupled to a transport (of, e.g., a chemistry species) in a uni-directional way.
+---
+
+{% note %}
+Get the [case files of this tutorial](https://github.com/precice/tutorials/tree/master/channel-transport). Read how in the [tutorials introduction](https://www.precice.org/tutorials.html).
+{% endnote %}
+
+## Setup
+
+We model a two-dimensional incompressible fluid flowing through a channel with an obstacle. The fluid problem is coupled to a simple transport problem in a uni-directional fashion. The transport problem is initialized with a circular blob close to the inflow. The density of the species is denoted with `T` (like temperature). The convected and diffused blob after 23 timesteps:
+
+![Flap setup](images/tutorials-channel-transport-physics.png)
+
+The behavior of the blob over the full 200 timesteps looks as follows:
+
+<video autoplay loop>
+  <source src="images/tutorials-channel-transport-animation.webm" type="video/webm">
+  Animation of blob over 200 timesteps.
+</video>
+  
+## Available solvers
+
+Fluid participant:
+
+* Nutils. For more information, have a look at the [Nutils adapter documentation](https://www.precice.org/adapter-nutils.html). This Nutils solver requires at least Nutils v7.0.
+
+Transport participant:
+
+* Nutils. For more information, have a look at the [Nutils adapter documentation](https://www.precice.org/adapter-nutils.html). This Nutils solver requires at least Nutils v7.0.
+
+## Running the simulation
+
+Open two separate terminals and start one fluid and one transport participant by calling the respective run scripts `run.sh` located in each of the participants' directory. For example:
+
+```bash
+cd fluid-nutils
+./run.sh
+```
+
+and
+
+```bash
+cd transport-nutils
+./run.sh
+```
+
+## Post-processing
+
+All solvers generate `vtk` files which can be visualized using, e.g., ParaView.
diff --git a/channel-transport/clean-tutorial.sh b/channel-transport/clean-tutorial.sh
new file mode 120000
index 000000000..4713f5092
--- /dev/null
+++ b/channel-transport/clean-tutorial.sh
@@ -0,0 +1 @@
+../tools/clean-tutorial-base.sh
\ No newline at end of file
diff --git a/channel-transport/fluid-nutils/clean.sh b/channel-transport/fluid-nutils/clean.sh
new file mode 100755
index 000000000..6893c1ea5
--- /dev/null
+++ b/channel-transport/fluid-nutils/clean.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e -u
+
+. ../../tools/cleaning-tools.sh
+
+clean_nutils .
diff --git a/channel-transport/fluid-nutils/fluid.py b/channel-transport/fluid-nutils/fluid.py
new file mode 100644
index 000000000..b6635864a
--- /dev/null
+++ b/channel-transport/fluid-nutils/fluid.py
@@ -0,0 +1,109 @@
+#! /usr/bin/env python3
+
+#
+# Incompressible NSE solved within a channel geometry with parabolic inflow profile and an obstacle attached to the bottom towards the middle of the domain. The fluid field is initialized with a Stokes solution. The resulting velocity field is written to preCICE on the complete volume.
+#
+
+from nutils import function, mesh, cli, solver, export
+import treelog as log
+import numpy as np
+import precice
+from mpi4py import MPI
+
+
+def main():
+
+    print("Running utils")
+
+    # define the Nutils mesh
+    nx = 48
+    ny = 16
+    step_start = nx // 3
+    step_end = nx // 2
+    step_hight = ny // 2
+
+    grid = np.linspace(0, 6, nx + 1), np.linspace(0, 2, ny + 1)
+    domain, geom = mesh.rectilinear(grid)
+    domain = domain.withboundary(inflow="left", outflow="right", wall="top,bottom") - domain[
+        step_start:step_end, :step_hight
+    ].withboundary(wall="left,top,right")
+
+    # cloud of Gauss points
+    gauss = domain.sample("gauss", degree=4)
+
+    # Nutils namespace
+    ns = function.Namespace()
+    ns.x = geom
+
+    ns.ubasis = domain.basis("std", degree=2).vector(2)
+    ns.pbasis = domain.basis("std", degree=1)
+    ns.u_i = "ubasis_ni ?u_n"  # solution
+    ns.p = "pbasis_n ?p_n"  # solution
+    ns.dudt_i = "ubasis_ni (?u_n - ?u0_n) / ?dt"  # time derivative
+    ns.μ = 0.5  # viscosity
+    ns.σ_ij = "μ (u_i,j + u_j,i) - p δ_ij"
+    ns.uin = "10 x_1 (2 - x_1)"  # inflow profile
+
+    # define the weak form, Stokes problem
+    ures = gauss.integral("ubasis_ni,j σ_ij d:x" @ ns)
+    pres = gauss.integral("pbasis_n u_k,k d:x" @ ns)
+
+    # define Dirichlet boundary condition
+    sqr = domain.boundary["inflow"].integral("(u_0 - uin)^2 d:x" @ ns, degree=2)
+    sqr += domain.boundary["inflow,outflow"].integral("u_1^2 d:x" @ ns, degree=2)
+    sqr += domain.boundary["wall"].integral("u_k u_k d:x" @ ns, degree=2)
+    cons = solver.optimize(["u"], sqr, droptol=1e-15)
+
+    # preCICE setup
+    interface = precice.Interface("Fluid", "../precice-config.xml", 0, 1)
+
+    # define coupling mesh
+    mesh_name = "Fluid-Mesh"
+    mesh_id = interface.get_mesh_id(mesh_name)
+    vertices = gauss.eval(ns.x)
+    vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
+
+    # coupling data
+    velocity_id = interface.get_data_id("Velocity", mesh_id)
+
+    precice_dt = interface.initialize()
+
+    timestep = 0
+    dt = 0.005
+
+    state = solver.solve_linear(("u", "p"), (ures, pres), constrain=cons)  # initial condition
+
+    # add convective term and time derivative for Navier-Stokes
+    ures += gauss.integral("ubasis_ni (dudt_i + μ (u_i u_j)_,j) d:x" @ ns)
+
+    while interface.is_coupling_ongoing():
+
+        if timestep % 1 == 0:  # visualize
+            bezier = domain.sample("bezier", 2)
+            x, u, p = bezier.eval(["x_i", "u_i", "p"] @ ns, **state)
+            with log.add(log.DataLog()):
+                export.vtk("Fluid_" + str(timestep), bezier.tri, x, u=u, p=p)
+
+        # potentially adjust non-matching timestep sizes
+        dt = min(dt, precice_dt)
+
+        # solve Nutils timestep
+        state["u0"] = state["u"]
+        state["dt"] = dt
+        state = solver.newton(("u", "p"), (ures, pres), constrain=cons, arguments=state).solve(1e-10)
+
+        if interface.is_write_data_required(dt):
+            velocity_values = gauss.eval(ns.u, **state)
+            interface.write_block_vector_data(velocity_id, vertex_ids, velocity_values)
+
+        # do the coupling
+        precice_dt = interface.advance(dt)
+
+        # advance variables
+        timestep += 1
+
+    interface.finalize()
+
+
+if __name__ == "__main__":
+    cli.run(main)
diff --git a/channel-transport/fluid-nutils/run.sh b/channel-transport/fluid-nutils/run.sh
new file mode 100755
index 000000000..788f40d13
--- /dev/null
+++ b/channel-transport/fluid-nutils/run.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -e -u
+
+python3 fluid.py
diff --git a/channel-transport/images/tutorials-channel-transport-animation.webm b/channel-transport/images/tutorials-channel-transport-animation.webm
new file mode 100644
index 000000000..f4c846ece
Binary files /dev/null and b/channel-transport/images/tutorials-channel-transport-animation.webm differ
diff --git a/channel-transport/images/tutorials-channel-transport-physics.png b/channel-transport/images/tutorials-channel-transport-physics.png
new file mode 100644
index 000000000..530822c85
Binary files /dev/null and b/channel-transport/images/tutorials-channel-transport-physics.png differ
diff --git a/channel-transport/precice-config.xml b/channel-transport/precice-config.xml
new file mode 100644
index 000000000..97b9736f5
--- /dev/null
+++ b/channel-transport/precice-config.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<precice-configuration>
+  <log>
+    <sink
+      filter="%Severity% > debug and %Rank% = 0"
+      format="---[precice] %ColorizedSeverity% %Message%"
+      enabled="true" />
+  </log>
+
+  <solver-interface dimensions="2">
+    <data:vector name="Velocity" />
+
+    <mesh name="Fluid-Mesh">
+      <use-data name="Velocity" />
+    </mesh>
+
+    <mesh name="Transport-Mesh">
+      <use-data name="Velocity" />
+    </mesh>
+
+    <participant name="Fluid">
+      <use-mesh name="Fluid-Mesh" provide="yes" />
+      <use-mesh name="Transport-Mesh" from="Transport" />
+      <write-data name="Velocity" mesh="Fluid-Mesh" />
+      <mapping:rbf-compact-tps-c2
+        direction="write"
+        support-radius="0.4"
+        from="Fluid-Mesh"
+        to="Transport-Mesh"
+        constraint="consistent" />
+    </participant>
+
+    <participant name="Transport">
+      <use-mesh name="Transport-Mesh" provide="yes" />
+      <read-data name="Velocity" mesh="Transport-Mesh" />
+    </participant>
+
+    <m2n:sockets from="Fluid" to="Transport" exchange-directory=".." />
+
+    <coupling-scheme:serial-explicit>
+      <time-window-size value="0.005" />
+      <max-time value="1.0" />
+      <participants first="Fluid" second="Transport" />
+      <exchange data="Velocity" mesh="Transport-Mesh" from="Fluid" to="Transport" />
+    </coupling-scheme:serial-explicit>
+  </solver-interface>
+</precice-configuration>
diff --git a/channel-transport/transport-nutils/clean.sh b/channel-transport/transport-nutils/clean.sh
new file mode 100755
index 000000000..6893c1ea5
--- /dev/null
+++ b/channel-transport/transport-nutils/clean.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e -u
+
+. ../../tools/cleaning-tools.sh
+
+clean_nutils .
diff --git a/channel-transport/transport-nutils/run.sh b/channel-transport/transport-nutils/run.sh
new file mode 100755
index 000000000..ac3581bb5
--- /dev/null
+++ b/channel-transport/transport-nutils/run.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -e -u
+
+python3 transport.py
diff --git a/channel-transport/transport-nutils/transport.py b/channel-transport/transport-nutils/transport.py
new file mode 100644
index 000000000..db7ad4427
--- /dev/null
+++ b/channel-transport/transport-nutils/transport.py
@@ -0,0 +1,108 @@
+#! /usr/bin/env python3
+
+#
+# Advection-Diffusion equation for a single species with a velocity field read from preCICE on the complete volume. 
+#
+
+from nutils import function, mesh, cli, solver, export
+import treelog as log
+import numpy as np
+import precice
+from mpi4py import MPI
+
+
+def main():
+
+    print("Running utils")
+
+    # define the Nutils mesh
+    nx = 120
+    ny = 32
+    step_start = nx // 3
+    step_end = nx // 2
+    step_hight = ny // 2
+
+    grid = np.linspace(0, 6, nx + 1), np.linspace(0, 2, ny + 1)
+    domain, geom = mesh.rectilinear(grid)
+    domain = domain.withboundary(inflow="left", outflow="right", wall="top,bottom") - domain[
+        step_start:step_end, :step_hight
+    ].withboundary(wall="left,top,right")
+
+    # cloud of Gauss points
+    gauss = domain.sample("gauss", degree=2)
+
+    # Nutils namespace
+    ns = function.Namespace(fallback_length=2)
+    ns.x = geom
+    ns.basis = domain.basis("std", degree=1)  # linear finite elements
+    ns.u = "basis_n ?lhs_n"  # solution
+    ns.dudt = "basis_n (?lhs_n - ?lhs0_n) / ?dt"  # time derivative
+    ns.vbasis = gauss.basis()
+    ns.velocity_i = "vbasis_n ?velocity_ni"
+    ns.k = 0.1  # diffusivity
+    ns.xblob = 1, 1
+    ns.uinit = ".5 - .5 tanh(((x_i - xblob_i) (x_i - xblob_i) - .5) / .1)"  # blob
+
+    # define the weak form
+    res = gauss.integral("(basis_n (dudt + (velocity_i u)_,i) + k basis_n,i u_,i) d:x" @ ns)
+
+    # define Dirichlet boundary condition
+    sqr = domain.boundary["inflow"].integral("u^2 d:x" @ ns, degree=2)
+    cons = solver.optimize("lhs", sqr, droptol=1e-15)
+
+    # preCICE setup
+    interface = precice.Interface("Transport", "../precice-config.xml", 0, 1)
+
+    # define coupling mesh
+    mesh_name = "Transport-Mesh"
+    mesh_id = interface.get_mesh_id(mesh_name)
+    vertices = gauss.eval(ns.x)
+    vertex_ids = interface.set_mesh_vertices(mesh_id, vertices)
+
+    # coupling data
+    velocity_id = interface.get_data_id("Velocity", mesh_id)
+
+    precice_dt = interface.initialize()
+
+    timestep = 0
+    dt = 0.005
+
+    # set blob as initial condition
+    sqr = domain.integral("(u - uinit)^2" @ ns, degree=2)
+    lhs0 = solver.optimize("lhs", sqr)
+
+    # initialize the velocity values
+    velocity_values = np.zeros_like(vertices)
+
+    while interface.is_coupling_ongoing():
+
+        if timestep % 1 == 0:  # visualize
+            bezier = domain.sample("bezier", 2)
+            x, u = bezier.eval(["x_i", "u"] @ ns, lhs=lhs0)
+            with log.add(log.DataLog()):
+                export.vtk("Transport_" + str(timestep), bezier.tri, x, T=u)
+
+        # read velocity values from interface
+        if interface.is_read_data_available():
+            velocity_values = interface.read_block_vector_data(velocity_id, vertex_ids)
+
+        # potentially adjust non-matching timestep sizes
+        dt = min(dt, precice_dt)
+
+        # solve nutils timestep
+        lhs = solver.solve_linear(
+            "lhs", res, constrain=cons, arguments=dict(lhs0=lhs0, dt=dt, velocity=velocity_values)
+        )
+
+        # do the coupling
+        precice_dt = interface.advance(dt)
+
+        # advance variables
+        timestep += 1
+        lhs0 = lhs
+
+    interface.finalize()
+
+
+if __name__ == "__main__":
+    cli.run(main)
diff --git a/tools/check-size.sh b/tools/check-size.sh
index e6d071382..92cec1b54 100755
--- a/tools/check-size.sh
+++ b/tools/check-size.sh
@@ -20,7 +20,7 @@ for tutorial in $tutorials; do
   for img in $images; do
     actualsize=$(du -k "$img" | cut -f 1)
     # Check gifs
-    if [[ "${img}" == *.gif ]]; then
+    if [[ "${img}" == *.gif || "${img}" == *.webp || "${img}" == *.webm ]]; then
       if [ "${actualsize}" -ge "${MAXIMUMGIFSIZE}" ]; then
           echo -e "$img:$RED $actualsize kb exceeds the limit of $MAXIMUMGIFSIZE kb. $NOCOLOR"
           CODE=1
