diff --git a/.gitignore b/.gitignore
index c3bebcb1e538aa633969355770bf1e531d8cf6a5..5c52c84e9050753b6c1f01897330aa8941af28be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,3 +78,7 @@ dist
 
 # generated by YouCompleteMe Vim plugin with clangd engine support.
 .clangd
+
+# generated image files by Python examples
+python/example/*.svg
+
diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index 3d3f25f1790b634493147077fae655aeb9cdcc42..a184c0e9ad2da0bfceaee4a1287a35ed44b5e8ae 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -31,7 +31,6 @@ set(arbor_sources
     memory/gpu_wrappers.cpp
     memory/util.cpp
     morph/embed_pwlin.cpp
-    morph/stitch.cpp
     morph/label_dict.cpp
     morph/label_parse.cpp
     morph/locset.cpp
@@ -42,6 +41,7 @@ set(arbor_sources
     morph/primitives.cpp
     morph/region.cpp
     morph/segment_tree.cpp
+    morph/stitch.cpp
     merge_events.cpp
     simulation.cpp
     partition_load_balance.cpp
diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp
index ca7cc23b15e14bd734271f00a283076925a9abc9..617a7c6e455e4f5eec56c0f81337ea11c640a5de 100644
--- a/arbor/cable_cell.cpp
+++ b/arbor/cable_cell.cpp
@@ -1,5 +1,6 @@
 #include <sstream>
 #include <unordered_map>
+#include <variant>
 #include <vector>
 
 #include <arbor/cable_cell.hpp>
@@ -31,20 +32,6 @@ struct cable_cell_impl {
     using index_type = cable_cell::index_type;
     using size_type  = cable_cell::size_type;
 
-    cable_cell_impl(const arb::morphology& m, const label_dict& dictionary):
-        provider(m, dictionary)
-    {}
-
-    cable_cell_impl(): cable_cell_impl({},{}) {}
-
-    cable_cell_impl(const cable_cell_impl& other):
-        provider(other.provider),
-        region_map(other.region_map),
-        location_map(other.location_map)
-    {}
-
-    cable_cell_impl(cable_cell_impl&& other) = default;
-
     // Embedded morphology and labelled region/locset lookup.
     mprovider provider;
 
@@ -57,6 +44,27 @@ struct cable_cell_impl {
     // Track number of point assignments by type for lid/target numbers.
     dynamic_typed_map<constant_type<cell_lid_type>::type> placed_count;
 
+    // The decorations on the cell.
+    decor decorations;
+
+    // The lid ranges of placements.
+    std::vector<lid_range> placed_lid_ranges;
+
+    cable_cell_impl(const arb::morphology& m, const label_dict& labels, const decor& decorations):
+        provider(m, labels),
+        decorations(decorations)
+    {
+        init(decorations);
+    }
+
+    cable_cell_impl(): cable_cell_impl({},{},{}) {}
+
+    cable_cell_impl(const cable_cell_impl& other) = default;
+
+    cable_cell_impl(cable_cell_impl&& other) = default;
+
+    void init(const decor&);
+
     template <typename T>
     mlocation_map<T>& get_location_map(const T&) {
         return location_map.get<T>();
@@ -122,6 +130,13 @@ struct cable_cell_impl {
     mextent concrete_region(const region& r) const {
         return thingify(r, provider);
     }
+
+    lid_range placed_lid_range(unsigned id) const {
+        if (id>=placed_lid_ranges.size()) {
+            throw cable_cell_error(util::pprintf("invalid placement identifier {}", id));
+        }
+        return placed_lid_ranges[id];
+    }
 };
 
 using impl_ptr = std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)>;
@@ -129,14 +144,28 @@ impl_ptr make_impl(cable_cell_impl* c) {
     return impl_ptr(c, [](cable_cell_impl* p){delete p;});
 }
 
-cable_cell::cable_cell(const arb::morphology& m, const label_dict& dictionary):
-    impl_(make_impl(new cable_cell_impl(m, dictionary)))
+void cable_cell_impl::init(const decor& d) {
+    for (const auto& p: d.paintings()) {
+        auto& where = p.first;
+        std::visit([this, &where] (auto&& what) {this->paint(where, what);},
+                   p.second);
+    }
+    for (const auto& p: d.placements()) {
+        auto& where = p.first;
+        auto lids =
+            std::visit([this, &where] (auto&& what) {return this->place(where, what);},
+                       p.second);
+        placed_lid_ranges.push_back(lids);
+    }
+}
+
+cable_cell::cable_cell(const arb::morphology& m, const label_dict& dictionary, const decor& decorations):
+    impl_(make_impl(new cable_cell_impl(m, dictionary, decorations)))
 {}
 
 cable_cell::cable_cell(): impl_(make_impl(new cable_cell_impl())) {}
 
 cable_cell::cable_cell(const cable_cell& other):
-    default_parameters(other.default_parameters),
     impl_(make_impl(new cable_cell_impl(*other.impl_)))
 {}
 
@@ -168,24 +197,16 @@ const cable_cell_region_map& cable_cell::region_assignments() const {
     return impl_->region_map;
 }
 
-// Forward paint methods to implementation class.
-
-#define FWD_PAINT(proptype)\
-void cable_cell::paint(const region& target, proptype prop) {\
-    impl_->paint(target, prop);\
+const decor& cable_cell::decorations() const {
+    return impl_->decorations;
 }
-ARB_PP_FOREACH(FWD_PAINT,\
-    mechanism_desc, init_membrane_potential, axial_resistivity,\
-    temperature_K, membrane_capacitance, init_int_concentration,
-    init_ext_concentration, init_reversal_potential)
 
-// Forward place methods to implementation class.
+const cable_cell_parameter_set& cable_cell::default_parameters() const {
+    return impl_->decorations.defaults();
+}
 
-#define FWD_PLACE(proptype)\
-lid_range cable_cell::place(const locset& target, proptype prop) {\
-    return impl_->place(target, prop);\
+lid_range cable_cell::placed_lid_range(unsigned id) const {
+    return impl_->placed_lid_range(id);
 }
-ARB_PP_FOREACH(FWD_PLACE,\
-    mechanism_desc, i_clamp, gap_junction_site, threshold_detector)
 
 } // namespace arb
diff --git a/arbor/cable_cell_param.cpp b/arbor/cable_cell_param.cpp
index a83d5d31db8dbfbc7dad24de8fd21089de4a4777..7f343967b22466abc64bac2485bd97dc7113500a 100644
--- a/arbor/cable_cell_param.cpp
+++ b/arbor/cable_cell_param.cpp
@@ -2,11 +2,13 @@
 #include <cmath>
 #include <numeric>
 #include <vector>
+#include <variant>
 
 #include <arbor/cable_cell.hpp>
 #include <arbor/cable_cell_param.hpp>
 
 #include "util/maputil.hpp"
+#include "s_expr.hpp"
 
 namespace arb {
 
@@ -68,4 +70,90 @@ cable_cell_parameter_set neuron_parameter_defaults = {
     },
 };
 
+
+std::vector<defaultable> cable_cell_parameter_set::serialize() const {
+    std::vector<defaultable> D;
+    if (init_membrane_potential) {
+        D.push_back(arb::init_membrane_potential{*this->init_membrane_potential});
+    }
+    if (temperature_K) {
+        D.push_back(arb::temperature_K{*this->temperature_K});
+    }
+    if (axial_resistivity) {
+        D.push_back(arb::axial_resistivity{*this->axial_resistivity});
+    }
+    if (membrane_capacitance) {
+        D.push_back(arb::membrane_capacitance{*this->membrane_capacitance});
+    }
+
+    for (const auto& [name, data]: ion_data) {
+        if (data.init_int_concentration) {
+            D.push_back(init_int_concentration{name, *data.init_int_concentration});
+        }
+        if (data.init_ext_concentration) {
+            D.push_back(init_ext_concentration{name, *data.init_ext_concentration});
+        }
+        if (data.init_reversal_potential) {
+            D.push_back(init_reversal_potential{name, *data.init_reversal_potential});
+        }
+    }
+
+    for (const auto& [name, mech]: reversal_potential_method) {
+        D.push_back(ion_reversal_potential_method{name, mech});
+    }
+
+    if (discretization) {
+        D.push_back(*discretization);
+    }
+
+    return D;
+}
+
+void decor::paint(region where, paintable what) {
+    paintings_.push_back({std::move(where), std::move(what)});
+}
+
+unsigned decor::place(locset where, placeable what) {
+    placements_.push_back({std::move(where), std::move(what)});
+    return std::size(placements_)-1;
+}
+
+void decor::set_default(defaultable what) {
+    std::visit(
+            [this] (auto&& p) {
+                using T = std::decay_t<decltype(p)>;
+                if constexpr (std::is_same_v<init_membrane_potential, T>) {
+                    defaults_.init_membrane_potential = p.value;
+                }
+                else if constexpr (std::is_same_v<axial_resistivity, T>) {
+                    defaults_.axial_resistivity = p.value;
+                }
+                else if constexpr (std::is_same_v<temperature_K, T>) {
+                    defaults_.temperature_K = p.value;
+                }
+                else if constexpr (std::is_same_v<membrane_capacitance, T>) {
+                    defaults_.membrane_capacitance = p.value;
+                }
+                else if constexpr (std::is_same_v<initial_ion_data, T>) {
+                    defaults_.ion_data[p.ion] = p.initial;
+                }
+                else if constexpr (std::is_same_v<init_int_concentration, T>) {
+                    defaults_.ion_data[p.ion].init_int_concentration = p.value;
+                }
+                else if constexpr (std::is_same_v<init_ext_concentration, T>) {
+                    defaults_.ion_data[p.ion].init_ext_concentration = p.value;
+                }
+                else if constexpr (std::is_same_v<init_reversal_potential, T>) {
+                    defaults_.ion_data[p.ion].init_reversal_potential = p.value;
+                }
+                else if constexpr (std::is_same_v<ion_reversal_potential_method, T>) {
+                    defaults_.reversal_potential_method[p.ion] = p.method;
+                }
+                else if constexpr (std::is_same_v<cv_policy, T>) {
+                    defaults_.discretization = std::forward<cv_policy>(p);
+                }
+            },
+            what);
+}
+
 } // namespace arb
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index 539aeeb53b75f700b214e52db1b3cd5969d2f89a..2125fb1e20872ada3125780613dfc3ea0febcaa3 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -341,7 +341,7 @@ fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discreti
 // ------------------
 
 fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt) {
-    const auto& dflt = cell.default_parameters;
+    const auto& dflt = cell.default_parameters();
     fvm_cv_discretization D;
 
     D.geometry = cv_geometry_from_ends(cell,
@@ -787,7 +787,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
     const auto& embedding = cell.embedding();
 
     const auto& global_dflt = gprop.default_parameters;
-    const auto& dflt = cell.default_parameters;
+    const auto& dflt = cell.default_parameters();
 
     fvm_mechanism_data M;
 
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index da2dc908d6da8cc2d36efccff763d5cf27f945cc..46468c903e4c0a86c4dc71777263b98ce83fee95 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -3,6 +3,7 @@
 #include <string>
 #include <unordered_map>
 #include <utility>
+#include <variant>
 #include <vector>
 
 #include <arbor/arbexcept.hpp>
@@ -183,6 +184,7 @@ struct cable_probe_ion_ext_concentration_cell {
 // Forward declare the implementation, for PIMPL.
 struct cable_cell_impl;
 
+
 // Typed maps for access to painted and placed assignments:
 //
 // Mechanisms and initial ion data are further keyed by
@@ -231,8 +233,6 @@ public:
 
     using gap_junction_instance = mlocation;
 
-    cable_cell_parameter_set default_parameters;
-
     // Default constructor.
     cable_cell();
 
@@ -240,87 +240,23 @@ public:
     cable_cell(const cable_cell& other);
     cable_cell(cable_cell&& other) = default;
 
-    // Copy and move assignment operators..
+    // Copy and move assignment operators.
     cable_cell& operator=(cable_cell&&) = default;
     cable_cell& operator=(const cable_cell& other) {
         return *this = cable_cell(other);
     }
 
-    /// construct from morphology
-    cable_cell(const class morphology& m, const label_dict& dictionary={});
+    /// Construct from morphology, label and decoration descriptions.
+    cable_cell(const class morphology&, const label_dict&, const decor&);
+    cable_cell(const class morphology& m):
+        cable_cell(m, {}, {})
+    {}
 
     /// Access to morphology and embedding
     const concrete_embedding& embedding() const;
     const arb::morphology& morphology() const;
     const mprovider& provider() const;
 
-    // Set cell-wide default physical and ion parameters.
-
-    void set_default(init_membrane_potential prop) {
-        default_parameters.init_membrane_potential = prop.value;
-    }
-
-    void set_default(axial_resistivity prop) {
-        default_parameters.axial_resistivity = prop.value;
-    }
-
-    void set_default(temperature_K prop) {
-        default_parameters.temperature_K = prop.value;
-    }
-
-    void set_default(membrane_capacitance prop) {
-        default_parameters.membrane_capacitance = prop.value;
-    }
-
-    void set_default(initial_ion_data prop) {
-        default_parameters.ion_data[prop.ion] = prop.initial;
-    }
-
-    void set_default(init_int_concentration prop) {
-        default_parameters.ion_data[prop.ion].init_int_concentration = prop.value;
-    }
-
-    void set_default(init_ext_concentration prop) {
-        default_parameters.ion_data[prop.ion].init_ext_concentration = prop.value;
-    }
-
-    void set_default(init_reversal_potential prop) {
-        default_parameters.ion_data[prop.ion].init_reversal_potential = prop.value;
-    }
-
-    void set_default(ion_reversal_potential_method prop) {
-        default_parameters.reversal_potential_method[prop.ion] = prop.method;
-    }
-
-    // Painters and placers.
-    //
-    // Used to describe regions and locations where density channels, stimuli,
-    // synapses, gap junctions and detectors are located.
-
-    // Density channels.
-    void paint(const region&, mechanism_desc);
-
-    // Properties.
-    void paint(const region&, init_membrane_potential);
-    void paint(const region&, axial_resistivity);
-    void paint(const region&, temperature_K);
-    void paint(const region&, membrane_capacitance);
-    void paint(const region&, init_int_concentration);
-    void paint(const region&, init_ext_concentration);
-    void paint(const region&, init_reversal_potential);
-
-    // Synapses.
-    lid_range place(const locset&, mechanism_desc);
-
-    // Stimuli.
-    lid_range place(const locset&, i_clamp);
-
-    // Gap junctions.
-    lid_range place(const locset&, gap_junction_site);
-
-    // Spike detectors.
-    lid_range place(const locset&, threshold_detector);
-
     // Convenience access to placed items.
 
     const std::unordered_map<std::string, mlocation_map<mechanism_desc>>& synapses() const {
@@ -349,6 +285,16 @@ public:
     const cable_cell_region_map& region_assignments() const;
     const cable_cell_location_map& location_assignments() const;
 
+    // The decorations on the cell.
+    const decor& decorations() const;
+
+    // The default parameter and ion settings on the cell.
+    const cable_cell_parameter_set& default_parameters() const;
+
+    // The range of lids assigned to the items with placement index idx, where
+    // the placement index is the value returned by calling decor::place().
+    lid_range placed_lid_range(unsigned idx) const;
+
 private:
     std::unique_ptr<cable_cell_impl, void (*)(cable_cell_impl*)> impl_;
 };
diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp
index e118b495257d7078f604487fe47d132a6a85e6d9..b98421902f8db7c1dff49b2607ae802c981fb1f0 100644
--- a/arbor/include/arbor/cable_cell_param.hpp
+++ b/arbor/include/arbor/cable_cell_param.hpp
@@ -5,6 +5,7 @@
 #include <optional>
 #include <unordered_map>
 #include <string>
+#include <variant>
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/cv_policy.hpp>
@@ -59,34 +60,41 @@ struct gap_junction_site {};
 // cell-wide default:
 
 struct init_membrane_potential {
-    double value = NAN; // [mV]
+    init_membrane_potential() = delete;
+    double value; // [mV]
 };
 
 struct temperature_K {
-    double value = NAN; // [K]
+    temperature_K() = delete;
+    double value; // [K]
 };
 
 struct axial_resistivity {
-    double value = NAN; // [[Ω·cm]
+    axial_resistivity() = delete;
+    double value; // [Ω·cm]
 };
 
 struct membrane_capacitance {
-    double value = NAN; // [F/m²]
+    membrane_capacitance() = delete;
+    double value; // [F/m²]
 };
 
 struct init_int_concentration {
+    init_int_concentration() = delete;
     std::string ion;
-    double value = NAN;
+    double value; // [mM]
 };
 
 struct init_ext_concentration {
+    init_ext_concentration() = delete;
     std::string ion;
-    double value = NAN;
+    double value; // [mM]
 };
 
 struct init_reversal_potential {
+    init_reversal_potential() = delete;
     std::string ion;
-    double value = NAN;
+    double value; // [mV]
 };
 
 // Mechanism description, viz. mechanism name and
@@ -162,6 +170,34 @@ struct ion_reversal_potential_method {
     mechanism_desc method;
 };
 
+using paintable =
+    std::variant<mechanism_desc,
+                 init_membrane_potential,
+                 axial_resistivity,
+                 temperature_K,
+                 membrane_capacitance,
+                 init_int_concentration,
+                 init_ext_concentration,
+                 init_reversal_potential>;
+
+using placeable =
+    std::variant<mechanism_desc,
+                 i_clamp,
+                 threshold_detector,
+                 gap_junction_site>;
+
+using defaultable =
+    std::variant<init_membrane_potential,
+                 axial_resistivity,
+                 temperature_K,
+                 membrane_capacitance,
+                 initial_ion_data,
+                 init_int_concentration,
+                 init_ext_concentration,
+                 init_reversal_potential,
+                 ion_reversal_potential_method,
+                 cv_policy>;
+
 // Cable cell ion and electrical defaults.
 
 // Parameters can be given as per-cell and global defaults via
@@ -182,6 +218,25 @@ struct cable_cell_parameter_set {
     std::unordered_map<std::string, mechanism_desc> reversal_potential_method;
 
     std::optional<cv_policy> discretization;
+
+    std::vector<defaultable> serialize() const;
+};
+
+// A flat description of defaults, paintings and placings that
+// are to be applied to a morphology in a cable_cell.
+class decor {
+    std::vector<std::pair<region, paintable>> paintings_;
+    std::vector<std::pair<locset, placeable>> placements_;
+    cable_cell_parameter_set defaults_;
+
+public:
+    const auto& paintings()  const {return paintings_;  }
+    const auto& placements() const {return placements_; }
+    const auto& defaults()   const {return defaults_;   }
+
+    void paint(region, paintable);
+    unsigned place(locset, placeable);
+    void set_default(defaultable);
 };
 
 extern cable_cell_parameter_set neuron_parameter_defaults;
diff --git a/arbor/include/arbor/util/typed_map.hpp b/arbor/include/arbor/util/typed_map.hpp
index 1bd6214da96a72f6bedd082850e2822bb4ffd297..e6e42bc10019b469a8868fb4cfac5282aec0d394 100644
--- a/arbor/include/arbor/util/typed_map.hpp
+++ b/arbor/include/arbor/util/typed_map.hpp
@@ -71,10 +71,10 @@ private:
     std::tuple<E<Keys>...> tmap_;
 
     template <typename T>
-    static constexpr int index() { return 1; }
+    static constexpr std::size_t index() { return 1; }
 
     template <typename T, typename H, typename... A>
-    static constexpr int index() {
+    static constexpr std::size_t index() {
         return std::is_same<H, T>::value? 0: 1+index<T, A...>();
     }
 };
diff --git a/doc/concepts/cable_cell.rst b/doc/concepts/cable_cell.rst
index 6ce770812f847c8971bdcb1a30b03cb3131fc54b..b54d8ab9903230d86674c4dfdaa0da8eba8f1301 100644
--- a/doc/concepts/cable_cell.rst
+++ b/doc/concepts/cable_cell.rst
@@ -4,57 +4,77 @@ Cable cells
 ===========
 
 An Arbor *cable cell* is a full description of a cell with morphology and cell
-dynamics, where cell dynamics include ion species and their properties, ion
+dynamics like ion species and their properties, ion
 channels, synapses, gap junction sites, stimuli and spike detectors.
-Arbor cable cells are constructed from a morphology and a label dictionary,
-and provide a rich interface for specifying the cell's dynamics.
 
-.. note::
-    The cable cell has more than *one* dedicated page, it has a few more! This page describes how to build a full
-    description of a cable cell, based on three components that are broken out into their own pages:
+Cable cells are constructed from three components:
+
+* :ref:`Morphology <morph>`: a decription of the geometry and branching structure of the cell shape.
+* :ref:`Label dictionary <labels>`: a set of rules that refer to regions and locations on the cell.
+* :ref:`Decor <cablecell-decoration>`: a description of the dynamics on the cell, placed according to the named rules in the dictionary.
+
+When a cable cell is constructued the following steps are performed using the inputs:
+
+1. Concrete regions and locsets are generated for the morphology for each labeled region and locset in the dictionary
+2. The default values for parameters specified in the decor, such as ion species concentration, are instantiated.
+3. Dynamics (mechanisms, parameters, synapses, etc.) are instaniated on the regions and locsets as specified by the decor.
+
+Once constructed, the cable cell can be queried for specific information about the cell, but it can't be modified (it is *immutable*).
 
-    * :ref:`Morphology descriptions <morph-morphology>`
-    * :ref:`Label dictionaries <labels-dictionary>`
-    * :ref:`Mechanisms <mechanisms>`
+.. Note::
 
-    It can be helpful to consult those pages for some of the sections of this page.
+    The inputs used to construct the cell (morphology, label definitions and decor) are orthogonal,
+    which allows a broad range of individual cells to be constructed from a handful of simple rules
+    encoded in the inputs.
+    For example, take a model with the following:
+
+    * three cell types: pyramidal, purkinje and granule.
+    * two different morphologies for each cell type (a total of 6 morphologies).
+    * all cells have the same basic region definitions: soma, axon, dendrites.
+    * all cells of the same type (e.g. Purkinje) have the same dynamics defined on their respective regions.
+
+    The basic building blocks required to construct all of the cells for the model would be:
+    * 6 morphologies (2 for each of purkinje, granule and pyramidal).
+    * 3 decors (1 for each of purkinje, granule and pyramidal).
+    * 1 label dictionary that defines the region types.
 
 .. _cablecell-decoration:
 
 Decoration
 ----------------
 
-A cable cell is *decorated* by specifying the distribution and placement of dynamics
-on the cell. The decorations, coupled with a description of a cell morphology, are all
-that is required to build a standalone single-cell model, or a cell that is part of
-a larger network.
-
-Decorations use :ref:`region <labels-region>` and :ref:`locset <labels-locset>`
-descriptions, with their respective use for this purpose reflected in the two broad
-classes of dynamics in Arbor:
+The distribution and placement of dynamics on a cable cell is called the *decor* of a cell.
+A decor is composed of individual *decorations*, which associate a property or dynamic process
+with a :ref:`region <labels-region>` or :ref:`locset <labels-locset>`.
+The choice of region or locset is reflected in the two broad classes of dynamics on cable cells:
 
 * *Painted dynamics* are applied to regions of a cell, and are associated with
   an area of the membrane or volume of the cable.
 
-  * :ref:`Cable properties <cable-properties>`.
-  * :ref:`Density mechanisms <cable-density-mechs>`.
-  * :ref:`Ion species <cable-ions>`.
+  * :ref:`Cable properties <cablecell-properties>`.
+  * :ref:`Density mechanisms <cablecell-density-mechs>`.
+  * :ref:`Ion species <cablecell-ions>`.
 
 * *Placed dynamics* are applied to locations on the cell, and are associated
   with entities that can be counted.
 
-  * :ref:`Synapses <cable-synapses>`.
-  * :ref:`Gap junction sites <cable-gj-sites>`.
-  * :ref:`Threshold detectors <cable-threshold-detectors>` (spike detectors).
-  * :ref:`Stimuli <cable-stimuli>`.
-  * :ref:`Probes <cable-probes>`.
+  * :ref:`Synapses <cablecell-synapses>`.
+  * :ref:`Gap junction sites <cablecell-gj-sites>`.
+  * :ref:`Threshold detectors <cablecell-threshold-detectors>` (spike detectors).
+  * :ref:`Stimuli <cablecell-stimuli>`.
+  * :ref:`Probes <cablecell-probes>`.
+
+Decorations are described by a **decor** object in Arbor.
+Provides facility for
+* setting properties defined over the whole cell
+* descriptions of dynamics applied to regions and locsets
 
 .. _cablecell-paint:
 
 Painted dynamics
 ''''''''''''''''
 
-Painted dynamics are applied to a subset of the surface and/or volume of cells.
+Painted dynamics are applied to a subset of the surface or volume of cells.
 They can be specified at three different levels:
 
 * *globally*: a global default for all cells in a model.
@@ -66,12 +86,12 @@ us to, for example, define a global default value for calcium concentration,
 then provide a different values on specific cell regions.
 
 Some dynamics, such as membrane capacitance and the initial concentration of ion species
-must be defined for all compartments. Others need only be applied where they are
+must be defined for all CVs. Others need only be applied where they are
 present, for example ion channels.
 The types of dynamics, and where they can be defined, are
-:ref:`tabulated <cable-painted-resolution>` below.
+:ref:`tabulated <cablecell-painted-resolution>` below.
 
-.. _cable-painted-resolution:
+.. _cablecell-painted-resolution:
 
 .. csv-table:: Painted property resolution options.
    :widths: 20, 10, 10, 10
@@ -92,54 +112,42 @@ will override any cell-local or global definition on that region.
     deterministically choose the correct definition, and an error will be
     raised during model instantiation.
 
-.. _cable-properties:
+.. _cablecell-properties:
 
 Cable properties
 ~~~~~~~~~~~~~~~~
 
-There are four cable properties that are defined everywhere on all cables:
+There are four cable properties that must be defined everywhere on a cell:
 
 * *Vm*: Initial membrane voltage [mV].
 * *cm*: Membrane capacitance [F/m²].
 * *rL*: Axial resistivity of cable [Ω·cm].
 * *tempK*: Temperature [Kelvin].
 
-In Python, the :py:class:`cable_cell` interface provides the :py:func:`cable_cell.set_properties` method
-for setting cell-wide defaults for properties, and the
-:py:meth:`cable_cell.paint` interface for overriding properties on specific regions.
+Each of the cable properties can be defined as a cell-wide default, that is then
+specialised on specific regions.
 
-.. code-block:: Python
-
-    import arbor
+.. note::
 
-    # Load a morphology from file and define basic regions.
-    tree = arbor.load_swc('granule.swc')
-    morph = arbor.morphology(tree, spherical_root=True)
-    labels = arbor.label_dict({'soma': '(tag 1)', 'axon': '(tag 2)', 'dend': '(tag 3)'})
+    In Python, the :py:class:`decor` interface provides the :py:func:`decor.set_properties` method
+    for setting cell-wide defaults for properties, and the
+    :py:meth:`decor.paint` interface for overriding properties on specific regions.
 
-    # Create a cable cell.
-    cell = arbor.cable_cell(morph, labels)
+    .. code-block:: Python
 
-    # Set cell-wide properties that will be applied by default to # the entire cell.
-    cell.set_properties(Vm=-70, cm=0.02, rL=30, tempK=30+273.5)
+        import arbor
 
-    # Override specific values on the soma and axon
-    cell.paint('"soma"', Vm=-50, cm=0.01, rL=35)
-    cell.paint('"axon"', Vm=-60, rL=40)
+        # Create an empty decor.
+        decor = arbor.decor
 
-.. _cable-discretisation:
+        # Set cell-wide properties that will be applied by default to the entire cell.
+        decor.set_properties(Vm=-70, cm=0.02, rL=30, tempK=30+273.5)
 
-Discretisation
-~~~~~~~~~~~~~~~~
-
-For the purpose of simulation, cable cells are decomposed into discrete
-subcomponents called *control volumes* (CVs), following the finite volume method
-terminology. Each control volume comprises a connected subset of the
-morphology. Each fork point in the morphology will be the responsibility of
-a single CV, and as a special case a zero-volume CV can be used to represent
-a single fork point in isolation.
+        # Override specific values on regions named "soma" and "axon".
+        decor.paint('"soma"', Vm=-50, cm=0.01, rL=35)
+        decor.paint('"axon"', Vm=-60, rL=40)
 
-.. _cable-density-mechs:
+.. _cablecell-density-mechs:
 
 Density mechanisms
 ~~~~~~~~~~~~~~~~~~~~~~
@@ -187,11 +195,13 @@ Take for example a mechanism passive leaky dynamics:
     # Create an instance of the same mechanism, that also sets conductance (range)
     m4 = arbor.mechanism('passive/el=-45', {'g': 0.1})
 
-    cell.paint('"soma"', m1)
-    cell.paint('"soma"', m2) # error: can't place the same mechanism on overlapping regions
-    cell.paint('"soma"', m3) # error: technically a different mechanism?
+    decor = arbor.decor()
+    decor.paint('"soma"', m1)
+    decor.paint('"soma"', m2) # error: can't place the same mechanism on overlapping regions
+    decor.paint('"soma"', m3) # error: can't have overlap between two instances of a mechanism
+                              #        with different values for a global parameter.
 
-.. _cable-ions:
+.. _cablecell-ions:
 
 Ion species
 ~~~~~~~~~~~
@@ -252,14 +262,16 @@ ion at the cell level using the Python interface:
 
 .. code-block:: Python
 
-    cell = arbor.cable_cell(morph, labels)
+    decor = arbor.decor()
 
-    # method 1: create the mechanism explicitly.
+    # Method 1: create the mechanism explicitly.
     ca = arbor.mechanism('nernst/x=ca')
-    cell.set_ion(ion='ca', method=ca)
+    decor.set_ion(ion='ca', method=ca)
 
-    # method 2: set directly using a string description
-    cell.set_ion(ion='ca', method='nernst/x=ca')
+    # Method 2: set directly using a string description.
+    decor.set_ion(ion='ca', method='nernst/x=ca')
+
+    cell = arbor.cable_cell(morph, labels, decor)
 
 
 The NMODL code for the
@@ -272,14 +284,14 @@ using the *paint* interface:
 
 .. code-block:: Python
 
-    # cell is an arbor.cable_cell
+    # decor is an arbor.decor
 
     # It is possible to define all of the initial condition values
     # for a ion species.
-    cell.paint('(tag 1)', arbor.ion('ca', int_con=2e-4, ext_con=2.5, rev_pot=114))
+    decor.paint('(tag 1)', arbor.ion('ca', int_con=2e-4, ext_con=2.5, rev_pot=114))
 
     # Alternatively, one can selectively overwrite the global defaults.
-    cell.paint('(tag 2)', arbor.ion('ca', rev_pot=126)
+    decor.paint('(tag 2)', arbor.ion('ca', rev_pot=126)
 
 .. _cablecell-place:
 
@@ -289,38 +301,112 @@ Placed dynamics
 Placed dynamics are discrete countable items that affect or record the dynamics of a cell,
 and are assigned to specific locations.
 
-.. _cable-synapses:
+.. _cablecell-synapses:
 
 Connection sites
 ~~~~~~~~~~~~~~~~
 
 Connections (synapses) are instances of NMODL POINT mechanisms. See also :ref:`modelconnections`.
 
-.. _cable-gj-sites:
+.. _cablecell-gj-sites:
 
 Gap junction sites
 ~~~~~~~~~~~~~~~~~~
 
 See :ref:`modelgapjunctions`.
 
-.. _cable-threshold-detectors:
+.. _cablecell-threshold-detectors:
 
 Threshold detectors (spike detectors).
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. _cable-stimuli:
+.. _cablecell-stimuli:
 
 Stimuli
 ~~~~~~~~
 
-.. _cable-probes:
+.. _cablecell-probes:
 
 Probes
 ~~~~~~
 
+.. _cablecell-cv-policies:
+
+Discretisation and CV policies
+------------------------------
+
+For the purpose of simulation, cable cells are decomposed into discrete
+subcomponents called *control volumes* (CVs). The CVs are
+uniquely determined by a set of *B* ``mlocation`` boundary points.
+For each non-terminal point *h* in *B*, there is a CV comprising the points
+{*x*: *h* ≤ *x* and ¬∃ *y* ∈ *B* s.t *h* < *y* < *x*}, where < and ≤ refer to the
+geometrical partial order of locations on the morphology. A fork point is
+owned by a CV if and only if all of its corresponding representative locations
+are in the CV.
+
+The set of boundary points used by the simulator is determined by a *CV policy*.
+
+Specific CV policies are created by functions that take a ``region`` parameter
+that restrict the domain of applicability of that policy; this facility is useful
+for specifying differing discretisations on different parts of a cell morphology.
+When a CV policy is constrained in this manner, the boundary of the domain will
+always constitute part of the CV boundary point set.
+
+``cv_policy_single``
+''''''''''''''''''''
+
+Use one CV for each connected component of a region. When applied to the whole cell
+will generate single CV for the whole cell.
+
+``cv_policy_explicit``
+''''''''''''''''''''''
+
+Define CV boundaries according to a user-supplied set of locations, optionally
+restricted to a region.
+
+``cv_policy_every_segment``
+'''''''''''''''''''''''''''
+
+Use every segment in the morphology to define CVs, optionally
+restricted to a region. Each fork point in the domain is
+represented by a trivial CV.
+
+``cv_policy_fixed_per_branch``
+''''''''''''''''''''''''''''''
+
+For each branch in each connected component of the region (or the whole cell,
+if no region is specified), evenly distribute boundary points along the branch so
+as to produce an exact number of CVs per branch.
+
+By default, CVs will terminate at branch ends. An optional flag
+``cv_policy_flag::interior_forks`` can be passed to specify that fork points
+will be included in non-trivial, branched CVs and CVs covering terminal points
+in the morphology will be half-sized.
+
+
+``cv_policy_max_extent``
+''''''''''''''''''''''''
+
+As for ``cv_policy_fixed_per_branch``, save that the number of CVs on any
+given branch will be chosen to be the smallest number that ensures no
+CV will have an extent on the branch longer than a user-provided CV length.
+
+.. _cablecell-cv-composition:
+
+Composition of CV policies
+'''''''''''''''''''''''''''''
+
+CV policies can be combined with ``+`` and ``|`` operators. For two policies
+*A* and *B*, *A* + *B* is a policy which gives boundary points from both *A*
+and *B*, while *A* | *B* is a policy which gives all the boundary points from
+*B* together with those from *A* which do not within the domain of *B*.
+The domain of *A* + *B* and *A* | *B* is the union of the domains of *A* and
+*B*.
+
+
 API
 ---
 
-* :ref:`Python <pycable_cell>`
-* :ref:`C++ <cppcable_cell>`
+* :ref:`Python <pycablecell>`
+* :ref:`C++ <cppcablecell>`
 
diff --git a/doc/concepts/cell.rst b/doc/concepts/cell.rst
index c60ece393b54bcf5255f06af31c72b44cc8845c0..79d3217a36ccd8a402646a75973947b8dde28e72 100644
--- a/doc/concepts/cell.rst
+++ b/doc/concepts/cell.rst
@@ -17,14 +17,13 @@ Cells interact with each other via spike exchange and gap junctions.
                                                                 For example the 7th synapse on a cell.
     .. generic:: cell_member  tuple (:gen:`gid`, :gen:`index`)  The global identification of a cell-local item with `index`
                                                                 into a cell-local collection on the cell identified by `gid`.
-                                                                For example, the 7th synapse on cell 42.
     ========================  ================================  ===========================================================
 
 Cell interactions via :ref:`connections <modelconnections>` and :ref:`gap junctions <modelgapjunctions>` occur
 between **source**, **target** and **gap junction site** locations on a cell. Connections are formed from sources
 to targets. Gap junctions are formed between two gap junction sites. An example of a source on a
-:ref:`cable cell<modelcablecell>` is a :ref:`threshold detector <cable-threshold-detectors>` (spike detector);
-an example of a target on a cable cell is a :ref:`synapse <cable-synapses>`.
+:ref:`cable cell<modelcablecell>` is a :ref:`threshold detector <cablecell-threshold-detectors>` (spike detector);
+an example of a target on a cable cell is a :ref:`synapse <cablecell-synapses>`.
 
 Each cell has a global identifier :gen:`gid`, and each **source**, **target** and **gap junction site** has a
 global identifier :gen:`cell_member`. These are used to refer to them in :ref:`recipes <modelrecipe>`.
@@ -58,6 +57,7 @@ Cell kind
     ========================  ===========================================================
 
 .. _modelcablecell:
+
 1. **Cable Cells**
 
    Cable cells are morphologically-detailed cells. They can be coupled to other cells via the following
@@ -69,6 +69,7 @@ Cell kind
    2. Direct electrical coupling between two cable cells via :ref:`gap junctions <modelgapjunctions>`.
 
 .. _modellifcell:
+
 2. **LIF Cells**
 
    LIF cells are single-compartment leaky integrate and fire neurons. They are typically used to simulate
@@ -79,6 +80,7 @@ Cell kind
    be a *source* of spikes to cells that have target sites (i.e. *cable* and *lif* cells).
 
 .. _modelspikecell:
+
 3. **Spiking Cells**
 
    Spiking cells act as spike sources from user-specified values inserted via a `schedule description`.
@@ -89,6 +91,7 @@ Cell kind
    (i.e. *cable* and *lif* cells), but they can not *receive* spikes.
 
 .. _modelbenchcell:
+
 4. **Benchmark Cells**
 
    Benchmark cells are proxy cells used for benchmarking, and used by developers to benchmark the spike
@@ -106,7 +109,7 @@ It details everything needed to build a cell. The degree of detail differs accor
 
    The description of a cable cell can include all the following:
 
-     * :ref:`Morphology <co_morphology>`: composed of a branching tree of one-dimensional line segments.
+     * :ref:`Morphology <morph>`: composed of a branching tree of one-dimensional line segments.
        Strictly speaking, Arbor represents a morphology as an *acyclic directed graph*, with the soma at
        the root.
      * Discretisation: specifies how to split the morphology into discrete components for the simulation.
@@ -130,7 +133,7 @@ It details everything needed to build a cell. The degree of detail differs accor
 
    Most Arbor users will want to use the cable cell because it is the only cell kind that supports complex
    morphologies and user-defined mechanisms. See the cable cell's :ref:`dedicated page <cablecell>` for more info.
-   And visit the :ref:`C++ <cppcable_cell>` and :ref:`Python <pycable_cell>` APIs to learn how to programmatically
+   And visit the :ref:`C++ <cppcablecell>` and :ref:`Python <pycablecell>` APIs to learn how to programmatically
    provide the cable cell description in Arbor.
 
 2. **LIF Cells**
diff --git a/doc/concepts/labels.rst b/doc/concepts/labels.rst
index 34b9c4b5ca96cfce4c9be86e2e76b81c5e1d65e5..91e8d5ab7d4b00d8e41cde365fc1dc0df2dd709f 100644
--- a/doc/concepts/labels.rst
+++ b/doc/concepts/labels.rst
@@ -63,8 +63,8 @@ which may contain multiple instances of the same location, for example:
   :align: center
 
   Examples of locsets on the example morphology.
-  The terminal points (right).
-  Fifty random locations on the dendritic tree (left).
+  The terminal points (left).
+  Fifty random locations on the dendritic tree (right).
   The :ref:`root <morph-segment-definitions>` of the morphology is shown with a red circle
   for reference.
 
@@ -626,4 +626,4 @@ API
 ---
 
 * :ref:`Python <py_labels>`
-* :ref:`C++ <cpp_labels>`
+* *TODO*: C++ documentation.
diff --git a/doc/concepts/mechanisms.rst b/doc/concepts/mechanisms.rst
index 2719c0de44778444de0f8007be5483a3883ee33a..f8072c14b53643da6c54d19201b6eac2a9229f15 100644
--- a/doc/concepts/mechanisms.rst
+++ b/doc/concepts/mechanisms.rst
@@ -5,7 +5,7 @@ Cell mechanisms
 
 Mechanisms describe biophysical processes such as ion channels and synapses.
 Mechanisms are assigned to regions and locations on a cell morphology
-through a process that is called :ref:`decoration <cablecell-decoration>`.
+through the process of :ref:`decoration <cablecell-decoration>`.
 Mechanisms are described using a dialect of the :ref:`NMODL <nmodl>` domain
 specific language that is similarly used in `NEURON <https://neuron.yale.edu/neuron/>`_.
 
@@ -158,4 +158,4 @@ API
 ---
 
 * :ref:`Python <py_mechanisms>`
-* :ref:`C++ <cpp_mechanisms>`
+* *TODO* C++ documentation.
diff --git a/doc/concepts/morphology.rst b/doc/concepts/morphology.rst
index 6cddcc0f6e1248081d331935672c40f1fd5a14dd..58c375fc03eaef1edcc9cda2c5bb621abc21a8f8 100644
--- a/doc/concepts/morphology.rst
+++ b/doc/concepts/morphology.rst
@@ -1,4 +1,4 @@
-.. _co_morphology:
+.. _morph:
 
 Cell morphology
 ===============
@@ -504,9 +504,10 @@ interpret SWC files similarly to how the NEURON simulator would, and how the All
 
 Despite the differences between the interpretations, there is a common set of checks that are always performed
 to validate an SWC file:
-   * Check that there are no duplicate ids.
-   * Check that the parent id of a sample is less than the id of the sample.
-   * Check that the parent id of a sample refers to an existing sample.
+
+* Check that there are no duplicate ids.
+* Check that the parent id of a sample is less than the id of the sample.
+* Check that the parent id of a sample refers to an existing sample.
 
 In addition, all interpretations agree that a *segment* is (in the common case) constructed between a sample and
 its parent and inherits the tag of the sample; and if more than 1 sample have the same parent, the parent sample
@@ -562,22 +563,23 @@ NEURON interpretation:
 The NEURON interpretation was obtained by experimenting with the ``Import3d_SWC_read`` function. We came up with the
 following set of rules that govern NEURON's SWC behavior and enforced them in arbor's NEURON-complaint SWC
 interpreter:
-   * SWC files must contain a soma sample and it must to be the first sample.
-   * A soma is represented by a series of n≥1 unbranched, serially listed samples.
-   * A soma is constructed as a single cylinder with diameter equal to the piecewise average diameter of all the
-     segments forming the soma.
-   * A single-sample soma at is constructed as a cylinder with length=diameter.
-   * If a non-soma sample is to have a soma sample as its parent, it must have the most distal sample of the soma
-     as the parent.
-   * Every non-soma sample that has a soma sample as its parent, attaches to the created soma cylinder at its midpoint.
-   * If a non-soma sample has a soma sample as its parent, no segment is created between the sample and its parent,
-     instead that sample is the proximal point of a new segment, and there is a gap in the morphology (represented
-     electrically as a zero-resistance wire)
-   * To create a segment with a certain tag, that is to be attached to the soma, we need at least 2 samples with that
-     tag.
+
+* SWC files must contain a soma sample and it must to be the first sample.
+* A soma is represented by a series of n≥1 unbranched, serially listed samples.
+* A soma is constructed as a single cylinder with diameter equal to the piecewise average diameter of all the
+  segments forming the soma.
+* A single-sample soma at is constructed as a cylinder with length=diameter.
+* If a non-soma sample is to have a soma sample as its parent, it must have the most distal sample of the soma
+  as the parent.
+* Every non-soma sample that has a soma sample as its parent, attaches to the created soma cylinder at its midpoint.
+* If a non-soma sample has a soma sample as its parent, no segment is created between the sample and its parent,
+  instead that sample is the proximal point of a new segment, and there is a gap in the morphology (represented
+  electrically as a zero-resistance wire)
+* To create a segment with a certain tag, that is to be attached to the soma, we need at least 2 samples with that
+  tag.
 
 API
 ---
 
-* :ref:`Python <py_morphology>`
-* :ref:`C++ <morphology-construction>`
+* :ref:`Python <pymorph>`
+* :ref:`C++ <cppcablecell-morphology-construction>`
diff --git a/doc/cpp/cable_cell.rst b/doc/cpp/cable_cell.rst
index b6e1bbedc15d68f8cf5f5292f58d4be12612d9ef..6f42ccd2d70f704f345a4e4def4ff7f604b59beb 100644
--- a/doc/cpp/cable_cell.rst
+++ b/doc/cpp/cable_cell.rst
@@ -1,4 +1,4 @@
-.. _cppcable_cell:
+.. _cppcablecell:
 
 Cable cells
 ===========
@@ -21,35 +21,34 @@ object of type :cpp:type:`cable_cell_global_properties`.
 The :cpp:type:`cable_cell` object
 ---------------------------------
 
-Cable cells are constructed from a :cpp:type:`morphology` and, optionally, a
+Cable cells are constructed from a :cpp:type:`morphology`; an optional
 :cpp:type:`label_dict` that associates names with particular points
 (:cpp:type:`locset` objects) or subsets (:cpp:type:`region` objects) of the
-morphology.
+morphology; and an optional :ref:`decor <cablecell-decoration>`.
 
 Morphologies are constructed from a :cpp:type:`segment_tree`, but can also
 be generated via the :cpp:type:`stitch_builder`, which offers a slightly
-higher level interface. Details are described in :ref:`morphology-construction`.
+higher level interface. Details are described in :ref:`cppcablecell-morphology-construction`.
 
 Each cell has particular values for its electrical and ionic properties. These
 are determined first by the set of global defaults, then the defaults
 associated with the cell, and finally by any values specified explicitly for a
-given subsection of the morphology via the ``paint`` method
-(see :ref:`electrical-properties` and :ref:`paint-properties`).
+given subsection of the morphology via the ``paint`` interface of the decor
+(see :ref:`cppcablecell-electrical-properties` and :ref:`cppcablecell-paint-properties`).
 
 Ion channels and other distributed dynamical processes are also specified
 on the cell via the ``paint`` method; while synapses, current clamps,
 gap junction locations, and the site for testing the threshold potential
-are specified via the ``place`` method. See :ref:`cable-cell-dynamics`, below.
+are specified via the ``place`` method. See :ref:`cppcablecell-dynamics`, below.
 
-.. _morphology-construction:
+.. _cppcablecell-morphology-construction:
 
 Constructing cell morphologies
 ------------------------------
 
-.. todo::
-
-   TODO: Description of segment trees is in the works.
-
+Arbor provides an interface for constructing arbitrary cell morphologies
+composed of *segments*, see :ref:`the general documentation <morph>`.
+**TODO** C++ documentation for segment trees is not yet complete.
 
 The stitch-builder interface
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -225,7 +224,7 @@ basic checks performed on them. The :cpp:type:`swc_data` object can then be used
 
    Returns a :cpp:type:`morphology` constructed according to NEURON's SWC specifications.
 
-.. _locsets-and-regions:
+.. _cppcablecell-locsets-and-regions:
 
 Identifying sites and subsets of the morphology
 -----------------------------------------------
@@ -234,7 +233,7 @@ Identifying sites and subsets of the morphology
 
    TODO: Region and locset documentation is under development.
 
-.. _cable-cell-dynamics:
+.. _cppcablecell-dynamics:
 
 Cell dynamics
 -------------
@@ -286,7 +285,7 @@ cable cell, are attached to a cell with:
    TODO: describe other ``place``-able things: current clamps, gap junction
    sites, threshold potential measurement point.
 
-.. _electrical-properties:
+.. _cppcablecell-electrical-properties:
 
 Electrical properties and ion values
 -------------------------------------
@@ -446,7 +445,7 @@ constants.
    gprop.default_parameters.reversal_potential_method["ca"] = "nernst1998/ca";
 
 
-.. _paint-properties:
+.. _cppcablecell-paint-properties:
 
 Overriding properties locally
 -----------------------------
@@ -457,7 +456,7 @@ Overriding properties locally
    the morphology.
 
 
-.. _cable-cell-probes:
+.. _cppcablecell--probes:
 
 Cable cell probes
 -----------------
@@ -776,18 +775,9 @@ with which it is associated.
 Discretisation and CV policies
 ------------------------------
 
-For the purpose of simulation, cable cells are decomposed into :ref:`discrete
-subcomponents <cable-discretisation>` called *control volumes* (CVs) The CVs are
-uniquely determined by a set of *B* of ``mlocation`` boundary points.
-For each non-terminal point *h* in *B*, there is a CV comprising the points
-{*x*: *h* ≤ *x* and ¬∃ *y* ∈ *B* s.t *h* < *y* < *x*}, where < and ≤ refer to the
-geometrical partial order of locations on the morphology. A fork point is
-owned by a CV if and only if all of its corresponding representative locations
-are in the CV.
-
-The set of boundary points used by the simulator is determined by a *CV policy*.
-These are objects of type ``cv_policy``, which has the following
-public methods:
+The set of boundary points used by the simulator is determined by a
+:ref:`CV policy <cablecell-cv-policies>`. These are objects of type
+:cpp:class:`cv_policy`, which has the following public methods:
 
 .. cpp:class:: cv_policy
 
@@ -808,7 +798,8 @@ differing discretisations on different parts of a cell morphology. When a CV
 policy is constrained in this manner, the boundary of the domain will always
 constitute part of the CV boundary point set.
 
-CV policies can be combined with ``+`` and ``|`` operators. For two policies
+CV policies can be :ref:`composed <cablecell-cv-composition>` with ``+`` and ``|`` operators.
+For two policies
 *A* and *B*, *A* + *B* is a policy which gives boundary points from both *A*
 and *B*, while *A* | *B* is a policy which gives all the boundary points from
 *B* together with those from *A* which do not within the domain of *B*.
@@ -835,14 +826,14 @@ supplied domain.
 Use the points given by ``locs`` for CV boundaries, optionally restricted to the
 supplied domain.
 
-``cv_policy_every_sample``
-^^^^^^^^^^^^^^^^^^^^^^^^^^
+``cv_policy_every_segment``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 .. code::
 
-   cv_policy_every_sample(region domain = reg::all())
+   cv_policy_every_segment(region domain = reg::all())
 
-Use every sample point in the morphology definition as a CV boundary, optionally
+Use every segment in the morphology as a CV, optionally
 restricted to the supplied domain. Each fork point in the domain is
 represented by a trivial CV.
 
diff --git a/doc/internals/util.rst b/doc/internals/util.rst
index d9a35ce40630541ef8085327e1e7b63d80dcf4d2..b8a2fa7b72ef6642d6fc440bc4e9a4a77932194c 100644
--- a/doc/internals/util.rst
+++ b/doc/internals/util.rst
@@ -31,4 +31,4 @@ Utility wrappers and containers
     also performs analagous casting for the :cpp:class:`unique_any` and
     :cpp:class:`any_ptr` utility classes.
 
-    See :ref:`cppcable_cell`.
+    See :ref:`cppcablecell`.
diff --git a/doc/python/cable_cell.rst b/doc/python/cable_cell.rst
index 922854eada6283b0b1ed8ededbcb3bc5cd21b573..03affce555da07ec6f7c1228d9fd1e04b76f0128 100644
--- a/doc/python/cable_cell.rst
+++ b/doc/python/cable_cell.rst
@@ -1,50 +1,30 @@
-.. _pycable_cell:
+.. _pycablecell:
 
 Cable cells
 ===========
 
 .. currentmodule:: arbor
 
-.. py:class:: cable_cell
-
-    A cable cell is constructed from a :ref:`morphology <morph-morphology>`
-    and an optional :ref:`label dictionary <labels-dictionary>`.
-
-    .. note::
-        The regions and locsets defined in the label dictionary are
-        :ref:`concretised <labels-concretise>` when the cable cell is constructed,
-        and an exception will be thrown if an invalid label expression is found.
-
-        There are two reasons an expression might be invalid:
-
-        1. Explicit reference to a location of cable that does not exist in the
-           morphology, for example ``(branch 12)`` on a cell with 6 branches.
-        2. Reference to an incorrect label: circular reference, or a label that does not exist.
-
-
-    .. code-block:: Python
+.. py:class:: decor
 
-        import arbor
+    A decor object contains a description of the cell dynamics, to be applied
+    to a morphology when used to instantiate a :py:class:`cable_cell`
 
-        # Construct the morphology from an SWC file.
-        tree = arbor.load_swc('granule.swc')
-        morph = arbor.morphology(tree, spherical_root=True)
+    .. method:: __init__()
 
-        # Define regions using standard SWC tags
-        labels = arbor.label_dict({'soma': '(tag 1)',
-                                   'axon': '(tag 2)',
-                                   'dend': '(join (tag 3) (tag 4))'})
+        Construct an empty decor description.
 
-        # Construct a cable cell.
-        cell = arbor.cable_cell(morph, labels)
+    Properties for which defaults can be defined over the entire cell, specifically
+    :ref:`cable properties <cablecell-properties>` and :ref:`ion properties <cablecell-ions>`,
+    are set with ``set_property`` and ``set_ion`` methods.
 
-    .. method:: set_properties(Vm=None, cm=None, rL=None, tempK=None)
+    .. method:: set_property(Vm=None, cm=None, rL=None, tempK=None)
 
         Set default values of cable properties on the whole cell.
         Overrides the default global values, and can be overridden by painting
         the values onto regions.
 
-        :param str region: name of the region.
+        :param str region: description of the region.
         :param Vm: Initial membrane voltage [mV].
         :type Vm: float or None
         :param cm: Membrane capacitance [F/m²].
@@ -56,22 +36,45 @@ Cable cells
 
         .. code-block:: Python
 
-            # Set cell-wide values for properties
-            cell.set_properties(Vm=-70, cm=0.01, rL=100, tempK=280)
+            # Set cell-wide values for properties for resistivity and capacitance
+            decor.set_property(rL=100, cm=0.1)
+
+    .. method:: set_ion(ion, int_con=None, ext_con=None, rev_pot=None, method=None)
 
-    .. method: compartments_length(length)
+        Set default value for one or more properties of a specific ion on the whole cell.
+        Set the properties of ion species named ``ion`` that will be applied
+        by default everywhere on the cell. Species concentrations and reversal
+        potential can be overridden on specific regions using the paint interface,
+        while the method for calculating reversal potential is global for all
+        CVs in the cell, and can't be overriden locally.
 
-        Adjust the :ref:`compartments length <cable-discretisation>`.
+        :param str ion: description of the ion species.
+        :param float int_con: initial internal concentration [mM].
+        :type int_con: float or None.
+        :param float ext_con: initial external concentration [mM].
+        :type ext_con: float or None.
+        :param float rev_pot: reversal potential [mV].
+        :type rev_pot: float or None
+        :param method: method for calculating reversal potential.
+        :type method: :py:class:`mechanism` or None
+
+        .. code-block:: Python
 
-        :param int length: length of compartments [μm].
+            # Set nernst reversal potential method for calcium.
+            decor.set_ion('ca', method=mech('nernst/x=ca'))
 
-        Defaults to one control volume per branch.
+            # Set reversal potential and concentration for sodium.
+            # The reversal potential is fixed, so we set the method to None.
+            decor.set_ion('na', int_con=5.0, rev_pot=70, method=None)
 
-    .. method:: paint(region, [Vm=None, cm=None, rL=None, tempK=None])
+    Verious specialisations of the ``paint`` method are available for setting properties
+    and mechanisms that are applied to regions.
+
+    .. method:: paint(region, Vm=None, cm=None, rL=None, tempK=None)
 
         Set cable properties on a region.
 
-        :param str region: name of the region.
+        :param str region: description of the region.
         :param Vm: Initial membrane voltage [mV].
         :type Vm: float or None
         :param cm: Membrane capacitance [F/m²].
@@ -84,22 +87,175 @@ Cable cells
         .. code-block:: Python
 
             # Specialize resistivity on soma
-            cell.paint('"soma"', rL=100)
+            decor.paint('"soma"', rL=100)
             # Specialize resistivity and capacitance on the axon, where
             # axon is defined using a region expression.
-            cell.paint('(tag 2)', cm=0.05, rL=80)
+            decor.paint('(tag 2)', cm=0.05, rL=80)
+
+    .. method:: paint(region, name, int_con=None, ext_con=None, rev_pot=None)
+        :noindex:
+
+        Set ion species properties initial conditions on a region.
+
+        :param str name: name of the ion species.
+        :param float int_con: initial internal concentration [mM].
+        :type int_con: float or None.
+        :param float ext_con: initial external concentration [mM].
+        :type ext_con: float or None.
+        :param float rev_pot: reversal potential [mV].
+        :type rev_pot: float or None
+
+    .. method:: paint(region, mechanism)
+        :noindex:
+
+        Apply a mechanism with a region.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str region: description of the region.
+        :param mechanism: the mechanism.
+        :type mechanism: :py:class:`mechanism`
+
+    .. method:: paint(region, mech_name)
+        :noindex:
+
+        Apply a mechanism with a region using the name of the mechanism.
+        The mechanism will use the parameter values set in the mechanism catalogue.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str region: description of the region.
+        :param str mechanism: the name of the mechanism.
+
+    .. method:: place(locations, const arb::mechanism_desc& d)
+
+        Place one instance of synapse described by ``mechanism`` to each location in ``locations``.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str locations: description of the locset.
+        :param str mechanism: the name of the mechanism.
+        :rtype: int
+
+    .. method:: place(locations, mechanism)
+        :noindex:
+
+        Place one instance of synapse described by ``mechanism`` to each location in ``locations``.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str locations: description of the locset.
+        :param mechanism: the mechanism.
+        :type mechanism: :py:class:`mechanism`
+        :rtype: int
+
+    .. method:: place(locations, site)
+        :noindex:
+
+        Place one gap junction site at each location in ``locations``.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str locations: description of the locset.
+        :param site: indicates a gap junction site..
+        :type site: :py:class:`gap_junction_site`
+        :rtype: int
+
+    .. method:: place(locations, stim)
+        :noindex:
+
+        Add a current stimulus at each location in ``locations``.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str locations: description of the locset.
+        :param stim: the current stim.
+        :type stim: :py:class:`i_clamp`
+        :rtype: int
+
+    .. method:: place(locations, d)
+        :noindex:
+
+        Add a voltage spike detector at each location in ``locations``.
+        Returns a unique identifier that can be used to query the local indexes (see :gen:`index`) assigned to the placed items on the cable cell.
+
+        :param str locations: description of the locset.
+        :param d: description of the detector.
+        :type d: :py:class:`threshold_detector`
+        :rtype: int
+
+    .. method:: discretization(policy)
+
+        Set the cv_policy used to discretise the cell into control volumes for simulation.
+
+        :param policy: The cv_policy.
+        :type policy: :py:class:`cv_policy`
+
+.. py:class:: cable_cell
+
+    A cable cell is constructed from a :ref:`morphology <morph-morphology>`,
+    a :ref:`label dictionary <labels-dictionary>` and a decor.
+
+    .. note::
+        The regions and locsets defined in the label dictionary are
+        :ref:`concretised <labels-concretise>` when the cable cell is constructed,
+        and an exception will be thrown if an invalid label expression is found.
+
+        There are two reasons an expression might be invalid:
+
+        1. Explicit reference to a location of cable that does not exist in the
+           morphology, for example ``(branch 12)`` on a cell with 6 branches.
+        2. Reference to an incorrect label: circular reference, or a label that does not exist.
+
+
+    .. code-block:: Python
+
+        import arbor
+
+        # Construct the morphology from an SWC file.
+        tree = arbor.load_swc('granule.swc')
+        morph = arbor.morphology(tree)
+
+        # Define regions using standard SWC tags
+        labels = arbor.label_dict({'soma': '(tag 1)',
+                                   'axon': '(tag 2)',
+                                   'dend': '(join (tag 3) (tag 4))'})
+
+        # Define decorations
+        decor = arbor.decor()
+        decor.paint('"dend"', 'pas')
+        decor.paint('"axon"', 'hh')
+        decor.paint('"soma"', 'hh')
+
+        # Construct a cable cell.
+        cell = arbor.cable_cell(morph, labels, decor)
+
+    .. method:: __init__(morphology, labels, decorations)
+
+        Constructor.
+
+        :param morphology: the morphology of the cell
+        :type morphology: :py:class:`morphology`
+        :param labels: dictionary of labeled regions and locsets
+        :type labels: :py:class:`label_dict`
+        :param decorations: the decorations on the cell
+        :type decorations: :py:class:`decor`
+
+    .. method:: placed_lid_range(index)
+
+        Returns the range of local indexes assigned to a placement in the decorations as a tuple of two integers,
+        that define the range of indexes as a half open interval.
+
+        :param index: the unique index of the placement.
+        :type index: int
+        :rtype: tuple(int, int)
+
 
 .. py:class:: ion
 
     properties of an ionic species.
 
-.. _pycableprobes:
+.. _pycablecell-probes:
 
 Cable cell probes
 -----------------
 
 Cable cell probe addresses are defined analagously to their counterparts in
-the C++ API (see :ref:`cable-cell-probes` for details). Sample data recorded
+the C++ API (see :ref:`cablecell-probes` for details). Sample data recorded
 by the Arbor simulation object is returned in the form of a NumPy array,
 with the first column holding sample times, and subsequent columns holding
 the corresponding scalar- or vector-valued sample.
@@ -244,3 +400,79 @@ Ionic external concentration
 
    Metadata: the list of corresponding :class:`cable` objects.
 
+
+.. _pycablecell-cv-policies:
+
+Discretisation and CV policies
+------------------------------
+
+The set of boundary points used by the simulator is determined by a
+:ref:`CV policy <cablecell-cv-policies>`. These are objects of type
+:cpp:class:`cv_policy`, which has the following public methods:
+
+.. py:class:: cv_policy
+
+   .. attribute:: domain
+
+       A read only string expression describing the subset of a cell morphology
+       (region) on which this policy has been declared.
+
+   CV policies can be :ref:`composed <cablecell-cv-composition>` with
+   ``+`` and ``|`` operators.
+
+   .. code-block:: Python
+
+       # The plus operator applies 
+       policy = arbor.cv_policy_single('"soma"') + cv_policy('"dend"')
+
+       # The | operator uses CVs of length 10 μm everywhere, except
+       # on the soma, to which a single CV policy is applied.
+       policy = arbor.cv_policy_max_extent(10) | cv_policy_single('"soma"')
+
+Specific CV policy objects are created by functions described below.
+These all take a ``region`` parameter that restrict the
+domain of applicability of that policy; this facility is useful for specifying
+differing discretisations on different parts of a cell morphology. When a CV
+policy is constrained in this manner, the boundary of the domain will always
+constitute part of the CV boundary point set.
+
+.. py:function:: cv_policy_single(domain='(all)')
+
+    Use one CV for the whole cell, or one for each connected component of the
+    supplied domain.
+
+    .. code-block:: Python
+
+        # Use one CV for the entire cell (a single compartment model)
+        single_comp = arbor.cv_policy_single()
+
+        # Use a single CV for the soma.
+        single_comp_soma = arbor.cv_policy_single('"soma"')
+
+    :param str domain: The region on which the policy is applied.
+
+.. py:function:: cv_policy_every_segment(domain='(all)')
+
+    Use every sample point in the morphology definition as a CV boundary, optionally
+    restricted to the supplied domain. Each fork point in the domain is
+    represented by a trivial CV.
+
+    :param str domain: The region on which the policy is applied.
+
+.. py:function:: cv_policy_fixed_per_branch(cv_per_branch, domain='(all)')
+
+    For each branch in each connected component of the domain (or the whole cell,
+    if no domain is given), evenly distribute boundary points along the branch so
+    as to produce exactly ``cv_per_branch`` CVs.
+
+    :param int cv_per_branch: The number of CVs per branch.
+    :param str domain: The region on which the policy is applied.
+
+.. py:function:: cv_policy_max_extent(max_extent, domain='(all)')
+
+    As for :py:func:`cv_policy_fixed_per_branch`, save that the number of CVs on any
+    given branch will be chosen to be the smallest number that ensures no
+    CV will have an extent on the branch longer than ``max_extent`` micrometres.
+
+    :param float max_etent: The maximum length for generated CVs.
+    :param str domain: The region on which the policy is applied.
diff --git a/doc/python/cell.rst b/doc/python/cell.rst
index 71a960f909bb84650c2d3c5e8ec3a0b9adac3496..19cdb7a804114792ce1494bb640e747a1fab2460 100644
--- a/doc/python/cell.rst
+++ b/doc/python/cell.rst
@@ -151,4 +151,4 @@ Cell kinds
 .. class:: cable_cell
     :noindex:
 
-    See :ref:`pycable_cell`.
+    See :ref:`pycablecell`.
diff --git a/doc/python/hardware.rst b/doc/python/hardware.rst
index 057a290d42905e4cd0b70496f8d81383eb3b62b4..78e2fb0252b5ae72b9023140826868e6f3c7b59d 100644
--- a/doc/python/hardware.rst
+++ b/doc/python/hardware.rst
@@ -48,6 +48,7 @@ Helper functions for checking cmake or environment variables, as well as configu
         By default sets MPI_COMM_WORLD as communicator.
 
     .. function:: mpi_comm(object)
+        :noindex:
 
         Converts a Python object to an MPI Communicator.
 
@@ -127,7 +128,27 @@ The Python wrapper provides an API for:
 
         Construct a local context with one thread, no GPU, no MPI.
 
+    .. function:: context(threads, gpu_id, mpi)
+        :noindex:
+
+        Create a context that uses a set number of :attr:`threads` and gpu identifier :attr:`gpu_id` and MPI communicator :attr:`mpi` for distributed calculation.
+
+        .. attribute:: threads
+
+            The number of threads available locally for execution, 1 by default.
+
+        .. attribute:: gpu_id
+
+            The identifier of the GPU to use, ``None`` by default.
+            Must be ``None``, or a non-negative integer.
+
+        .. attribute:: mpi
+
+            The MPI communicator (see :class:`mpi_comm`).
+            mpi must be ``None``, or an MPI communicator.
+
     .. function:: context(alloc)
+        :noindex:
 
         Create a local context, with no distributed/MPI, that uses the local resources described by :class:`proc_allocation`.
 
@@ -136,6 +157,7 @@ The Python wrapper provides an API for:
             The computational resources, one thread and no GPU by default.
 
     .. function:: context(alloc, mpi)
+        :noindex:
 
         Create a distributed context, that uses the local resources described by :class:`proc_allocation`, and
         uses the MPI communicator for distributed calculation.
@@ -150,6 +172,7 @@ The Python wrapper provides an API for:
             mpi must be ``None``, or an MPI communicator.
 
     .. function:: context(threads, gpu_id)
+        :noindex:
 
         Create a context that uses a set number of :attr:`threads` and the GPU with id :attr:`gpu_id`.
 
@@ -162,24 +185,6 @@ The Python wrapper provides an API for:
             The identifier of the GPU to use, ``None`` by default.
             Must be ``None``, or a non-negative integer.
 
-    .. function:: context(threads, gpu_id, mpi)
-
-        Create a context that uses a set number of :attr:`threads` and gpu identifier :attr:`gpu_id` and MPI communicator :attr:`mpi` for distributed calculation.
-
-        .. attribute:: threads
-
-            The number of threads available locally for execution, 1 by default.
-
-        .. attribute:: gpu_id
-
-            The identifier of the GPU to use, ``None`` by default.
-            Must be ``None``, or a non-negative integer.
-
-        .. attribute:: mpi
-
-            The MPI communicator (see :class:`mpi_comm`).
-            mpi must be ``None``, or an MPI communicator.
-
     Contexts can be queried for information about which features a context has enabled,
     whether it has a GPU, how many threads are in its thread pool.
 
diff --git a/doc/python/morphology.rst b/doc/python/morphology.rst
index 84252e93ad77721f8edaac13ddb723f0759c7220..65b83032238f5221996de501b8ed279600fca061 100644
--- a/doc/python/morphology.rst
+++ b/doc/python/morphology.rst
@@ -1,4 +1,6 @@
-.. _py_morphology:
+.. _pymorph:
+
+   .. 
 
 Cell morphology
 ===============
@@ -30,7 +32,7 @@ Cell morphology
                     dist=arbor.mpoint(0, 0,50, 0.2),
                     tag=3)
 
-        # mnpos can also be used when querying a sample_tree or morphology,
+        # mnpos can also be used when querying a segment_tree or morphology,
         # for example the following snippet that finds all branches in the
         # morphology that are attached to the root of the morphology.
         m = arbor.morphology(tree)
@@ -391,4 +393,4 @@ Cell morphology
 
     :param str filename: the name of the SWC file.
     :param bool no_gaps: enforce that distance between soma center and branches attached to soma is the soma radius.
-    :rtype: morphology
\ No newline at end of file
+    :rtype: morphology
diff --git a/doc/python/simulation.rst b/doc/python/simulation.rst
index 23e49b1e4ecf1a7b744ea65d7dfd12bbe647ff20..c162d1e91ab69a637e69f47f66d890185444ccc8 100644
--- a/doc/python/simulation.rst
+++ b/doc/python/simulation.rst
@@ -245,7 +245,7 @@ probe
     A measurement that can be perfomed on a cell. Each cell kind will have its own sorts of probe;
     Cable cells (:attr:`arbor.cable_probe`) allow the monitoring of membrane voltage, total membrane
     current, mechanism state, and a number of other quantities, measured either over the whole cell,
-    or at specific sites (see :ref:`pycableprobes`).
+    or at specific sites (see :ref:`pycablecell-probes`).
 
     Probes are described by probe addresses, and the collection of probe addresses for a given cell is
     provided by the :class:`recipe` object. One address may correspond to more than one probe:
@@ -283,7 +283,7 @@ Procedure
 
        Each probe address is an opaue object describing what to measure and where, and each cell kind
        will have its own set of functions for generating valid address specifications. Possible cable
-       cell probes are described in the cable cell documentation: :ref:`pycableprobes`.
+       cell probes are described in the cable cell documentation: :ref:`pycablecell-probes`.
 
     2. Instructing the simulator to record data.
 
diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst
index 0c57a0c3269959da49591f139edec9ad6a2714a5..77d30e81944219605f8a3fc5122fdb18b75e12dd 100644
--- a/doc/tutorial/index.rst
+++ b/doc/tutorial/index.rst
@@ -4,13 +4,11 @@ Tutorials
 =========
 
 .. Note::
-    You can find some examples of full Arbor simulations in the ``python/examples`` directory of the `Arbor repository <https://github.com/arbor-sim/arbor>`_. Note that some examples use ``pandas`` and ``seaborn`` for analysis and plotting which are expected to be installed independently from Arbor.
-
-.. Todo::
-    Add more in-depth tutorial-like pages building up examples here.
+    You can find some examples of full Arbor simulations in the ``python/examples`` directory of the `Arbor repository <https://github.com/arbor-sim/arbor>`_.
+    Note that some examples use ``pandas`` and ``seaborn`` for analysis and plotting which are expected to be installed independently from Arbor.
 
 .. toctree::
    :maxdepth: 1
    :caption: Tutorials
 
-   single_cell_model
\ No newline at end of file
+   single_cell_model
diff --git a/doc/tutorial/single_cell_model.rst b/doc/tutorial/single_cell_model.rst
index 388c1254599efc7c271cb24f3e3a4fa390a41b69..0167bba6c3db5f176b3dc8eaf90193dd93125fb6 100644
--- a/doc/tutorial/single_cell_model.rst
+++ b/doc/tutorial/single_cell_model.rst
@@ -25,12 +25,11 @@ cylindrical cell with a length of 6 μm and a radius of 3 μm; Hodgkin–Huxley
 and a current clamp stimulus, then run the model for 30 ms.
 
 The first step is to construct the cell. In Arbor, the abstract representation used to
-define a cell with branching "cable" morphology is a ``cable_cell``, which holds a
+define a cell with branching cable morphology is a ``cable_cell``, which holds a
 description of the cell's morphology, named regions and locations on the morphology, and
 descriptions of ion channels, synapses, spike detectors and electrical properties.
 
-Our "single-segment HH cell" has a simple morphology and dynamics, so the steps to
-create the ``cable_cell`` that represents it are as follows:
+Our *single-segment HH cell* has a simple morphology and dynamics, constructed as follows:
 
 .. code-block:: python
 
@@ -45,13 +44,14 @@ create the ``cable_cell`` that represents it are as follows:
                                'center': '(location 0 0.5)'})
 
     # (3) Create cell and set properties
-    cell = arbor.cable_cell(tree, labels)
-    cell.set_properties(Vm=-40)
-    cell.paint('"soma"', 'hh')
-    cell.place('"center"', arbor.iclamp( 10, 2, 0.8))
-    cell.place('"center"', arbor.spike_detector(-10))
+    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))
 
-Let's unpack that.
+    # (4) Create cell
+    cell = arbor.cable_cell(tree, labels, decor)
 
 Step **(1)** constructs a :class:`arbor.segment_tree` (see also :ref:`segment tree<morph-segment_tree>`).
 The segment tree is the representation used to construct the morphology of a cell. A segment is
@@ -72,65 +72,67 @@ dynamics, stimulii and probes to the cell. We add two labels:
 * ``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 the :class:`arbor.cable_cell` from the segment tree and dictionary of labeled
-regions and locations. The resulting cell's default properties can be modified, and we can use
-:meth:`arbor.cable_cell.paint` and :meth:`arbor.cable_cell.place` to further customise it in the
+Step **(3)** constructs a :class:`arbor.decor` that describes the distributation 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:
 
-* :meth:`arbor.cable_cell.set_properties` is used to set some default properties on the entire cell.
+* :meth:`arbor.decor.set_property` is used to set some default properties on the entire cell.
   In the above example we set the initial membrane potential to -40 mV.
-* :meth:`arbor.cable_cell.paint` is used to set properties or add dynamics to a region of the cell.
+* :meth:`arbor.decor.paint` is used to set properties or add dynamics to a region of the cell.
   We call this method 'painting' to convey that we are working on sections of a cell, as opposed to
-  precise locations: for example, we might want to ``paint`` an ion channel on all dendrites, and then
-  ``place`` a synapse at the tip of the axon. In the above example we :meth:`arbor.cable_cell.paint`
-  HH dynamics on the region we previously named 'soma' in our label dictionary.
-* :meth:`arbor.cable_cell.place<arbor.cable_cell.place>` is used to add objects on a precise
-  :class:`arbor.location` on a cell. Examples of objects that are ``placed`` are synapses,
+  precise locations: for example, we might want to *paint* an ion channel on all dendrites, and then
+  *place* a synapse at the tip of the axon. In the above example we paint
+  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
-  :class:`arbor.iclamp<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<arbor.spike_detector>`
+  :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.
+
 Single cell model
 ----------------------------------------------------
 
-Great, we have defined our cell! Now, let's move on to the simulation. Arbor is able to simulate
-networks with multiple individual cells; this requires a *recipe* to describe the cells,
-connections, gap junctions, etc. However, for single cell models, arbor does not require the recipe
-to be provided by the user. Arbor provides a :class:`arbor.single_cell_model<arbor.single_cell_model>`
-helper that wraps a cell description and creates a recipe under the hood, providing an interface for
-recording potentials and running the simulation more easily.
+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
+:class:`arbor.single_cell_model<arbor.single_cell_model>`
+helper that creates a model from a cell description, with an interface for
+recording outputs and running the simulation.
 
 .. code-block:: python
 
-    # (4) Make single cell model.
+    # (5) Make single cell model.
     m = arbor.single_cell_model(cell)
 
-    # (5) Attach voltage probe sampling at 10 kHz (every 0.1 ms).
+    # (6) Attach voltage probe sampling at 10 kHz (every 0.1 ms).
     m.probe('voltage', '"center"', frequency=10000)
 
-    # (6) Run simulation for 30 ms of simulated activity.
+    # (7) Run simulation for 30 ms of simulated activity.
     m.run(tfinal=30)
 
-Step **(4)** instantiates the :class:`arbor.single_cell_model<arbor.single_cell_model>`
+Step **(5)** instantiates the :class:`arbor.single_cell_model<arbor.single_cell_model>`
 with our single-compartment cell.
 
-Step **(5)** adds a :meth:`arbor.single_cell_model.probe()<arbor.single_cell_model.probe>`
+Step **(6)** adds a :meth:`arbor.single_cell_model.probe()<arbor.single_cell_model.probe>`
 used to record variables from the model. Three pieces of information are
 provided: the type of quantity we want probed (voltage), the location where we want to
 probe ('"center"'), and the frequency at which we want to sample (10kHz).
 
-Step **(6)** runs the actual simulation for a duration of 30 ms.
+Step **(7)** runs the actual simulation for a duration of 30 ms.
 
 Results
 ----------------------------------------------------
 
-Our cell and model have been defined and we have run our simulation. However, we have not seen any
-results! Let's take a look at what the spike detector and a voltage probes from our model have produced.
+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.
 
 .. code-block:: python
 
-    # (7) Print spike times, if any.
+    # (8) Print spike times.
     if len(m.spikes)>0:
         print('{} spikes:'.format(len(m.spikes)))
         for s in m.spikes:
@@ -138,17 +140,17 @@ results! Let's take a look at what the spike detector and a voltage probes from
     else:
         print('no spikes')
 
-    # (8) Plot the recorded voltages over time.
+    # (9) Plot the recorded voltages over time.
     import pandas, seaborn # You may have to pip install these.
     seaborn.set_theme() # Apply some styling to the plot
     df = pandas.DataFrame({'t/ms': m.traces[0].time, 'U/mV': m.traces[0].value})
     seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV",ci=None).savefig('single_cell_model_result.svg')
 
-Step **(7)** accesses :meth:`arbor.single_cell_model.spikes<arbor.single_cell_model.spikes>`
+Step **(8)** accesses :meth:`arbor.single_cell_model.spikes<arbor.single_cell_model.spikes>`
 to print the spike times. A single spike should be generated at around the same time the stimulus
 we provided in step (3) gets activated (10ms).
 
-Step **(8)** plots the measured potentials during the runtime of the simulation. The sampled quantities
+Step **(9)** plots the measured potentials during the runtime of the simulation. The sampled quantities
 can be accessed through :meth:`arbor.single_cell_model.traces<arbor.single_cell_model.traces>`.
 We should be seeing something like this:
 
@@ -156,9 +158,15 @@ We should be seeing something like this:
     :width: 400
     :align: center
 
-    Plot of the potential over time for the voltage probe added in step (5).
+    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::
-    Add equivalent but more comprehensive recipe implementation in parallel, such that the reader learns how single_cell_model works.
+    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.
+
+.. 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.
diff --git a/example/dryrun/branch_cell.hpp b/example/dryrun/branch_cell.hpp
index 837a6569e83eaa2d5af65d18b901816d3c4e69eb..15344fc81964420e01072e6350e3c5776c3dd30c 100644
--- a/example/dryrun/branch_cell.hpp
+++ b/example/dryrun/branch_cell.hpp
@@ -5,8 +5,9 @@
 
 #include <nlohmann/json.hpp>
 
-#include <arbor/common_types.hpp>
 #include <arbor/cable_cell.hpp>
+#include <arbor/cable_cell_param.hpp>
+#include <arbor/common_types.hpp>
 #include <arbor/morph/segment_tree.hpp>
 #include <arbor/string_literals.hpp>
 
@@ -102,31 +103,34 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
         dist_from_soma += l;
     }
 
-    arb::label_dict d;
+    arb::label_dict labels;
 
     using arb::reg::tagged;
-    d.set("soma",      tagged(stag));
-    d.set("dend", tagged(dtag));
+    labels.set("soma", tagged(stag));
+    labels.set("dend", tagged(dtag));
 
-    arb::cable_cell cell(arb::morphology(tree), d);
+    arb::decor decor;
 
-    cell.paint("soma"_lab, "hh");
-    cell.paint("dend"_lab, "pas");
-    cell.default_parameters.axial_resistivity = 100; // [Ω·cm]
+    decor.paint("soma"_lab, "hh");
+    decor.paint("dend"_lab, "pas");
+
+    decor.set_default(arb::axial_resistivity{100}); // [Ω·cm]
 
     // Add spike threshold detector at the soma.
-    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
+    decor.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.place(arb::mlocation{0, 0.5}, "expsyn");
+    decor.place(arb::mlocation{0, 0.5}, "expsyn");
 
     // Add additional synapses that will not be connected to anything.
     for (unsigned i=1u; i<params.synapses; ++i) {
-        cell.place(arb::mlocation{1, 0.5}, "expsyn");
+        decor.place(arb::mlocation{1, 0.5}, "expsyn");
     }
 
     // Make a CV between every sample in the sample tree.
-    cell.default_parameters.discretization = arb::cv_policy_every_segment();
+    decor.set_default(arb::cv_policy_every_segment());
+
+    arb::cable_cell cell(arb::morphology(tree), labels, decor);
 
     return cell;
 }
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index 686a73fe45c4a3d33e59bd1ca16f001d5cdae0b7..316efbcd227190bf24236cd9b0b5e020659553e4 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -286,10 +286,10 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration)
     double dend_rad = 3./2; // μm
     tree.append(0, {0,0,2*soma_rad, dend_rad}, {0,0,2*soma_rad+300, dend_rad}, 3);  // dendrite
 
-    // Create the cell and set its electrical properties.
-    arb::cable_cell cell(tree);
-    cell.default_parameters.axial_resistivity = 100;       // [Ω·cm]
-    cell.default_parameters.membrane_capacitance = 0.018;  // [F/m²]
+    arb::decor decor;
+
+    decor.set_default(arb::axial_resistivity{100});       // [Ω·cm]
+    decor.set_default(arb::membrane_capacitance{0.018});  // [F/m²]
 
     // Define the density channels and their parameters.
     arb::mechanism_desc nax("nax");
@@ -307,28 +307,29 @@ arb::cable_cell gj_cell(cell_gid_type gid, unsigned ncell, double stim_duration)
     pas["e"] =  -65;
 
     // Paint density channels on all parts of the cell
-    cell.paint("(all)", nax);
-    cell.paint("(all)", kdrmt);
-    cell.paint("(all)", kamt);
-    cell.paint("(all)", pas);
+    decor.paint("(all)", nax);
+    decor.paint("(all)", kdrmt);
+    decor.paint("(all)", kamt);
+    decor.paint("(all)", pas);
 
     // Add a spike detector to the soma.
-    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
+    decor.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add two gap junction sites.
-    cell.place(arb::mlocation{0, 1}, arb::gap_junction_site{});
-    cell.place(arb::mlocation{0, 1}, arb::gap_junction_site{});
+    decor.place(arb::mlocation{0, 1}, arb::gap_junction_site{});
+    decor.place(arb::mlocation{0, 1}, arb::gap_junction_site{});
 
     // Attach a stimulus to the second cell.
     if (!gid) {
         arb::i_clamp stim(0, stim_duration, 0.4);
-        cell.place(arb::mlocation{0, 0.5}, stim);
+        decor.place(arb::mlocation{0, 0.5}, stim);
     }
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.place(arb::mlocation{0, 0.5}, "expsyn");
+    decor.place(arb::mlocation{0, 0.5}, "expsyn");
 
-    return cell;
+    // Create the cell and set its electrical properties.
+    return arb::cable_cell(tree, {}, decor);
 }
 
 gap_params read_options(int argc, char** argv) {
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index 734486369eb8fbc3f77edcda2b1b8c41287b3beb..5b62c95f25e4e763deef3fc91a3fabddeecf2722 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -52,18 +52,18 @@ public:
         double r = 18.8/2.0; // convert 18.8 μm diameter to radius
         tree.append(arb::mnpos, {0,0,-r,r}, {0,0,r,r}, 1);
 
-        arb::label_dict d;
-        d.set("soma", arb::reg::tagged(1));
+        arb::label_dict labels;
+        labels.set("soma", arb::reg::tagged(1));
 
-        arb::cable_cell c(tree, d);
-        c.paint("\"soma\"", "pas");
+        arb::decor decor;
+        decor.paint("\"soma\"", "pas");
 
         // Add one synapse at the soma.
         // This synapse will be the target for all events, from both
         // event_generators.
-        c.place(arb::mlocation{0, 0.5}, "expsyn");
+        decor.place(arb::mlocation{0, 0.5}, "expsyn");
 
-        return std::move(c);
+        return arb::cable_cell(tree, labels, decor);
     }
 
     cell_kind get_cell_kind(cell_gid_type gid) const override {
diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp
index 54520d5c21e6f2927ab9b3ea4c15bcce268a845f..dabba6de26be039baaf56f1e840027a966032a85 100644
--- a/example/lfp/lfp.cpp
+++ b/example/lfp/lfp.cpp
@@ -80,22 +80,24 @@ private:
         // Apical dendrite, length 490 μm, radius 1 μm, with SWC tag 4.
         tree.append(soma_apex, {0, 0, 10, 1},  {0, 0, 500, 1}, 4);
 
-        cell_ = cable_cell(tree);
+        decor dec;
 
         // Use NEURON defaults for reversal potentials, ion concentrations etc., but override ra, cm.
-        cell_.default_parameters.axial_resistivity = 100;     // [Ω·cm]
-        cell_.default_parameters.membrane_capacitance = 0.01; // [F/m²]
+        dec.set_default(axial_resistivity{100});     // [Ω·cm]
+        dec.set_default(membrane_capacitance{0.01}); // [F/m²]
 
         // Twenty CVs per branch on the dendrites (tag 4).
-        cell_.default_parameters.discretization = cv_policy_fixed_per_branch(20, arb::reg::tagged(4));
+        dec.set_default(cv_policy_fixed_per_branch(20, arb::reg::tagged(4)));
 
         // Add pas and hh mechanisms:
-        cell_.paint(reg::tagged(1), "hh"); // (default parameters)
-        cell_.paint(reg::tagged(4), mechanism_desc("pas").set("e", -70));
+        dec.paint(reg::tagged(1), "hh"); // (default parameters)
+        dec.paint(reg::tagged(4), mechanism_desc("pas").set("e", -70));
 
         // Add exponential synapse at centre of soma.
         synapse_location_ = ls::on_components(0.5, reg::tagged(1));
-        cell_.place(synapse_location_, mechanism_desc("expsyn").set("e", 0).set("tau", 2));
+        dec.place(synapse_location_, mechanism_desc("expsyn").set("e", 0).set("tau", 2));
+
+        cell_ = cable_cell(tree, {}, dec);
     }
 };
 
diff --git a/example/probe-demo/probe-demo.cpp b/example/probe-demo/probe-demo.cpp
index 860ffbc64c4c8529d2659a56718d8713272d95bb..9e9c16002a503306f0834dea1367828a90bcd4aa 100644
--- a/example/probe-demo/probe-demo.cpp
+++ b/example/probe-demo/probe-demo.cpp
@@ -114,17 +114,17 @@ struct cable_recipe: public arb::recipe {
     }
 
     arb::util::unique_any get_cell_description(arb::cell_gid_type) const override {
-        using namespace arb;
         const double length = 1000; // [µm]
-        const double diam = 1; // [µm]
+        const double diam   = 1;    // [µm]
 
-        segment_tree tree;
+        arb::segment_tree tree;
         tree.append(arb::mnpos, {0, 0, 0, 0.5*diam}, {length, 0, 0, 0.5*diam}, 1);
-        cable_cell c(tree);
 
-        c.paint(reg::all(), "hh"); // HH mechanism over whole cell.
-        c.place(mlocation{0, 0.}, i_clamp{0., INFINITY, 1.}); // Inject a 1 nA current indefinitely.
-        return c;
+        arb::decor decor;
+        decor.paint(arb::reg::all(), "hh"); // HH mechanism over whole cell.
+        decor.place(arb::mlocation{0, 0.}, arb::i_clamp{0., INFINITY, 1.}); // Inject a 1 nA current indefinitely.
+
+        return arb::cable_cell(tree, {}, decor);
     }
 };
 
diff --git a/example/ring/branch_cell.hpp b/example/ring/branch_cell.hpp
index ec8d007578521c51e53925e92b9d455d4de629b3..15344fc81964420e01072e6350e3c5776c3dd30c 100644
--- a/example/ring/branch_cell.hpp
+++ b/example/ring/branch_cell.hpp
@@ -5,8 +5,9 @@
 
 #include <nlohmann/json.hpp>
 
-#include <arbor/common_types.hpp>
 #include <arbor/cable_cell.hpp>
+#include <arbor/cable_cell_param.hpp>
+#include <arbor/common_types.hpp>
 #include <arbor/morph/segment_tree.hpp>
 #include <arbor/string_literals.hpp>
 
@@ -102,31 +103,34 @@ arb::cable_cell branch_cell(arb::cell_gid_type gid, const cell_parameters& param
         dist_from_soma += l;
     }
 
-    arb::label_dict d;
+    arb::label_dict labels;
 
     using arb::reg::tagged;
-    d.set("soma", tagged(stag));
-    d.set("dend", tagged(dtag));
+    labels.set("soma", tagged(stag));
+    labels.set("dend", tagged(dtag));
 
-    arb::cable_cell cell(arb::morphology(tree), d);
+    arb::decor decor;
 
-    cell.paint("soma"_lab, "hh");
-    cell.paint("dend"_lab, "pas");
-    cell.default_parameters.axial_resistivity = 100; // [Ω·cm]
+    decor.paint("soma"_lab, "hh");
+    decor.paint("dend"_lab, "pas");
+
+    decor.set_default(arb::axial_resistivity{100}); // [Ω·cm]
 
     // Add spike threshold detector at the soma.
-    cell.place(arb::mlocation{0,0}, arb::threshold_detector{10});
+    decor.place(arb::mlocation{0,0}, arb::threshold_detector{10});
 
     // Add a synapse to the mid point of the first dendrite.
-    cell.place(arb::mlocation{0, 0.5}, "expsyn");
+    decor.place(arb::mlocation{0, 0.5}, "expsyn");
 
     // Add additional synapses that will not be connected to anything.
     for (unsigned i=1u; i<params.synapses; ++i) {
-        cell.place(arb::mlocation{1, 0.5}, "expsyn");
+        decor.place(arb::mlocation{1, 0.5}, "expsyn");
     }
 
     // Make a CV between every sample in the sample tree.
-    cell.default_parameters.discretization = arb::cv_policy_every_segment();
+    decor.set_default(arb::cv_policy_every_segment());
+
+    arb::cable_cell cell(arb::morphology(tree), labels, decor);
 
     return cell;
 }
diff --git a/example/single/single.cpp b/example/single/single.cpp
index 2510ed46de07c67157d1c80abc3db4cdd2693c91..b3be1a02aa126e3fe6beb08456d8e055f8bdcd3f 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -61,19 +61,20 @@ struct single_recipe: public arb::recipe {
         using arb::reg::tagged;
         dict.set("soma", tagged(1));
         dict.set("dend", join(tagged(3), tagged(4), tagged(42)));
-        arb::cable_cell c(morpho, dict);
+
+        arb::decor decor;
 
         // Add HH mechanism to soma, passive channels to dendrites.
-        c.paint("\"soma\"", "hh");
-        c.paint("\"dend\"", "pas");
+        decor.paint("\"soma\"", "hh");
+        decor.paint("\"dend\"", "pas");
 
         // Add synapse to last branch.
 
-        arb::cell_lid_type last_branch = c.morphology().num_branches()-1;
+        arb::cell_lid_type last_branch = morpho.num_branches()-1;
         arb::mlocation end_last_branch = { last_branch, 1. };
-        c.place(end_last_branch, "exp2syn");
+        decor.place(end_last_branch, "exp2syn");
 
-        return c;
+        return arb::cable_cell(morpho, dict, decor);
     }
 
     arb::morphology morpho;
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index d0cd67f9fba2a4c942e6573d642a427d87107d13..8615232f1623e92ddc51406cd46d12c72faff448 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -22,7 +22,6 @@ set(pyarb_source
     domain_decomposition.cpp
     error.cpp
     event_generator.cpp
-    flat_cell_builder.cpp
     identifiers.cpp
     mechanism.cpp
     morphology.cpp
diff --git a/python/cells.cpp b/python/cells.cpp
index 7953593e9c8869629424baa34e083304e847899b..9017e4453cf07be99a6dff0251b466d8883dfc5f 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -7,6 +7,7 @@
 #include <utility>
 #include <vector>
 
+#include <pybind11/operators.h>
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
@@ -23,9 +24,13 @@
 #include <arbor/util/any_cast.hpp>
 #include <arbor/util/unique_any.hpp>
 
+#include "arbor/cable_cell_param.hpp"
+#include "arbor/cv_policy.hpp"
 #include "cells.hpp"
 #include "conversion.hpp"
 #include "error.hpp"
+#include "pybind11/cast.h"
+#include "pybind11/pytypes.h"
 #include "schedule.hpp"
 #include "strprintf.hpp"
 
@@ -165,6 +170,42 @@ std::string to_string(const global_props_shim& G) {
     return s;
 }
 
+//
+// cv_policy helpers
+//
+
+arb::cv_policy make_cv_policy_single(const std::string& reg) {
+    return arb::cv_policy_single(reg);
+}
+
+arb::cv_policy make_cv_policy_every_segment(const std::string& reg) {
+    return arb::cv_policy_every_segment(reg);
+}
+
+arb::cv_policy make_cv_policy_fixed_per_branch(unsigned cv_per_branch, const std::string& reg) {
+    return arb::cv_policy_fixed_per_branch(cv_per_branch, reg);
+}
+
+arb::cv_policy make_cv_policy_max_extent(double cv_length, const std::string& reg) {
+    return arb::cv_policy_max_extent(cv_length, reg);
+}
+
+// Helper for finding a mechanism description in a Python object.
+// Allows rev_pot_method to be specified with string or mechanism_desc
+std::optional<arb::mechanism_desc> maybe_method(pybind11::object method) {
+    if (!method.is_none()) {
+        if (auto m=try_cast<std::string>(method)) {
+            return *m;
+        }
+        else if (auto m=try_cast<arb::mechanism_desc>(method)) {
+            return *m;
+        }
+        else {
+            throw std::runtime_error(util::pprintf("invalid rev_pot_method: {}", method));
+        }
+    }
+    return {};
+}
 
 //
 // string printers
@@ -291,6 +332,41 @@ void register_cells(pybind11::module& m) {
         .def("__repr__", [](const label_dict_proxy& d){return d.to_string();})
         .def("__str__",  [](const label_dict_proxy& d){return d.to_string();});
 
+    // arb::cv_policy wrappers
+
+    pybind11::class_<arb::cv_policy> cv_policy(m, "cv_policy",
+            "Describes the rules used to discretize (compartmentalise) a cable cell morphology.");
+    cv_policy
+        .def_property_readonly("domain",
+                               [](const arb::cv_policy& p) {return util::pprintf("{}", p.domain());},
+                               "The domain on which the policy is applied.")
+        .def(pybind11::self + pybind11::self)
+        .def(pybind11::self | pybind11::self)
+        .def("__repr__", [](const arb::cv_policy& p) {return "(cv-policy)";})
+        .def("__str__",  [](const arb::cv_policy& p) {return "(cv-policy)";});
+
+    m.def("cv_policy_single",
+          &make_cv_policy_single,
+          "domain"_a="(all)", "the domain to which the policy is to be applied",
+          "Policy to create one compartment per component of a region.");
+
+    m.def("cv_policy_every_segment",
+          &make_cv_policy_every_segment,
+          "domain"_a="(all)", "the domain to which the policy is to be applied",
+          "Policy to create one compartment per component of a region.");
+
+    m.def("cv_policy_max_extent",
+          &make_cv_policy_max_extent,
+          "length"_a, "the maximum CV length",
+          "domain"_a="(all)", "the domain to which the policy is to be applied",
+          "Policy to use as many CVs as required to ensure that no CV has a length longer than a given value.");
+
+    m.def("cv_policy_fixed_per_branch",
+          &make_cv_policy_fixed_per_branch,
+          "n"_a, "the number of CVs per branch",
+          "domain"_a="(all)", "the domain to which the policy is to be applied",
+          "Policy to use the same number of CVs for each branch.");
+
     // arb::gap_junction_site
 
     pybind11::class_<arb::gap_junction_site> gjsite(m, "gap_junction",
@@ -367,9 +443,9 @@ void register_cells(pybind11::module& m) {
         .def(pybind11::init<const global_props_shim&>())
         .def("check", [](const global_props_shim& shim) {
                 arb::check_global_properties(shim.props);},
-                "Test whether all default parameters and ion specids properties have been set.")
+                "Test whether all default parameters and ion species properties have been set.")
         // set cable properties
-        .def("set_properties",
+        .def("set_property",
             [](global_props_shim& G,
                optional<double> Vm, optional<double> cm,
                optional<double> rL, optional<double> tempK)
@@ -379,15 +455,11 @@ void register_cells(pybind11::module& m) {
                 if (rL) G.props.default_parameters.axial_resistivity=rL;
                 if (tempK) G.props.default_parameters.temperature_K=tempK;
             },
-            "Vm"_a=pybind11::none(), "cm"_a=pybind11::none(), "rL"_a=pybind11::none(), "tempK"_a=pybind11::none(),
-            "Set global default values for cable and cell properties.\n"
-            " Vm:    initial membrane voltage [mV].\n"
-            " cm:    membrane capacitance [F/m²].\n"
-            " rL:    axial resistivity [Ω·cm].\n"
-            " tempK: temperature [Kelvin].")
-        .def("foo", [](global_props_shim& G, double x, pybind11::object method) {
-                        std::cout << "foo(" << x << ", " << method << ")\n";},
-                    "x"_a, "method"_a=pybind11::none())
+            pybind11::arg_v("Vm",    pybind11::none(), "initial membrane voltage [mV]."),
+            pybind11::arg_v("cm",    pybind11::none(), "membrane capacitance [F/m²]."),
+            pybind11::arg_v("rL",    pybind11::none(), "axial resistivity [Ω·cm]."),
+            pybind11::arg_v("tempK", pybind11::none(), "temperature [Kelvin]."),
+            "Set global default values for cable and cell properties.")
         // add/modify ion species
         .def("set_ion",
             [](global_props_shim& G, const char* ion,
@@ -398,179 +470,170 @@ void register_cells(pybind11::module& m) {
                 if (int_con) data.init_int_concentration = *int_con;
                 if (ext_con) data.init_ext_concentration = *ext_con;
                 if (rev_pot) data.init_reversal_potential = *rev_pot;
-                // allow rev_pot_method to be specified with string or mechanism_desc
-                if (!method.is_none()) {
-                    if (auto m=try_cast<std::string>(method)) {
-                        G.props.default_parameters.reversal_potential_method[ion] = *m;
-                    }
-                    else if (auto m=try_cast<arb::mechanism_desc>(method)) {
-                        G.props.default_parameters.reversal_potential_method[ion] = *m;
-                    }
-                    else {
-                        throw std::runtime_error(util::pprintf("invalid rev_pot_method: {}", method));
-                    }
+                if (auto m = maybe_method(method)) {
+                    G.props.default_parameters.reversal_potential_method[ion] = *m;
                 }
             },
-            "ion"_a,
-            "int_con"_a=pybind11::none(),
-            "ext_con"_a=pybind11::none(),
-            "rev_pot"_a=pybind11::none(),
-            "rev_pot_method"_a=pybind11::none(),
+            pybind11::arg_v("ion", "name of the ion species."),
+            pybind11::arg_v("int_con", pybind11::none(), "initial internal concentration [mM]."),
+            pybind11::arg_v("ext_con", pybind11::none(), "initial external concentration [mM]."),
+            pybind11::arg_v("rev_pot", pybind11::none(), "reversal potential [mV]."),
+            pybind11::arg_v("method",  pybind11::none(), "method for calculating reversal potential."),
             "Set the global default propoerties of ion species named 'ion'.\n"
             "Species concentrations and reversal potential can be overridden on\n"
             "specific regions using the paint interface, while the method for calculating\n"
             "reversal potential is global for all compartments in the cell, and can't be\n"
-            "overriden locally.\n"
-            " ion:     name of ion species.\n"
-            " int_con: initial internal concentration [mM].\n"
-            " ext_con: initial external concentration [mM].\n"
-            " rev_pot: initial reversal potential [mV].\n"
-            " rev_pot_method:  method for calculating reversal potential.")
+            "overriden locally.")
         .def_readwrite("catalogue", &global_props_shim::cat, "The mechanism catalogue.")
         .def("__str__", [](const global_props_shim& p){return to_string(p);});
 
-    // arb::cable_cell
 
-    pybind11::class_<arb::cable_cell> cable_cell(m, "cable_cell",
-        "Represents morphologically-detailed cell models, with morphology represented as a\n"
-        "tree of one-dimensional cable segments.");
-    cable_cell
-        .def(pybind11::init(
-            [](const arb::morphology& m, const label_dict_proxy& labels) {
-                return arb::cable_cell(m, labels.dict);
-            }), "morphology"_a, "labels"_a)
-        .def(pybind11::init(
-            [](const arb::segment_tree& t, const label_dict_proxy& labels) {
-                return arb::cable_cell(arb::morphology(t), labels.dict);
-            }),
-            "segment_tree"_a, "labels"_a,
-            "Construct with a morphology derived from a segment tree.")
-        .def_property_readonly("num_branches",
-            [](const arb::cable_cell& c) {return c.morphology().num_branches();},
-            "The number of unbranched cable sections in the morphology.")
-        // Set cell-wide properties
-        .def("set_properties",
-            [](arb::cable_cell& c,
+    pybind11::class_<arb::decor> decor(m, "decor",
+            "Description of the decorations to be applied to a cable cell, that is the painted,\n"
+            "placed and defaulted properties, mecahanisms, ion species etc.");
+    decor
+        .def(pybind11::init<>())
+        .def(pybind11::init<const arb::decor&>())
+        // Set cell-wide default values for properties
+        .def("set_property",
+            [](arb::decor& d,
                optional<double> Vm, optional<double> cm,
                optional<double> rL, optional<double> tempK)
             {
-                if (Vm) c.default_parameters.init_membrane_potential = Vm;
-                if (cm) c.default_parameters.membrane_capacitance=cm;
-                if (rL) c.default_parameters.axial_resistivity=rL;
-                if (tempK) c.default_parameters.temperature_K=tempK;
+                if (Vm) d.set_default(arb::init_membrane_potential{*Vm});
+                if (cm) d.set_default(arb::membrane_capacitance{*cm});
+                if (rL) d.set_default(arb::axial_resistivity{*rL});
+                if (tempK) d.set_default(arb::temperature_K{*tempK});
             },
-            "Vm"_a=pybind11::none(), "cm"_a=pybind11::none(), "rL"_a=pybind11::none(), "tempK"_a=pybind11::none(),
-            "Set default values for cable and cell properties. These values can be overridden on specific regions using the paint interface.\n"
-            " Vm:    initial membrane voltage [mV].\n"
-            " cm:    membrane capacitance [F/m²].\n"
-            " rL:    axial resistivity [Ω·cm].\n"
-            " tempK: temperature [Kelvin].")
+            pybind11::arg_v("Vm",    pybind11::none(), "initial membrane voltage [mV]."),
+            pybind11::arg_v("cm",    pybind11::none(), "membrane capacitance [F/m²]."),
+            pybind11::arg_v("rL",    pybind11::none(), "axial resistivity [Ω·cm]."),
+            pybind11::arg_v("tempK", pybind11::none(), "temperature [Kelvin]."),
+            "Set default values for cable and cell properties. These values can be overridden on specific regions using the paint interface.")
+        // modify parameters for an ion species.
         .def("set_ion",
-            [](arb::cable_cell& c, const char* ion,
+            [](arb::decor& d, const char* ion,
                optional<double> int_con, optional<double> ext_con,
-               optional<double> rev_pot, optional<arb::mechanism_desc> method)
+               optional<double> rev_pot, pybind11::object method)
             {
-                auto& data = c.default_parameters.ion_data[ion];
-                if (int_con) data.init_int_concentration = *int_con;
-                if (ext_con) data.init_ext_concentration = *ext_con;
-                if (rev_pot) data.init_reversal_potential = *rev_pot;
-                if (method)  c.default_parameters.reversal_potential_method[ion] = *method;
+                if (int_con) d.set_default(arb::init_int_concentration{ion, *int_con});
+                if (ext_con) d.set_default(arb::init_ext_concentration{ion, *ext_con});
+                if (rev_pot) d.set_default(arb::init_reversal_potential{ion, *rev_pot});
+                if (auto m = maybe_method(method)) {
+                    d.set_default(arb::ion_reversal_potential_method{ion, *m});
+                }
             },
-            "ion"_a,
-            "int_con"_a=pybind11::none(),
-            "ext_con"_a=pybind11::none(),
-            "rev_pot"_a=pybind11::none(),
-            "method"_a=pybind11::none(),
-            "Set the propoerties of ion species named 'ion' that will be applied\n"
+            pybind11::arg_v("ion", "name of the ion species."),
+            pybind11::arg_v("int_con", pybind11::none(), "initial internal concentration [mM]."),
+            pybind11::arg_v("ext_con", pybind11::none(), "initial external concentration [mM]."),
+            pybind11::arg_v("rev_pot", pybind11::none(), "reversal potential [mV]."),
+            pybind11::arg_v("method",  pybind11::none(), "method for calculating reversal potential."),
+            "Set the properties of ion species named 'ion' that will be applied\n"
             "by default everywhere on the cell. Species concentrations and reversal\n"
             "potential can be overridden on specific regions using the paint interface, \n"
             "while the method for calculating reversal potential is global for all\n"
-            "compartments in the cell, and can't be overriden locally.\n"
-            " ion:     name of ion species.\n"
-            " int_con: initial internal concentration [mM].\n"
-            " ext_con: initial external concentration [mM].\n"
-            " rev_pot: reversal potential [mV].\n"
-            " method:  method for calculating reversal potential.")
+            "compartments in the cell, and can't be overriden locally.")
         // Paint mechanisms.
         .def("paint",
-            [](arb::cable_cell& c, const char* region, const arb::mechanism_desc& d) {
-                c.paint(region, d);
+            [](arb::decor& dec, const char* region, const arb::mechanism_desc& d) {
+                dec.paint(region, d);
             },
             "region"_a, "mechanism"_a,
             "Associate a mechanism with a region.")
         .def("paint",
-            [](arb::cable_cell& c, const char* region, const char* mech_name) {
-                c.paint(region, mech_name);
+            [](arb::decor& dec, const char* region, const char* mech_name) {
+                dec.paint(region, arb::mechanism_desc(mech_name));
             },
             "region"_a, "mechanism"_a,
             "Associate a mechanism with a region.")
         // Paint membrane/static properties.
         .def("paint",
-            [](arb::cable_cell& c,
+            [](arb::decor& dec,
                 const char* region,
                optional<double> Vm, optional<double> cm,
                optional<double> rL, optional<double> tempK)
             {
-                if (Vm) c.paint(region, arb::init_membrane_potential{*Vm});
-                if (cm) c.paint(region, arb::membrane_capacitance{*cm});
-                if (rL) c.paint(region, arb::axial_resistivity{*rL});
-                if (tempK) c.paint(region, arb::temperature_K{*tempK});
+                if (Vm) dec.paint(region, arb::init_membrane_potential{*Vm});
+                if (cm) dec.paint(region, arb::membrane_capacitance{*cm});
+                if (rL) dec.paint(region, arb::axial_resistivity{*rL});
+                if (tempK) dec.paint(region, arb::temperature_K{*tempK});
             },
-            "region"_a, "Vm"_a=pybind11::none(), "cm"_a=pybind11::none(), "rL"_a=pybind11::none(), "tempK"_a=pybind11::none(),
-            "Set cable properties on a region.\n"
-            " Vm:    initial membrane voltage [mV].\n"
-            " cm:    membrane capacitance [F/m²].\n"
-            " rL:    axial resistivity [Ω·cm].\n"
-            " tempK: temperature [Kelvin].")
-
-
-
+            pybind11::arg_v("region", "the region label or description."),
+            pybind11::arg_v("Vm",    pybind11::none(), "initial membrane voltage [mV]."),
+            pybind11::arg_v("cm",    pybind11::none(), "membrane capacitance [F/m²]."),
+            pybind11::arg_v("rL",    pybind11::none(), "axial resistivity [Ω·cm]."),
+            pybind11::arg_v("tempK", pybind11::none(), "temperature [Kelvin]."),
+            "Set cable properties on a region.")
         // Paint ion species initial conditions on a region.
         .def("paint",
-            [](arb::cable_cell& c, const char* region, const char* name,
+            [](arb::decor& dec, const char* region, const char* name,
                optional<double> int_con, optional<double> ext_con, optional<double> rev_pot) {
-                if (int_con) c.paint(region, arb::init_int_concentration{name, *int_con});
-                if (ext_con) c.paint(region, arb::init_ext_concentration{name, *ext_con});
-                if (rev_pot) c.paint(region, arb::init_reversal_potential{name, *rev_pot});
+                if (int_con) dec.paint(region, arb::init_int_concentration{name, *int_con});
+                if (ext_con) dec.paint(region, arb::init_ext_concentration{name, *ext_con});
+                if (rev_pot) dec.paint(region, arb::init_reversal_potential{name, *rev_pot});
             },
             "region"_a, "ion_name"_a,
-             pybind11::arg_v("int_con", pybind11::none(), "Intial internal concentration [mM]"),
-             pybind11::arg_v("ext_con", pybind11::none(), "Intial external concentration [mM]"),
-             pybind11::arg_v("rev_pot", pybind11::none(), "Intial reversal potential [mV]"),
+            pybind11::arg_v("int_con", pybind11::none(), "Intial internal concentration [mM]"),
+            pybind11::arg_v("ext_con", pybind11::none(), "Intial external concentration [mM]"),
+            pybind11::arg_v("rev_pot", pybind11::none(), "Intial reversal potential [mV]"),
             "Set ion species properties conditions on a region.")
         // Place synapses
         .def("place",
-            [](arb::cable_cell& c, const char* locset, const arb::mechanism_desc& d) {
-                c.place(locset, d); },
+            [](arb::decor& dec, const char* locset, const arb::mechanism_desc& d) -> int {
+                return dec.place(locset, d); },
             "locations"_a, "mechanism"_a,
             "Place one instance of synapse described by 'mechanism' to each location in 'locations'.")
         .def("place",
-            [](arb::cable_cell& c, const char* locset, const char* mech_name) {
-                c.place(locset, mech_name);
+            [](arb::decor& dec, const char* locset, const char* mech_name) -> int {
+                return dec.place(locset, mech_name);
             },
             "locations"_a, "mechanism"_a,
             "Place one instance of synapse described by 'mechanism' to each location in 'locations'.")
         // Place gap junctions.
         .def("place",
-            [](arb::cable_cell& c, const char* locset, const arb::gap_junction_site& site) {
-                c.place(locset, site);
+            [](arb::decor& dec, const char* locset, const arb::gap_junction_site& site) -> int {
+                return dec.place(locset, site);
             },
             "locations"_a, "gapjunction"_a,
             "Place one gap junction site at each location in 'locations'.")
         // Place current clamp stimulus.
         .def("place",
-            [](arb::cable_cell& c, const char* locset, const arb::i_clamp& stim) {
-                c.place(locset, stim);
+            [](arb::decor& dec, const char* locset, const arb::i_clamp& stim) -> int {
+                return dec.place(locset, stim);
             },
             "locations"_a, "iclamp"_a,
             "Add a current stimulus at each location in locations.")
         // Place spike detector.
         .def("place",
-            [](arb::cable_cell& c, const char* locset, const arb::threshold_detector& d) {
-                c.place(locset, d);
+            [](arb::decor& dec, const char* locset, const arb::threshold_detector& d) -> int {
+                return dec.place(locset, d);
             },
             "locations"_a, "detector"_a,
             "Add a voltage spike detector at each location in locations.")
+        .def("discretization",
+            [](arb::decor& dec, const arb::cv_policy& p) { dec.set_default(p); },
+            pybind11::arg_v("policy", "A cv_policy used to discretise the cell into compartments for simulation"));
+
+
+    // arb::cable_cell
+
+    pybind11::class_<arb::cable_cell> cable_cell(m, "cable_cell",
+        "Represents morphologically-detailed cell models, with morphology represented as a\n"
+        "tree of one-dimensional cable segments.");
+    cable_cell
+        .def(pybind11::init(
+            [](const arb::morphology& m, const label_dict_proxy& labels, const arb::decor& d) {
+                return arb::cable_cell(m, labels.dict, d);
+            }), "morphology"_a, "labels"_a, "decor"_a)
+        .def(pybind11::init(
+            [](const arb::segment_tree& t, const label_dict_proxy& labels, const arb::decor& d) {
+                return arb::cable_cell(arb::morphology(t), labels.dict, d);
+            }),
+            "segment_tree"_a, "labels"_a, "decor"_a,
+            "Construct with a morphology derived from a segment tree.")
+        .def_property_readonly("num_branches",
+            [](const arb::cable_cell& c) {return c.morphology().num_branches();},
+            "The number of unbranched cable sections in the morphology.")
         // Get locations associated with a locset label.
         .def("locations",
             [](arb::cable_cell& c, const char* label) {return c.concrete_locset(label);},
@@ -579,18 +642,14 @@ void register_cells(pybind11::module& m) {
         .def("cables",
             [](arb::cable_cell& c, const char* label) {return c.concrete_region(label).cables();},
             "label"_a, "The cable segments of the cell morphology for a region label.")
-        // Discretization control.
-        .def("compartments_on_segments",
-            [](arb::cable_cell& c) {c.default_parameters.discretization = arb::cv_policy_every_segment{};},
-            "Decompose each branch into compartments defined by segments.")
-        .def("compartments_length",
-            [](arb::cable_cell& c, double len) {
-                c.default_parameters.discretization = arb::cv_policy_max_extent{len};
+        // Get lid range associated with a placement.
+        .def("placed_lid_range",
+            [](arb::cable_cell& c, int idx) -> pybind11::tuple {
+                auto range = c.placed_lid_range(idx);
+                return pybind11::make_tuple(range.begin, range.end);
             },
-            "maxlen"_a, "Decompose each branch into compartments of equal length, not exceeding maxlen.")
-        .def("compartments_per_branch",
-            [](arb::cable_cell& c, unsigned n) {c.default_parameters.discretization = arb::cv_policy_fixed_per_branch{n};},
-            "n"_a, "Decompose each branch into n compartments of equal length.")
+            "index"_a,
+            "The range of lids assigned to the items from a placement, for the lids assigned to synapses.")
         // Stringification
         .def("__repr__", [](const arb::cable_cell&){return "<arbor.cable_cell>";})
         .def("__str__",  [](const arb::cable_cell&){return "<arbor.cable_cell>";});
diff --git a/python/example/network_ring.py b/python/example/network_ring.py
index baffa01d79ea680811bb8421d5d5ed99df02b4c2..b080ab026d3c9397ac738835a68dbcd223073f1a 100755
--- a/python/example/network_ring.py
+++ b/python/example/network_ring.py
@@ -41,15 +41,17 @@ def make_cable_cell(gid):
     # Mark the root of the tree.
     labels['root'] = '(root)'
 
-    cell = arbor.cable_cell(tree, labels)
+    decor = arbor.decor()
 
     # Put hh dynamics on soma, and passive properties on the dendrites.
-    cell.paint('"soma"', 'hh')
-    cell.paint('"dend"', 'pas')
+    decor.paint('"soma"', 'hh')
+    decor.paint('"dend"', 'pas')
     # Attach a single synapse.
-    cell.place('"synapse_site"', 'expsyn')
+    decor.place('"synapse_site"', 'expsyn')
     # Attach a spike detector with threshold of -10 mV.
-    cell.place('"root"', arbor.spike_detector(-10))
+    decor.place('"root"', arbor.spike_detector(-10))
+
+    cell = arbor.cable_cell(tree, labels, decor)
 
     return cell
 
diff --git a/python/example/single_cell_model.py b/python/example/single_cell_model.py
index 9d88ced09c3ab0bc3c23e6ed2078cdef5e17241d..6701b179785648c3cc09ac9ebb9717fae5807f2c 100755
--- a/python/example/single_cell_model.py
+++ b/python/example/single_cell_model.py
@@ -12,22 +12,25 @@ labels = arbor.label_dict({'soma':   '(tag 1)',
                            'center': '(location 0 0.5)'})
 
 # (3) Create cell and set properties
-cell = arbor.cable_cell(tree, labels)
-cell.set_properties(Vm=-40)
-cell.paint('"soma"', 'hh')
-cell.place('"center"', arbor.iclamp( 10, 2, 0.8))
-cell.place('"center"', arbor.spike_detector(-10))
+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))
 
-# (4) Make single cell model.
+# (4) Create cell and the single cell model based on it
+cell = arbor.cable_cell(tree, labels, decor)
+
+# (5) Make single cell model.
 m = arbor.single_cell_model(cell)
 
-# (5) Attach voltage probe sampling at 10 kHz (every 0.1 ms).
+# (6) Attach voltage probe sampling at 10 kHz (every 0.1 ms).
 m.probe('voltage', '"center"', frequency=10000)
 
-# (6) Run simulation for 30 ms of simulated activity.
+# (7) Run simulation for 30 ms of simulated activity.
 m.run(tfinal=30)
 
-# (7) Print spike times, if any.
+# (8) Print spike times.
 if len(m.spikes)>0:
     print('{} spikes:'.format(len(m.spikes)))
     for s in m.spikes:
diff --git a/python/example/single_cell_multi_branch.py b/python/example/single_cell_multi_branch.py
deleted file mode 100755
index 4e8d929e01d3ed7435dbddb9e43a9c268c375008..0000000000000000000000000000000000000000
--- a/python/example/single_cell_multi_branch.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python3
-
-import arbor
-import seaborn
-import pandas
-from math import sqrt
-
-# Make a ball and stick cell model
-
-tree = arbor.segment_tree()
-
-# Construct a cell with the following morphology.
-# The soma (at the root of the tree) is marked 's', and
-# the end of each branch i is marked 'bi'.
-#
-#               b4
-#              /
-#             /
-#            b1---b3
-#           /
-#          /
-# s-------b0
-#          \
-#           \
-#            b2
-
-# Start with a spherical soma with radius 6 μm,
-# approximated with a cylinder of: length = diameter = 12 μm.
-
-s = tree.append(arbor.mnpos, arbor.mpoint(-12, 0, 0, 6), arbor.mpoint(0, 0, 0, 6), tag=1)
-
-# Add the dendrite cables, labelling those closest to the soma "dendn",
-# and those furthest with "dendx" because we will set different electrical
-# properties for the two regions.
-
-labels = arbor.label_dict()
-labels['soma'] = '(tag 1)'
-labels['dendn'] = '(tag 5)'
-labels['dendx'] = '(tag 6)'
-
-b0 = tree.append(s, arbor.mpoint(0, 0, 0, 2), arbor.mpoint(100, 0, 0, 2), tag=5)
-
-# Radius tapers from 2 to 0.5 over the length of the branch.
-
-b1 = tree.append(b0, arbor.mpoint(100, 0, 0, 2), arbor.mpoint(100+50/sqrt(2), 50/sqrt(2), 0, 0.5), tag=5)
-b2 = tree.append(b0, arbor.mpoint(100, 0, 0, 1), arbor.mpoint(100+50/sqrt(2), -50/sqrt(2), 0, 1), tag=5)
-b3 = tree.append(b1, arbor.mpoint(100+50/sqrt(2), 50/sqrt(2), 0, 1), arbor.mpoint(100+50/sqrt(2)+50, 50/sqrt(2), 0, 1), tag=6)
-b4 = tree.append(b1, arbor.mpoint(100+50/sqrt(2), 50/sqrt(2), 0, 1), arbor.mpoint(100+2*50/sqrt(2), 2*50/sqrt(2), 0, 1), tag=6)
-
-# Combine the "dendn" and "dendx" regions into a single "dend" region.
-# The dendrites were labelled as such so that we can set different
-# properties on each sub-region, and then combined so that we can
-# set other properties on the whole dendrites.
-labels['dend'] = '(join (region "dendn") (region "dendx"))'
-# Location of stimuli, in the middle of branch 2.
-labels['stim_site'] = '(location 1 0.5)'
-# The root of the tree (equivalent to '(location 0 0)')
-labels['root'] = '(root)'
-# The tips of the dendrites (3 locations at b4, b3, b2).
-labels['dtips'] = '(terminal)'
-
-# Extract the cable cell from the builder.
-# cell = b.build()
-cell = arbor.cable_cell(tree, labels)
-
-# Set initial membrane potential everywhere on the cell to -40 mV.
-cell.set_properties(Vm=-40)
-# Put hh dynamics on soma, and passive properties on the dendrites.
-cell.paint('"soma"', 'hh')
-cell.paint('"dend"', 'pas')
-# Set axial resistivity in dendrite regions (Ohm.cm)
-cell.paint('"dendn"', rL=500)
-cell.paint('"dendx"', rL=10000)
-# Attach stimuli with duration of 2 ms and current of 0.8 nA.
-# There are three stimuli, which activate at 10 ms, 50 ms and 80 ms.
-cell.place('"stim_site"', arbor.iclamp( 10, 2, 0.8))
-cell.place('"stim_site"', arbor.iclamp( 50, 2, 0.8))
-cell.place('"stim_site"', arbor.iclamp( 80, 2, 0.8))
-# Add a spike detector with threshold of -10 mV.
-cell.place('"root"', arbor.spike_detector(-10))
-
-# Discretization: the default discretization in Arbor is 1 compartment per branch.
-# Let's be a bit more precise and make that every 2 micron:
-cell.compartments_length(2)
-
-# Make single cell model.
-m = arbor.single_cell_model(cell)
-
-# Attach voltage probes, sampling at 10 kHz.
-m.probe('voltage', '(location 0 0)', 10000) # at the soma.
-m.probe('voltage', '"dtips"',  10000) # at the tips of the dendrites.
-
-# Run simulation for 100 ms of simulated activity.
-tfinal=100
-m.run(tfinal)
-print("Simulation done.")
-
-# Print spike times.
-if len(m.spikes)>0:
-    print('{} spikes:'.format(len(m.spikes)))
-    for s in m.spikes:
-        print('  {:7.4f}'.format(s))
-else:
-    print('no spikes')
-
-# Plot the recorded voltages over time.
-print("Plotting results...")
-df = pandas.DataFrame()
-for t in m.traces:
-    df=df.append(pandas.DataFrame({'t/ms': t.time, 'U/mV': t.value, 'Location': str(t.location), "Variable": t.variable}) )
-
-seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV",hue="Location",col="Variable",ci=None).savefig('single_cell_multi_branch_result.svg')
diff --git a/python/example/single_cell_recipe.py b/python/example/single_cell_recipe.py
index f11927f70323809d3661276b81c6ed3546164f94..a9b7288087531262b29a05bf3e10c5bf65476ef8 100644
--- a/python/example/single_cell_recipe.py
+++ b/python/example/single_cell_recipe.py
@@ -30,17 +30,22 @@ class single_recipe (arbor.recipe):
 
 # (2) Create a cell.
 
+# Morphology
 tree = arbor.segment_tree()
 tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1)
 
+# Label dictionary
 labels = arbor.label_dict()
 labels['centre'] = '(location 0 0.5)'
 
-cell = arbor.cable_cell(tree, labels)
-cell.set_properties(Vm=-40)
-cell.paint('(all)', 'hh')
-cell.place('"centre"', arbor.iclamp( 10, 2, 0.8))
-cell.place('"centre"', arbor.spike_detector(-10))
+# 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.
 
diff --git a/python/example/single_cell_swc.py b/python/example/single_cell_swc.py
index 6fa4b967f1805b9d538595b4cfbfd934fbfd3341..7133178c4b57a509a0820c549570fcc92c34f6cc 100755
--- a/python/example/single_cell_swc.py
+++ b/python/example/single_cell_swc.py
@@ -34,37 +34,45 @@ defs = {'soma': '(tag 1)',  # soma has tag 1 in swc files.
         'axon_end': '(restrict (terminal) (region "axon"))'} # end of the axon.
 labels = arbor.label_dict(defs)
 
-# Combine morphology with region and locset definitions to make a cable cell.
-cell = arbor.cable_cell(morpho, labels)
-
-print(cell.locations('"axon_end"'))
+decor = arbor.decor()
 
 # Set initial membrane potential to -55 mV
-cell.set_properties(Vm=-55)
+decor.set_property(Vm=-55)
 # Use Nernst to calculate reversal potential for calcium.
-cell.set_ion('ca', method=mech('nernst/x=ca'))
+decor.set_ion('ca', method=mech('nernst/x=ca'))
+#decor.set_ion('ca', method='nernst/x=ca')
 # hh mechanism on the soma and axon.
-cell.paint('"soma"', 'hh')
-cell.paint('"axon"', 'hh')
+decor.paint('"soma"', 'hh')
+decor.paint('"axon"', 'hh')
 # pas mechanism the dendrites.
-cell.paint('"dend"', 'pas')
+decor.paint('"dend"', 'pas')
 # Increase resistivity on dendrites.
-cell.paint('"dend"', rL=500)
-# Attach stimuli that inject 0.8 nA currents for 1 ms, starting at 3 and 8 ms.
-cell.place('"stim_site"', arbor.iclamp(3, 1, current=2))
-cell.place('"stim_site"', arbor.iclamp(8, 1, current=4))
+decor.paint('"dend"', rL=500)
+# Attach stimuli that inject 4 nA current for 1 ms, starting at 3 and 8 ms.
+decor.place('"root"', arbor.iclamp(10, 1, current=5))
+decor.place('"stim_site"', arbor.iclamp(3, 1, current=0.5))
+decor.place('"stim_site"', arbor.iclamp(10, 1, current=0.5))
+decor.place('"stim_site"', arbor.iclamp(8, 1, current=4))
 # Detect spikes at the soma with a voltage threshold of -10 mV.
-cell.place('"axon_end"', arbor.spike_detector(-10))
+decor.place('"axon_end"', arbor.spike_detector(-10))
+
+# Create the policy used to discretise the cell into CVs.
+# Use a single CV for the soma, and CVs of maximum length 1 μm elsewhere.
+soma_policy = arbor.cv_policy_single('"soma"')
+dflt_policy = arbor.cv_policy_max_extent(1.0)
+policy = dflt_policy | soma_policy
+decor.discretization(policy)
 
-# Have one compartment between each sample point.
-cell.compartments_on_segments()
+# Combine morphology with region and locset definitions to make a cable cell.
+cell = arbor.cable_cell(morpho, labels, decor)
+
+print(cell.locations('"axon_end"'))
 
 # Make single cell model.
 m = arbor.single_cell_model(cell)
 
 # Attach voltage probes that sample at 50 kHz.
 m.probe('voltage', where='"root"',  frequency=50000)
-m.probe('voltage', where=loc(2,1),  frequency=50000)
 m.probe('voltage', where='"stim_site"',  frequency=50000)
 m.probe('voltage', where='"axon_end"', frequency=50000)
 
diff --git a/python/flat_cell_builder.cpp b/python/flat_cell_builder.cpp
deleted file mode 100644
index 822ae9c08a92c4ce6902abf54a817f36d3d0499b..0000000000000000000000000000000000000000
--- a/python/flat_cell_builder.cpp
+++ /dev/null
@@ -1,253 +0,0 @@
-#include <any>
-#include <mutex>
-#include <string>
-#include <unordered_map>
-#include <utility>
-#include <vector>
-
-#include <pybind11/pybind11.h>
-#include <pybind11/stl.h>
-
-#include <arbor/cable_cell.hpp>
-#include <arbor/morph/label_parse.hpp>
-#include <arbor/morph/morphology.hpp>
-#include <arbor/morph/primitives.hpp>
-#include <arbor/morph/segment_tree.hpp>
-#include <arbor/util/any_cast.hpp>
-
-#include "conversion.hpp"
-#include "error.hpp"
-#include "strprintf.hpp"
-
-namespace pyarb {
-
-class flat_cell_builder {
-    // The segment tree describing the morphology, constructed additively with
-    // segments that are attached to existing segments using add_cable.
-    arb::segment_tree tree_;
-
-    // The number of unique region names used to label cables as they
-    // are added to the cell.
-    int tag_count_ = 0;
-    // Map from region names to the tag used to identify them.
-    std::unordered_map<std::string, int> tag_map_;
-
-    std::vector<arb::msize_t> cable_distal_segs_;
-
-    arb::label_dict dict_;
-
-    // The morphology is cached, and only updated on request when it is out of date.
-    mutable bool cached_morpho_ = true;
-    mutable arb::morphology morpho_;
-    mutable std::mutex mutex_;
-
-public:
-
-    flat_cell_builder() = default;
-
-
-    // Add a new cable that is attached to the last cable added to the cell.
-    // Returns the id of the new cable.
-    arb::msize_t add_cable(double len,
-                           double r1, double r2, const char* region, int ncomp)
-    {
-        return add_cable(size()? size()-1: arb::mnpos, len, r1, r2, region, ncomp);
-    }
-
-    // Add a new cable that is attached to the parent cable.
-    // Returns the id of the new cable.
-    arb::msize_t add_cable(arb::msize_t parent, double len,
-                           double r1, double r2, const char* region, int ncomp)
-    {
-        using arb::mnpos;
-
-        cached_morpho_ = false;
-
-        // Get tag id of region (add a new tag if region does not already exist).
-        int tag = get_tag(region);
-        const bool at_root = parent==mnpos;
-
-        // Parent id must be in the range [0, size())
-        if (!at_root && parent>=size()) {
-            throw pyarb_error("Invalid parent id.");
-        }
-
-        arb::msize_t p = at_root? mnpos: cable_distal_segs_[parent];
-
-        double z = at_root? 0:                      // attach to root
-                   tree_.segments()[p].dist.z;      // attach to end of a cable
-
-        double dz = len/ncomp;
-        double dr = (r2-r1)/ncomp;
-        for (auto i=0; i<ncomp; ++i) {
-            p = tree_.append(p, {0,0,z+i*dz, r1+i*dr}, {0,0,z+(i+1)*dz, r1+(i+1)*dr}, tag);
-        }
-
-        cable_distal_segs_.push_back(p);
-
-        return cable_distal_segs_.size()-1;
-    }
-
-    void add_label(const char* name, const char* description) {
-        if (auto result = arb::parse_label_expression(description) ) {
-            // The description is a region.
-            if (result->type()==typeid(arb::region)) {
-                if (dict_.locset(name)) {
-                    throw pyarb_error("Region name clashes with a locset.");
-                }
-                auto& reg = std::any_cast<arb::region&>(*result);
-                if (auto r = dict_.region(name)) {
-                    dict_.set(name, join(std::move(reg), std::move(*r)));
-                }
-                else {
-                    dict_.set(name, std::move(reg));
-                }
-            }
-            // The description is a locset.
-            else if (result->type()==typeid(arb::locset)) {
-                if (dict_.region(name)) {
-                    throw pyarb_error("Locset name clashes with a region.");
-                }
-                auto& loc = std::any_cast<arb::locset&>(*result);
-                if (auto l = dict_.locset(name)) {
-                    dict_.set(name, sum(std::move(loc), std::move(*l)));
-                }
-                else {
-                    dict_.set(name, std::move(loc));
-                }
-            }
-            // Error: the description is neither.
-            else {
-                throw pyarb_error("Label describes neither a region nor a locset.");
-            }
-        }
-        else {
-            throw pyarb_error(result.error().what());
-        }
-    }
-
-    const arb::segment_tree& segments() const {
-        return tree_;
-    }
-
-    std::unordered_map<std::string, std::string> labels() const {
-        std::unordered_map<std::string, std::string> map;
-        for (auto& r: dict_.regions()) {
-            map[r.first] = util::pprintf("{}", r.second);
-        }
-        for (auto& l: dict_.locsets()) {
-            map[l.first] = util::pprintf("{}", l.second);
-        }
-
-        return map;
-    }
-
-    const arb::morphology& morphology() const {
-        const std::lock_guard<std::mutex> guard(mutex_);
-        if (!cached_morpho_) {
-            morpho_ = arb::morphology(tree_);
-            cached_morpho_ = true;
-        }
-        return morpho_;
-    }
-
-    arb::cable_cell build() const {
-        auto c = arb::cable_cell(morphology(), dict_);
-        c.default_parameters.discretization = arb::cv_policy_every_segment{};
-        return c;
-    }
-
-    private:
-
-    // Get tag id of region with name.
-    // Add a new tag if region with that name has not already had a tag associated with it.
-    int get_tag(const std::string& name) {
-        using arb::reg::tagged;
-
-        // Name is in the map: return the tag.
-        auto it = tag_map_.find(name);
-        if (it!=tag_map_.end()) {
-            return it->second;
-        }
-        // If the name is not in the map, the next step depends on
-        // whether the name is used for a locst, or a region that is
-        // not in the map, or is not used at all.
-
-        // Name is a locset: error.
-        if (dict_.locset(name)) {
-            throw pyarb_error(util::pprintf("'{}' is a label for a locset."));
-        }
-        // Name is a region: add tag to region definition.
-        else if(auto reg = dict_.region(name)) {
-            tag_map_[name] = ++tag_count_;
-            dict_.set(name, join(*reg, tagged(tag_count_)));
-            return tag_count_;
-        }
-        // Name has not been registerd: make a unique tag and new region.
-        else {
-            tag_map_[name] = ++tag_count_;
-            dict_.set(name, tagged(tag_count_));
-            return tag_count_;
-        }
-    }
-
-    // The number of cable segements used in the cell.
-    std::size_t size() const {
-        return tree_.size();
-    }
-};
-
-void register_flat_builder(pybind11::module& m) {
-    using namespace pybind11::literals;
-
-    pybind11::class_<flat_cell_builder> builder(m, "flat_cell_builder");
-    builder
-        .def(pybind11::init<>())
-        .def("add_cable",
-                [](flat_cell_builder& b, double len, pybind11::object rad, const char* name, int ncomp) {
-                    using pybind11::isinstance;
-                    using pybind11::cast;
-                    if (auto radius = try_cast<double>(rad) ) {
-                        return b.add_cable(len, *radius, *radius, name, ncomp);
-                    }
-
-                    if (auto radii = try_cast<std::pair<double, double>>(rad)) {
-                        return b.add_cable(len, radii->first, radii->second, name, ncomp);
-                    }
-                    else {
-                        throw pyarb_error(
-                            "Radius parameter is not a scalar (constant branch radius) or "
-                            "a tuple (radius at proximal and distal ends respectively).");
-                    }
-                },
-            "length"_a, "radius"_a, "name"_a, "ncomp"_a=1)
-        .def("add_cable",
-                [](flat_cell_builder& b, arb::msize_t p, double len, pybind11::object rad, const char* name, int ncomp) {
-                    using pybind11::isinstance;
-                    using pybind11::cast;
-                    if (auto radius = try_cast<double>(rad) ) {
-                        return b.add_cable(p, len, *radius, *radius, name, ncomp);
-                    }
-
-                    if (auto radii = try_cast<std::pair<double, double>>(rad)) {
-                        return b.add_cable(p, len, radii->first, radii->second, name, ncomp);
-                    }
-                    else {
-                        throw pyarb_error(
-                            "Radius parameter is not a scalar (constant branch radius) or "
-                            "a tuple (radius at proximal and distal ends respectively).");
-                    }
-                },
-            "parent"_a, "length"_a, "radius"_a, "name"_a, "ncomp"_a=1)
-        .def("add_label", &flat_cell_builder::add_label,
-            "name"_a, "description"_a)
-        .def_property_readonly("segments",
-            [](const flat_cell_builder& b) { return b.segments(); })
-        .def_property_readonly("labels",
-            [](const flat_cell_builder& b) { return b.labels(); })
-        .def_property_readonly("morphology",
-            [](const flat_cell_builder& b) { return b.morphology(); })
-        .def("build", &flat_cell_builder::build);
-}
-
-} // namespace pyarb
diff --git a/python/pyarb.cpp b/python/pyarb.cpp
index 3ac9bcf0c8b821fd3da58a049b31660c475d7e60..6d4146c01f844892833f89884d68a990f54851da 100644
--- a/python/pyarb.cpp
+++ b/python/pyarb.cpp
@@ -17,7 +17,6 @@ void register_config(pybind11::module& m);
 void register_contexts(pybind11::module& m);
 void register_domain_decomposition(pybind11::module& m);
 void register_event_generators(pybind11::module& m);
-void register_flat_builder(pybind11::module& m);
 void register_identifiers(pybind11::module& m);
 void register_mechanisms(pybind11::module& m);
 void register_morphology(pybind11::module& m);
@@ -50,7 +49,6 @@ PYBIND11_MODULE(_arbor, m) {
     pyarb::register_contexts(m);
     pyarb::register_domain_decomposition(m);
     pyarb::register_event_generators(m);
-    pyarb::register_flat_builder(m);
     pyarb::register_identifiers(m);
     pyarb::register_mechanisms(m);
     pyarb::register_morphology(m);
diff --git a/python/test/unit/test_cable_probes.py b/python/test/unit/test_cable_probes.py
index 2640eaaa38c9149e373063cdacb1a36127ca3de2..791b2397e22dbe65ff74ed15d02de6e211039d9f 100644
--- a/python/test/unit/test_cable_probes.py
+++ b/python/test/unit/test_cable_probes.py
@@ -25,10 +25,13 @@ class cc_recipe(A.recipe):
         st = A.segment_tree()
         st.append(A.mnpos, (0, 0, 0, 10), (1, 0, 0, 10), 1)
 
-        self.cell = A.cable_cell(st, A.label_dict())
-        self.cell.place('(location 0 0.08)', "expsyn")
-        self.cell.place('(location 0 0.09)', "exp2syn")
-        self.cell.paint('(all)', "hh")
+        dec = A.decor()
+
+        dec.place('(location 0 0.08)', "expsyn")
+        dec.place('(location 0 0.09)', "exp2syn")
+        dec.paint('(all)', "hh")
+
+        self.cell = A.cable_cell(st, A.label_dict(), dec)
 
     def num_cells(self):
         return 1
diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh
index 03111283e361e6aa64ae10af6b0671b90aab0820..316594b01b586a07daa0d75da2ac661e3401e315 100755
--- a/scripts/travis/build.sh
+++ b/scripts/travis/build.sh
@@ -110,11 +110,13 @@ if [[ "${WITH_PYTHON}" == "true" ]]; then
     make pyarb -j4                                                                              || error "building pyarb"
     progress "Python unit tests"
     python$PY $python_path/test/unit/runner.py -v2                                              || error "running python unit tests (serial)"
-    progress "Python examples"
+    progress "Python example: network_ring"
     python$PY $python_path/example/network_ring.py                                              || error "running python network_ring example"
+    progress "Python example: single_cell_model"
     python$PY $python_path/example/single_cell_model.py                                         || error "running python single_cell_model example"
+    progress "Python example: single_cell_recipe"
     python$PY $python_path/example/single_cell_recipe.py                                        || error "running python single_cell_recipe example"
-    python$PY $python_path/example/single_cell_multi_branch.py                                  || error "running python single_cell_multi_branch example"
+    progress "Python example: single_cell_swc"
     python$PY $python_path/example/single_cell_swc.py  $base_path/test/unit/swc/pyramidal.swc   || error "running python single_cell_swc example"
     if [[ "${WITH_DISTRIBUTED}" = "mpi" ]]; then
         if [[ "$TRAVIS_OS_NAME" = "osx" ]]; then
diff --git a/test/common_cells.cpp b/test/common_cells.cpp
index 2b485360b8ba4e5a125449e3397d4ffa41fff561..7a5deb48f7c52a3a6690087085adff174db51d65 100644
--- a/test/common_cells.cpp
+++ b/test/common_cells.cpp
@@ -1,4 +1,5 @@
 #include <arbor/string_literals.hpp>
+#include "arbor/morph/morphology.hpp"
 #include "common_cells.hpp"
 
 namespace arb {
@@ -131,7 +132,7 @@ msize_t soma_cell_builder::add_branch(
     return bid;
 }
 
-cable_cell soma_cell_builder::make_cell() const {
+cable_cell_description  soma_cell_builder::make_cell() const {
     // Test that a valid tree was generated, that is, every branch has
     // either 0 children, or at least 2 children.
     for (auto i: branch_distal_id) {
@@ -150,14 +151,14 @@ cable_cell soma_cell_builder::make_cell() const {
         dict.set(tag.first, reg::tagged(tag.second));
     }
 
-    // Make cable_cell from sample tree and dictionary.
-    cable_cell c(tree, dict);
     auto boundaries = cv_boundaries;
     for (auto& b: boundaries) {
         b = location(b);
     }
-    c.default_parameters.discretization = cv_policy_explicit(boundaries);
-    return c;
+    decor decorations;
+    decorations.set_default(cv_policy_explicit(boundaries));
+    // Construct cable_cell from sample tree, dictionary and decorations.
+    return {std::move(tree), std::move(dict), std::move(decorations)};
 }
 
 /*
@@ -173,17 +174,17 @@ cable_cell soma_cell_builder::make_cell() const {
  *    soma centre, t=[10 ms, 110 ms), 0.1 nA
  */
 
