diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,12 @@
 # Revision history for reflex
 
+## 0.7.1.0
+
+* ([#413](https://github.com/reflex-frp/reflex/pull/413), [#417](https://github.com/reflex-frp/reflex/pull/417)) Add `Reflex.Host.Headless` module which provides `runHeadlessApp` as an easy way to run a Reflex network in a "headless" environment.
+* ([#420](https://github.com/reflex-frp/reflex/pull/420)) Add a [`Data.Zip.Unzip`](https://hackage.haskell.org/package/semialign-1.1/docs/Data-Zip.html#t:Unzip) instance for `Event`.
+* ([#419](https://github.com/reflex-frp/reflex/pull/419)) Add `distributeIntMapOverDynPure` and `joinDynThroughIntMap` as convenience functions for working with `Dynamic` `IntMap`s.
+
+
 ## 0.7.0.0
 
 * Add lifting instances for most classes to `Reflex.Profiled.Profiled`. ([#398](https://github.com/reflex-frp/reflex/pull/398))
diff --git a/reflex.cabal b/reflex.cabal
--- a/reflex.cabal
+++ b/reflex.cabal
@@ -1,5 +1,5 @@
 Name: reflex
-Version: 0.7.0.0
+Version: 0.7.1.0
 Synopsis: Higher-order Functional Reactive Programming
 Description: Reflex is a high-performance, deterministic, higher-order Functional Reactive Programming system
 License: BSD3
@@ -119,6 +119,7 @@
     Reflex.FastWeak,
     Reflex.FunctorMaybe,
     Reflex.Host.Class,
+    Reflex.Host.Headless,
     Reflex.Network,
     Reflex.NotReady.Class,
     Reflex.PerformEvent.Base,
@@ -243,7 +244,7 @@
                , directory
                , filepath
                , filemanip
-               , hlint < 2.1 || >= 2.2.2
+               , hlint (< 2.1 || >= 2.2.2) && < 3
   if impl(ghcjs)
     buildable: False
 
diff --git a/src/Reflex/Class.hs b/src/Reflex/Class.hs
--- a/src/Reflex/Class.hs
+++ b/src/Reflex/Class.hs
@@ -180,7 +180,7 @@
 import Data.These.Combinators (justThese)
 #endif
 #if MIN_VERSION_semialign(1,1,0)
-import Data.Zip (Zip (..))
+import Data.Zip (Zip (..), Unzip (..))
 #endif
 #endif
 
@@ -1091,6 +1091,12 @@
   zip x y = mapMaybe justThese $ align x y
 #endif
 
+#ifdef MIN_VERSION_semialign
+#if MIN_VERSION_semialign(1,1,0)
+instance Reflex t => Unzip (Event t) where
+  unzip = splitE
+#endif
+#endif
 
 -- | Create a new 'Event' that only occurs if the supplied 'Event' occurs and
 -- the 'Behavior' is true at the time of occurrence.
diff --git a/src/Reflex/Dynamic.hs b/src/Reflex/Dynamic.hs
--- a/src/Reflex/Dynamic.hs
+++ b/src/Reflex/Dynamic.hs
@@ -53,10 +53,12 @@
   , foldDynMaybe
   , foldDynMaybeM
   , joinDynThroughMap
+  , joinDynThroughIntMap
   , traceDyn
   , traceDynWith
   , splitDynPure
   , distributeMapOverDynPure
+  , distributeIntMapOverDynPure
   , distributeDMapOverDynPure
   , distributeListOverDynPure
   , Demux
@@ -88,6 +90,8 @@
 import qualified Data.Dependent.Map as DMap
 import Data.Dependent.Sum (DSum (..))
 import Data.GADT.Compare (GCompare (..), GEq (..), GOrdering (..))
+import Data.IntMap (IntMap)
+import qualified Data.IntMap as IntMap
 import Data.Map (Map)
 import Data.Maybe
 import Data.Monoid ((<>))
@@ -202,6 +206,11 @@
 distributeMapOverDynPure :: (Reflex t, Ord k) => Map k (Dynamic t v) -> Dynamic t (Map k v)
 distributeMapOverDynPure = fmap dmapToMap . distributeDMapOverDynPure . mapWithFunctorToDMap
 
+-- | Convert an 'IntMap' with 'Dynamic' elements into a 'Dynamic' of an 'IntMap' with
+-- non-'Dynamic' elements.
+distributeIntMapOverDynPure :: (Reflex t) => IntMap (Dynamic t v) -> Dynamic t (IntMap v)
+distributeIntMapOverDynPure = fmap dmapToIntMap . distributeDMapOverDynPure . intMapWithFunctorToDMap
+
 -- | Convert a list with 'Dynamic' elements into a 'Dynamic' of a list with
 -- non-'Dynamic' elements, preserving the order of the elements.
 {-# DEPRECATED distributeListOverDynPure "Use 'distributeListOverDyn' instead" #-}
@@ -213,6 +222,11 @@
 -- with the current values of the 'Dynamic's in a map.
 joinDynThroughMap :: forall t k a. (Reflex t, Ord k) => Dynamic t (Map k (Dynamic t a)) -> Dynamic t (Map k a)
 joinDynThroughMap = (distributeMapOverDynPure =<<)
+
+-- | Combine a 'Dynamic' of an 'IntMap' of 'Dynamic's into a 'Dynamic'
+-- with the current values of the 'Dynamic's in a map.
+joinDynThroughIntMap :: forall t a. (Reflex t) => Dynamic t (IntMap (Dynamic t a)) -> Dynamic t (IntMap a)
+joinDynThroughIntMap = (distributeIntMapOverDynPure =<<)
 
 -- | Print the value of the 'Dynamic' when it is first read and on each
 -- subsequent change that is observed (as 'traceEvent'), prefixed with the
diff --git a/src/Reflex/Host/Headless.hs b/src/Reflex/Host/Headless.hs
new file mode 100644
--- /dev/null
+++ b/src/Reflex/Host/Headless.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE ConstraintKinds #-}
+
+module Reflex.Host.Headless where
+
+import Control.Concurrent.Chan (newChan, readChan)
+import Control.Monad.Fix (MonadFix, fix)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Primitive (PrimMonad)
+import Control.Monad.Ref (MonadRef, Ref, readRef)
+import Data.Dependent.Sum (DSum (..), (==>))
+import Data.Foldable (for_)
+import Data.Functor.Identity (Identity(..))
+import Data.IORef (IORef, readIORef)
+import Data.Maybe (catMaybes)
+import Data.Traversable (for)
+
+import Reflex
+import Reflex.Host.Class
+
+type MonadHeadlessApp t m =
+  ( Adjustable t m
+  , MonadFix m
+  , MonadHold t m
+  , MonadIO (HostFrame t)
+  , MonadIO (Performable m)
+  , MonadIO m
+  , MonadRef (HostFrame t)
+  , NotReady t m
+  , PerformEvent t m
+  , PostBuild t m
+  , PrimMonad (HostFrame t)
+  , Ref (HostFrame t) ~ IORef
+  , Ref m ~ IORef
+  , Reflex t
+  , ReflexHost t
+  , TriggerEvent t m
+  )
+
+-- | Run a headless FRP network. Inside the action, you will most probably use
+-- the capabilities provided by the 'TriggerEvent' and 'PerformEvent' type
+-- classes to interface the FRP network with the outside world. Useful for
+-- testing. Each headless network runs on its own spider timeline.
+runHeadlessApp
+  :: (forall t m. MonadHeadlessApp t m => m (Event t ()))
+  -- ^ The action to be run in the headless FRP network. The FRP network is
+  -- closed at the first occurrence of the resulting 'Event'.
+  -> IO ()
+runHeadlessApp guest =
+  -- We are using the 'Spider' implementation of reflex. Running the host
+  -- allows us to take actions on the FRP timeline.
+  withSpiderTimeline $ runSpiderHostForTimeline $ do
+    -- Create the "post-build" event and associated trigger. This event fires
+    -- once, when the application starts.
+    (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
+    -- Create a queue to which we will write 'Event's that need to be
+    -- processed.
+    events <- liftIO newChan
+    -- Run the "guest" application, providing the appropriate context. We'll
+    -- pure the result of the action, and a 'FireCommand' that will be used to
+    -- trigger events.
+    (result, fc@(FireCommand fire)) <- do
+      hostPerformEventT $                 -- Allows the guest app to run
+                                          -- 'performEvent', so that actions
+                                          -- (e.g., IO actions) can be run when
+                                          -- 'Event's fire.
+
+        flip runPostBuildT postBuild $    -- Allows the guest app to access to
+                                          -- a "post-build" 'Event'
+
+          flip runTriggerEventT events $  -- Allows the guest app to create new
+                                          -- events and triggers and write
+                                          -- those triggers to a channel from
+                                          -- which they will be read and
+                                          -- processed.
+            guest
+
+    -- Read the trigger reference for the post-build event. This will be
+    -- 'Nothing' if the guest application hasn't subscribed to this event.
+    mPostBuildTrigger <- readRef postBuildTriggerRef
+
+    -- When there is a subscriber to the post-build event, fire the event.
+    for_ mPostBuildTrigger $ \postBuildTrigger ->
+      fire [postBuildTrigger :=> Identity ()] $ pure ()
+
+    -- Subscribe to an 'Event' of that the guest application can use to
+    -- request application shutdown. We'll check whether this 'Event' is firing
+    -- to determine whether to terminate.
+    shutdown <- subscribeEvent result
+
+    -- The main application loop. We wait for new events and fire those that
+    -- have subscribers. If we detect a shutdown request, the application
+    -- terminates.
+    fix $ \loop -> do
+      -- Read the next event (blocking).
+      ers <- liftIO $ readChan events
+      stop <- do
+        -- Fire events that have subscribers.
+        fireEventTriggerRefs fc ers $
+          -- Check if the shutdown 'Event' is firing.
+          readEvent shutdown >>= \case
+            Nothing -> pure False
+            Just _ -> pure True
+      if or stop
+        then pure ()
+        else loop
+  where
+    -- Use the given 'FireCommand' to fire events that have subscribers
+    -- and call the callback for the 'TriggerInvocation' of each.
+    fireEventTriggerRefs
+      :: MonadIO m
+      => FireCommand t m
+      -> [DSum (EventTriggerRef t) TriggerInvocation]
+      -> ReadPhase m a
+      -> m [a]
+    fireEventTriggerRefs (FireCommand fire) ers rcb = do
+      mes <- liftIO $
+        for ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do
+          me <- readIORef er
+          pure $ fmap (==> a) me
+      a <- fire (catMaybes mes) rcb
+      liftIO $ for_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
+      pure a
