diff --git a/doc/install/python.rst b/doc/install/python.rst
index 051836fa1c06044de38c9132728252b50b4ca303..ce64f662094584459582fc0f1696818bba0e5cf5 100644
--- a/doc/install/python.rst
+++ b/doc/install/python.rst
@@ -43,7 +43,7 @@ to see information about the version and enabled features:
     >>> print(arbor.__config__)
 
 You are now ready to use Arbor! You can continue reading these documentation pages, have a look at the
-:ref:`Python API reference<pyoverview>` , or visit the :ref:`Quick Start page<tutorialsimplecell>`.
+:ref:`Python API reference<pyoverview>` , or visit the :ref:`Quick Start page<tutorialsinglecell>`.
 
 .. Note::
     To get help in case of problems installing with pip, run pip with the ``--verbose`` flag, and attach the output
diff --git a/doc/python/single_cell_model.rst b/doc/python/single_cell_model.rst
index 20be222e25930e25c859238f10e54b1867c63c03..fb030c80d92b84142ccdf055b8b2c5fabc2d38ae 100644
--- a/doc/python/single_cell_model.rst
+++ b/doc/python/single_cell_model.rst
@@ -70,7 +70,7 @@ Single cell model
 
 .. Note::
 
-   The :class:`single_cell_model` is used in our :ref:`tutorials <gs_other_examples>`.
+   The :class:`single_cell_model` is used in our :ref:`tutorials <tutorialsinglecell>`.
    The examples illustrate how to construct a :class:`cable_cell` and use it to form
    a :class:`single_cell_model`; how to add probes; how to run the model; and how to
    visualize the results.
\ No newline at end of file
diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst
index f52907ddf6a2241b91be1c2543b942854ac92bff..c7f38777bb56480ea965ade3d790b500034c3763 100644
--- a/doc/tutorial/index.rst
+++ b/doc/tutorial/index.rst
@@ -21,5 +21,6 @@ Tutorials
    :caption: Tutorials
 
    single_cell_model
+   single_cell_recipe
    single_cell_detailed
    single_cell_detailed_recipe
diff --git a/doc/tutorial/single_cell_detailed.rst b/doc/tutorial/single_cell_detailed.rst
index 6558a5b0e6f9807bbac1d414c85b7bbda8fd1cd6..78823febbf05f276824bc1bd9ac1f44b9ec76407 100644
--- a/doc/tutorial/single_cell_detailed.rst
+++ b/doc/tutorial/single_cell_detailed.rst
@@ -3,7 +3,7 @@
 A detailed single cell model
 ============================
 
-We can expand on the :ref:`single segment cell example <tutorialsimplecell>` to create a more
+We can expand on the :ref:`single segment cell example <tutorialsinglecell>` to create a more
 complex single cell model, and go through the process in more detail.
 
 .. Note::
diff --git a/doc/tutorial/single_cell_detailed_recipe.rst b/doc/tutorial/single_cell_detailed_recipe.rst
index 32df752764c9c91f4d3b8649a1718ee84a06e784..ad00e01a658bd1d279f0d876d5620b52a8a063f8 100644
--- a/doc/tutorial/single_cell_detailed_recipe.rst
+++ b/doc/tutorial/single_cell_detailed_recipe.rst
@@ -1,9 +1,9 @@
-.. _tutorialsinglecellrecipe:
+.. _tutorialsinglecellswcrecipe:
 
 A detailed single cell recipe
 =============================
 
-This example aims to build the same single cell model as
+This example builds the same single cell model as
 :ref:`the previous tutorial <tutorialsinglecellswc>`, except using a :class:`arbor.recipe`
 and :class:`arbor.simulation` instead of a :class:`arbor.single_cell_model`.
 
@@ -113,7 +113,7 @@ We will add one more thing to this section. We will create the voltage probe at
 In the previous example, this probe was registered directly using the :class:`arbor.single_cell_model` object.
 Now it has to be explicitly created and registered in the recipe.
 
-.. _tutorialsinglecellrecipe-probe:
+.. _tutorialsinglecellswcrecipe-probe:
 .. code-block:: python
 
    probe = arbor.cable_probe_membrane_voltage('"custom_terminal"')
@@ -345,7 +345,7 @@ to plot the voltage registered by the probe on the "custom_terminal" locset.
 The lines handling probe sampling warrant a second look. First, we declared ``probe_id`` to be a
 :class:`arbor.cell_member`, with :class:`arbor.cell_member.gid` = 0 and :class:`arbor.cell_member.index` = 0.
 This variable serves as a global identifier of a probe on a cell, namely the first declared probe on the