-cable_cell make_cell_soma_only(bool with_stim) {
+cable_cell_description make_cell_soma_only(bool with_stim) {
     using namespace arb::literals;
     soma_cell_builder builder(18.8/2.0);
 
     auto c = builder.make_cell();
-    c.paint("soma"_lab, "hh");
+    c.decorations.paint("soma"_lab, "hh");
     if (with_stim) {
-        c.place(builder.location({0,0.5}), i_clamp{10., 100., 0.1});
+        c.decorations.place(builder.location({0,0.5}), i_clamp{10., 100., 0.1});
     }
 
-    return c;
+    return {c.morph, c.labels, c.decorations};
 }
 
 /*
@@ -207,26 +208,24 @@ cable_cell make_cell_soma_only(bool with_stim) {
  *    end of dendrite, t=[5 ms, 85 ms), 0.3 nA
  */
 
-cable_cell make_cell_ball_and_stick(bool with_stim) {
+cable_cell_description make_cell_ball_and_stick(bool with_stim) {
     using namespace arb::literals;
     soma_cell_builder builder(12.6157/2.0);
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
 
     auto c = builder.make_cell();
-    c.paint("soma"_lab, "hh");
-    c.paint("dend"_lab, "pas");
+    c.decorations.paint("soma"_lab, "hh");
+    c.decorations.paint("dend"_lab, "pas");
     if (with_stim) {
-        c.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+        c.decorations.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
     }
 
-    return c;
+    return {c.morph, c.labels, c.decorations};
 }
 
 /*
  * Create cell with a soma and three-branch dendrite with single branch point:
  *
- * O----======
- *
  * Common properties:
  *    bulk resistivity: 100 Ω·cm [default]
  *    capacitance: 0.01 F/m² [default]
@@ -246,7 +245,7 @@ cable_cell make_cell_ball_and_stick(bool with_stim) {
  *    end of second terminal branch, t=[40 ms, 50 ms), -0.2 nA
  */
 
-cable_cell make_cell_ball_and_3stick(bool with_stim) {
+cable_cell_description make_cell_ball_and_3stick(bool with_stim) {
     using namespace arb::literals;
     soma_cell_builder builder(12.6157/2.0);
     builder.add_branch(0, 100, 0.5, 0.5, 4, "dend");
@@ -254,13 +253,14 @@ cable_cell make_cell_ball_and_3stick(bool with_stim) {
     builder.add_branch(1, 100, 0.5, 0.5, 4, "dend");
 
     auto c = builder.make_cell();
-    c.paint("soma"_lab, "hh");
-    c.paint("dend"_lab, "pas");
+    c.decorations.paint("soma"_lab, "hh");
+    c.decorations.paint("dend"_lab, "pas");
     if (with_stim) {
-        c.place(builder.location({2,1}), i_clamp{5.,  80., 0.45});
-        c.place(builder.location({3,1}), i_clamp{40., 10.,-0.2});
+        c.decorations.place(builder.location({2,1}), i_clamp{5.,  80., 0.45});
+        c.decorations.place(builder.location({3,1}), i_clamp{40., 10.,-0.2});
     }
 
-    return c;
+    return {c.morph, c.labels, c.decorations};
 }
+
 } // namespace arb
diff --git a/test/common_cells.hpp b/test/common_cells.hpp
index bad660a9b0d9434b4e4d89dd97e5637e9026c0a4..44212794a31569d1f104c7fe5e75012c9c80fd59 100644
--- a/test/common_cells.hpp
+++ b/test/common_cells.hpp
@@ -12,6 +12,16 @@ arb::segment_tree segments_from_points(std::vector<arb::mpoint> points,
                                        std::vector<arb::msize_t> parents,
                                        std::vector<int> tags={});
 
+struct cable_cell_description {
+    morphology morph;
+    label_dict labels;
+    decor decorations;
+
+    operator cable_cell() const {
+        return cable_cell(morph, labels, decorations);
+    }
+};
+
 class soma_cell_builder {
     segment_tree tree;
     std::vector<msize_t> branch_distal_id;
@@ -35,7 +45,7 @@ public:
     mlocation location(mlocation) const;
     mcable cable(mcable) const;
 
-    cable_cell make_cell() const;
+    cable_cell_description make_cell() const;
 };
 
 /*
@@ -51,7 +61,7 @@ public:
  *    soma centre, t=[10 ms, 110 ms), 0.1 nA
  */
 
-cable_cell make_cell_soma_only(bool with_stim = true);
+cable_cell_description make_cell_soma_only(bool with_stim = true);
 
 /*
  * Create cell with a soma and unbranched dendrite:
@@ -74,7 +84,7 @@ cable_cell make_cell_soma_only(bool with_stim = true);
  *    end of dendrite, t=[5 ms, 85 ms), 0.3 nA
  */
 
-cable_cell make_cell_ball_and_stick(bool with_stim = true);
+cable_cell_description make_cell_ball_and_stick(bool with_stim = true);
 
 /*
  * Create cell with a soma and three-branch dendrite with single branch point:
@@ -100,6 +110,6 @@ cable_cell make_cell_ball_and_stick(bool with_stim = true);
  *    end of second terminal branch, t=[40 ms, 50 ms), -0.2 nA
  */
 
-cable_cell make_cell_ball_and_3stick(bool with_stim = true);
+cable_cell_description make_cell_ball_and_3stick(bool with_stim = true);
 
 } // namespace arb
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index e709899bd2d15e3e7814d4d6943b85669dc3c0a5..7a0abc1d8ad016724f22bca074635d26a2326775 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -89,6 +89,7 @@ set(unit_sources
     test_any_ptr.cpp
     test_any_visitor.cpp
     test_backend.cpp
+    test_cable_cell.cpp
     test_counter.cpp
     test_cv_geom.cpp
     test_cv_layout.cpp
diff --git a/test/unit/test_cable_cell.cpp b/test/unit/test_cable_cell.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..df2fc66ad91d06eb5ebb441a6e0db626d77c4091
--- /dev/null
+++ b/test/unit/test_cable_cell.cpp
@@ -0,0 +1,58 @@
+#include "../gtest.h"
+#include "common_cells.hpp"
+
+#include "s_expr.hpp"
+
+#include <arbor/cable_cell.hpp>
+#include <arbor/cable_cell_param.hpp>
+#include <arbor/string_literals.hpp>
+
+using namespace arb;
+using namespace arb::literals;
+
+TEST(cable_cell, lid_ranges) {
+
+    // Create a morphology with two branches:
+    //   * branch 0 is a single segment soma followed by a single segment dendrite.
+    //   * branch 1 is an axon attached to the root.
+    segment_tree tree;
+    tree.append(mnpos, {0, 0, 0, 10}, {0, 0, 10, 10}, 1);
+    tree.append(0,     {0, 0, 10, 1}, {0, 0, 100, 1}, 3);
+    tree.append(mnpos, {0, 0, 0, 2}, {0, 0, -20, 2}, 2);
+
+    arb::morphology morph(tree);
+
+    label_dict dict;
+    dict.set("term", locset("(terminal)"));
+
+    decor decorations;
+
+    mlocation_list empty_sites = {};
+    mlocation_list three_sites = {{0, 0.1}, {1, 0.2}, {1, 0.7}};
+
+    // Place synapses and threshold detectors in interleaved order.
+    // Note: there are 2 terminal points.
+    auto idx1 = decorations.place("term"_lab, "expsyn");
+    auto idx2 = decorations.place("term"_lab, "expsyn");
+    auto idx3 = decorations.place("term"_lab, threshold_detector{-10});
+    auto idx4 = decorations.place(empty_sites, "expsyn");
+    auto idx5 = decorations.place("term"_lab, threshold_detector{-20});
+    auto idx6 = decorations.place(three_sites, "expsyn");
+
+    cable_cell cell(morph, dict, decorations);
+
+    // Get the assigned lid ranges for each placement
+    auto r1 = cell.placed_lid_range(idx1);
+    auto r2 = cell.placed_lid_range(idx2);
+    auto r3 = cell.placed_lid_range(idx3);
+    auto r4 = cell.placed_lid_range(idx4);
+    auto r5 = cell.placed_lid_range(idx5);
+    auto r6 = cell.placed_lid_range(idx6);
+
+    EXPECT_EQ(idx1, 0u); EXPECT_EQ(r1.begin, 0u); EXPECT_EQ(r1.end, 2u);
+    EXPECT_EQ(idx2, 1u); EXPECT_EQ(r2.begin, 2u); EXPECT_EQ(r2.end, 4u);
+    EXPECT_EQ(idx3, 2u); EXPECT_EQ(r3.begin, 0u); EXPECT_EQ(r3.end, 2u);
+    EXPECT_EQ(idx4, 3u); EXPECT_EQ(r4.begin, 4u); EXPECT_EQ(r4.end, 4u);
+    EXPECT_EQ(idx5, 4u); EXPECT_EQ(r5.begin, 2u); EXPECT_EQ(r5.end, 4u);
+    EXPECT_EQ(idx6, 5u); EXPECT_EQ(r6.begin, 4u); EXPECT_EQ(r6.end, 7u);
+}
diff --git a/test/unit/test_cv_geom.cpp b/test/unit/test_cv_geom.cpp
index 8e883ab0809037cf63baaf328c0e10bf0ec027bd..13410e7f861659a5db90a353bb3fc0364bdb4515 100644
--- a/test/unit/test_cv_geom.cpp
+++ b/test/unit/test_cv_geom.cpp
@@ -448,7 +448,7 @@ TEST(cv_geom, multicell) {
     using namespace common_morphology;
     using index_type = cv_geometry::index_type;
 
-    cable_cell cell = cable_cell{m_reg_b6};
+    cable_cell cell = cable_cell(m_reg_b6);
 
     cv_geometry geom = cv_geometry_from_ends(cell, ls::on_branches(0.5));
     unsigned n_cv = geom.size();
diff --git a/test/unit/test_cv_layout.cpp b/test/unit/test_cv_layout.cpp
index bcc0c5a32cf35a46281f2ccfebb84e9fe0617791..d4fbe24dc4a8396d216e8479e8ac380283309da2 100644
--- a/test/unit/test_cv_layout.cpp
+++ b/test/unit/test_cv_layout.cpp
@@ -19,7 +19,7 @@ using util::make_span;
 TEST(cv_layout, empty) {
     using namespace common_morphology;
 
-    cable_cell empty_cell{m_empty};
+    cable_cell empty_cell{m_empty, {}, {}};
     fvm_cv_discretization D = fvm_cv_discretize(empty_cell, neuron_parameter_defaults);
 
     EXPECT_TRUE(D.empty());
@@ -50,7 +50,7 @@ TEST(cv_layout, trivial) {
         // they are not 'connected', and will generate multiple CVs.
         if (p.second.branch_children(mnpos).size()>1u) continue;
 
-        cells.emplace_back(p.second);
+        cells.emplace_back(p.second, label_dict{}, decor{});
         n_cv += !p.second.empty(); // one cv per non-empty cell
     }
 
@@ -91,10 +91,11 @@ TEST(cv_layout, cable) {
     auto params = neuron_parameter_defaults;
     params.init_membrane_potential = 0;
 
-    cable_cell c(morph);
-    c.paint(reg::cable(0, 0.0, 0.2), init_membrane_potential{10});
-    c.paint(reg::cable(0, 0.2, 0.7), init_membrane_potential{20});
-    c.paint(reg::cable(0, 0.7, 1.0), init_membrane_potential{30});
+    decor decs;
+    decs.paint(reg::cable(0, 0.0, 0.2), init_membrane_potential{10});
+    decs.paint(reg::cable(0, 0.2, 0.7), init_membrane_potential{20});
+    decs.paint(reg::cable(0, 0.7, 1.0), init_membrane_potential{30});
+    cable_cell c(morph, {}, decs);
 
     params.discretization = cv_policy_explicit(ls::nil());
     fvm_cv_discretization D = fvm_cv_discretize(c, params);
@@ -117,7 +118,7 @@ TEST(cv_layout, cable_conductance) {
     auto params = neuron_parameter_defaults;
     params.axial_resistivity = rho;
 
-    cable_cell c(morph);
+    cable_cell c(morph, {}, {});
     double radius = c.embedding().radius(mlocation{0, 0.5});
     double length = c.embedding().branch_length(0);
 
@@ -139,7 +140,7 @@ TEST(cv_layout, zero_size_cv) {
     // Six branches; branches 0, 1 and 2 meet at (0, 1); branches
     // 2, 3, 4, and 5 meet at (2, 1). Terminal branches are 1, 3, 4, and 5.
     auto morph = common_morphology::m_reg_b6;
-    cable_cell cell(morph);
+    cable_cell cell(morph, {}, {});
 
     auto params = neuron_parameter_defaults;
     const double rho = 5.; // [Ω·cm]
diff --git a/test/unit/test_domain_decomposition.cpp b/test/unit/test_domain_decomposition.cpp
index 8d66041dca1ea26e9add71b53fd390f561a19a80..8d7e97506722f07a93be786233bd210f24834d31 100644
--- a/test/unit/test_domain_decomposition.cpp
+++ b/test/unit/test_domain_decomposition.cpp
@@ -68,8 +68,8 @@ namespace {
 
         arb::util::unique_any get_cell_description(cell_gid_type) const override {
             auto c = arb::make_cell_soma_only(false);
-            c.place(mlocation{0,1}, gap_junction_site{});
-            return {std::move(c)};
+            c.decorations.place(mlocation{0,1}, gap_junction_site{});
+            return {arb::cable_cell(c)};
         }
 
         cell_kind get_cell_kind(cell_gid_type gid) const override {
diff --git a/test/unit/test_event_delivery.cpp b/test/unit/test_event_delivery.cpp
index d21fa4a5ec7f578bb35f33a14fadcc3b85bb36e7..f8fa888e2dd224178a80059be637f4213e92a65b 100644
--- a/test/unit/test_event_delivery.cpp
+++ b/test/unit/test_event_delivery.cpp
@@ -31,13 +31,14 @@ struct test_recipe: public n_cable_cell_recipe {
         segment_tree st;
         st.append(mnpos, {0,0, 0,10}, {0,0,20,10}, 1);
 
-        label_dict d;
-        d.set("soma", arb::reg::tagged(1));
-
-        cable_cell c(st, d);
-        c.place(mlocation{0, 0.5}, "expsyn");
-        c.place(mlocation{0, 0.5}, threshold_detector{-64});
-        c.place(mlocation{0, 0.5}, gap_junction_site{});
+        label_dict labels;
+        labels.set("soma", arb::reg::tagged(1));
+
+        decor decorations;
+        decorations.place(mlocation{0, 0.5}, "expsyn");
+        decorations.place(mlocation{0, 0.5}, threshold_detector{-64});
+        decorations.place(mlocation{0, 0.5}, gap_junction_site{});
+        cable_cell c(st, labels, decorations);
 
         return c;
     }
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index 4c6dc4cbfe9b81896af722b4e9bb92e8d8dc170a..6132b3b1346996d2c00dc3f35fe6b41fbd64022b 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -6,6 +6,7 @@
 #include <arbor/math.hpp>
 #include <arbor/mechcat.hpp>
 
+#include "arbor/cable_cell_param.hpp"
 #include "arbor/morph/morphology.hpp"
 #include "arbor/morph/segment_tree.hpp"
 #include "fvm_layout.hpp"
@@ -30,25 +31,35 @@ using util::value_by_key;
 namespace {
     struct system {
         std::vector<soma_cell_builder> builders;
-        std::vector<cable_cell> cells;
+        std::vector<cable_cell_description> descriptions;
+
+        std::vector<arb::cable_cell> cells() const {
+            std::vector<arb::cable_cell> C;
+            C.reserve(descriptions.size());
+            for (auto& d: descriptions) {
+                C.emplace_back(d);
+            }
+            return C;
+        }
+
     };
 
     system two_cell_system() {
         system s;
-        auto& cells = s.cells;
+        auto& descriptions = s.descriptions;
 
         // Cell 0: simple ball and stick
         {
             soma_cell_builder builder(12.6157/2.0);
             builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
 
-            cells.push_back(builder.make_cell());
-            auto& cell = cells.back();
-            cell.paint("\"soma\"", "hh");
-            cell.paint("\"dend\"", "pas");
-            cell.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+            auto description = builder.make_cell();
+            description.decorations.paint("\"soma\"", "hh");
+            description.decorations.paint("\"dend\"", "pas");
+            description.decorations.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
 
             s.builders.push_back(std::move(builder));
+            descriptions.push_back(description);
         }
 
         // Cell 1: ball and 3-stick, but with uneven dendrite
@@ -84,26 +95,26 @@ namespace {
             auto b1 = b.add_branch(0, 200, 0.5,  0.5, 4,  "dend");
             auto b2 = b.add_branch(1, 300, 0.4,  0.4, 4,  "dend");
             auto b3 = b.add_branch(1, 180, 0.35, 0.35, 4, "dend");
-            cells.push_back(b.make_cell());
-            auto& cell = cells.back();
+            auto desc = b.make_cell();
 
-            cell.paint("\"soma\"", "hh");
-            cell.paint("\"dend\"", "pas");
+            desc.decorations.paint("\"soma\"", "hh");
+            desc.decorations.paint("\"dend\"", "pas");
 
             using ::arb::reg::branch;
             auto c1 = reg::cable(b1-1, b.location({b1, 0}).pos, 1);
             auto c2 = reg::cable(b2-1, b.location({b2, 0}).pos, 1);
             auto c3 = reg::cable(b3-1, b.location({b3, 0}).pos, 1);
-            cell.paint(c1, membrane_capacitance{0.017});
-            cell.paint(c2, membrane_capacitance{0.013});
-            cell.paint(c3, membrane_capacitance{0.018});
+            desc.decorations.paint(c1, membrane_capacitance{0.017});
+            desc.decorations.paint(c2, membrane_capacitance{0.013});
+            desc.decorations.paint(c3, membrane_capacitance{0.018});
 
-            cell.place(b.location({2,1}), i_clamp{5.,  80., 0.45});
-            cell.place(b.location({3,1}), i_clamp{40., 10.,-0.2});
+            desc.decorations.place(b.location({2,1}), i_clamp{5.,  80., 0.45});
+            desc.decorations.place(b.location({3,1}), i_clamp{40., 10.,-0.2});
 
-            cell.default_parameters.axial_resistivity = 90;
+            desc.decorations.set_default(axial_resistivity{90});
 
             s.builders.push_back(std::move(b));
+            descriptions.push_back(desc);
         }
 
         return s;
@@ -118,19 +129,20 @@ namespace {
 
 TEST(fvm_layout, mech_index) {
     auto system = two_cell_system();
-    auto& cells = system.cells;
+    auto& descriptions = system.descriptions;
     auto& builders = system.builders;
-    check_two_cell_system(cells);
 
     // Add four synapses of two varieties across the cells.
-    cells[0].place(builders[0].location({1, 0.4}), "expsyn");
-    cells[0].place(builders[0].location({1, 0.4}), "expsyn");
-    cells[1].place(builders[1].location({2, 0.4}), "exp2syn");
-    cells[1].place(builders[1].location({3, 0.4}), "expsyn");
+    descriptions[0].decorations.place(builders[0].location({1, 0.4}), "expsyn");
+    descriptions[0].decorations.place(builders[0].location({1, 0.4}), "expsyn");
+    descriptions[1].decorations.place(builders[1].location({2, 0.4}), "exp2syn");
+    descriptions[1].decorations.place(builders[1].location({3, 0.4}), "expsyn");
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
 
+    auto cells = system.cells();
+    check_two_cell_system(cells);
     fvm_cv_discretization D = fvm_cv_discretize(cells, gprop.default_parameters);
     fvm_mechanism_data M = fvm_build_mechanism_data(gprop, cells, D);
 
@@ -239,13 +251,14 @@ TEST(fvm_layout, coalescing_synapses) {
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 4, "dend");
 
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
-        cell.place(builder.location({1, 0.3}), "expsyn");
-        cell.place(builder.location({1, 0.5}), "expsyn");
-        cell.place(builder.location({1, 0.7}), "expsyn");
-        cell.place(builder.location({1, 0.9}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.3}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.5}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.7}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.9}), "expsyn");
 
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
@@ -254,14 +267,15 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_EQ(ivec({1, 1, 1, 1}), expsyn_config.multiplicity);
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(builder.location({1, 0.3}), "expsyn");
-        cell.place(builder.location({1, 0.5}), "exp2syn");
-        cell.place(builder.location({1, 0.7}), "expsyn");
-        cell.place(builder.location({1, 0.9}), "exp2syn");
+        desc.decorations.place(builder.location({1, 0.3}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.5}), "exp2syn");
+        desc.decorations.place(builder.location({1, 0.7}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.9}), "exp2syn");
 
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
@@ -274,13 +288,14 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_EQ(ivec({1, 1}), exp2syn_config.multiplicity);
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
-        cell.place(builder.location({1, 0.3}), "expsyn");
-        cell.place(builder.location({1, 0.5}), "expsyn");
-        cell.place(builder.location({1, 0.7}), "expsyn");
-        cell.place(builder.location({1, 0.9}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.3}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.5}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.7}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.9}), "expsyn");
 
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_no_coalesce, {cell}, D);
 
@@ -289,14 +304,15 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_TRUE(expsyn_config.multiplicity.empty());
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(builder.location({1, 0.3}), "expsyn");
-        cell.place(builder.location({1, 0.5}), "exp2syn");
-        cell.place(builder.location({1, 0.7}), "expsyn");
-        cell.place(builder.location({1, 0.9}), "exp2syn");
+        desc.decorations.place(builder.location({1, 0.3}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.5}), "exp2syn");
+        desc.decorations.place(builder.location({1, 0.7}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.9}), "exp2syn");
 
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_no_coalesce, {cell}, D);
 
