diff --git a/arbor/arbexcept.cpp b/arbor/arbexcept.cpp
index d12883b5fcb1d6f0ad853a6a0dbf308fbdc28f7e..e27f04fad7501d7e5358eca41fc10e2c7303c602 100644
--- a/arbor/arbexcept.cpp
+++ b/arbor/arbexcept.cpp
@@ -38,7 +38,8 @@ bad_connection_source_gid::bad_connection_source_gid(cell_gid_type gid, cell_gid
 {}
 
 bad_connection_source_lid::bad_connection_source_lid(cell_gid_type gid, cell_lid_type src_lid, cell_size_type num_sources):
-    arbor_exception(pprintf("Model building error on cell {}: connection source index {} is out of range. Cell {} has {} sources, in the range [{}:{}].", gid, src_lid, gid, num_sources, 0, num_sources-1)),
+    arbor_exception(pprintf("Model building error on cell {}: connection source index {} is out of range. Cell {} has {} sources", gid, src_lid, gid, num_sources) +
+                    (num_sources? pprintf(", in the range [{}:{}].",  0, num_sources-1) : ".")),
     gid(gid), src_lid(src_lid), num_sources(num_sources)
 {}
 
@@ -48,7 +49,19 @@ bad_connection_target_gid::bad_connection_target_gid(cell_gid_type gid, cell_gid
 {}
 
 bad_connection_target_lid::bad_connection_target_lid(cell_gid_type gid, cell_lid_type tgt_lid, cell_size_type num_targets):
-    arbor_exception(pprintf("Model building error on cell {}: connection target index {} is out of range. Cell {} has {} targets, in the range [{}:{}].", gid, tgt_lid, gid, num_targets, 0, num_targets-1)),
+    arbor_exception(pprintf("Model building error on cell {}: connection target index {} is out of range. Cell {} has {} targets", gid, tgt_lid, gid, num_targets) +
+                    (num_targets ? pprintf(", in the range [{}:{}].", 0, num_targets-1) : ".")),
+    gid(gid), tgt_lid(tgt_lid), num_targets(num_targets)
+{}
+
+bad_event_generator_target_gid::bad_event_generator_target_gid(cell_gid_type gid, cell_gid_type tgt_gid):
+    arbor_exception(pprintf("Model building error on cell {}: event_generator target gid {} has to match cell gid {}].", gid, tgt_gid, gid)),
+    gid(gid), tgt_gid(tgt_gid)
+{}
+
+bad_event_generator_target_lid::bad_event_generator_target_lid(cell_gid_type gid, cell_lid_type tgt_lid, cell_size_type num_targets):
+    arbor_exception(pprintf("Model building error on cell {}: event_generator target index {} is out of range. Cell {} has {} targets", gid, tgt_lid, gid, num_targets) +
+                    (num_targets ? pprintf(", in the range [{}:{}].", 0, num_targets-1) : ".")),
     gid(gid), tgt_lid(tgt_lid), num_targets(num_targets)
 {}
 
diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp
index f2c49675604bc038267c01ec3652cb70431057e4..814c628fc98e96e405727cf2ff680f95eb627ff7 100644
--- a/arbor/include/arbor/arbexcept.hpp
+++ b/arbor/include/arbor/arbexcept.hpp
@@ -79,6 +79,18 @@ struct bad_connection_target_lid: arbor_exception {
     cell_size_type num_targets;
 };
 
+struct bad_event_generator_target_gid: arbor_exception {
+    bad_event_generator_target_gid(cell_gid_type gid, cell_gid_type tgt_gid);
+    cell_gid_type gid, tgt_gid;
+};
+
+struct bad_event_generator_target_lid: arbor_exception {
+    bad_event_generator_target_lid(cell_gid_type gid, cell_lid_type tgt_lid, cell_size_type num_targets);
+    cell_gid_type gid;
+    cell_lid_type tgt_lid;
+    cell_size_type num_targets;
+};
+
 struct bad_global_property: arbor_exception {
     explicit bad_global_property(cell_kind kind);
     cell_kind kind;
diff --git a/arbor/include/arbor/event_generator.hpp b/arbor/include/arbor/event_generator.hpp
index 8d12a21817ad54e2579dfea9625d052a3cbea77d..4e526b58eb7c8f3460b724cb4c114fe148cd3757 100644
--- a/arbor/include/arbor/event_generator.hpp
+++ b/arbor/include/arbor/event_generator.hpp
@@ -16,12 +16,12 @@ namespace arb {
 
 // An `event_generator` generates a sequence of events to be delivered to a cell.
 // The sequence of events is always in ascending order, i.e. each event will be
-// greater than the event that proceded it, where events are ordered by:
+// greater than the event that proceeded it, where events are ordered by:
 //  - delivery time;
 //  - then target id for events with the same delivery time;
 //  - then weight for events with the same delivery time and target.
 //
-// An `event_generator` supports two operations:
+// An `event_generator` supports three operations:
 //
 // `void event_generator::reset()`
 //
@@ -32,6 +32,10 @@ namespace arb {
 //     Provide a non-owning view on to the events in the time interval
 //     [to, from).
 //
+// `std::vector<cell_member_type> targets()`
+//
+//     Return a vector of all the targets of the generator.
+//
 // The `event_seq` type is a pair of `spike_event` pointers that
 // provide a view onto an internally-maintained contiguous sequence
 // of generated spike event objects. This view is valid only for
@@ -64,6 +68,9 @@ struct empty_generator {
     event_seq events(time_type, time_type) {
         return {nullptr, nullptr};
     }
+    std::vector<cell_member_type> targets() {
+        return {};
+    };
 };
 
 class event_generator {
@@ -95,10 +102,15 @@ public:
         return impl_->events(t0, t1);
     }
 
+    std::vector<cell_member_type> targets() const {
+        return impl_->targets();
+    }
+
 private:
     struct interface {
         virtual void reset() = 0;
         virtual event_seq events(time_type, time_type) = 0;
+        virtual std::vector<cell_member_type> targets() = 0;
         virtual std::unique_ptr<interface> clone() = 0;
         virtual ~interface() {}
     };
@@ -114,6 +126,10 @@ private:
             return wrapped.events(t0, t1);
         }
 
+        std::vector<cell_member_type> targets() override {
+            return wrapped.targets();
+        }
+
         void reset() override {
             wrapped.reset();
         }
@@ -153,6 +169,10 @@ struct schedule_generator {
         return {events_.data(), events_.data()+events_.size()};
     }
 
+    std::vector<cell_member_type> targets() {
+        return {target_};
+    }
+
 private:
     pse_vector events_;
     cell_member_type target_;
@@ -217,6 +237,12 @@ struct explicit_generator {
         return {lb, ub};
     }
 
+    std::vector<cell_member_type> targets() {
+        std::vector<cell_member_type> tgts;
+        std::transform(events_.begin(), events_.end(), std::back_inserter(tgts), [](auto&& e){ return e.target;});
+        return tgts;
+    }
+
 private:
     pse_vector events_;
     std::size_t start_index_ = 0;
diff --git a/arbor/simulation.cpp b/arbor/simulation.cpp
index 9dae450c4f51cc610d647e752ead57222ad7f9ab..4564487df80ffc400a6c50a04a8d76474cf3b8f5 100644
--- a/arbor/simulation.cpp
+++ b/arbor/simulation.cpp
@@ -203,8 +203,23 @@ simulation_state::simulation_state(
             // Store mapping of gid to local cell index.
             gid_to_local_[gid] = gid_local_info{lidx, grpidx};
 
+            // Check validity of event_generator targets
+            auto event_gens = rec.event_generators(gid);
+            auto num_targets = rec.num_targets(gid);
+            for (const auto& g: event_gens) {
+                for (const auto& t: g.targets()) {
+                    if (t.gid != gid) {
+                        throw arb::bad_event_generator_target_gid(gid,  t.gid);
+                    }
+                    if (t.index >= num_targets) {
+                        throw arb::bad_event_generator_target_lid(gid, t.index, num_targets);
+                    }
+                }
+            }
+
             // Set up the event generators for cell gid.
-            event_generators_[lidx] = rec.event_generators(gid);
+            event_generators_[lidx] = event_gens;
+
             ++lidx;
         }
         ++grpidx;
diff --git a/python/example/single_cell_stdp.py b/python/example/single_cell_stdp.py
index 92816a210d68548cb237a6f5bb4524fb49533ffc..84fa6550fa2e02acc721717f129a3845fb69a714 100755
--- a/python/example/single_cell_stdp.py
+++ b/python/example/single_cell_stdp.py
@@ -22,6 +22,9 @@ class single_recipe(arbor.recipe):
     def num_sources(self, gid):
         return 1
 
+    def num_targets(self, gid):
+        return 2
+
     def cell_kind(self, gid):
         return arbor.cell_kind.cable
 
diff --git a/test/unit/test_recipe.cpp b/test/unit/test_recipe.cpp
index 13abf4fb3a0a958e69b9f897977cb632776bfe3a..0d2a648985bf268c4dae50e707513224931b26a4 100644
--- a/test/unit/test_recipe.cpp
+++ b/test/unit/test_recipe.cpp
@@ -26,12 +26,14 @@ namespace {
                       std::vector<cell_size_type> num_sources,
                       std::vector<cell_size_type> num_targets,
                       std::vector<std::vector<cell_connection>> conns,
-                      std::vector<std::vector<gap_junction_connection>> gjs):
+                      std::vector<std::vector<gap_junction_connection>> gjs,
+                      std::vector<std::vector<arb::event_generator>> gens):
             num_cells_(cells.size()),
             num_sources_(num_sources),
             num_targets_(num_targets),
             connections_(conns),
             gap_junctions_(gjs),
+            event_generators_(gens),
             cells_(cells) {}
 
         cell_size_type num_cells() const override {
@@ -55,6 +57,9 @@ namespace {
         cell_size_type num_targets(cell_gid_type gid) const override {
             return num_targets_[gid];
         }
+        std::vector<arb::event_generator> event_generators(cell_gid_type gid) const override {
+            return event_generators_[gid];
+        }
         std::any get_global_properties(cell_kind) const override {
             arb::cable_cell_global_properties a;
             a.default_parameters = arb::neuron_parameter_defaults;
@@ -66,6 +71,7 @@ namespace {
         std::vector<cell_size_type> num_sources_, num_targets_;
         std::vector<std::vector<cell_connection>> connections_;
         std::vector<std::vector<gap_junction_connection>> gap_junctions_;
+        std::vector<std::vector<arb::event_generator>> event_generators_;
         std::vector<cable_cell> cells_;
     };
 
@@ -111,13 +117,13 @@ TEST(recipe, num_sources)
     auto cell = custom_cell(1, 0, 0);
 
     {
-        auto recipe_0 = custom_recipe({cell}, {1}, {0}, {{}}, {{}});
+        auto recipe_0 = custom_recipe({cell}, {1}, {0}, {{}}, {{}}, {{}});
         auto decomp_0 = partition_load_balance(recipe_0, context);
 
         EXPECT_NO_THROW(simulation(recipe_0, decomp_0, context));
     }
     {
-        auto recipe_1 = custom_recipe({cell}, {2}, {0}, {{}}, {{}});
+        auto recipe_1 = custom_recipe({cell}, {2}, {0}, {{}}, {{}}, {{}});
         auto decomp_1 = partition_load_balance(recipe_1, context);
 
         EXPECT_THROW(simulation(recipe_1, decomp_1, context), arb::bad_source_description);
@@ -137,13 +143,13 @@ TEST(recipe, num_targets)
     auto cell = custom_cell(0, 2, 0);
 
     {
-        auto recipe_0 = custom_recipe({cell}, {0}, {2}, {{}}, {{}});
+        auto recipe_0 = custom_recipe({cell}, {0}, {2}, {{}}, {{}}, {{}});
         auto decomp_0 = partition_load_balance(recipe_0, context);
 
         EXPECT_NO_THROW(simulation(recipe_0, decomp_0, context));
     }
     {
-        auto recipe_1 = custom_recipe({cell}, {0}, {3}, {{}}, {{}});
+        auto recipe_1 = custom_recipe({cell}, {0}, {3}, {{}}, {{}}, {{}});
         auto decomp_1 = partition_load_balance(recipe_1, context);
 
         EXPECT_THROW(simulation(recipe_1, decomp_1, context), arb::bad_target_description);
@@ -169,7 +175,7 @@ TEST(recipe, gap_junctions)
                                                            {{0, 1}, {1, 2}, 0.1},
                                                            {{0, 2}, {1, 0}, 0.1}};
 
-        auto recipe_0 = custom_recipe({cell_0, cell_1}, {0, 0}, {0, 0}, {{}, {}}, {gjs_0, gjs_0});
+        auto recipe_0 = custom_recipe({cell_0, cell_1}, {0, 0}, {0, 0}, {{}, {}}, {gjs_0, gjs_0}, {{}, {}});
         auto decomp_0 = partition_load_balance(recipe_0, context);
 
         EXPECT_NO_THROW(simulation(recipe_0, decomp_0, context));
@@ -179,7 +185,7 @@ TEST(recipe, gap_junctions)
                                                            {{0, 1}, {1, 2}, 0.1},
                                                            {{0, 2}, {1, 5}, 0.1}};
 
-        auto recipe_1 = custom_recipe({cell_0, cell_1}, {0, 0}, {0, 0}, {{}, {}}, {gjs_1, gjs_1});
+        auto recipe_1 = custom_recipe({cell_0, cell_1}, {0, 0}, {0, 0}, {{}, {}}, {gjs_1, gjs_1}, {{}, {}});
         auto decomp_1 = partition_load_balance(recipe_1, context);
 
         EXPECT_THROW(simulation(recipe_1, decomp_1, context), arb::bad_gj_connection_lid);
@@ -190,7 +196,7 @@ TEST(recipe, gap_junctions)
                                                            {{0, 1}, {1, 2}, 0.1},
                                                            {{0, 2}, {3, 0}, 0.1}};
 
-        auto recipe_2 = custom_recipe({cell_0, cell_1}, {0, 0}, {0, 0}, {{}, {}}, {gjs_2, gjs_2});
+        auto recipe_2 = custom_recipe({cell_0, cell_1}, {0, 0}, {0, 0}, {{}, {}}, {gjs_2, gjs_2}, {{}, {}});
         auto context = make_context(resources);
 
         EXPECT_THROW(partition_load_balance(recipe_2, context), arb::bad_gj_connection_gid);
@@ -220,7 +226,7 @@ TEST(recipe, connections)
                    {{0, 0}, {1, 0}, 0.3, 0.1},
                    {{0, 0}, {1, 0}, 0.1, 0.8}};
 
-        auto recipe_0 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}});
+        auto recipe_0 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}},  {{}, {}});
         auto decomp_0 = partition_load_balance(recipe_0, context);
 
         EXPECT_NO_THROW(simulation(recipe_0, decomp_0, context));