-cell with gid = 0, which is id of the :ref:`only probe <tutorialsinglecellrecipe-probe>` we created on
+cell with gid = 0, which is id of the :ref:`only probe <tutorialsinglecellswcrecipe-probe>` we created on
 the only cell in the model.
 
 Next, we instructed the simulation to sample ``probe_id`` at a frequency of 50KHz. That function returns a
diff --git a/doc/tutorial/single_cell_model.rst b/doc/tutorial/single_cell_model.rst
index ac6acdf3aa66e6aa7927b403bbdb3510cb49261e..14e2aeb8dcfe8673967a2dc0b83b0c0e438b1971 100644
--- a/doc/tutorial/single_cell_model.rst
+++ b/doc/tutorial/single_cell_model.rst
@@ -1,4 +1,4 @@
-.. _tutorialsimplecell:
+.. _tutorialsinglecell:
 
 A simple single cell model
 ==========================
@@ -19,6 +19,10 @@ introduce Arbor's cell modelling concepts and approach.
    5. Building a :class:`arbor.single_cell_model` object.
    6. Running a simulation and visualising the results.
 
+.. _tutorialsinglecell-cell:
+
+The cell
+--------
 
 The most trivial representation of a cell in Arbor is to model the entire cell as a
 single cylinder. The following example shows the steps required to construct a model of a
@@ -66,14 +70,14 @@ of the segment; and the tag.
 Step **(2)** creates a dictionary of labels (:class:`arbor.label_dict<arbor.label_dict>`). Labels give
 names to :ref:`regions<labels-region>` and :ref:`location<labels-locset>` described using a DSL
 based on s-expressions. Labels from the dictionary can then be used to facilitate adding synapses,
-dynamics, stimulii and probes to the cell. We add two labels:
+dynamics, stimuli and probes to the cell. We add two labels:
 
 * ``soma`` defines a *region* with ``(tag  1)``. Note that this corresponds to the
   ``tag`` parameter that was used to define the single segment in step (1).
 * ``center`` defines a *location* at ``(location 0 0.5)``, which is the mid point ``0.5``
   of branch ``0``, which corresponds to the center of the soma on the morphology defined in step (1).
 
-Step **(3)** constructs a :class:`arbor.decor` that describes the distributation and placement
+Step **(3)** constructs a :class:`arbor.decor` that describes the distribution and placement
 of dynamics and properties on a cell.  The cell's default properties can be modified, and we can use
 :meth:`arbor.decor.paint` and :meth:`arbor.decor.place` to further customise it in the
 following way:
@@ -87,16 +91,15 @@ following way:
   HH dynamics on the region we previously named ``"soma"`` in our label dictionary.
 * :meth:`arbor.decor.place` is used to add objects on a precise
   :class:`arbor.location` on a cell. Examples of objects that are *placed* are synapses,
-  spike detectors, current stimulii, and probes. In the above example we place a current stimulus
+  spike detectors, current stimuli, and probes. In the above example we place a current stimulus
   :class:`arbor.iclamp` with a duration of 2 ms and a current of 0.8 nA, starting at 10 ms
   on the location we previously labelled ``"center"``. We also place a :class:`arbor.spike_detector`
   with a threshold of -10 mV on the same location.
 
-Step **(4)** constructs the :class:`arbor.cable_cell` from the segment tree and dictionary of labeled
-regions and locations.
+Step **(4)** constructs the :class:`arbor.cable_cell` from the segment tree and dictionary of labelled regions and locations.
 
-Single cell model
-----------------------------------------------------
+The single cell model
+---------------------
 
 Once the cell description has been built, the next step is to build and run the simulation.
 Arbor provides an interface for constructing single cell models with the
@@ -124,8 +127,8 @@ probe ('"center"'), and the frequency at which we want to sample (10kHz).
 
 Step **(7)** runs the actual simulation for a duration of 30 ms.
 
-Results
-----------------------------------------------------
+The results
+-----------
 
 Our cell and model have been defined and we have run our simulation. Now we can look at what
 the spike detector and a voltage probes from our model have produced.
@@ -160,13 +163,7 @@ We should be seeing something like this:
 
     Plot of the potential over time for the voltage probe added in step (6).
 
-You can find the source code for this example in full at ``python/examples/single_cell_model.py``.
-
-.. Todo::
-    An example with a more complex cell geometry (loaded from NeuroML/SWC?).
-    This would show how to define and use morphology regions and locsets.
-    Introduce CV discretization control.
-    Probe and sample state variables in hh mechanism along with voltage.
+The full code
+-------------
 