@@ -309,14 +325,15 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_TRUE(exp2syn_config.multiplicity.empty());
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(builder.location({1, 0.3}), "expsyn");
-        cell.place(builder.location({1, 0.3}), "expsyn");
-        cell.place(builder.location({1, 0.7}), "expsyn");
-        cell.place(builder.location({1, 0.7}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.3}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.3}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.7}), "expsyn");
+        desc.decorations.place(builder.location({1, 0.7}), "expsyn");
 
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
@@ -325,14 +342,15 @@ TEST(fvm_layout, coalescing_synapses) {
         EXPECT_EQ(ivec({2, 2}), expsyn_config.multiplicity);
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 0.2));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 0.2));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0.1, 0.2));
-        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 0.1, 0.2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 0.2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 0.2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 0.1, 0.2));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc("expsyn", 0.1, 0.2));
 
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
@@ -347,18 +365,19 @@ TEST(fvm_layout, coalescing_synapses) {
         }
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 0, 3));
-        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 1, 3));
-        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 0, 3));
-        cell.place(builder.location({1, 0.7}), syn_desc("expsyn", 1, 3));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 2));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 1, 2));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 2));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn", 1, 2));
-
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc("expsyn", 0, 3));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc("expsyn", 1, 3));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc("expsyn", 0, 3));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc("expsyn", 1, 3));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 1, 2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 0, 2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn", 1, 2));
+
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
@@ -374,20 +393,21 @@ TEST(fvm_layout, coalescing_synapses) {
         }
     }
     {
-        auto cell = builder.make_cell();
+        auto desc = builder.make_cell();
 
         // Add synapses of two varieties.
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
-        cell.place(builder.location({1, 0.3}), syn_desc_2("exp2syn", 4, 1));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  5, 1));
-        cell.place(builder.location({1, 0.3}), syn_desc_2("exp2syn", 1, 3));
-        cell.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
-        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 2));
-        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 1));
-        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 1));
-        cell.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 2));
-
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc_2("exp2syn", 4, 1));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn",  5, 1));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc_2("exp2syn", 1, 3));
+        desc.decorations.place(builder.location({1, 0.3}), syn_desc("expsyn",  1, 2));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 2));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 1));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 1));
+        desc.decorations.place(builder.location({1, 0.7}), syn_desc_2("exp2syn", 2, 2));
+
+        cable_cell cell(desc);
         fvm_cv_discretization D = fvm_cv_discretize({cell}, neuron_parameter_defaults);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop_coalesce, {cell}, D);
 