@@ -234,7 +240,7 @@ TEST(recipe, connections)
                    {{0, 0}, {1, 0}, 0.3, 0.1},
                    {{0, 0}, {1, 0}, 0.1, 0.8}};
 
-        auto recipe_1 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}});
+        auto recipe_1 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}},  {{}, {}});
         auto decomp_1 = partition_load_balance(recipe_1, context);
 
         EXPECT_THROW(simulation(recipe_1, decomp_1, context), arb::bad_connection_source_gid);
@@ -248,7 +254,7 @@ TEST(recipe, connections)
                    {{0, 0}, {1, 0}, 0.3, 0.1},
                    {{0, 0}, {1, 0}, 0.1, 0.8}};
 
-        auto recipe_2 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}});
+        auto recipe_2 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}},  {{}, {}});
         auto decomp_2 = partition_load_balance(recipe_2, context);
 
         EXPECT_THROW(simulation(recipe_2, decomp_2, context), arb::bad_connection_source_lid);
@@ -262,7 +268,7 @@ TEST(recipe, connections)
                    {{0, 0}, {7, 0}, 0.3, 0.1},
                    {{0, 0}, {1, 0}, 0.1, 0.8}};
 
-        auto recipe_3 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}});
+        auto recipe_3 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}},  {{}, {}});
         auto decomp_3 = partition_load_balance(recipe_3, context);
 
         EXPECT_THROW(simulation(recipe_3, decomp_3, context), arb::bad_connection_target_gid);