-.. Todo::
-    Add a small ring network implemented via a recipe. This introduces connections, gids, and reveals the recipe plumbing that is hidden inside the single_cell_model.
+You can find the source code for this example in full at ``python/examples/single_cell_model.py``.
diff --git a/doc/tutorial/single_cell_recipe.rst b/doc/tutorial/single_cell_recipe.rst
new file mode 100644
index 0000000000000000000000000000000000000000..dc1f7e7b7eda528b4d3a87a266da3e360c9c5ce4
--- /dev/null
+++ b/doc/tutorial/single_cell_recipe.rst
@@ -0,0 +1,207 @@
+.. _tutorialsinglecellrecipe:
+
+A simple single cell recipe
+===========================
+
+This example builds the same single cell model as
+:ref:`the previous tutorial <tutorialsinglecell>`, except using a :class:`arbor.recipe`
+and :class:`arbor.simulation` instead of a :class:`arbor.single_cell_model`.
+
+.. Note::
+
+   **Concepts covered in this example:**
+
+   1. Building a :class:`arbor.recipe`.
+   2. Using the recipe, context and domain decomposition to create a :class:`arbor.simulation`
+   3. Running the simulation and visualizing the results.
+
+The cell
+--------
+
+We can immediately paste the cell description code from the
+:ref:`previous example <tutorialsinglecell-cell>` where it is explained in detail.
+
+.. code-block:: python
+
+    import arbor
+
+    # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm
+    tree = arbor.segment_tree()
+    tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1)
+
+    # (2) Define the soma and its center
+    labels = arbor.label_dict({'soma':   '(tag 1)',
+                              'center': '(location 0 0.5)'})
+
+    # (3) Create cell and set properties
+    decor = arbor.decor()
+    decor.set_property(Vm=-40)
+    decor.paint('"soma"', 'hh')
+    decor.place('"center"', arbor.iclamp( 10, 2, 0.8))
+    decor.place('"center"', arbor.spike_detector(-10))
+    cell = arbor.cable_cell(tree, labels, decor)
+
+The recipe
+----------
+
+The :class:`arbor.single_cell_model` of the previous example created a :class:`arbor.recipe` under
+the hood, and abstracted away the details so we were unaware of its existence.
+
+Creating an analogous recipe starts with creating a class that inherits from :class:`arbor.recipe`
+and overrides and implements some of :class:`arbor.recipe` methods. Not all methods
+have to be overridden, but some will always have to be, such as :meth:`arbor.recipe.num_cells`.
+It returns `0` by default and models without cells are quite boring!
+
+.. code-block:: python
+
+    # (4) Define a recipe for a single cell and set of probes upon it.
+    # This constitutes the corresponding generic recipe version of
+    # `single_cell_model.py`.
+
+    class single_recipe (arbor.recipe):
+        def __init__(self, cell, probes):
+            # (4.1) The base C++ class constructor must be called first, to ensure that
+            # all memory in the C++ class is initialized correctly.
+            arbor.recipe.__init__(self)
+            self.the_cell = cell
+            self.the_probes = probes
+            self.the_props = arbor.neuron_cable_propetries()
+            self.the_cat = arbor.default_catalogue()
+            self.the_props.register(self.the_cat)
+
+        def num_cells(self):
+            # (4.2) Override the num_cells method
+            return 1
+
+        def num_sources(self, gid):
+            # (4.3) Override the num_sources method
+            return 1
+
+        def cell_kind(self, gid):
+            # (4.4) Override the cell_kind method
+            return arbor.cell_kind.cable
+
+        def cell_description(self, gid):
+            # (4.5) Override the cell_description method
+            return self.the_cell
+
+        def probes(self, gid):
+            # (4.6) Override the probes method
+            return self.the_probes
+
+        def global_properties(self, kind):
+            # (4.7) Override the global_properties method
+            return self.the_props
+
+Step **(4)** describes the recipe that will reflect our single cell model.
+
+Step **(4.1)** defines the class constructor. It can take any shape you need, but it
+is important to call base class' constructor. If the overridden methods of the class
+need to return an object, it may be a good idea to have the returned object be a
+member of the class. With this constructor, we could easily change the cell and probes
+of the model, should we want to do so. Here we initialize the cell properties to match
+Neuron's defaults using Arbor's built-in :meth:`arbor.neuron_cable_properties` and
+extend with Arbor's own :meth:`arbor.default_catalogue`.
+
+Step **(4.2)** defines that this model has one cell.
+
+Step **(4.3)** defines that this model has one source.
+
+Step **(4.4)** returns :class:`arbor.cell_kind.cable`, the :class:`arbor.cell_kind`
+associated with the cable cell defined above. If you mix multiple cell kinds and
+descriptions in one recipe, make sure a particular ``gid`` returns matching cell kinds
+and descriptions.
+
+Step **(4.5)** returns the cell description passed in on class initialisation. If we
+were modelling multiple cells of different kinds, we would need to make sure that the
+cell returned by :meth:arbor.recipe.cell_description has the same cell kind as
+returned by :meth:arbor.recipe.cell_kind for every :gen:gid.
+
+Step **(4.6)** returns the probes passed in at class initialisation.
+
+Step **(4.7)** returns the properties that will be applied to all cells of that kind in the model.
+
+More methods can be overridden if your model requires that, see :class:`arbor.recipe` for options.
+
+Step **(5)** instantiates the recipe with the cable cell described earlier, and a single voltage probe located at "center".
+
+The context and domain decomposition
+------------------------------------
+
+:class:`arbor.single_cell_model` does not only take care of the recipe, it also takes
+care of defining how the simulation will be run. When you create and use your own
+recipe, you'll need to do this manually, in the form of defining a execution context
+and a domain decomposition. Fortunately, the default constructors of :class:`arbor.
+context` and :class:`arbor.partition_load_balance` are sufficient for this model, and
+is what :class:`arbor.single_cell_model` does under the hood! We'll leave the details
+of this subject for another tutorial.
+
+.. code-block:: python
+
+    # (6) Create a default execution context and a default domain decomposition.
+
+    context = arbor.context()
+    domains = arbor.partition_load_balance(recipe, context)
+
+Step **(6)** sets up a default context and domains.
+
+The simulation
+--------------
+
+.. code-block:: python
+
+    # (7) Create and run simulation and set up 10 kHz (every 0.1 ms) sampling on the probe.
+    # The probe is located on cell 0, and is the 0th probe on that cell, thus has probe_id (0, 0).
+
+    sim = arbor.simulation(recipe, domains, context)
+    sim.record(arbor.spike_recording.all)
+    handle = sim.sample((0, 0), arbor.regular_schedule(0.1))
+    sim.run(tfinal=30)
+
+Step **(7)** instantiates the simulation and sets up the probe added in step 5. In the
+:class:`arbor.single_cell_model` version of this example, the probe frequency and
+simulation duration are the same. Note that the frequency is set with a :class:`arbor.regular_schedule`,
+which takes a time and not a frequency. Also note that spike recording must be
+switched on. For extraction of the probe traces later on, we store a handle.
+
+The results
+----------------------------------------------------
+
+Apart from creating :class:`arbor.recipe` ourselves, we have changed nothing
+about this simulation compared to :ref:`the previous tutorial <tutorialsinglecell>`.
+If we create the same analysis of the results we therefore expect the same results.
+
+.. code-block:: python
+
+    # (8) Collect results.
+
+    spikes = sim.spikes()
+    data, meta = sim.samples(handle)[0]
+
+    if len(spikes)>0:
+        print('{} spikes:'.format(len(spikes)))
+        for t in spikes['time']:
+            print('{:3.3f}'.format(t))
+    else:
+        print('no spikes')
+
+    print("Plotting results ...")
+    seaborn.set_theme() # Apply some styling to the plot
+    df = pandas.DataFrame({'t/ms': data[:, 0], 'U/mV': data[:, 1]})
+    seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", ci=None).savefig('single_cell_recipe_result.svg')
+
+    df.to_csv('single_cell_recipe_result.csv', float_format='%g')
+
+Step **(8)** plots the measured potentials during the runtime of the simulation.
+Retrieving the sampled quantities is a little different, these have to be accessed
+through the simulation object: :meth:`arbor.simulation.spikes` and :meth:`arbor.simulation.samples`.
+
+We should be seeing something like this:
+
+.. figure:: single_cell_model_result.svg
+    :width: 400
+    :align: center
+
+    Plot of the potential over time for the voltage probe added in step (5).
+
+You can find the source code for this example in full at ``python/examples/single_cell_recipe.py``.
diff --git a/python/example/single_cell_detailed_recipe.py b/python/example/single_cell_detailed_recipe.py
index 46a9cd6410dd5e2cf9212ba3ee1252a789983e3d..f7dfaf082470a95f30d28dcf4235ec1041a56b52 100644
--- a/python/example/single_cell_detailed_recipe.py
+++ b/python/example/single_cell_detailed_recipe.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python3
+
 import arbor
 import pandas
 import seaborn