@@ -407,7 +427,7 @@ TEST(fvm_layout, coalescing_synapses) {
 
 TEST(fvm_layout, synapse_targets) {
     auto system = two_cell_system();
-    auto& cells = system.cells;
+    auto& descriptions = system.descriptions;
     auto& builders = system.builders;
 
     // Add synapses with different parameter values so that we can
@@ -425,18 +445,19 @@ TEST(fvm_layout, synapse_targets) {
         return mechanism_desc(name).set("e", syn_e.at(idx));
     };
 
-    cells[0].place(builders[0].location({1, 0.9}), syn_desc("expsyn", 0));
-    cells[0].place(builders[0].location({0, 0.5}), syn_desc("expsyn", 1));
-    cells[0].place(builders[0].location({1, 0.4}), syn_desc("expsyn", 2));
+    descriptions[0].decorations.place(builders[0].location({1, 0.9}), syn_desc("expsyn", 0));
+    descriptions[0].decorations.place(builders[0].location({0, 0.5}), syn_desc("expsyn", 1));
+    descriptions[0].decorations.place(builders[0].location({1, 0.4}), syn_desc("expsyn", 2));
 
-    cells[1].place(builders[1].location({2, 0.4}), syn_desc("exp2syn", 3));
-    cells[1].place(builders[1].location({1, 0.4}), syn_desc("exp2syn", 4));
-    cells[1].place(builders[1].location({3, 0.4}), syn_desc("expsyn", 5));
-    cells[1].place(builders[1].location({3, 0.7}), syn_desc("exp2syn", 6));
+    descriptions[1].decorations.place(builders[1].location({2, 0.4}), syn_desc("exp2syn", 3));
+    descriptions[1].decorations.place(builders[1].location({1, 0.4}), syn_desc("exp2syn", 4));
+    descriptions[1].decorations.place(builders[1].location({3, 0.4}), syn_desc("expsyn", 5));
+    descriptions[1].decorations.place(builders[1].location({3, 0.7}), syn_desc("exp2syn", 6));
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
 
+    auto cells = system.cells();
     fvm_cv_discretization D = fvm_cv_discretize(cells, gprop.default_parameters);
     fvm_mechanism_data M = fvm_build_mechanism_data(gprop, cells, D);
 
@@ -541,13 +562,13 @@ TEST(fvm_layout, density_norm_area) {
     hh_3["gkbar"] = seg3_gkbar;
     hh_3["gl"] = seg3_gl;
 
-    auto cell = builder.make_cell();
-    cell.paint("\"soma\"", std::move(hh_0));
-    cell.paint("\"reg1\"", std::move(hh_1));
-    cell.paint("\"reg2\"", std::move(hh_2));
-    cell.paint("\"reg3\"", std::move(hh_3));
+    auto desc = builder.make_cell();
+    desc.decorations.paint("\"soma\"", std::move(hh_0));
+    desc.decorations.paint("\"reg1\"", std::move(hh_1));
+    desc.decorations.paint("\"reg2\"", std::move(hh_2));
+    desc.decorations.paint("\"reg3\"", std::move(hh_3));
 
-    std::vector<cable_cell> cells{std::move(cell)};
+    std::vector<cable_cell> cells{desc};
 
     int ncv = 11;
     std::vector<double> expected_gkbar(ncv, dflt_gkbar);
@@ -639,13 +660,13 @@ TEST(fvm_layout, density_norm_area_partial) {
     hh_end["gl"] = end_gl;
     hh_end["gkbar"] = end_gkbar;
 
-    auto cell = builder.make_cell();
-    cell.default_parameters.discretization = cv_policy_fixed_per_branch(1);
+    auto desc = builder.make_cell();
+    desc.decorations.set_default(cv_policy_fixed_per_branch(1));
 
-    cell.paint(builder.cable({1, 0., 0.3}), hh_begin);
-    cell.paint(builder.cable({1, 0.4, 1.}), hh_end);
+    desc.decorations.paint(builder.cable({1, 0., 0.3}), hh_begin);
+    desc.decorations.paint(builder.cable({1, 0.4, 1.}), hh_end);
 
-    std::vector<cable_cell> cells{std::move(cell)};
+    std::vector<cable_cell> cells{desc};
 
     // Area of whole cell (which is area of the 1 branch)
     double area = cells[0].embedding().integrate_area({0, 0., 1});
@@ -690,9 +711,9 @@ TEST(fvm_layout, density_norm_area_partial) {
 }
 
 TEST(fvm_layout, valence_verify) {
-    auto cell = soma_cell_builder(6).make_cell();
-    cell.paint("\"soma\"", "test_cl_valence");
-    std::vector<cable_cell> cells{std::move(cell)};
+    auto desc = soma_cell_builder(6).make_cell();
+    desc.decorations.paint("\"soma\"", "test_cl_valence");
+    std::vector<cable_cell> cells{desc};
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
@@ -780,14 +801,14 @@ TEST(fvm_layout, ion_weights) {
 
     for (auto run: count_along(mech_branches)) {
         SCOPED_TRACE("run "+std::to_string(run));
-        auto c = builder.make_cell();
+        auto desc = builder.make_cell();
 
         for (auto i: mech_branches[run]) {
             auto cab = builder.cable({i, 0, 1});
-            c.paint(reg::cable(cab.branch, cab.prox_pos, cab.dist_pos), "test_ca");
+            desc.decorations.paint(reg::cable(cab.branch, cab.prox_pos, cab.dist_pos), "test_ca");
         }
 
-        std::vector<cable_cell> cells{std::move(c)};
+        std::vector<cable_cell> cells{desc};
 
         fvm_cv_discretization D = fvm_cv_discretize(cells, gprop.default_parameters);
         fvm_mechanism_data M = fvm_build_mechanism_data(gprop, cells, D);
@@ -819,12 +840,12 @@ TEST(fvm_layout, revpot) {
     builder.add_branch(0, 100, 0.5, 0.5, 1, "dend");
     builder.add_branch(1, 200, 0.5, 0.5, 1, "dend");
     builder.add_branch(1, 100, 0.5, 0.5, 1, "dend");
-    auto cell = builder.make_cell();
-    cell.paint("\"soma\"", "read_eX/c");
-    cell.paint("\"soma\"", "read_eX/a");
-    cell.paint("\"dend\"", "read_eX/a");
+    auto desc = builder.make_cell();
+    desc.decorations.paint("\"soma\"", "read_eX/c");
+    desc.decorations.paint("\"soma\"", "read_eX/a");
+    desc.decorations.paint("\"dend\"", "read_eX/a");
 
-    std::vector<cable_cell> cells{cell, cell};
+    std::vector<cable_cell_description> descriptions{desc, desc};
 
     cable_cell_global_properties gprop;
     gprop.default_parameters = neuron_parameter_defaults;
@@ -843,6 +864,7 @@ TEST(fvm_layout, revpot) {
         auto test_gprop = gprop;
         test_gprop.default_parameters.reversal_potential_method["b"] = write_eb_ec;
 
+        std::vector<cable_cell> cells{descriptions[0], descriptions[1]};
         fvm_cv_discretization D = fvm_cv_discretize(cells, test_gprop.default_parameters);
         EXPECT_THROW(fvm_build_mechanism_data(test_gprop, cells, D), cable_cell_error);
     }
@@ -852,25 +874,29 @@ TEST(fvm_layout, revpot) {
         auto test_gprop = gprop;
         test_gprop.default_parameters.reversal_potential_method["b"] = write_eb_ec;
         test_gprop.default_parameters.reversal_potential_method["c"] = write_eb_ec;
-        cells[1].default_parameters.reversal_potential_method["c"] = "write_eX/c";
+        descriptions[1].decorations.set_default(ion_reversal_potential_method{"c", "write_eX/c"});
+        std::vector<cable_cell> cells{descriptions[0], descriptions[1]};
 
         fvm_cv_discretization D = fvm_cv_discretize(cells, test_gprop.default_parameters);
         EXPECT_THROW(fvm_build_mechanism_data(test_gprop, cells, D), cable_cell_error);
     }
 
-    auto& cell1_prop = cells[1].default_parameters;
-    cell1_prop.reversal_potential_method.clear();
-    cell1_prop.reversal_potential_method["b"] = write_eb_ec;
-    cell1_prop.reversal_potential_method["c"] = write_eb_ec;
+    {
+        auto& cell1_prop = const_cast<cable_cell_parameter_set&>(descriptions[1].decorations.defaults());
+        cell1_prop.reversal_potential_method.clear();
+        descriptions[1].decorations.set_default(ion_reversal_potential_method{"b", write_eb_ec});
+        descriptions[1].decorations.set_default(ion_reversal_potential_method{"c", write_eb_ec});
 
-    fvm_cv_discretization D = fvm_cv_discretize(cells, gprop.default_parameters);
-    fvm_mechanism_data M = fvm_build_mechanism_data(gprop, cells, D);
+        std::vector<cable_cell> cells{descriptions[0], descriptions[1]};
+        fvm_cv_discretization D = fvm_cv_discretize(cells, gprop.default_parameters);
+        fvm_mechanism_data M = fvm_build_mechanism_data(gprop, cells, D);
 
-    // Only CV which needs write_multiple_eX/x=b,y=c is the soma (first CV)
-    // of the second cell.
-    auto soma1_index = D.geometry.cell_cv_divs[1];
-    ASSERT_EQ(1u, M.mechanisms.count(write_eb_ec.name()));
-    EXPECT_EQ((std::vector<fvm_index_type>(1, soma1_index)), M.mechanisms.at(write_eb_ec.name()).cv);
+        // Only CV which needs write_multiple_eX/x=b,y=c is the soma (first CV)
+        // of the second cell.
+        auto soma1_index = D.geometry.cell_cv_divs[1];
+        ASSERT_EQ(1u, M.mechanisms.count(write_eb_ec.name()));
+        EXPECT_EQ((std::vector<fvm_index_type>(1, soma1_index)), M.mechanisms.at(write_eb_ec.name()).cv);
+    }
 }
 
 TEST(fvm_layout, vinterp_cable) {
@@ -883,11 +909,12 @@ TEST(fvm_layout, vinterp_cable) {
     arb::segment_tree tree;
     tree.append(mnpos, { 0,0,0,1}, {10,0,0,1}, 1);
     arb::morphology m(tree);
-    cable_cell cell(m);
+    decor d;
 
     // CV midpoints at branch pos 0.1, 0.3, 0.5, 0.7, 0.9.
     // Expect voltage reference locations to be CV modpoints.
-    cell.default_parameters.discretization = cv_policy_fixed_per_branch(5);
+    d.set_default(cv_policy_fixed_per_branch(5));
+    cable_cell cell{m, {}, d};
     fvm_cv_discretization D = fvm_cv_discretize(cell, neuron_parameter_defaults);
 
     // Test locations, either side of CV midpoints plus extrema, CV boundaries.
@@ -941,12 +968,13 @@ TEST(fvm_layout, vinterp_forked) {
     tree.append(    0, {10., 20., 0., 1}, 1);
     tree.append(    0, {10.,-20., 0., 1}, 1);
     morphology m(tree);
-    cable_cell cell(m);
+    decor d;
 
     // CV 0 contains branch 0 and the fork point; CV 1 and CV 2 have CV 0 as parent,
     // and contain branches 1 and 2 respectively, excluding the fork point.
     mlocation_list cv_ends{{1, 0.}, {2, 0.}};
-    cell.default_parameters.discretization = cv_policy_explicit(cv_ends);
+    d.set_default(cv_policy_explicit(cv_ends));
+    cable_cell cell{m, {}, d};
     fvm_cv_discretization D = fvm_cv_discretize(cell, neuron_parameter_defaults);
 
     // Points in branch 0 should only get CV 0 for interpolation.
@@ -996,13 +1024,14 @@ TEST(fvm_layout, iinterp) {
     std::vector<std::string> label;
     for (auto& p: test_morphologies) {
         if (p.second.empty()) continue;
+        decor d;
 
-        cells.emplace_back(p.second);
-        cells.back().default_parameters.discretization = cv_policy_fixed_per_branch(3);
+        d.set_default(cv_policy_fixed_per_branch(3));
+        cells.emplace_back(cable_cell{p.second, {}, d});
         label.push_back(p.first+": forks-at-end"s);
 
-        cells.emplace_back(p.second);
-        cells.back().default_parameters.discretization = cv_policy_fixed_per_branch(3, cv_policy_flag::interior_forks);
+        d.set_default(cv_policy_fixed_per_branch(3, cv_policy_flag::interior_forks));
+        cells.emplace_back(cable_cell{p.second, {}, d});
         label.push_back(p.first+": interior-forks"s);
     }
 
@@ -1039,18 +1068,19 @@ TEST(fvm_layout, iinterp) {
     // 2. Weird discretization: test points where the interpolated current has to be zero.
     // Use the same cell/discretiazation as in vinterp_forked test:
 
-    // Cable cell with three branchses; branches 0 has child branches 1 and 2.
+    // Cable cell with three branches; branch 0 has child branches 1 and 2.
     segment_tree tree;
     tree.append(mnpos, {0., 0., 0., 1.}, {10., 0., 0., 1}, 1);
     tree.append(    0, {10., 20., 0., 1}, 1);
     tree.append(    0, {10.,-20., 0., 1}, 1);
     morphology m(tree);
-    cable_cell cell(m);
+    decor d;
 
     // CV 0 contains branch 0 and the fork point; CV 1 and CV 2 have CV 0 as parent,
     // and contain branches 1 and 2 respectively, excluding the fork point.
     mlocation_list cv_ends{{1, 0.}, {2, 0.}};
-    cell.default_parameters.discretization = cv_policy_explicit(cv_ends);
+    d.set_default(cv_policy_explicit(cv_ends));
+    cable_cell cell{m, {}, d};
     D = fvm_cv_discretize(cell, neuron_parameter_defaults);
 
     // Expect axial current interpolations on branches 1 and 2 to match CV 1 and 2
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 455ed8257f6f8d47456f4459b0a0ccf3fbf8ff27..e95018ac0884a24fd91ac209fee1f47f96b41930 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -96,9 +96,9 @@ public:
     }
 
     arb::util::unique_any get_cell_description(cell_gid_type gid) const override {
-        cable_cell c = soma_cell_builder(20).make_cell();
-        c.place(mlocation{0, 1}, gap_junction_site{});
-        return {std::move(c)};
+        auto c = soma_cell_builder(20).make_cell();
+        c.decorations.place(mlocation{0, 1}, gap_junction_site{});
+        return {cable_cell{c}};
     }
 
     cell_kind get_cell_kind(cell_gid_type gid) const override {
@@ -161,9 +161,9 @@ public:
     }
 
     arb::util::unique_any get_cell_description(cell_gid_type) const override {
-        cable_cell c = soma_cell_builder(20).make_cell();
-        c.place(mlocation{0,1}, gap_junction_site{});
-        return {std::move(c)};
+        auto c = soma_cell_builder(20).make_cell();
+        c.decorations.place(mlocation{0,1}, gap_junction_site{});
+        return {cable_cell{c}};
     }
 
     cell_kind get_cell_kind(cell_gid_type gid) const override {
@@ -262,21 +262,23 @@ TEST(fvm_lowered, target_handles) {
     }
     arb::execution_context context(resources);
 
-    cable_cell cells[] = {
+    cable_cell_description descriptions[] = {
         make_cell_ball_and_stick(),
         make_cell_ball_and_3stick()
     };
 
-    EXPECT_EQ(cells[0].morphology().num_branches(), 1u);
-    EXPECT_EQ(cells[1].morphology().num_branches(), 3u);
-
     // (in increasing target order)
-    cells[0].place(mlocation{0, 0.7}, "expsyn");
-    cells[0].place(mlocation{0, 0.3}, "expsyn");
-    cells[1].place(mlocation{2, 0.2}, "exp2syn");
-    cells[1].place(mlocation{2, 0.8}, "expsyn");
+    descriptions[0].decorations.place(mlocation{0, 0.7}, "expsyn");
+    descriptions[0].decorations.place(mlocation{0, 0.3}, "expsyn");
+    descriptions[1].decorations.place(mlocation{2, 0.2}, "exp2syn");
+    descriptions[1].decorations.place(mlocation{2, 0.8}, "expsyn");
 
-    cells[1].place(mlocation{0, 0}, threshold_detector{3.3});
+    descriptions[1].decorations.place(mlocation{0, 0}, threshold_detector{3.3});
+
+    cable_cell cells[] = {descriptions[0], descriptions[1]};
+
+    EXPECT_EQ(cells[0].morphology().num_branches(), 1u);
+    EXPECT_EQ(cells[1].morphology().num_branches(), 3u);
 
     std::vector<target_handle> targets;
     std::vector<fvm_index_type> cell_to_intdom;
@@ -320,6 +322,7 @@ TEST(fvm_lowered, target_handles) {
 
 }
 
+
 TEST(fvm_lowered, stimulus) {
     // Ball-and-stick with two stimuli:
     //
@@ -339,13 +342,14 @@ TEST(fvm_lowered, stimulus) {
     }
     arb::execution_context context(resources);
 
-    std::vector<cable_cell> cells;
-    cells.push_back(make_cell_ball_and_stick(false));
+    auto desc = make_cell_ball_and_stick(false);
 
     // At end of stick
-    cells[0].place(mlocation{0,1},   i_clamp{5., 80., 0.3});
+    desc.decorations.place(mlocation{0,1},   i_clamp{5., 80., 0.3});
     // On the soma CV, which is over the approximate interval: (cable 0 0 0.1)
-    cells[0].place(mlocation{0,0.05}, i_clamp{1., 2.,  0.1});
+    desc.decorations.place(mlocation{0,0.05}, i_clamp{1., 2.,  0.1});
+
+    std::vector<cable_cell> cells{desc};
 
     const fvm_size_type soma_cv = 0u;
     const fvm_size_type tip_cv = 5u;
@@ -437,17 +441,17 @@ TEST(fvm_lowered, derived_mechs) {
 
         switch (i) {
             case 0:
-                cell.paint(reg::all(), "test_kin1");
+                cell.decorations.paint(reg::all(), "test_kin1");
                 break;
             case 1:
-                cell.paint(reg::all(), "custom_kin1");
+                cell.decorations.paint(reg::all(), "custom_kin1");
                 break;
             case 2:
-                cell.paint(reg::all(), "test_kin1");
-                cell.paint(reg::all(), "custom_kin1");
+                cell.decorations.paint(reg::all(), "test_kin1");
+                cell.decorations.paint(reg::all(), "custom_kin1");
                 break;
         }
-        cells.push_back(std::move(cell));
+        cells.push_back(cell);
     }
 
     cable1d_recipe rec(cells);
@@ -543,8 +547,8 @@ TEST(fvm_lowered, read_valence) {
 
         soma_cell_builder builder(6);
         auto cell = builder.make_cell();
-        cell.paint("\"soma\"", "test_ca_read_valence");
-        cable1d_recipe rec({std::move(cell)});
+        cell.decorations.paint("\"soma\"", "test_ca_read_valence");
+        cable1d_recipe rec(cable_cell{cell});
         rec.catalogue() = make_unit_test_catalogue();
 
         arb::execution_context context(resources);
@@ -566,8 +570,8 @@ TEST(fvm_lowered, read_valence) {
         // Check ion renaming.
         soma_cell_builder builder(6);
         auto cell = builder.make_cell();
-        cell.paint("\"soma\"", "cr_read_valence");
-        cable1d_recipe rec({std::move(cell)});
+        cell.decorations.paint("\"soma\"", "cr_read_valence");
+        cable1d_recipe rec(cable_cell{cell});
         rec.catalogue() = make_unit_test_catalogue();
         rec.catalogue() = make_unit_test_catalogue();
 
@@ -683,10 +687,10 @@ TEST(fvm_lowered, ionic_currents) {
     m2["coeff"] = coeff;
 
     auto c = b.make_cell();
-    c.paint("soma"_lab, m1);
-    c.paint("soma"_lab, m2);
+    c.decorations.paint("soma"_lab, m1);
+    c.decorations.paint("soma"_lab, m2);
 
-    cable1d_recipe rec(std::move(c));
+    cable1d_recipe rec({cable_cell{c}});
     rec.catalogue() = make_unit_test_catalogue();
 
     std::vector<target_handle> targets;
@@ -724,14 +728,14 @@ TEST(fvm_lowered, point_ionic_current) {
 
     double r = 6.0; // [µm]
     soma_cell_builder b(r);
-    cable_cell c = b.make_cell();
+    auto c = b.make_cell();
 
     double soma_area_m2 = 4*math::pi<double>*r*r*1e-12; // [m²]
 
     // Event weight is translated by point_ica_current into a current contribution in nA.
-    c.place(mlocation{0u, 0.5}, "point_ica_current");
+    c.decorations.place(mlocation{0u, 0.5}, "point_ica_current");
 
-    cable1d_recipe rec(c);
+    cable1d_recipe rec({cable_cell{c}});
     rec.catalogue() = make_unit_test_catalogue();
 
     std::vector<target_handle> targets;
@@ -800,18 +804,18 @@ TEST(fvm_lowered, weighted_write_ion) {
     b.add_branch(1, 200, 0.5, 0.5, 1, "dend");
     b.add_branch(1, 100, 0.5, 0.5, 1, "dend");
 
-    cable_cell c = b.make_cell();
+    auto c = b.make_cell();
 
     const double con_int = 80;
     const double con_ext = 120;
 
     // Ca ion reader test_kinlva on CV 2 and 3 via branch 2:
-    c.paint(reg::branch(1), "test_kinlva");
+    c.decorations.paint(reg::branch(1), "test_kinlva");
 
     // Ca ion writer test_ca on CV 2 and 4 via branch 3:
-    c.paint(reg::branch(2), "test_ca");
+    c.decorations.paint(reg::branch(2), "test_ca");
 
-    cable1d_recipe rec(c);
+    cable1d_recipe rec({cable_cell{c}});
     rec.catalogue() = make_unit_test_catalogue();
     rec.add_ion("ca", 2, con_int, con_ext, 0.0);
 
@@ -901,16 +905,16 @@ TEST(fvm_lowered, gj_coords_simple) {
         soma_cell_builder b(2.1);
         b.add_branch(0, 10, 0.3, 0.2, 5, "dend");
         auto c = b.make_cell();
-        c.place(b.location({1, 0.8}), gap_junction_site{});
-        cells.push_back(std::move(c));
+        c.decorations.place(b.location({1, 0.8}), gap_junction_site{});
+        cells.push_back(c);
     }
 
     {
         soma_cell_builder b(2.4);
         b.add_branch(0, 10, 0.3, 0.2, 2, "dend");
         auto c = b.make_cell();
-        c.place(b.location({1, 1}), gap_junction_site{});
-        cells.push_back(std::move(c));
+        c.decorations.place(b.location({1, 1}), gap_junction_site{});
+        cells.push_back(c);
     }
 
     fvm_cv_discretization D = fvm_cv_discretize(cells, neuron_parameter_defaults, context);
@@ -986,8 +990,8 @@ TEST(fvm_lowered, gj_coords_complex) {
     auto c0 = b0.make_cell();
     mlocation c0_gj[2] = {b0.location({1, 1}), b0.location({1, 0.5})};
 
-    c0.place(c0_gj[0], gap_junction_site{});
-    c0.place(c0_gj[1], gap_junction_site{});
+    c0.decorations.place(c0_gj[0], gap_junction_site{});
+    c0.decorations.place(c0_gj[1], gap_junction_site{});
 
     soma_cell_builder b1(1.4);
     b1.add_branch(0, 12, 0.3, 0.5, 6, "dend");
@@ -997,10 +1001,10 @@ TEST(fvm_lowered, gj_coords_complex) {
     auto c1 = b1.make_cell();
     mlocation c1_gj[4] = {b1.location({2, 1}), b1.location({1, 1}), b1.location({1, 0.45}), b1.location({1, 0.1})};
 
-    c1.place(c1_gj[0], gap_junction_site{});
-    c1.place(c1_gj[1], gap_junction_site{});
-    c1.place(c1_gj[2], gap_junction_site{});
-    c1.place(c1_gj[3], gap_junction_site{});
+    c1.decorations.place(c1_gj[0], gap_junction_site{});
+    c1.decorations.place(c1_gj[1], gap_junction_site{});
+    c1.decorations.place(c1_gj[2], gap_junction_site{});
+    c1.decorations.place(c1_gj[3], gap_junction_site{});
 
 
     soma_cell_builder b2(2.9);
@@ -1013,11 +1017,11 @@ TEST(fvm_lowered, gj_coords_complex) {
     auto c2 = b2.make_cell();
     mlocation c2_gj[3] = {b2.location({1, 0.5}), b2.location({4, 1}), b2.location({2, 1})};
 
-    c2.place(c2_gj[0], gap_junction_site{});
-    c2.place(c2_gj[1], gap_junction_site{});
-    c2.place(c2_gj[2], gap_junction_site{});
+    c2.decorations.place(c2_gj[0], gap_junction_site{});
+    c2.decorations.place(c2_gj[1], gap_junction_site{});
+    c2.decorations.place(c2_gj[2], gap_junction_site{});
 
-    std::vector<cable_cell> cells{std::move(c0), std::move(c1), std::move(c2)};
+    std::vector<cable_cell> cells{c0, c1, c2};
 
     std::vector<fvm_index_type> cell_to_intdom;
 
@@ -1114,15 +1118,15 @@ TEST(fvm_lowered, cell_group_gj) {
 
     // Make 20 cells
     for (unsigned i = 0; i < 20; i++) {
-        cable_cell c = soma_cell_builder(2.1).make_cell();
+        cable_cell_description c = soma_cell_builder(2.1).make_cell();
         if (i % 2 == 0) {
-            c.place(mlocation{0, 1}, gap_junction_site{});
+            c.decorations.place(mlocation{0, 1}, gap_junction_site{});
         }
         if (i < 10) {
-            cell_group0.push_back(std::move(c));
+            cell_group0.push_back(c);
         }
         else {
-            cell_group1.push_back(std::move(c));
+            cell_group1.push_back(c);
         }
     }
 
diff --git a/test/unit/test_mc_cell_group.cpp b/test/unit/test_mc_cell_group.cpp
index 327852cdc3fd0392accf37293467a68af88ef43c..25f54f2bee9d13ec222463e67e32fe146ae75659 100644
--- a/test/unit/test_mc_cell_group.cpp
+++ b/test/unit/test_mc_cell_group.cpp
@@ -22,15 +22,15 @@ namespace {
         return make_fvm_lowered_cell(backend_kind::multicore, context);
     }
 
-    cable_cell make_cell() {
+    cable_cell_description make_cell() {
         soma_cell_builder builder(12.6157/2.0);
         builder.add_branch(0, 200, 0.5, 0.5, 101, "dend");
-        cable_cell c = builder.make_cell();
-        c.paint("soma"_lab, "hh");
-        c.paint("dend"_lab, "pas");
-        c.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
-        c.place(builder.location({0, 0}), threshold_detector{0});
-        return c;
+        auto d = builder.make_cell();
+        d.decorations.paint("soma"_lab, "hh");
+        d.decorations.paint("dend"_lab, "pas");
+        d.decorations.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+        d.decorations.place(builder.location({0, 0}), threshold_detector{0});
+        return d;
     }
 }
 
@@ -40,14 +40,15 @@ ACCESS_BIND(
     &mc_cell_group::spike_sources_)
 
 TEST(mc_cell_group, get_kind) {
-    auto x = make_cell();
-    mc_cell_group group{{0}, cable1d_recipe(make_cell()), lowered_cell()};
+    cable_cell cell = make_cell();
+    mc_cell_group group{{0}, cable1d_recipe({cell}), lowered_cell()};
 
     EXPECT_EQ(cell_kind::cable, group.get_cell_kind());
 }
 
 TEST(mc_cell_group, test) {
-    auto rec = cable1d_recipe(make_cell());
+    cable_cell cell = make_cell();
+    auto rec = cable1d_recipe({cell});
     rec.nernst_ion("na");
     rec.nernst_ion("ca");
     rec.nernst_ion("k");
@@ -66,10 +67,11 @@ TEST(mc_cell_group, sources) {
     std::vector<cable_cell> cells;
 
     for (int i=0; i<20; ++i) {
-        cells.push_back(make_cell());
+        auto desc = make_cell();
         if (i==0 || i==3 || i==17) {
-            cells.back().place(mlocation{0, 0.3}, threshold_detector{2.3});
+            desc.decorations.place(mlocation{0, 0.3}, threshold_detector{2.3});
         }
+        cells.emplace_back(desc);
 
         EXPECT_EQ(1u + (i==0 || i==3 || i==17), cells.back().detectors().size());
     }
diff --git a/test/unit/test_mc_cell_group_gpu.cpp b/test/unit/test_mc_cell_group_gpu.cpp
index bb7f1791e1a38ec4a4590b98467c41dfa1a15479..a482d7c84d6893b3135d1e1b2c48b70c3de374f3 100644
--- a/test/unit/test_mc_cell_group_gpu.cpp
+++ b/test/unit/test_mc_cell_group_gpu.cpp
@@ -1,6 +1,7 @@
 #include "../gtest.h"
 
 #include <arbor/common_types.hpp>
+#include <arbor/string_literals.hpp>
 #include <arborenv/gpu_env.hpp>
 
 #include "epoch.hpp"
@@ -12,6 +13,7 @@
 #include "../simple_recipes.hpp"
 
 using namespace arb;
+using namespace arb::literals;
 
 namespace {
     fvm_lowered_cell_ptr lowered_cell() {
@@ -21,21 +23,22 @@ namespace {
         return make_fvm_lowered_cell(backend_kind::gpu, context);
     }
 
-    cable_cell make_cell() {
+    cable_cell_description make_cell() {
         soma_cell_builder builder(12.6157/2.0);
         builder.add_branch(0, 200, 0.5, 0.5, 101, "dend");
-        cable_cell c = builder.make_cell();
-        c.paint("\"soma\"", "hh");
-        c.paint("\"dend\"", "pas");
-        c.place(builder.location({1, 1}), i_clamp{5, 80, 0.3});
-        c.place(builder.location({0, 0}), threshold_detector{0});
-        return c;
+        auto d = builder.make_cell();
+        d.decorations.paint("soma"_lab, "hh");
+        d.decorations.paint("dend"_lab, "pas");
+        d.decorations.place(builder.location({1,1}), i_clamp{5, 80, 0.3});
+        d.decorations.place(builder.location({0, 0}), threshold_detector{0});
+        return d;
     }
 }
 
 TEST(mc_cell_group, gpu_test)
 {
-    auto rec = cable1d_recipe(make_cell());
+    cable_cell cell = make_cell();
+    auto rec = cable1d_recipe({cell});
     rec.nernst_ion("na");
     rec.nernst_ion("ca");
     rec.nernst_ion("k");
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index 3c11796542bd78adfce5d81610bc4c9dfa327845..867821992a765ad4b050f4dd2e4604096aee66a5 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -105,14 +105,14 @@ void run_v_i_probe_test(const context& ctx) {
     soma_cell_builder builder(12.6157/2.0);
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
-    cable_cell bs = builder.make_cell();
+    auto bs = builder.make_cell();
 
-    bs.default_parameters.discretization = cv_policy_fixed_per_branch(1);
+    bs.decorations.set_default(cv_policy_fixed_per_branch(1));
 
     i_clamp stim(0, 100, 0.3);
-    bs.place(mlocation{1, 1}, stim);
+    bs.decorations.place(mlocation{1, 1}, stim);
 
-    cable1d_recipe rec(bs);
+    cable1d_recipe rec((cable_cell(bs)));
 
     mlocation loc0{0, 0};
     mlocation loc1{1, 1};
@@ -204,7 +204,7 @@ void run_v_cell_probe_test(const context& ctx) {
     // to determine the corresponding CVs for each cable, and the raw
     // pointer to backend data checked against the expected CV offset.
 
-    cable_cell cell(make_y_morphology());
+    auto m = make_y_morphology();
 
     std::pair<const char*, cv_policy> test_policies[] = {
         {"trivial fork", cv_policy_fixed_per_branch(3, cv_policy_flag::none)},
@@ -213,7 +213,10 @@ void run_v_cell_probe_test(const context& ctx) {
 
     for (auto& testcase: test_policies) {
         SCOPED_TRACE(testcase.first);
-        cell.default_parameters.discretization = testcase.second;
+        decor d;
+        d.set_default(testcase.second);
+
+        cable_cell cell(m, {}, d);
 
         cable1d_recipe rec(cell, false);
         rec.add_probe(0, 0, cable_probe_membrane_voltage_cell{});
@@ -271,13 +274,13 @@ void run_expsyn_g_probe_test(const context& ctx) {
     soma_cell_builder builder(12.6157/2.0);
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
-    cable_cell bs = builder.make_cell();
-    bs.place(loc0, "expsyn");
-    bs.place(loc1, "expsyn");
-    bs.default_parameters.discretization = cv_policy_fixed_per_branch(2);
+    auto bs = builder.make_cell();
+    bs.decorations.place(loc0, "expsyn");
+    bs.decorations.place(loc1, "expsyn");
+    bs.decorations.set_default(cv_policy_fixed_per_branch(2));
 
     auto run_test = [&](bool coalesce_synapses) {
-        cable1d_recipe rec(bs, coalesce_synapses);
+        cable1d_recipe rec(cable_cell(bs), coalesce_synapses);
         rec.add_probe(0, 10, cable_probe_point_state{0u, "expsyn", "g"});
         rec.add_probe(0, 20, cable_probe_point_state{1u, "expsyn", "g"});
 
@@ -367,25 +370,24 @@ void run_expsyn_g_cell_probe_test(const context& ctx) {
 
     cv_policy policy = cv_policy_fixed_per_branch(3);
 
-    cable_cell cell(make_y_morphology());
-    cell.default_parameters.discretization = policy;
+    auto m  = make_y_morphology();
+    arb::decor d;
+    d.set_default(policy);
 
     std::unordered_map<cell_lid_type, mlocation> expsyn_target_loc_map;
 
+    unsigned n_expsyn = 0;
     for (unsigned bid = 0; bid<3u; ++bid) {
         for (unsigned j = 0; j<10; ++j) {
             mlocation expsyn_loc{bid, 0.1*j};
-            lid_range target_lids = cell.place(expsyn_loc, "expsyn");
-
-            ASSERT_EQ(1u, target_lids.end-target_lids.begin);
-            expsyn_target_loc_map[target_lids.begin] = expsyn_loc;
-
-            cell.place(mlocation{bid, 0.1*j+0.05}, "exp2syn");
+            d.place(expsyn_loc, "expsyn");
+            expsyn_target_loc_map[2*n_expsyn] = expsyn_loc;
+            d.place(mlocation{bid, 0.1*j+0.05}, "exp2syn");
+            ++n_expsyn;
         }
     }
-    const unsigned n_expsyn = 30;
 
-    std::vector<cable_cell> cells(2, cell);
+    std::vector<cable_cell> cells(2, arb::cable_cell(m, {}, d));
 
     auto run_test = [&](bool coalesce_synapses) {
         cable1d_recipe rec(cells, coalesce_synapses);
@@ -500,15 +502,16 @@ void run_ion_density_probe_test(const context& ctx) {
 
     // Simple constant diameter cable, 3 CVs.
 
-    cable_cell cable(make_stick_morphology());
-    cable.default_parameters.discretization = cv_policy_fixed_per_branch(3);
+    auto m = make_stick_morphology();
+    decor d;
+    d.set_default(cv_policy_fixed_per_branch(3));
 
     // Calcium ions everywhere, half written by write_ca1, half by write_ca2.
     // Sodium ions only on distal half.
 
-    cable.paint(mcable{0, 0., 0.5}, "write_ca1");
-    cable.paint(mcable{0, 0.5, 1.}, "write_ca2");
-    cable.paint(mcable{0, 0.5, 1.}, "write_na3");
+    d.paint(mcable{0, 0., 0.5}, "write_ca1");
+    d.paint(mcable{0, 0.5, 1.}, "write_ca2");
+    d.paint(mcable{0, 0.5, 1.}, "write_na3");
 
     // Place probes in each CV.
 
@@ -516,7 +519,7 @@ void run_ion_density_probe_test(const context& ctx) {
     mlocation loc1{0, 0.5};
     mlocation loc2{0, 0.9};
 
-    cable1d_recipe rec(cable);
+    cable1d_recipe rec(cable_cell(m, {}, d));
     rec.catalogue() = cat;
 
     // Probe (0, 0): ca internal on CV 0.
@@ -664,11 +667,9 @@ void run_partial_density_probe_test(const context& ctx) {
 
     auto m = make_stick_morphology();
 
-    cells[0] = cable_cell(m);
-    cells[0].default_parameters.discretization = cv_policy_fixed_per_branch(3);
-
-    cells[1] = cable_cell(m);
-    cells[1].default_parameters.discretization = cv_policy_fixed_per_branch(3);
+    decor d0, d1;
+    d0.set_default(cv_policy_fixed_per_branch(3));
+    d1.set_default(cv_policy_fixed_per_branch(3));
 
     // Paint the mechanism on every second 10% interval of each cell.
     // Expected values on a CV are the weighted mean of the parameter values
@@ -690,17 +691,20 @@ void run_partial_density_probe_test(const context& ctx) {
 
     auto mk_mech = [](double param) { return mechanism_desc("param_as_state").set("p", param); };
 
-    cells[0].paint(mcable{0, 0.0, 0.1}, mk_mech(2));
-    cells[0].paint(mcable{0, 0.2, 0.3}, mk_mech(3));
-    cells[0].paint(mcable{0, 0.4, 0.5}, mk_mech(4));
-    cells[0].paint(mcable{0, 0.6, 0.7}, mk_mech(5));
-    cells[0].paint(mcable{0, 0.8, 0.9}, mk_mech(6));
+    d0.paint(mcable{0, 0.0, 0.1}, mk_mech(2));
+    d0.paint(mcable{0, 0.2, 0.3}, mk_mech(3));
+    d0.paint(mcable{0, 0.4, 0.5}, mk_mech(4));
+    d0.paint(mcable{0, 0.6, 0.7}, mk_mech(5));
+    d0.paint(mcable{0, 0.8, 0.9}, mk_mech(6));
+
+    d1.paint(mcable{0, 0.1, 0.2}, mk_mech(7));
+    d1.paint(mcable{0, 0.3, 0.4}, mk_mech(8));
+    d1.paint(mcable{0, 0.5, 0.6}, mk_mech(9));
+    d1.paint(mcable{0, 0.7, 0.8}, mk_mech(10));
+    d1.paint(mcable{0, 0.9, 1.0}, mk_mech(11));
 
-    cells[1].paint(mcable{0, 0.1, 0.2}, mk_mech(7));
-    cells[1].paint(mcable{0, 0.3, 0.4}, mk_mech(8));
-    cells[1].paint(mcable{0, 0.5, 0.6}, mk_mech(9));
-    cells[1].paint(mcable{0, 0.7, 0.8}, mk_mech(10));
-    cells[1].paint(mcable{0, 0.9, 1.0}, mk_mech(11));
+    cells[0] = cable_cell(m, {}, d0);
+    cells[1] = cable_cell(m, {}, d1);
 
     // Place probes in the middle of each 10% interval, i.e. at 0.05, 0.15, etc.
     struct test_probe {
@@ -777,25 +781,27 @@ void run_axial_and_ion_current_sampled_probe_test(const context& ctx) {
     // Cell is a tapered cable with 3 CVs.
 
     auto m = make_stick_morphology();
-    cable_cell cell(m);
+    arb::decor d;
 
     const unsigned n_cv = 3;
     cv_policy policy = cv_policy_fixed_per_branch(n_cv);
-    cell.default_parameters.discretization = policy;
+    d.set_default(policy);
 
-    cell.place(mlocation{0, 0}, i_clamp(0, INFINITY, 0.3));
+    d.place(mlocation{0, 0}, i_clamp(0, INFINITY, 0.3));
 
     // The time constant will be membrane capacitance / membrane conductance.
     // For τ = 0.1 ms, set conductance to 0.01 S/cm² and membrance capacitance
     // to 0.01 F/m².
 
-    cell.paint(reg::all(), mechanism_desc("ca_linear").set("g", 0.01)); // [S/cm²]
-    cell.default_parameters.membrane_capacitance = 0.01; // [F/m²]
+    d.paint(reg::all(), mechanism_desc("ca_linear").set("g", 0.01)); // [S/cm²]
+    d.set_default(membrane_capacitance{0.01}); // [F/m²]
     const double tau = 0.1; // [ms]
 
-    cable1d_recipe rec(cell);
+    cable1d_recipe rec(cable_cell(m, {}, d));
     rec.catalogue() = cat;
 
+    cable_cell cell(m, {}, d);
+
     // Place axial current probes at CV boundaries and make a cell-wide probe for
     // total ionic membrane current.
 
@@ -879,7 +885,6 @@ void run_axial_and_ion_current_sampled_probe_test(const context& ctx) {
     }
 }
 
-
 // Run given cells taking samples from the provied probes on one of the cells.
 //
 // Use the default mechanism catalogue augmented by unit test specific mechanisms.
@@ -934,14 +939,15 @@ void run_multi_probe_test(const context& ctx) {
     // cell terminal points; check metadata and values.
 
     // m_mlt_b6 has terminal branches 1, 2, 4, and 5.
-    cable_cell cell(common_morphology::m_mlt_b6);
+    auto m = common_morphology::m_mlt_b6;
+    decor d;
 
     // Paint mechanism on branches 1, 2, and 5, omitting branch 4.
-    cell.paint(reg::branch(1), mechanism_desc("param_as_state").set("p", 10.));
-    cell.paint(reg::branch(2), mechanism_desc("param_as_state").set("p", 20.));
-    cell.paint(reg::branch(5), mechanism_desc("param_as_state").set("p", 50.));
+    d.paint(reg::branch(1), mechanism_desc("param_as_state").set("p", 10.));
+    d.paint(reg::branch(2), mechanism_desc("param_as_state").set("p", 20.));
+    d.paint(reg::branch(5), mechanism_desc("param_as_state").set("p", 50.));
 
-    auto tracev = run_simple_sampler<double, mlocation>(ctx, 0.1, {cell}, 0, cable_probe_density_state{ls::terminal(), "param_as_state", "s"}, {0.});
+    auto tracev = run_simple_sampler<double, mlocation>(ctx, 0.1, {cable_cell{m, {}, d}}, 0, cable_probe_density_state{ls::terminal(), "param_as_state", "s"}, {0.});
 
     // Expect to have received a sample on each of the terminals of branches 1, 2, and 5.
     ASSERT_EQ(3u, tracev.size());
@@ -967,20 +973,21 @@ void run_v_sampled_probe_test(const context& ctx) {
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
     builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
 
-    cable_cell bs = builder.make_cell();
-
-    bs.default_parameters.discretization = cv_policy_fixed_per_branch(1);
-
-    std::vector<cable_cell> cells = {bs, bs};
+    auto bs = builder.make_cell();
+    bs.decorations.set_default(cv_policy_fixed_per_branch(1));
+    auto d0 = bs.decorations;
+    auto d1 = bs.decorations;
 
     // Add stims, up to 0.5 ms on cell 0, up to 1.0 ms on cell 1, so that
     // samples at the same point on each cell will give the same value at
     // 0.3 ms, but different at 0.6 ms.
 
-    cells[0].place(mlocation{1, 1}, i_clamp(0, 0.5, 1.));
-    cells[1].place(mlocation{1, 1}, i_clamp(0, 1.0, 1.));
+    d0.place(mlocation{1, 1}, i_clamp(0, 0.5, 1.));
+    d1.place(mlocation{1, 1}, i_clamp(0, 1.0, 1.));
     mlocation probe_loc{1, 0.2};
 
+    std::vector<cable_cell> cells = {{bs.morph, bs.labels, d0}, {bs.morph, bs.labels, d1}};
+
     const double t_end = 1.; // [ms]
     std::vector<double> when = {0.3, 0.6}; // Sample at 0.3 and 0.6 ms.
 
@@ -1014,7 +1021,8 @@ void run_total_current_probe_test(const context& ctx) {
     // Net current flux in each cell should be zero, but currents should
     // differ between the cells.
 
-    cable_cell cell(make_y_morphology());
+    auto m = make_y_morphology();
+    decor d0;
 
     const unsigned n_cv_per_branch = 3;
     const unsigned n_branch = 3;
@@ -1024,15 +1032,13 @@ void run_total_current_probe_test(const context& ctx) {
     // to 0.01 F/m².
 
     const double tau = 0.1;     // [ms]
-    cell.place(mlocation{0, 0}, i_clamp(0, INFINITY, 0.3));
-
-    cell.paint(reg::all(), mechanism_desc("ca_linear").set("g", 0.01)); // [S/cm²]
-    cell.default_parameters.membrane_capacitance = 0.01; // [F/m²]
-
-    std::vector<cable_cell> cells = {cell, cell};
+    d0.place(mlocation{0, 0}, i_clamp(0, INFINITY, 0.3));
 
+    d0.paint(reg::all(), mechanism_desc("ca_linear").set("g", 0.01)); // [S/cm²]
+    d0.set_default(membrane_capacitance{0.01}); // [F/m²]
     // Tweak membrane capacitance on cells[1] so as to change dynamics a bit.
-    cells[1].default_parameters.membrane_capacitance = 0.009; // [F/m²]
+    auto d1 = d0;
+    d1.set_default(membrane_capacitance{0.009}); // [F/m²]
 
     // We'll run each set of tests twice: once with a trivial (zero-volume) CV
     // at the fork points, and once with a non-trivial CV centred on the fork
@@ -1047,7 +1053,11 @@ void run_total_current_probe_test(const context& ctx) {
     auto run_cells = [&](bool interior_forks) {
         auto flags = interior_forks? cv_policy_flag::interior_forks: cv_policy_flag::none;
         cv_policy policy = cv_policy_fixed_per_branch(n_cv_per_branch, flags);
-        for (auto& c: cells) { c.default_parameters.discretization = policy; }
+        //for (auto& c: cells) { c.discretization() = policy; }
+        d0.set_default(policy);
+        d1.set_default(policy);
+        std::vector<cable_cell> cells = {{m, {}, d0}, {m, {}, d1}};
+
 
         for (unsigned i = 0; i<2; ++i) {
             SCOPED_TRACE(i);
@@ -1141,15 +1151,18 @@ void run_exact_sampling_probe_test(const context& ctx) {
             builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
             builder.add_branch(0, 200, 1.0/2, 1.0/2, 1, "dend");
 
-            cells_.assign(4, builder.make_cell());
-            cells_[0].place(mlocation{1, 0.1}, "expsyn");
-            cells_[1].place(mlocation{1, 0.1}, "exp2syn");
-            cells_[2].place(mlocation{1, 0.9}, "expsyn");
-            cells_[3].place(mlocation{1, 0.9}, "exp2syn");
+            std::vector<cable_cell_description> cd;
+            cd.assign(4, builder.make_cell());
+
+            cd[0].decorations.place(mlocation{1, 0.1}, "expsyn");
+            cd[1].decorations.place(mlocation{1, 0.1}, "exp2syn");
+            cd[2].decorations.place(mlocation{1, 0.9}, "expsyn");
+            cd[3].decorations.place(mlocation{1, 0.9}, "exp2syn");
 
-            cells_[1].place(mlocation{1, 0.2}, gap_junction_site{});
-            cells_[3].place(mlocation{1, 0.2}, gap_junction_site{});
+            cd[1].decorations.place(mlocation{1, 0.2}, gap_junction_site{});
+            cd[3].decorations.place(mlocation{1, 0.2}, gap_junction_site{});
 
+            for (auto& d: cd) cells_.push_back(d);
         }
 
         cell_size_type num_cells() const override { return cells_.size(); }
@@ -1251,9 +1264,9 @@ void run_exact_sampling_probe_test(const context& ctx) {
 
 #undef PROBE_TESTS
 #define PROBE_TESTS \
-    v_i, v_cell, v_sampled, expsyn_g, expsyn_g_cell, \
-    ion_density, axial_and_ion_current_sampled, partial_density, total_current, exact_sampling, \
-    multi
+    v_i, v_cell, v_sampled, expsyn_g, expsyn_g_cell, ion_density, \
+    axial_and_ion_current_sampled, partial_density, exact_sampling, \
+    multi, total_current
 
 #undef RUN_MULTICORE
 #define RUN_MULTICORE(x) \
@@ -1286,14 +1299,15 @@ TEST(probe, get_probe_metadata) {
     // Reuse multiprobe test set-up to confirm simulator::get_probe_metadata returns
     // correct vector of metadata.
 
-    cable_cell cell(common_morphology::m_mlt_b6);
+    auto m = common_morphology::m_mlt_b6;
+    decor d;
 
     // Paint mechanism on branches 1, 2, and 5, omitting branch 4.
-    cell.paint(reg::branch(1), mechanism_desc("param_as_state").set("p", 10.));
-    cell.paint(reg::branch(2), mechanism_desc("param_as_state").set("p", 20.));
-    cell.paint(reg::branch(5), mechanism_desc("param_as_state").set("p", 50.));
+    d.paint(reg::branch(1), mechanism_desc("param_as_state").set("p", 10.));
+    d.paint(reg::branch(2), mechanism_desc("param_as_state").set("p", 20.));
+    d.paint(reg::branch(5), mechanism_desc("param_as_state").set("p", 50.));
 
-    cable1d_recipe rec({cell}, false);
+    cable1d_recipe rec(cable_cell{m, {}, d}, false);
     rec.catalogue() = make_unit_test_catalogue(global_default_catalogue());
     rec.add_probe(0, 7, cable_probe_density_state{ls::terminal(), "param_as_state", "s"});
 
diff --git a/test/unit/test_ratelem.cpp b/test/unit/test_ratelem.cpp
index f393ae75456522ea83a7320c4d643f650baef7a6..2f21777f3a1f907567dc4a3030626dfc19b96895 100644
--- a/test/unit/test_ratelem.cpp
+++ b/test/unit/test_ratelem.cpp
@@ -109,7 +109,9 @@ TYPED_TEST(ratelem_pq, interpolate_monotonic) {
     }
 
     for (unsigned i = 1; i<p+q; ++i) {
-        double x = (double)i/(p+q);
-        EXPECT_DOUBLE_EQ(f(x), fpq(x));
+        if constexpr (p+q!=0) { // avoid a spurious gcc 10 divide by zero warning.
+            double x = (double)i/(p+q);
+            EXPECT_DOUBLE_EQ(f(x), fpq(x));
+        }
     }
 }
diff --git a/test/unit/test_recipe.cpp b/test/unit/test_recipe.cpp
index c325af6497ab1a628cb5c5777f4dcacc782da7a1..13abf4fb3a0a958e69b9f897977cb632776bfe3a 100644
--- a/test/unit/test_recipe.cpp
+++ b/test/unit/test_recipe.cpp
@@ -74,24 +74,26 @@ namespace {
         tree.append(arb::mnpos, {0,0,0,10}, {0,0,20,10}, 1); // soma
         tree.append(0, {0,0, 20, 2}, {0,0, 320, 2}, 3);  // dendrite
 
-        arb::cable_cell cell(tree, {});
+        arb::cable_cell cell(tree);
+
+        arb::decor decorations;
 
         // Add a num_detectors detectors to the cell.
         for (auto i: util::make_span(num_detectors)) {
-            cell.place(arb::mlocation{0,(double)i/num_detectors}, arb::threshold_detector{10});
+            decorations.place(arb::mlocation{0,(double)i/num_detectors}, arb::threshold_detector{10});
         }
 
         // Add a num_synapses synapses to the cell.
         for (auto i: util::make_span(num_synapses)) {
-            cell.place(arb::mlocation{0,(double)i/num_synapses}, "expsyn");
+            decorations.place(arb::mlocation{0,(double)i/num_synapses}, "expsyn");
         }
 
         // Add a num_gj gap_junctions to the cell.
         for (auto i: util::make_span(num_gj)) {
-            cell.place(arb::mlocation{0,(double)i/num_gj}, arb::gap_junction_site{});
+            decorations.place(arb::mlocation{0,(double)i/num_gj}, arb::gap_junction_site{});
         }
 
-        return cell;
+        return arb::cable_cell(tree, {}, decorations);
     }
 }
 
diff --git a/test/unit/test_spikes.cpp b/test/unit/test_spikes.cpp
index f3b0fcbcb2dab2bdbc7a3e52d036f87f245ae573..dd1f8d1e1298c4a8bc9c998f5b569cfe4f68f1b1 100644
--- a/test/unit/test_spikes.cpp
+++ b/test/unit/test_spikes.cpp
@@ -180,12 +180,13 @@ TEST(SPIKES_TEST_CLASS, threshold_watcher_interpolation) {
     std::vector<arb::spike> spikes;
 
     for (unsigned i = 0; i < 8; i++) {
-        arb::cable_cell cell(morpho, dict);
-        cell.default_parameters.discretization = arb::cv_policy_every_segment();
-        cell.place("\"mid\"", arb::threshold_detector{10});
-        cell.place("\"mid\"", arb::i_clamp(0.01+i*dt, duration, 0.5));
-        cell.place("\"mid\"", arb::mechanism_desc("hh"));
+        arb::decor decor;
+        decor.set_default(arb::cv_policy_every_segment());
+        decor.place("\"mid\"", arb::threshold_detector{10});
+        decor.place("\"mid\"", arb::i_clamp(0.01+i*dt, duration, 0.5));
+        decor.place("\"mid\"", arb::mechanism_desc("hh"));
 
+        arb::cable_cell cell(morpho, dict, decor);
         cable1d_recipe rec({cell});
 
         auto decomp = arb::partition_load_balance(rec, context);
diff --git a/test/unit/test_synapses.cpp b/test/unit/test_synapses.cpp
index 6c9bdeb2b0e820870b317a27671da497d2a47e40..daab5fe6bb26ad6fb7e407fa580bf6b7e60a6488 100644
--- a/test/unit/test_synapses.cpp
+++ b/test/unit/test_synapses.cpp
@@ -32,11 +32,13 @@ ACCESS_BIND(value_type* multicore::mechanism::*, vec_i_ptr, &multicore::mechanis
 TEST(synapses, add_to_cell) {
     using namespace arb;
 
-    auto cell = make_cell_soma_only(false);
+    auto description = make_cell_soma_only(false);
 
-    cell.place(mlocation{0, 0.1}, "expsyn");
-    cell.place(mlocation{0, 0.2}, "exp2syn");
-    cell.place(mlocation{0, 0.3}, "expsyn");
+    description.decorations.place(mlocation{0, 0.1}, "expsyn");
+    description.decorations.place(mlocation{0, 0.2}, "exp2syn");
+    description.decorations.place(mlocation{0, 0.3}, "expsyn");
+
+    cable_cell cell(description);
 
     auto syns = cell.synapses();
 
@@ -53,7 +55,8 @@ TEST(synapses, add_to_cell) {
     EXPECT_EQ("exp2syn", syns["exp2syn"][0].item.name());
 
     // adding a synapse to an invalid branch location should throw.
-    EXPECT_THROW(cell.place(mlocation{1, 0.3}, "expsyn"), std::runtime_error);
+    description.decorations.place(mlocation{1, 0.3}, "expsyn");
+    EXPECT_THROW((cell=description), std::runtime_error);
 }
 
 template <typename Seq>