@@ -276,7 +282,7 @@ TEST(recipe, connections)
                    {{0, 0}, {0, 0}, 0.3, 0.1},
                    {{0, 0}, {1, 0}, 0.1, 0.8}};
 
-        auto recipe_5 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}});
+        auto recipe_5 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}},  {{}, {}});
         auto decomp_5 = partition_load_balance(recipe_5, context);
 
         EXPECT_THROW(simulation(recipe_5, decomp_5, context), arb::bad_connection_target_gid);
@@ -290,9 +296,52 @@ TEST(recipe, connections)
                    {{0, 0}, {1, 9}, 0.3, 0.1},
                    {{0, 0}, {1, 0}, 0.1, 0.8}};
 
-        auto recipe_4 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}});
+        auto recipe_4 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {conns_0, conns_1}, {{}, {}},  {{}, {}});
         auto decomp_4 = partition_load_balance(recipe_4, context);
 
         EXPECT_THROW(simulation(recipe_4, decomp_4, context), arb::bad_connection_target_lid);
     }
 }
+
+TEST(recipe, event_generators) {
+    arb::proc_allocation resources;
+    if (auto nt = arbenv::get_env_num_threads()) {
+        resources.num_threads = nt;
+    }
+    else {
+        resources.num_threads = arbenv::thread_concurrency();
+    }
+    auto context = make_context(resources);
+
+    auto cell_0 = custom_cell(1, 2, 0);
+    auto cell_1 = custom_cell(2, 1, 0);
+    std::vector<arb::event_generator> gens_0, gens_1;
+    {
+        gens_0 = {arb::explicit_generator(arb::pse_vector{{{0, 0}, 1.0, 0.1}, {{0, 1}, 2.0, 0.1}})};
+
+        gens_1 = {arb::explicit_generator(arb::pse_vector{{{1, 0}, 1.0, 0.1}})};
+
+        auto recipe_0 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {{}, {}}, {{}, {}},  {gens_0, gens_1});
+        auto decomp_0 = partition_load_balance(recipe_0, context);
+
+        EXPECT_NO_THROW(simulation(recipe_0, decomp_0, context));
+    }
+    {
+        gens_0.clear();
+        gens_1 = {arb::explicit_generator(arb::pse_vector{{{0, 0}, 1.0, 0.1}})};
+
+        auto recipe_0 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {{}, {}}, {{}, {}},  {gens_0, gens_1});
+        auto decomp_0 = partition_load_balance(recipe_0, context);
+
+        EXPECT_THROW(simulation(recipe_0, decomp_0, context), arb::bad_event_generator_target_gid);
+    }
+    {
+        gens_0 = {arb::explicit_generator(arb::pse_vector{{{0, 0}, 1.0, 0.1}, {{0, 3}, 2.0, 0.1}})};
+        gens_1.clear();
+
+        auto recipe_0 = custom_recipe({cell_0, cell_1}, {1, 2}, {2, 1}, {{}, {}}, {{}, {}},  {gens_0, gens_1});
+        auto decomp_0 = partition_load_balance(recipe_0, context);
+
+        EXPECT_THROW(simulation(recipe_0, decomp_0, context), arb::bad_event_generator_target_lid);
+    }
+}