diff --git a/python/example/single_cell_recipe.py b/python/example/single_cell_recipe.py
index f910afa27ce4cd3be10d4195348f1c1db96f2de3..057eb4964d734a636793b9672a8c98f10363be0f 100644
--- a/python/example/single_cell_recipe.py
+++ b/python/example/single_cell_recipe.py
@@ -1,13 +1,33 @@
+#!/usr/bin/env python3
+
 import arbor
-import numpy, pandas, seaborn # You may have to pip install these.
+import pandas, seaborn # You may have to pip install these.
 
 # The corresponding generic recipe version of `single_cell_model.py`.
 
-# (1) Define a recipe for a single cell and set of probes upon it.
+# (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm
+tree = arbor.segment_tree()
+tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1)
+
+# (2) Define the soma and its center
+labels = arbor.label_dict({'soma':   '(tag 1)',
+                           'center': '(location 0 0.5)'})
+
+# (3) Create cell and set properties
+decor = arbor.decor()
+decor.set_property(Vm=-40)
+decor.paint('"soma"', 'hh')
+decor.place('"center"', arbor.iclamp( 10, 2, 0.8))
+decor.place('"center"', arbor.spike_detector(-10))
+cell = arbor.cable_cell(tree, labels, decor)
+
+# (4) Define a recipe for a single cell and set of probes upon it.
+# This constitutes the corresponding generic recipe version of
+# `single_cell_model.py`.
 
 class single_recipe (arbor.recipe):
     def __init__(self, cell, probes):
-        # The base C++ class constructor must be called first, to ensure that
+        # (4.1) The base C++ class constructor must be called first, to ensure that
         # all memory in the C++ class is initialized correctly.
         arbor.recipe.__init__(self)
         self.the_cell = cell
@@ -34,46 +54,28 @@ class single_recipe (arbor.recipe):
     def global_properties(self, kind):
         return self.the_props
 
-# (2) Create a cell.
+# (5) Instantiate recipe with a voltage probe located on "center".
 
-# Morphology
-tree = arbor.segment_tree()
-tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1)
+recipe = single_recipe(cell, [arbor.cable_probe_membrane_voltage('"center"')])
 
-# Label dictionary
-labels = arbor.label_dict()
-labels['centre'] = '(location 0 0.5)'
-
-# Decorations
-decor = arbor.decor()
-decor.set_property(Vm=-40)
-decor.paint('(all)', 'hh')
-decor.place('"centre"', arbor.iclamp( 10, 2, 0.8))
-decor.place('"centre"', arbor.spike_detector(-10))
-
-cell = arbor.cable_cell(tree, labels, decor)
-
-# (3) Instantiate recipe with a voltage probe.
-
-recipe = single_recipe(cell, [arbor.cable_probe_membrane_voltage('"centre"')])
-
-# (4) Instantiate simulation and set up sampling on probe id (0, 0).
+# (6) Create a default execution context and a default domain decomposition.
 
 context = arbor.context()
 domains = arbor.partition_load_balance(recipe, context)
-sim = arbor.simulation(recipe, domains, context)
 
+# (7) Create and run simulation and set up 10 kHz (every 0.1 ms) sampling on the probe.
+# The probe is located on cell 0, and is the 0th probe on that cell, thus has probe_id (0, 0).
+
+sim = arbor.simulation(recipe, domains, context)
 sim.record(arbor.spike_recording.all)
 handle = sim.sample((0, 0), arbor.regular_schedule(0.1))
+sim.run(tfinal=30)
 
-# (6) Run simulation for 30 ms of simulated activity and collect results.
+# (8) Collect results.
 
-sim.run(tfinal=30)
 spikes = sim.spikes()
 data, meta = sim.samples(handle)[0]
 
-# (7) Print spike times, if any.
-
 if len(spikes)>0:
     print('{} spikes:'.format(len(spikes)))
     for t in spikes['time']:
@@ -81,13 +83,9 @@ if len(spikes)>0:
 else:
     print('no spikes')
 
-# (8) Plot the recorded voltages over time.
-
 print("Plotting results ...")
 seaborn.set_theme() # Apply some styling to the plot
 df = pandas.DataFrame({'t/ms': data[:, 0], 'U/mV': data[:, 1]})
 seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", ci=None).savefig('single_cell_recipe_result.svg')
 
-# (9) Optionally, you can store your results for later processing.
-
 df.to_csv('single_cell_recipe_result.csv', float_format='%g')