Repository: swimos/swim-rust (language: rust)

Issue:
Add `with_value` handler for value like items in agents.
The `HandlerContext` provides the following handler for map like items:

```rust
    pub fn with_entry<'a, Item, K, V, F>(
        &self,
        lane: fn(&Agent) -> &Item,
        key: K,
        f: F,
    ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'a
    where
        Agent: 'static,
        Item: TransformableMapLikeItem<K, V> + 'static,
        K: Send + Clone + Eq + Hash + 'static,
        V: Clone + 'static, // Clone here is spurious, see #607
        F: FnOnce(Option<V>) -> Option<V> + Send + 'a,
```

This creates a handler that will transform the value associated with a key in the map like item.

This is inflexible as it requires that the value be cloned. It should be altered to borrow the current value instead. An additional combinator called `transform_entry` can be introduced with the existing functionality.

Equivalent combinators should be added for value like items.


Discussion / hints:

Fix patch:
diff --git a/example_apps/tutorial_app/src/unit_agent.rs b/example_apps/tutorial_app/src/unit_agent.rs
index 7501ead6c..55d8fef61 100644
--- a/example_apps/tutorial_app/src/unit_agent.rs
+++ b/example_apps/tutorial_app/src/unit_agent.rs
@@ -153,8 +153,8 @@ fn update_histogram(
     item: HistoryItem,
 ) -> impl EventHandler<UnitAgent> {
     let bucket = bucket_of(&item.timestamp);
-    context.with_entry(UnitAgent::HISTOGRAM, bucket, |maybe| {
-        let mut counter = maybe.unwrap_or_default();
+    context.transform_entry(UnitAgent::HISTOGRAM, bucket, |maybe| {
+        let mut counter = maybe.copied().unwrap_or_default();
         counter.count += rand::thread_rng().gen_range(0..20);
         Some(counter)
     })
diff --git a/server/swimos_agent/src/agent_lifecycle/utility/mod.rs b/server/swimos_agent/src/agent_lifecycle/utility/mod.rs
index 6f18a3882..aec999a17 100644
--- a/server/swimos_agent/src/agent_lifecycle/utility/mod.rs
+++ b/server/swimos_agent/src/agent_lifecycle/utility/mod.rs
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 use std::any::Any;
+use std::borrow::Borrow;
 use std::fmt::Debug;
 use std::hash::Hash;
 use std::time::Duration;
@@ -41,7 +42,7 @@ use crate::event_handler::{
 };
 use crate::event_handler::{GetAgentUri, HandlerAction, SideEffect};
 use crate::item::{
-    JoinLikeItem, MapLikeItem, MutableMapLikeItem, MutableValueLikeItem, TransformableMapLikeItem,
+    InspectableMapLikeItem, JoinLikeItem, MapLikeItem, MutableMapLikeItem, MutableValueLikeItem,
     ValueLikeItem,
 };
 use crate::lanes::command::{CommandLane, DoCommand};
@@ -157,45 +158,86 @@ impl<Agent: 'static> HandlerContext<Agent> {
 
     /// Create an event handler that will get the value of a value lane store of the agent.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the value lane.
+    /// #Arguments
+    /// * `item` - Projection to the value lane or store.
     pub fn get_value<Item, T>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
     ) -> impl HandlerAction<Agent, Completion = T> + Send + 'static
     where
         Item: ValueLikeItem<T>,
         T: Clone + Send + 'static,
     {
-        Item::get_handler::<Agent>(lane)
+        Item::get_handler::<Agent>(item)
     }
 
-    /// Create an event handler that will set a new value into value lane or store of the agent.
+    /// Create an event handler that will set a new value into a value lane or store of the agent.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the value lane.
+    /// #Arguments
+    /// * `item` - Projection to the value lane or store.
     /// * `value` - The value to set.
     pub fn set_value<Item, T>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
         value: T,
     ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
     where
         Item: MutableValueLikeItem<T>,
         T: Send + 'static,
     {
-        Item::set_handler::<Agent>(lane, value)
+        Item::set_handler::<Agent>(item, value)
+    }
+
+    /// Create an event handler that will transform the value of a value lane or store of the agent.
+    ///
+    /// #Arguments
+    /// * `item` - Projection to the value lane or store.
+    /// * `f` - A closure that produces a new value from a reference to the existing value.
+    pub fn transform_value<'a, Item, T, F>(
+        &self,
+        item: fn(&Agent) -> &Item,
+        f: F,
+    ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'a
+    where
+        Agent: 'static,
+        Item: ValueLikeItem<T> + MutableValueLikeItem<T> + 'static,
+        T: 'static,
+        F: FnOnce(&T) -> T + Send + 'a,
+    {
+        Item::with_value_handler::<Item, Agent, F, T, T>(item, f)
+            .and_then(move |v| Item::set_handler(item, v))
+    }
+
+    /// Create an event handler that will inspect the value of a value lane or store and generate a result from it.
+    /// This differs from using [`Self::get_value`] in that it does not require a clone to be made of the existing value.
+    ///
+    /// #Arguments
+    /// * `item` - Projection to the value lane or store.
+    /// * `f` - A closure that produces a value from a reference to the current value of the item.
+    pub fn with_value<'a, Item, T, F, B, U>(
+        &self,
+        item: fn(&Agent) -> &Item,
+        f: F,
+    ) -> impl HandlerAction<Agent, Completion = U> + Send + 'a
+    where
+        Agent: 'static,
+        Item: ValueLikeItem<T> + 'static,
+        T: Borrow<B>,
+        B: 'static,
+        F: FnOnce(&B) -> U + Send + 'a,
+    {
+        Item::with_value_handler::<Item, Agent, F, B, U>(item, f)
     }
 
     /// Create an event handler that will update an entry in a map lane or store of the agent.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map lane or store.
     /// * `key - The key to update.
     /// * `value` - The new value.
     pub fn update<Item, K, V>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
         key: K,
         value: V,
     ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
@@ -204,39 +246,66 @@ impl<Agent: 'static> HandlerContext<Agent> {
         K: Send + Clone + Eq + Hash + 'static,
         V: Send + 'static,
     {
-        Item::update_handler::<Agent>(lane, key, value)
+        Item::update_handler::<Agent>(item, key, value)
     }
 
-    /// Create an event handler that will transform the value in an entry of a map lane or store of the agent.
+    /// Create an event handler that will inspect an entry in the map and produce a new value from it.
+    /// This differs from using [`Self::get_entry`] in that it does not require that a clone be made of the existing value.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map lane or store.
     /// * `key - The key to update.
     /// * `f` - A function to apple to the entry in the map.
-    pub fn with_entry<'a, Item, K, V, F>(
+    pub fn with_entry<'a, Item, K, V, F, B, U>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
+        key: K,
+        f: F,
+    ) -> impl HandlerAction<Agent, Completion = U> + Send + 'a
+    where
+        Agent: 'static,
+        Item: InspectableMapLikeItem<K, V> + 'static,
+        K: Send + Clone + Eq + Hash + 'static,
+        V: Borrow<B> + 'static,
+        B: ?Sized + 'static,
+        F: FnOnce(Option<&B>) -> U + Send + 'a,
+    {
+        Item::with_entry_handler::<Agent, F, B, U>(item, key, f)
+    }
+
+    /// Create an event handler that will transform the value in an entry of a map lane or store of the agent.
+    /// If the map contains an entry with that key, it will be updated (or removed) based on the result of the calling
+    /// the closure on it. If the map does not contain an entry with that key, the closure will be called with [`None`]
+    /// and an entry will be inserted if it returns a value.
+    ///
+    /// #Arguments
+    /// * `item` - Projection to the map lane.
+    /// * `key - The key to update.
+    /// * `f` - A closure to apply to the entry in the map to produce the replacement.
+    pub fn transform_entry<'a, Item, K, V, F>(
+        &self,
+        item: fn(&Agent) -> &Item,
         key: K,
         f: F,
     ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'a
     where
         Agent: 'static,
-        Item: TransformableMapLikeItem<K, V> + 'static,
+        Item: MutableMapLikeItem<K, V> + 'static,
         K: Send + Clone + Eq + Hash + 'static,
-        V: Clone + 'static,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a,
+        V: 'static,
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a,
     {
-        Item::with_handler::<Agent, F>(lane, key, f)
+        Item::transform_entry_handler::<Agent, F>(item, key, f)
     }
 
     /// Create an event handler that will remove an entry from a map lane or store of the agent.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map lane or store.
     /// * `key - The key to remove.
     pub fn remove<Item, K, V>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
         key: K,
     ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
     where
@@ -244,33 +313,33 @@ impl<Agent: 'static> HandlerContext<Agent> {
         K: Send + Clone + Eq + Hash + 'static,
         V: Send + 'static,
     {
-        Item::remove_handler::<Agent>(lane, key)
+        Item::remove_handler::<Agent>(item, key)
     }
 
     /// Create an event handler that will clear a map lane or store of the agent.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map lane or store.
     pub fn clear<Item, K, V>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
     ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
     where
         Item: MutableMapLikeItem<K, V>,
         K: Send + Clone + Eq + Hash + 'static,
         V: Send + 'static,
     {
-        Item::clear_handler::<Agent>(lane)
+        Item::clear_handler::<Agent>(item)
     }
 
     /// Create an event handler that replaces the entire contents of a map lane or store.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map lane or store.
     /// * `entries` - The new entries for the lane.
     pub fn replace_map<Item, K, V, I>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
         entries: I,
     ) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
     where
@@ -283,19 +352,19 @@ impl<Agent: 'static> HandlerContext<Agent> {
         let context = *self;
         let insertions = entries
             .into_iter()
-            .map(move |(k, v)| context.update(lane, k, v));
-        self.clear(lane).followed_by(Sequentially::new(insertions))
+            .map(move |(k, v)| context.update(item, k, v));
+        self.clear(item).followed_by(Sequentially::new(insertions))
     }
 
     /// Create an event handler that will attempt to get an entry from a map-like item of the agent.
     /// This includes map lanes and stores and join lanes.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map-like item.
     /// * `key - The key to fetch.
     pub fn get_entry<Item, K, V>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
         key: K,
     ) -> impl HandlerAction<Agent, Completion = Option<V>> + Send + 'static
     where
@@ -303,24 +372,24 @@ impl<Agent: 'static> HandlerContext<Agent> {
         K: Send + Clone + Eq + Hash + 'static,
         V: Send + Clone + 'static,
     {
-        Item::get_handler::<Agent>(lane, key)
+        Item::get_handler::<Agent>(item, key)
     }
 
     /// Create an event handler that will attempt to get the entire contents of a map-like item of the
     /// agent. This includes map lanes and stores and join lanes.
     ///
-    /// # Arguments
-    /// * `lane` - Projection to the map lane.
+    /// #Arguments
+    /// * `item` - Projection to the map-like item.
     pub fn get_map<Item, K, V>(
         &self,
-        lane: fn(&Agent) -> &Item,
+        item: fn(&Agent) -> &Item,
     ) -> impl HandlerAction<Agent, Completion = HashMap<K, V>> + Send + 'static
     where
         Item: MapLikeItem<K, V>,
         K: Send + Clone + Eq + Hash + 'static,
         V: Send + Clone + 'static,
     {
-        Item::get_map_handler::<Agent>(lane)
+        Item::get_map_handler::<Agent>(item)
     }
 
     /// Create an event handler that will send a command to a command lane of the agent.
diff --git a/server/swimos_agent/src/item.rs b/server/swimos_agent/src/item.rs
index 40f1cf22a..81a78d287 100644
--- a/server/swimos_agent/src/item.rs
+++ b/server/swimos_agent/src/item.rs
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use std::collections::HashMap;
+use std::{borrow::Borrow, collections::HashMap};
 
 use crate::{
     event_handler::{EventHandler, HandlerAction},
@@ -51,9 +51,32 @@ pub trait MapLikeItem<K, V> {
         C: 'static;
 
     fn get_handler<C: 'static>(projection: fn(&C) -> &Self, key: K) -> Self::GetHandler<C>;
+
     fn get_map_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::GetMapHandler<C>;
 }
 
+pub trait InspectableMapLikeItem<K, V> {
+    type WithEntryHandler<'a, C, F, B, U>: HandlerAction<C, Completion = U> + Send + 'a
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a;
+
+    fn with_entry_handler<'a, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        key: K,
+        f: F,
+    ) -> Self::WithEntryHandler<'a, C, F, B, U>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a;
+}
+
 pub trait MutableMapLikeItem<K, V> {
     type UpdateHandler<C>: EventHandler<C> + Send + 'static
     where
@@ -72,24 +95,22 @@ pub trait MutableMapLikeItem<K, V> {
     ) -> Self::UpdateHandler<C>;
     fn remove_handler<C: 'static>(projection: fn(&C) -> &Self, key: K) -> Self::RemoveHandler<C>;
     fn clear_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::ClearHandler<C>;
-}
 
-pub trait TransformableMapLikeItem<K, V> {
-    type WithEntryHandler<'a, C, F>: EventHandler<C> + Send + 'a
+    type TransformEntryHandler<'a, C, F>: EventHandler<C> + Send + 'a
     where
         Self: 'static,
         C: 'a,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a;
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a;
 
-    fn with_handler<'a, C, F>(
+    fn transform_entry_handler<'a, C, F>(
         projection: fn(&C) -> &Self,
         key: K,
         f: F,
-    ) -> Self::WithEntryHandler<'a, C, F>
+    ) -> Self::TransformEntryHandler<'a, C, F>
     where
         Self: 'static,
         C: 'a,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a;
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a;
 }
 
 pub trait ValueLikeItem<T> {
@@ -97,7 +118,25 @@ pub trait ValueLikeItem<T> {
     where
         C: 'static;
 
+    type WithValueHandler<'a, C, F, B, U>: HandlerAction<C, Completion = U> + Send + 'a
+    where
+        Self: 'static,
+        C: 'a,
+        T: Borrow<B>,
+        B: ?Sized + 'static,
+        F: FnOnce(&B) -> U + Send + 'a;
+
     fn get_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::GetHandler<C>;
+
+    fn with_value_handler<'a, Item, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        f: F,
+    ) -> Self::WithValueHandler<'a, C, F, B, U>
+    where
+        C: 'a,
+        T: Borrow<B>,
+        B: ?Sized + 'static,
+        F: FnOnce(&B) -> U + Send + 'a;
 }
 
 pub trait MutableValueLikeItem<T> {
diff --git a/server/swimos_agent/src/lanes/join/map/mod.rs b/server/swimos_agent/src/lanes/join/map/mod.rs
index 307061ebd..7f590c3e5 100644
--- a/server/swimos_agent/src/lanes/join/map/mod.rs
+++ b/server/swimos_agent/src/lanes/join/map/mod.rs
@@ -18,6 +18,7 @@ use std::{
     cell::RefCell,
     collections::{hash_map::Entry, BTreeSet, HashMap, HashSet},
     hash::Hash,
+    marker::PhantomData,
 };
 
 use bytes::BytesMut;
@@ -38,7 +39,7 @@ use crate::{
     event_handler::{
         ActionContext, EventHandler, EventHandlerError, HandlerAction, Modification, StepResult,
     },
-    item::{AgentItem, MapItem, MapLikeItem},
+    item::{AgentItem, InspectableMapLikeItem, MapItem, MapLikeItem},
     lanes::{
         join_map::default_lifecycle::DefaultJoinMapLifecycle, map::MapLaneEvent, LaneItem, MapLane,
     },
@@ -685,6 +686,55 @@ where
     }
 }
 
+/// A [`HandlerAction`] that will produce a value by applying a closure to a reference to
+/// and entry in the lane.
+pub struct JoinMapLaneWithEntry<C, L, K, V, F, B: ?Sized> {
+    projection: for<'a> fn(&'a C) -> &'a JoinMapLane<L, K, V>,
+    key: K,
+    f: Option<F>,
+    _type: PhantomData<fn(&B)>,
+}
+
+impl<C, L, K, V, F, B: ?Sized> JoinMapLaneWithEntry<C, L, K, V, F, B> {
+    /// #Arguments
+    /// * `projection` - Projection from the agent context to the lane.
+    /// * `key` - Key of the entry.
+    /// * `f` - The closure to apply to the entry.
+    pub fn new(projection: for<'a> fn(&'a C) -> &'a JoinMapLane<L, K, V>, key: K, f: F) -> Self {
+        JoinMapLaneWithEntry {
+            projection,
+            key,
+            f: Some(f),
+            _type: PhantomData,
+        }
+    }
+}
+
+impl<'a, C, L, K, V, F, B, U> HandlerAction<C> for JoinMapLaneWithEntry<C, L, K, V, F, B>
+where
+    K: Eq + Hash + 'static,
+    C: 'a,
+    B: ?Sized + 'static,
+    V: Borrow<B>,
+    F: FnOnce(Option<&B>) -> U + Send + 'a,
+{
+    type Completion = U;
+
+    fn step(
+        &mut self,
+        _action_context: &mut ActionContext<C>,
+        _meta: AgentMetadata,
+        context: &C,
+    ) -> StepResult<Self::Completion> {
+        if let Some(f) = self.f.take() {
+            let item = (self.projection)(context);
+            StepResult::done(item.inner.with_entry(&self.key, f))
+        } else {
+            StepResult::after_done()
+        }
+    }
+}
+
 impl<L, K, V> MapLikeItem<K, V> for JoinMapLane<L, K, V>
 where
     L: Send + 'static,
@@ -708,6 +758,36 @@ where
     }
 }
 
+impl<L, K, V> InspectableMapLikeItem<K, V> for JoinMapLane<L, K, V>
+where
+    L: Send + 'static,
+    K: Clone + Eq + Hash + Send + 'static,
+    V: Send + 'static,
+{
+    type WithEntryHandler<'a, C, F, B, U> = JoinMapLaneWithEntry<C, L, K, V, F, B>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a;
+
+    fn with_entry_handler<'a, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        key: K,
+        f: F,
+    ) -> Self::WithEntryHandler<'a, C, F, B, U>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a,
+    {
+        JoinMapLaneWithEntry::new(projection, key, f)
+    }
+}
+
 /// An [`EventHandler`] that will remove a downlink from the lane.
 pub struct JoinMapRemoveDownlink<L, C, K, V> {
     projection: fn(&C) -> &JoinMapLane<L, K, V>,
diff --git a/server/swimos_agent/src/lanes/join/value/mod.rs b/server/swimos_agent/src/lanes/join/value/mod.rs
index 132528608..bd307b4b6 100644
--- a/server/swimos_agent/src/lanes/join/value/mod.rs
+++ b/server/swimos_agent/src/lanes/join/value/mod.rs
@@ -16,6 +16,7 @@ use std::any::{Any, TypeId};
 use std::borrow::Borrow;
 use std::collections::hash_map::Entry;
 use std::hash::Hash;
+use std::marker::PhantomData;
 use std::{cell::RefCell, collections::HashMap};
 
 use bytes::BytesMut;
@@ -28,7 +29,7 @@ use uuid::Uuid;
 use crate::agent_model::downlink::OpenEventDownlinkAction;
 use crate::config::SimpleDownlinkConfig;
 use crate::event_handler::{EventHandler, EventHandlerError, Modification};
-use crate::item::{JoinLikeItem, MapLikeItem};
+use crate::item::{InspectableMapLikeItem, JoinLikeItem, MapLikeItem};
 use crate::{
     agent_model::WriteResult,
     event_handler::{ActionContext, HandlerAction, StepResult},
@@ -460,6 +461,55 @@ impl<C, K, V> JoinValueAddDownlink<C, K, V> {
     }
 }
 
+/// A [`HandlerAction`] that will produce a value by applying a closure to a reference to
+/// and entry in the lane.
+pub struct JoinValueLaneWithEntry<C, K, V, F, B: ?Sized> {
+    projection: for<'a> fn(&'a C) -> &'a JoinValueLane<K, V>,
+    key: K,
+    f: Option<F>,
+    _type: PhantomData<fn(&B)>,
+}
+
+impl<C, K, V, F, B: ?Sized> JoinValueLaneWithEntry<C, K, V, F, B> {
+    /// #Arguments
+    /// * `projection` - Projection from the agent context to the lane.
+    /// * `key` - Key of the entry.
+    /// * `f` - The closure to apply to the entry.
+    pub fn new(projection: for<'a> fn(&'a C) -> &'a JoinValueLane<K, V>, key: K, f: F) -> Self {
+        JoinValueLaneWithEntry {
+            projection,
+            key,
+            f: Some(f),
+            _type: PhantomData,
+        }
+    }
+}
+
+impl<'a, C, K, V, F, B, U> HandlerAction<C> for JoinValueLaneWithEntry<C, K, V, F, B>
+where
+    K: Eq + Hash + 'static,
+    C: 'a,
+    B: ?Sized + 'static,
+    V: Borrow<B>,
+    F: FnOnce(Option<&B>) -> U + Send + 'a,
+{
+    type Completion = U;
+
+    fn step(
+        &mut self,
+        _action_context: &mut ActionContext<C>,
+        _meta: AgentMetadata,
+        context: &C,
+    ) -> StepResult<Self::Completion> {
+        if let Some(f) = self.f.take() {
+            let item = (self.projection)(context);
+            StepResult::done(item.inner.with_entry(&self.key, f))
+        } else {
+            StepResult::after_done()
+        }
+    }
+}
+
 impl<K, V> MapLikeItem<K, V> for JoinValueLane<K, V>
 where
     K: Clone + Eq + Hash + Send + 'static,
@@ -482,6 +532,35 @@ where
     }
 }
 
+impl<K, V> InspectableMapLikeItem<K, V> for JoinValueLane<K, V>
+where
+    K: Clone + Eq + Hash + Send + 'static,
+    V: Send + 'static,
+{
+    type WithEntryHandler<'a, C, F, B, U> = JoinValueLaneWithEntry<C, K, V, F, B>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a;
+
+    fn with_entry_handler<'a, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        key: K,
+        f: F,
+    ) -> Self::WithEntryHandler<'a, C, F, B, U>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a,
+    {
+        JoinValueLaneWithEntry::new(projection, key, f)
+    }
+}
+
 /// An [`EventHandler`] that will remove a downlink from the lane.
 pub struct JoinValueRemoveDownlink<C, K, V> {
     projection: fn(&C) -> &JoinValueLane<K, V>,
diff --git a/server/swimos_agent/src/lanes/map/mod.rs b/server/swimos_agent/src/lanes/map/mod.rs
index 1ed763896..19ccf3ef3 100644
--- a/server/swimos_agent/src/lanes/map/mod.rs
+++ b/server/swimos_agent/src/lanes/map/mod.rs
@@ -34,8 +34,8 @@ use crate::{
         ActionContext, AndThen, EventHandlerError, HandlerAction, HandlerActionExt, HandlerTrans,
         Modification, StepResult,
     },
-    item::{AgentItem, MapItem, MapLikeItem, MutableMapLikeItem, TransformableMapLikeItem},
-    map_storage::{MapStoreInner, WithEntryResult},
+    item::{AgentItem, InspectableMapLikeItem, MapItem, MapLikeItem, MutableMapLikeItem},
+    map_storage::{MapStoreInner, TransformEntryResult},
     meta::AgentMetadata,
 };
 
@@ -104,12 +104,11 @@ where
     }
 
     /// Transform the value associated with a key.
-    pub(crate) fn with_entry<F>(&self, key: K, f: F) -> WithEntryResult
+    pub fn transform_entry<F>(&self, key: K, f: F) -> TransformEntryResult
     where
-        V: Clone,
-        F: FnOnce(Option<V>) -> Option<V>,
+        F: FnOnce(Option<&V>) -> Option<V>,
     {
-        self.inner.borrow_mut().with_entry(key, f)
+        self.inner.borrow_mut().transform_entry(key, f)
     }
 
     /// Remove and entry from the map.
@@ -129,7 +128,7 @@ where
         Q: Hash + Eq,
         F: FnOnce(Option<&V>) -> R,
     {
-        self.inner.borrow().get(key, f)
+        self.inner.borrow().with_entry(key, f)
     }
 
     /// Read the complete state of the map.
@@ -147,6 +146,20 @@ where
     }
 }
 
+impl<K, V> MapLane<K, V>
+where
+    K: Eq + Hash,
+{
+    pub fn with_entry<F, B, U>(&self, key: &K, f: F) -> U
+    where
+        B: ?Sized,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U,
+    {
+        self.inner.borrow().with_entry(key, f)
+    }
+}
+
 const INFALLIBLE_SER: &str = "Serializing lane responses to recon should be infallible.";
 
 impl<K, V> LaneItem for MapLane<K, V>
@@ -381,6 +394,56 @@ where
     }
 }
 
+impl<C, K, V, F, B, U> HandlerAction<C> for MapLaneWithEntry<C, K, V, F, B>
+where
+    K: Eq + Hash,
+    B: ?Sized,
+    V: Borrow<B>,
+    F: FnOnce(Option<&B>) -> U,
+{
+    type Completion = U;
+
+    fn step(
+        &mut self,
+        _action_context: &mut ActionContext<C>,
+        _meta: AgentMetadata,
+        context: &C,
+    ) -> StepResult<Self::Completion> {
+        let MapLaneWithEntry {
+            projection,
+            key_and_f,
+            ..
+        } = self;
+        if let Some((key, f)) = key_and_f.take() {
+            let lane = projection(context);
+            StepResult::done(lane.with_entry(&key, f))
+        } else {
+            StepResult::after_done()
+        }
+    }
+}
+
+///  An [event handler](crate::event_handler::EventHandler)`] that will alter an entry in the map.
+pub struct MapLaneWithEntry<C, K, V, F, B: ?Sized> {
+    projection: for<'a> fn(&'a C) -> &'a MapLane<K, V>,
+    key_and_f: Option<(K, F)>,
+    _type: PhantomData<fn(&B)>,
+}
+
+impl<C, K, V, F, B: ?Sized> MapLaneWithEntry<C, K, V, F, B> {
+    /// #Arguments
+    /// * `projection` - Projection from the agent context to the lane.
+    /// * `key` - Key of the entry.
+    /// * `f` - The closure to apply to the entry.
+    pub fn new(projection: for<'a> fn(&'a C) -> &'a MapLane<K, V>, key: K, f: F) -> Self {
+        MapLaneWithEntry {
+            projection,
+            key_and_f: Some((key, f)),
+            _type: PhantomData,
+        }
+    }
+}
+
 ///  An [event handler](crate::event_handler::EventHandler)`] that will request a sync from the lane.
 pub struct MapLaneSync<C, K, V> {
     projection: for<'a> fn(&'a C) -> &'a MapLane<K, V>,
@@ -528,26 +591,25 @@ where
     decode.and_then(ProjTransform::new(projection))
 }
 
-///  An [event handler](crate::event_handler::EventHandler)`] that will alter an entry in the map.
-pub struct MapLaneWithEntry<C, K, V, F> {
+/// An (event handler)[`crate::event_handler::EventHandler`] that will alter an entry in the map.
+pub struct MapLaneTransformEntry<C, K, V, F> {
     projection: for<'a> fn(&'a C) -> &'a MapLane<K, V>,
     key_and_f: Option<(K, F)>,
 }
 
-impl<C, K, V, F> MapLaneWithEntry<C, K, V, F> {
+impl<C, K, V, F> MapLaneTransformEntry<C, K, V, F> {
     pub fn new(projection: for<'a> fn(&'a C) -> &'a MapLane<K, V>, key: K, f: F) -> Self {
-        MapLaneWithEntry {
+        MapLaneTransformEntry {
             projection,
             key_and_f: Some((key, f)),
         }
     }
 }
 
-impl<C, K, V, F> HandlerAction<C> for MapLaneWithEntry<C, K, V, F>
+impl<C, K, V, F> HandlerAction<C> for MapLaneTransformEntry<C, K, V, F>
 where
     K: Clone + Eq + Hash,
-    V: Clone,
-    F: FnOnce(Option<V>) -> Option<V>,
+    F: FnOnce(Option<&V>) -> Option<V>,
 {
     type Completion = ();
 
@@ -557,13 +619,13 @@ where
         _meta: AgentMetadata,
         context: &C,
     ) -> StepResult<Self::Completion> {
-        let MapLaneWithEntry {
+        let MapLaneTransformEntry {
             projection,
             key_and_f,
         } = self;
         if let Some((key, f)) = key_and_f.take() {
             let lane = projection(context);
-            if matches!(lane.with_entry(key, f), WithEntryResult::NoChange) {
+            if matches!(lane.transform_entry(key, f), TransformEntryResult::NoChange) {
                 StepResult::done(())
             } else {
                 StepResult::Complete {
@@ -599,6 +661,35 @@ where
     }
 }
 
+impl<K, V> InspectableMapLikeItem<K, V> for MapLane<K, V>
+where
+    K: Eq + Hash + Send + 'static,
+    V: 'static,
+{
+    type WithEntryHandler<'a, C, F, B, U> = MapLaneWithEntry<C, K, V, F, B>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized +'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a;
+
+    fn with_entry_handler<'a, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        key: K,
+        f: F,
+    ) -> Self::WithEntryHandler<'a, C, F, B, U>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a,
+    {
+        MapLaneWithEntry::new(projection, key, f)
+    }
+}
+
 impl<K, V> MutableMapLikeItem<K, V> for MapLane<K, V>
 where
     K: Clone + Eq + Hash + Send + 'static,
@@ -631,29 +722,23 @@ where
     fn clear_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::ClearHandler<C> {
         MapLaneClear::new(projection)
     }
-}
 
-impl<K, V> TransformableMapLikeItem<K, V> for MapLane<K, V>
-where
-    K: Clone + Eq + Hash + Send + 'static,
-    V: Clone + Send + 'static,
-{
-    type WithEntryHandler<'a, C, F> = MapLaneWithEntry<C, K, V, F>
+    type TransformEntryHandler<'a, C, F> = MapLaneTransformEntry<C, K, V, F>
     where
         Self: 'static,
         C: 'a,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a;
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a;
 
-    fn with_handler<'a, C, F>(
+    fn transform_entry_handler<'a, C, F>(
         projection: fn(&C) -> &Self,
         key: K,
         f: F,
-    ) -> Self::WithEntryHandler<'a, C, F>
+    ) -> Self::TransformEntryHandler<'a, C, F>
     where
         Self: 'static,
         C: 'a,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a,
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a,
     {
-        MapLaneWithEntry::new(projection, key, f)
+        MapLaneTransformEntry::new(projection, key, f)
     }
 }
diff --git a/server/swimos_agent/src/lanes/value/mod.rs b/server/swimos_agent/src/lanes/value/mod.rs
index 969ad2657..55d845df1 100644
--- a/server/swimos_agent/src/lanes/value/mod.rs
+++ b/server/swimos_agent/src/lanes/value/mod.rs
@@ -17,7 +17,7 @@ pub mod lifecycle;
 #[cfg(test)]
 mod tests;
 
-use std::{cell::RefCell, collections::VecDeque};
+use std::{borrow::Borrow, cell::RefCell, collections::VecDeque, marker::PhantomData};
 
 use bytes::BytesMut;
 use static_assertions::assert_impl_all;
@@ -77,6 +77,23 @@ impl<T> ValueLane<T> {
         let ValueLane { sync_queue, .. } = self;
         sync_queue.borrow_mut().push_back(id);
     }
+
+    /// Replace the contents of the lane.
+    pub fn replace<F>(&self, f: F)
+    where
+        F: FnOnce(&T) -> T,
+    {
+        self.store.replace(f);
+    }
+
+    pub(crate) fn with<F, B, U>(&self, f: F) -> U
+    where
+        B: ?Sized,
+        T: Borrow<B>,
+        F: FnOnce(&B) -> U,
+    {
+        self.store.with(f)
+    }
 }
 
 impl<T> AgentItem for ValueLane<T> {
@@ -257,6 +274,49 @@ impl<C, T> HandlerAction<C> for ValueLaneSync<C, T> {
     }
 }
 
+/// An [`HandlerAction`] that will produce a value from a reference to the contents of the lane.
+pub struct ValueLaneWithValue<C, T, F, B: ?Sized> {
+    projection: for<'a> fn(&'a C) -> &'a ValueLane<T>,
+    f: Option<F>,
+    _type: PhantomData<fn(&B)>,
+}
+
+impl<C, T, F, B: ?Sized> ValueLaneWithValue<C, T, F, B> {
+    /// #Arguments
+    /// * `projection` - Projection from the agent context to the lane.
+    /// * `f` - Closure to apply to the value of the lane.
+    pub fn new(projection: for<'a> fn(&'a C) -> &'a ValueLane<T>, f: F) -> Self {
+        ValueLaneWithValue {
+            projection,
+            f: Some(f),
+            _type: PhantomData,
+        }
+    }
+}
+
+impl<C, T, F, B, U> HandlerAction<C> for ValueLaneWithValue<C, T, F, B>
+where
+    B: ?Sized,
+    T: Borrow<B>,
+    F: FnOnce(&B) -> U,
+{
+    type Completion = U;
+
+    fn step(
+        &mut self,
+        _action_context: &mut ActionContext<C>,
+        _meta: AgentMetadata,
+        context: &C,
+    ) -> StepResult<Self::Completion> {
+        if let Some(f) = self.f.take() {
+            let lane = (self.projection)(context);
+            StepResult::done(lane.with(f))
+        } else {
+            StepResult::after_done()
+        }
+    }
+}
+
 impl<C, T> HandlerTrans<T> for ProjTransform<C, ValueLane<T>> {
     type Out = ValueLaneSet<C, T>;
 
@@ -286,9 +346,30 @@ where
     where
         C: 'static;
 
+    type WithValueHandler<'a, C, F, B, U> = ValueLaneWithValue<C, T, F, B>
+    where
+        Self: 'static,
+        C: 'a,
+        T: Borrow<B>,
+        B: ?Sized + 'static,
+        F: FnOnce(&B) -> U + Send + 'a;
+
     fn get_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::GetHandler<C> {
         ValueLaneGet::new(projection)
     }
+
+    fn with_value_handler<'a, Item, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        f: F,
+    ) -> Self::WithValueHandler<'a, C, F, B, U>
+    where
+        C: 'a,
+        T: Borrow<B>,
+        B: ?Sized + 'static,
+        F: FnOnce(&B) -> U + Send + 'a,
+    {
+        ValueLaneWithValue::new(projection, f)
+    }
 }
 
 impl<T> MutableValueLikeItem<T> for ValueLane<T>
diff --git a/server/swimos_agent/src/map_storage/mod.rs b/server/swimos_agent/src/map_storage/mod.rs
index 53d923508..411580c84 100644
--- a/server/swimos_agent/src/map_storage/mod.rs
+++ b/server/swimos_agent/src/map_storage/mod.rs
@@ -50,7 +50,7 @@ impl<K, V, Q: Default> MapStoreInner<K, V, Q> {
     }
 }
 
-pub enum WithEntryResult {
+pub enum TransformEntryResult {
     NoChange,
     Update,
     Remove,
@@ -76,10 +76,9 @@ where
         queue.push(MapOperation::Update { key, value: () });
     }
 
-    pub fn with_entry<F>(&mut self, key: K, f: F) -> WithEntryResult
+    pub fn transform_entry<F>(&mut self, key: K, f: F) -> TransformEntryResult
     where
-        V: Clone,
-        F: FnOnce(Option<V>) -> Option<V>,
+        F: FnOnce(Option<&V>) -> Option<V>,
     {
         let MapStoreInner {
             content,
@@ -87,17 +86,17 @@ where
             queue,
         } = self;
         match content.remove(&key) {
-            Some(v) => match f(Some(v.clone())) {
+            Some(v) => match f(Some(&v)) {
                 Some(v2) => {
                     content.insert(key.clone(), v2);
                     *previous = Some(MapLaneEvent::Update(key.clone(), Some(v)));
                     queue.push(MapOperation::Update { key, value: () });
-                    WithEntryResult::Update
+                    TransformEntryResult::Update
                 }
                 _ => {
                     *previous = Some(MapLaneEvent::Remove(key.clone(), v));
                     queue.push(MapOperation::Remove { key: key.clone() });
-                    WithEntryResult::Remove
+                    TransformEntryResult::Remove
                 }
             },
             _ => match f(None) {
@@ -105,9 +104,9 @@ where
                     content.insert(key.clone(), v2);
                     *previous = Some(MapLaneEvent::Update(key.clone(), None));
                     queue.push(MapOperation::Update { key, value: () });
-                    WithEntryResult::Update
+                    TransformEntryResult::Update
                 }
-                _ => WithEntryResult::NoChange,
+                _ => TransformEntryResult::NoChange,
             },
         }
     }
@@ -135,16 +134,6 @@ where
         queue.push(MapOperation::Clear);
     }
 
-    pub fn get<B, F, R>(&self, key: &B, f: F) -> R
-    where
-        K: Borrow<B>,
-        B: Hash + Eq,
-        F: FnOnce(Option<&V>) -> R,
-    {
-        let MapStoreInner { content, .. } = self;
-        f(content.get(key))
-    }
-
     pub fn get_map<F, R>(&self, f: F) -> R
     where
         F: FnOnce(&HashMap<K, V>) -> R,
@@ -172,3 +161,21 @@ where
         queue.pop(content)
     }
 }
+
+impl<K, V, Q> MapStoreInner<K, V, Q>
+where
+    K: Eq + Hash,
+{
+    pub fn with_entry<B1, B2, F, R>(&self, key: &B1, f: F) -> R
+    where
+        B1: ?Sized,
+        B2: ?Sized,
+        K: Borrow<B1>,
+        V: Borrow<B2>,
+        B1: Hash + Eq,
+        F: FnOnce(Option<&B2>) -> R,
+    {
+        let MapStoreInner { content, .. } = self;
+        f(content.get(key).map(Borrow::borrow))
+    }
+}
diff --git a/server/swimos_agent/src/stores/map/mod.rs b/server/swimos_agent/src/stores/map/mod.rs
index ac24123ca..52cdea627 100644
--- a/server/swimos_agent/src/stores/map/mod.rs
+++ b/server/swimos_agent/src/stores/map/mod.rs
@@ -14,6 +14,7 @@
 
 use std::borrow::Borrow;
 use std::hash::Hash;
+use std::marker::PhantomData;
 use std::{cell::RefCell, collections::HashMap};
 
 use bytes::BytesMut;
@@ -25,8 +26,8 @@ use tokio_util::codec::Encoder;
 use crate::agent_model::WriteResult;
 use crate::event_handler::{ActionContext, HandlerAction, Modification, StepResult};
 use crate::event_queue::EventQueue;
-use crate::item::{AgentItem, MapItem, MapLikeItem, MutableMapLikeItem, TransformableMapLikeItem};
-use crate::map_storage::{MapStoreInner, WithEntryResult};
+use crate::item::{AgentItem, InspectableMapLikeItem, MapItem, MapLikeItem, MutableMapLikeItem};
+use crate::map_storage::{MapStoreInner, TransformEntryResult};
 use crate::meta::AgentMetadata;
 
 use super::StoreItem;
@@ -91,12 +92,11 @@ where
     }
 
     /// Transform the value associated with a key.
-    pub fn with_entry<F>(&self, key: K, f: F) -> WithEntryResult
+    pub fn transform_entry<F>(&self, key: K, f: F) -> TransformEntryResult
     where
-        V: Clone,
-        F: FnOnce(Option<V>) -> Option<V>,
+        F: FnOnce(Option<&V>) -> Option<V>,
     {
-        self.inner.borrow_mut().with_entry(key, f)
+        self.inner.borrow_mut().transform_entry(key, f)
     }
 
     /// Remove an entry from the map.
@@ -112,11 +112,11 @@ where
     /// Read a value from the map, if it exists.
     pub fn get<Q, F, R>(&self, key: &Q, f: F) -> R
     where
-        K: Borrow<Q>,
-        Q: Hash + Eq,
+        K: Borrow<Q> + Eq + Hash,
+        Q: Eq + Hash,
         F: FnOnce(Option<&V>) -> R,
     {
-        self.inner.borrow().get(key, f)
+        self.inner.borrow().with_entry(key, f)
     }
 
     /// Read the complete state of the map.
@@ -128,6 +128,23 @@ where
     }
 }
 
+impl<K, V> MapStore<K, V>
+where
+    K: Eq + Hash,
+{
+    pub fn with_entry<F, B1, B2, U>(&self, key: &B1, f: F) -> U
+    where
+        B1: ?Sized,
+        B2: ?Sized,
+        K: Borrow<B1>,
+        B1: Eq + Hash,
+        V: Borrow<B2>,
+        F: FnOnce(Option<&B2>) -> U,
+    {
+        self.inner.borrow().with_entry(key, f)
+    }
+}
+
 const INFALLIBLE_SER: &str = "Serializing store responses to recon should be infallible.";
 
 impl<K, V> StoreItem for MapStore<K, V>
@@ -363,25 +380,24 @@ where
 }
 
 ///  An [event handler](crate::event_handler::EventHandler)`] that may alter an entry in the map.
-pub struct MapStoreWithEntry<C, K, V, F> {
+pub struct MapStoreTransformEntry<C, K, V, F> {
     projection: for<'a> fn(&'a C) -> &'a MapStore<K, V>,
     key_and_f: Option<(K, F)>,
 }
 
-impl<C, K, V, F> MapStoreWithEntry<C, K, V, F> {
+impl<C, K, V, F> MapStoreTransformEntry<C, K, V, F> {
     pub fn new(projection: for<'a> fn(&'a C) -> &'a MapStore<K, V>, key: K, f: F) -> Self {
-        MapStoreWithEntry {
+        MapStoreTransformEntry {
             projection,
             key_and_f: Some((key, f)),
         }
     }
 }
 
-impl<C, K, V, F> HandlerAction<C> for MapStoreWithEntry<C, K, V, F>
+impl<C, K, V, F> HandlerAction<C> for MapStoreTransformEntry<C, K, V, F>
 where
     K: Clone + Eq + Hash,
-    V: Clone,
-    F: FnOnce(Option<V>) -> Option<V>,
+    F: FnOnce(Option<&V>) -> Option<V>,
 {
     type Completion = ();
 
@@ -391,13 +407,16 @@ where
         _meta: AgentMetadata,
         context: &C,
     ) -> StepResult<Self::Completion> {
-        let MapStoreWithEntry {
+        let MapStoreTransformEntry {
             projection,
             key_and_f,
         } = self;
         if let Some((key, f)) = key_and_f.take() {
             let store = projection(context);
-            if matches!(store.with_entry(key, f), WithEntryResult::NoChange) {
+            if matches!(
+                store.transform_entry(key, f),
+                TransformEntryResult::NoChange
+            ) {
                 StepResult::done(())
             } else {
                 StepResult::Complete {
@@ -411,6 +430,56 @@ where
     }
 }
 
+/// A [handler action][`HandlerAction`] that will produce a value by applying a closure to a reference to
+/// and entry in the store.
+pub struct MapStoreWithEntry<C, K, V, F, B: ?Sized> {
+    projection: for<'a> fn(&'a C) -> &'a MapStore<K, V>,
+    key_and_f: Option<(K, F)>,
+    _type: PhantomData<fn(&B)>,
+}
+
+impl<C, K, V, F, B: ?Sized> MapStoreWithEntry<C, K, V, F, B> {
+    /// #Arguments
+    /// * `projection` - Projection from the agent context to the store.
+    /// * `key` - Key of the entry.
+    /// * `f` - The closure to apply to the entry.
+    pub fn new(projection: for<'a> fn(&'a C) -> &'a MapStore<K, V>, key: K, f: F) -> Self {
+        MapStoreWithEntry {
+            projection,
+            key_and_f: Some((key, f)),
+            _type: PhantomData,
+        }
+    }
+}
+
+impl<C, K, V, F, B, U> HandlerAction<C> for MapStoreWithEntry<C, K, V, F, B>
+where
+    K: Eq + Hash,
+    B: ?Sized,
+    V: Borrow<B>,
+    F: FnOnce(Option<&B>) -> U,
+{
+    type Completion = U;
+
+    fn step(
+        &mut self,
+        _action_context: &mut ActionContext<C>,
+        _meta: AgentMetadata,
+        context: &C,
+    ) -> StepResult<Self::Completion> {
+        let MapStoreWithEntry {
+            projection,
+            key_and_f,
+            ..
+        } = self;
+        if let Some((key, f)) = key_and_f.take() {
+            let store = projection(context);
+            StepResult::done(store.with_entry(&key, f))
+        } else {
+            StepResult::after_done()
+        }
+    }
+}
 impl<K, V> MapLikeItem<K, V> for MapStore<K, V>
 where
     K: Clone + Eq + Hash + Send + 'static,
@@ -433,6 +502,35 @@ where
     }
 }
 
+impl<K, V> InspectableMapLikeItem<K, V> for MapStore<K, V>
+where
+    K: Eq + Hash + Send + 'static,
+    V: 'static,
+{
+    type WithEntryHandler<'a, C, F, B, U> = MapStoreWithEntry<C, K, V, F, B>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized +'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a;
+
+    fn with_entry_handler<'a, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        key: K,
+        f: F,
+    ) -> Self::WithEntryHandler<'a, C, F, B, U>
+    where
+        Self: 'static,
+        C: 'a,
+        B: ?Sized + 'static,
+        V: Borrow<B>,
+        F: FnOnce(Option<&B>) -> U + Send + 'a,
+    {
+        MapStoreWithEntry::new(projection, key, f)
+    }
+}
+
 impl<K, V> MutableMapLikeItem<K, V> for MapStore<K, V>
 where
     K: Clone + Eq + Hash + Send + 'static,
@@ -465,29 +563,23 @@ where
     fn clear_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::ClearHandler<C> {
         MapStoreClear::new(projection)
     }
-}
 
-impl<K, V> TransformableMapLikeItem<K, V> for MapStore<K, V>
-where
-    K: Clone + Eq + Hash + Send + 'static,
-    V: Clone + Send + 'static,
-{
-    type WithEntryHandler<'a, C, F> = MapStoreWithEntry<C, K, V, F>
+    type TransformEntryHandler<'a, C, F> = MapStoreTransformEntry<C, K, V, F>
     where
         Self: 'static,
         C: 'a,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a;
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a;
 
-    fn with_handler<'a, C, F>(
+    fn transform_entry_handler<'a, C, F>(
         projection: fn(&C) -> &Self,
         key: K,
         f: F,
-    ) -> Self::WithEntryHandler<'a, C, F>
+    ) -> Self::TransformEntryHandler<'a, C, F>
     where
         Self: 'static,
         C: 'a,
-        F: FnOnce(Option<V>) -> Option<V> + Send + 'a,
+        F: FnOnce(Option<&V>) -> Option<V> + Send + 'a,
     {
-        MapStoreWithEntry::new(projection, key, f)
+        MapStoreTransformEntry::new(projection, key, f)
     }
 }
diff --git a/server/swimos_agent/src/stores/value/mod.rs b/server/swimos_agent/src/stores/value/mod.rs
index bf92bd571..36247dbcb 100644
--- a/server/swimos_agent/src/stores/value/mod.rs
+++ b/server/swimos_agent/src/stores/value/mod.rs
@@ -12,7 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use std::cell::{Cell, RefCell};
+use std::{
+    borrow::Borrow,
+    cell::{Cell, RefCell},
+    marker::PhantomData,
+};
 
 use bytes::BytesMut;
 use static_assertions::assert_impl_all;
@@ -120,6 +124,31 @@ impl<T> ValueStore<T> {
             false
         }
     }
+
+    pub(crate) fn replace<F>(&self, f: F)
+    where
+        F: FnOnce(&T) -> T,
+    {
+        let ValueStore { inner, dirty, .. } = self;
+        let mut guard = inner.borrow_mut();
+        let Inner { content, previous } = &mut *guard;
+        let new_value = f(content);
+        let prev = std::mem::replace(content, new_value);
+        *previous = Some(prev);
+        dirty.replace(true);
+    }
+
+    pub(crate) fn with<F, B, U>(&self, f: F) -> U
+    where
+        B: ?Sized,
+        T: Borrow<B>,
+        F: FnOnce(&B) -> U,
+    {
+        let ValueStore { inner, .. } = self;
+        let guard = inner.borrow();
+        let Inner { content, .. } = &*guard;
+        f(content.borrow())
+    }
 }
 
 impl<T> AgentItem for ValueStore<T> {
@@ -247,6 +276,49 @@ impl<C, T> HandlerAction<C> for ValueStoreSet<C, T> {
     }
 }
 
+/// An [`HandlerAction`] that will produce a value from a reference to the contents of the store.
+pub struct ValueStoreWithValue<C, T, F, B: ?Sized> {
+    projection: for<'a> fn(&'a C) -> &'a ValueStore<T>,
+    f: Option<F>,
+    _type: PhantomData<fn(&B)>,
+}
+
+impl<C, T, F, B: ?Sized> ValueStoreWithValue<C, T, F, B> {
+    /// #Arguments
+    /// * `projection` - Projection from the agent context to the store.
+    /// * `f` - Closure to apply to the value of the store.
+    pub fn new(projection: for<'a> fn(&'a C) -> &'a ValueStore<T>, f: F) -> Self {
+        ValueStoreWithValue {
+            projection,
+            f: Some(f),
+            _type: PhantomData,
+        }
+    }
+}
+
+impl<C, T, F, B, U> HandlerAction<C> for ValueStoreWithValue<C, T, F, B>
+where
+    T: Borrow<B>,
+    B: ?Sized,
+    F: FnOnce(&B) -> U,
+{
+    type Completion = U;
+
+    fn step(
+        &mut self,
+        _action_context: &mut ActionContext<C>,
+        _meta: AgentMetadata,
+        context: &C,
+    ) -> StepResult<Self::Completion> {
+        if let Some(f) = self.f.take() {
+            let store = (self.projection)(context);
+            StepResult::done(store.with(f))
+        } else {
+            StepResult::after_done()
+        }
+    }
+}
+
 impl<T> ValueLikeItem<T> for ValueStore<T>
 where
     T: Clone + Send + 'static,
@@ -255,9 +327,30 @@ where
     where
         C: 'static;
 
+    type WithValueHandler<'a, C, F, B, U> = ValueStoreWithValue<C, T, F, B>
+    where
+        Self: 'static,
+        C: 'a,
+        T: Borrow<B>,
+        B: ?Sized + 'static,
+        F: FnOnce(&B) -> U + Send + 'a;
+
     fn get_handler<C: 'static>(projection: fn(&C) -> &Self) -> Self::GetHandler<C> {
         ValueStoreGet::new(projection)
     }
+
+    fn with_value_handler<'a, Item, C, F, B, U>(
+        projection: fn(&C) -> &Self,
+        f: F,
+    ) -> Self::WithValueHandler<'a, C, F, B, U>
+    where
+        C: 'a,
+        T: Borrow<B>,
+        B: ?Sized + 'static,
+        F: FnOnce(&B) -> U + Send + 'a,
+    {
+        ValueStoreWithValue::new(projection, f)
+    }
 }
 
 impl<T> MutableValueLikeItem<T> for ValueStore<T>


Test patch:
diff --git a/server/swimos_agent/src/lanes/join/map/tests.rs b/server/swimos_agent/src/lanes/join/map/tests.rs
index 4cc199137..12f060538 100644
--- a/server/swimos_agent/src/lanes/join/map/tests.rs
+++ b/server/swimos_agent/src/lanes/join/map/tests.rs
@@ -33,7 +33,8 @@ use crate::item::AgentItem;
 use crate::lanes::join::test_util::{TestDlContextInner, TestDownlinkContext};
 use crate::lanes::join_map::default_lifecycle::DefaultJoinMapLifecycle;
 use crate::lanes::join_map::{
-    AddDownlinkAction, JoinMapAddDownlink, JoinMapLaneGet, JoinMapLaneGetMap, JoinMapRemoveDownlink,
+    AddDownlinkAction, JoinMapAddDownlink, JoinMapLaneGet, JoinMapLaneGetMap, JoinMapLaneWithEntry,
+    JoinMapRemoveDownlink,
 };
 use crate::test_context::{dummy_context, run_event_handlers, run_with_futures};
 use crate::{event_handler::StepResult, item::MapItem, meta::AgentMetadata};
@@ -327,6 +328,55 @@ async fn open_downlink_from_registered() {
     assert_eq!(count.load(Ordering::Relaxed), 1);
 }
 
+#[test]
+fn join_map_lane_with_entry_event_handler() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::with_init();
+
+    let mut handler =
+        JoinMapLaneWithEntry::new(TestAgent::LANE, K1, |v: Option<&str>| v.map(str::to_owned));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(Some(V1.to_string())));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    assert!(matches!(
+        result,
+        StepResult::Fail(EventHandlerError::SteppedAfterComplete)
+    ));
+
+    let mut handler = JoinMapLaneWithEntry::new(TestAgent::LANE, ABSENT, |v: Option<&str>| {
+        v.map(str::to_owned)
+    });
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(None));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    assert!(matches!(
+        result,
+        StepResult::Fail(EventHandlerError::SteppedAfterComplete)
+    ));
+}
+
 #[tokio::test]
 async fn stop_downlink() {
     let uri = make_uri();
diff --git a/server/swimos_agent/src/lanes/join/value/tests.rs b/server/swimos_agent/src/lanes/join/value/tests.rs
index dea151ef6..6d0649878 100644
--- a/server/swimos_agent/src/lanes/join/value/tests.rs
+++ b/server/swimos_agent/src/lanes/join/value/tests.rs
@@ -39,7 +39,7 @@ use crate::{
         join::test_util::{TestDlContextInner, TestDownlinkContext},
         join_value::{
             default_lifecycle::DefaultJoinValueLifecycle, AddDownlinkAction, JoinValueLaneGet,
-            JoinValueLaneGetMap,
+            JoinValueLaneGetMap, JoinValueLaneWithEntry,
         },
     },
     meta::AgentMetadata,
@@ -191,6 +191,55 @@ fn join_value_lane_get_event_handler() {
     ));
 }
 
+#[test]
+fn join_value_lane_with_entry_event_handler() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::with_init();
+
+    let mut handler =
+        JoinValueLaneWithEntry::new(TestAgent::LANE, K1, |v: Option<&str>| v.map(str::to_owned));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(Some(V1.to_string())));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    assert!(matches!(
+        result,
+        StepResult::Fail(EventHandlerError::SteppedAfterComplete)
+    ));
+
+    let mut handler = JoinValueLaneWithEntry::new(TestAgent::LANE, ABSENT, |v: Option<&str>| {
+        v.map(str::to_owned)
+    });
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(None));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    assert!(matches!(
+        result,
+        StepResult::Fail(EventHandlerError::SteppedAfterComplete)
+    ));
+}
+
 #[test]
 fn join_value_lane_get_map_event_handler() {
     let uri = make_uri();
diff --git a/server/swimos_agent/src/lanes/map/tests.rs b/server/swimos_agent/src/lanes/map/tests.rs
index 387995d1b..07e83554b 100644
--- a/server/swimos_agent/src/lanes/map/tests.rs
+++ b/server/swimos_agent/src/lanes/map/tests.rs
@@ -20,7 +20,6 @@ use swimos_agent_protocol::{
     encoding::lane::RawMapLaneResponseDecoder, MapLaneResponse, MapOperation,
 };
 use swimos_api::agent::AgentConfig;
-use swimos_model::Text;
 use swimos_recon::parser::parse_recognize;
 use swimos_utilities::routing::RouteUri;
 use tokio_util::codec::Decoder;
@@ -33,7 +32,7 @@ use crate::{
     lanes::{
         map::{
             MapLane, MapLaneClear, MapLaneEvent, MapLaneGet, MapLaneGetMap, MapLaneRemove,
-            MapLaneSync, MapLaneUpdate, MapLaneWithEntry,
+            MapLaneSync, MapLaneTransformEntry, MapLaneUpdate,
         },
         LaneItem,
     },
@@ -41,6 +40,8 @@ use crate::{
     test_context::dummy_context,
 };
 
+use super::MapLaneWithEntry;
+
 const ID: u64 = 74;
 
 const K1: i32 = 5;
@@ -53,10 +54,10 @@ const V1: &str = "first";
 const V2: &str = "second";
 const V3: &str = "third";
 
-fn init() -> HashMap<i32, Text> {
+fn init() -> HashMap<i32, String> {
     [(K1, V1), (K2, V2), (K3, V3)]
         .into_iter()
-        .map(|(k, v)| (k, Text::new(v)))
+        .map(|(k, v)| (k, v.to_owned()))
         .collect()
 }
 
@@ -65,7 +66,7 @@ fn get_from_map_lane() {
     let lane = MapLane::new(ID, init());
 
     let value = lane.get(&K1, |v| v.cloned());
-    assert_eq!(value, Some(Text::new(V1)));
+    assert_eq!(value.as_deref(), Some(V1));
 
     let value = lane.get(&ABSENT, |v| v.cloned());
     assert!(value.is_none());
@@ -83,23 +84,23 @@ fn get_map_lane() {
 fn update_map_lane() {
     let lane = MapLane::new(ID, init());
 
-    lane.update(K2, Text::new("altered"));
+    lane.update(K2, "altered".to_owned());
 
     lane.get_map(|m| {
         assert_eq!(m.len(), 3);
-        assert_eq!(m.get(&K1), Some(&Text::new(V1)));
-        assert_eq!(m.get(&K2), Some(&Text::new("altered")));
-        assert_eq!(m.get(&K3), Some(&Text::new(V3)));
+        assert_eq!(m.get(&K1).map(String::as_str), Some(V1));
+        assert_eq!(m.get(&K2).map(String::as_str), Some("altered"));
+        assert_eq!(m.get(&K3).map(String::as_str), Some(V3));
     });
 
-    lane.update(ABSENT, Text::new("added"));
+    lane.update(ABSENT, "added".to_owned());
 
     lane.get_map(|m| {
         assert_eq!(m.len(), 4);
-        assert_eq!(m.get(&K1), Some(&Text::new(V1)));
-        assert_eq!(m.get(&K2), Some(&Text::new("altered")));
-        assert_eq!(m.get(&K3), Some(&Text::new(V3)));
-        assert_eq!(m.get(&ABSENT), Some(&Text::new("added")));
+        assert_eq!(m.get(&K1).map(String::as_str), Some(V1));
+        assert_eq!(m.get(&K2).map(String::as_str), Some("altered"));
+        assert_eq!(m.get(&K3).map(String::as_str), Some(V3));
+        assert_eq!(m.get(&ABSENT).map(String::as_str), Some("added"));
     });
 }
 
@@ -111,8 +112,8 @@ fn remove_from_map_lane() {
 
     lane.get_map(|m| {
         assert_eq!(m.len(), 2);
-        assert_eq!(m.get(&K1), Some(&Text::new(V1)));
-        assert_eq!(m.get(&K3), Some(&Text::new(V3)));
+        assert_eq!(m.get(&K1).map(String::as_str), Some(V1));
+        assert_eq!(m.get(&K3).map(String::as_str), Some(V3));
     });
 }
 
@@ -141,7 +142,7 @@ fn write_to_buffer_no_data() {
 fn write_to_buffer_one_update() {
     let lane = MapLane::new(ID, init());
 
-    lane.update(K2, Text::new("altered"));
+    lane.update(K2, "altered".to_owned());
 
     let mut buffer = BytesMut::new();
 
@@ -225,11 +226,11 @@ fn write_to_buffer_clear() {
 
 #[derive(Debug)]
 struct Operations {
-    events: Vec<MapOperation<i32, Text>>,
-    sync: HashMap<Uuid, Vec<MapOperation<i32, Text>>>,
+    events: Vec<MapOperation<i32, String>>,
+    sync: HashMap<Uuid, Vec<MapOperation<i32, String>>>,
 }
 
-fn consume_events(lane: &MapLane<i32, Text>) -> Operations {
+fn consume_events(lane: &MapLane<i32, String>) -> Operations {
     let mut events = vec![];
     let mut sync_pending = HashMap::new();
     let mut sync = HashMap::new();
@@ -276,13 +277,13 @@ fn consume_events(lane: &MapLane<i32, Text>) -> Operations {
     Operations { events, sync }
 }
 
-fn interpret(op: MapOperation<BytesMut, BytesMut>) -> MapOperation<i32, Text> {
+fn interpret(op: MapOperation<BytesMut, BytesMut>) -> MapOperation<i32, String> {
     match op {
         MapOperation::Update { key, value } => {
             let key_str = std::str::from_utf8(key.as_ref()).expect("Bad key bytes.");
             let val_str = std::str::from_utf8(value.as_ref()).expect("Bad value bytes.");
             let key = parse_recognize::<i32>(key_str, false).expect("Bad key recon.");
-            let value = parse_recognize::<Text>(val_str, false).expect("Bad value recon.");
+            let value = parse_recognize::<String>(val_str, false).expect("Bad value recon.");
             MapOperation::Update { key, value }
         }
         MapOperation::Remove { key } => {
@@ -298,9 +299,9 @@ fn interpret(op: MapOperation<BytesMut, BytesMut>) -> MapOperation<i32, Text> {
 fn write_multiple_events_to_buffer() {
     let lane = MapLane::new(ID, init());
 
-    lane.update(ABSENT, Text::new("added"));
+    lane.update(ABSENT, "added".to_owned());
     lane.remove(&K1);
-    lane.update(K3, Text::new("altered"));
+    lane.update(K3, "altered".to_owned());
 
     let Operations { events, sync } = consume_events(&lane);
 
@@ -309,12 +310,12 @@ fn write_multiple_events_to_buffer() {
     let expected = vec![
         MapOperation::Update {
             key: ABSENT,
-            value: Text::new("added"),
+            value: "added".to_owned(),
         },
         MapOperation::Remove { key: K1 },
         MapOperation::Update {
             key: K3,
-            value: Text::new("altered"),
+            value: "altered".to_owned(),
         },
     ];
     assert_eq!(events, expected);
@@ -324,9 +325,9 @@ fn write_multiple_events_to_buffer() {
 fn updates_to_one_key_overwrite() {
     let lane = MapLane::new(ID, init());
 
-    lane.update(ABSENT, Text::new("added"));
-    lane.update(K3, Text::new("altered"));
-    lane.update(ABSENT, Text::new("changed"));
+    lane.update(ABSENT, "added".to_owned());
+    lane.update(K3, "altered".to_string());
+    lane.update(ABSENT, "changed".to_owned());
 
     let Operations { events, sync } = consume_events(&lane);
 
@@ -335,11 +336,11 @@ fn updates_to_one_key_overwrite() {
     let expected = vec![
         MapOperation::Update {
             key: ABSENT,
-            value: Text::new("changed"),
+            value: "changed".to_owned(),
         },
         MapOperation::Update {
             key: K3,
-            value: Text::new("altered"),
+            value: "altered".to_owned(),
         },
     ];
     assert_eq!(events, expected);
@@ -349,9 +350,9 @@ fn updates_to_one_key_overwrite() {
 fn clear_resets_event_queue() {
     let lane = MapLane::new(ID, init());
 
-    lane.update(ABSENT, Text::new("added"));
+    lane.update(ABSENT, "added".to_owned());
     lane.remove(&K1);
-    lane.update(K3, Text::new("altered"));
+    lane.update(K3, "altered".to_owned());
     lane.clear();
 
     let Operations { events, sync } = consume_events(&lane);
@@ -365,7 +366,7 @@ fn clear_resets_event_queue() {
 const SYNC_ID1: Uuid = Uuid::from_u128(8578393934);
 const SYNC_ID2: Uuid = Uuid::from_u128(2847474);
 
-fn to_updates(sync_messages: &Vec<MapOperation<i32, Text>>) -> HashMap<i32, Text> {
+fn to_updates(sync_messages: &[MapOperation<i32, String>]) -> HashMap<i32, String> {
     let mut map = HashMap::new();
     for op in sync_messages {
         match op {
@@ -393,7 +394,7 @@ fn sync_lane_state() {
 
     let expected: HashMap<_, _> = [(K1, V1), (K2, V2), (K3, V3)]
         .into_iter()
-        .map(|(k, v)| (k, Text::new(v)))
+        .map(|(k, v)| (k, v.to_owned()))
         .collect();
     assert_eq!(sync_map, expected);
 }
@@ -414,7 +415,7 @@ fn sync_twice_lane_state() {
 
     let expected: HashMap<_, _> = [(K1, V1), (K2, V2), (K3, V3)]
         .into_iter()
-        .map(|(k, v)| (k, Text::new(v)))
+        .map(|(k, v)| (k, v.to_owned()))
         .collect();
     assert_eq!(sync_map1, expected);
     assert_eq!(sync_map2, expected);
@@ -425,13 +426,13 @@ fn sync_lane_state_and_event() {
     let lane = MapLane::new(ID, init());
 
     lane.sync(SYNC_ID1);
-    lane.update(ABSENT, Text::new("added"));
+    lane.update(ABSENT, "added".to_owned());
 
     let Operations { events, sync } = consume_events(&lane);
 
     let expected_events = vec![MapOperation::Update {
         key: ABSENT,
-        value: Text::new("added"),
+        value: "added".to_owned(),
     }];
     assert_eq!(events, expected_events);
 
@@ -441,7 +442,7 @@ fn sync_lane_state_and_event() {
 
     let expected_sync: HashMap<_, _> = [(K1, V1), (K2, V2), (K3, V3)]
         .into_iter()
-        .map(|(k, v)| (k, Text::new(v)))
+        .map(|(k, v)| (k, v.to_owned()))
         .collect();
     assert_eq!(sync_map, expected_sync);
 }
@@ -461,7 +462,7 @@ fn make_meta<'a>(
 }
 
 struct TestAgent {
-    lane: MapLane<i32, Text>,
+    lane: MapLane<i32, String>,
 }
 
 const LANE_ID: u64 = 9;
@@ -478,7 +479,7 @@ impl TestAgent {
     fn with_init() -> Self {
         let init: HashMap<_, _> = [(K1, V1), (K2, V2), (K3, V3)]
             .into_iter()
-            .map(|(k, v)| (k, Text::new(v)))
+            .map(|(k, v)| (k, v.to_owned()))
             .collect();
 
         TestAgent {
@@ -488,7 +489,7 @@ impl TestAgent {
 }
 
 impl TestAgent {
-    pub const LANE: fn(&TestAgent) -> &MapLane<i32, Text> = |agent| &agent.lane;
+    pub const LANE: fn(&TestAgent) -> &MapLane<i32, String> = |agent| &agent.lane;
 }
 
 fn check_result<T: Eq + Debug>(
@@ -533,7 +534,7 @@ fn map_lane_update_event_handler() {
     let meta = make_meta(&uri, &route_params);
     let agent = TestAgent::default();
 
-    let mut handler = MapLaneUpdate::new(TestAgent::LANE, K1, Text::new(V1));
+    let mut handler = MapLaneUpdate::new(TestAgent::LANE, K1, V1.to_owned());
 
     let result = handler.step(
         &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
@@ -544,7 +545,7 @@ fn map_lane_update_event_handler() {
 
     agent.lane.get_map(|map| {
         assert_eq!(map.len(), 1);
-        assert_eq!(map.get(&K1), Some(&Text::new(V1)));
+        assert_eq!(map.get(&K1).map(String::as_str), Some(V1));
     });
 
     let result = handler.step(
@@ -576,8 +577,8 @@ fn map_lane_remove_event_handler() {
 
     agent.lane.get_map(|map| {
         assert_eq!(map.len(), 2);
-        assert_eq!(map.get(&K2), Some(&Text::new(V2)));
-        assert_eq!(map.get(&K3), Some(&Text::new(V3)));
+        assert_eq!(map.get(&K2).map(String::as_str), Some(V2));
+        assert_eq!(map.get(&K3).map(String::as_str), Some(V3));
     });
 
     let result = handler.step(
@@ -636,7 +637,7 @@ fn map_lane_get_event_handler() {
         meta,
         &agent,
     );
-    check_result(result, false, false, Some(Some(Text::new(V1))));
+    check_result(result, false, false, Some(Some(V1.to_owned())));
 
     let mut handler = MapLaneGet::new(TestAgent::LANE, ABSENT);
 
@@ -722,20 +723,20 @@ fn map_lane_sync_event_handler() {
 
     let expected: HashMap<_, _> = [(K1, V1), (K2, V2), (K3, V3)]
         .into_iter()
-        .map(|(k, v)| (k, Text::new(v)))
+        .map(|(k, v)| (k, v.to_owned()))
         .collect();
     assert_eq!(sync_map, expected);
 }
 
 #[test]
-fn map_lane_with_event_handler_update() {
+fn map_lane_transform_entry_handler_update() {
     let uri = make_uri();
     let route_params = HashMap::new();
     let meta = make_meta(&uri, &route_params);
     let agent = TestAgent::with_init();
 
-    let mut handler = MapLaneWithEntry::new(TestAgent::LANE, K1, |maybe: Option<Text>| {
-        maybe.map(|v| Text::from(v.as_str().to_uppercase()))
+    let mut handler = MapLaneTransformEntry::new(TestAgent::LANE, K1, |maybe: Option<&String>| {
+        maybe.map(|v| v.to_uppercase())
     });
 
     let result = handler.step(
@@ -747,9 +748,9 @@ fn map_lane_with_event_handler_update() {
 
     agent.lane.get_map(|map| {
         assert_eq!(map.len(), 3);
-        assert_eq!(map.get(&K1), Some(&Text::from(V1.to_uppercase())));
-        assert_eq!(map.get(&K2), Some(&Text::new(V2)));
-        assert_eq!(map.get(&K3), Some(&Text::new(V3)));
+        assert_eq!(map.get(&K1), Some(&V1.to_uppercase()));
+        assert_eq!(map.get(&K2).map(String::as_str), Some(V2));
+        assert_eq!(map.get(&K3).map(String::as_str), Some(V3));
     });
 
     let result = handler.step(
@@ -763,17 +764,17 @@ fn map_lane_with_event_handler_update() {
     ));
 
     let event = agent.lane.read_with_prev(|event, _| event);
-    assert_eq!(event, Some(MapLaneEvent::Update(K1, Some(Text::new(V1)))));
+    assert_eq!(event, Some(MapLaneEvent::Update(K1, Some(V1.to_owned()))));
 }
 
 #[test]
-fn map_lane_with_event_handler_remove() {
+fn map_lane_transform_entry_handler_remove() {
     let uri = make_uri();
     let route_params = HashMap::new();
     let meta = make_meta(&uri, &route_params);
     let agent = TestAgent::with_init();
 
-    let mut handler = MapLaneWithEntry::new(TestAgent::LANE, K1, |_: Option<Text>| None);
+    let mut handler = MapLaneTransformEntry::new(TestAgent::LANE, K1, |_: Option<&String>| None);
 
     let result = handler.step(
         &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
@@ -784,8 +785,8 @@ fn map_lane_with_event_handler_remove() {
 
     agent.lane.get_map(|map| {
         assert_eq!(map.len(), 2);
-        assert_eq!(map.get(&K2), Some(&Text::new(V2)));
-        assert_eq!(map.get(&K3), Some(&Text::new(V3)));
+        assert_eq!(map.get(&K2).map(String::as_str), Some(V2));
+        assert_eq!(map.get(&K3).map(String::as_str), Some(V3));
     });
 
     let result = handler.step(
@@ -799,5 +800,43 @@ fn map_lane_with_event_handler_remove() {
     ));
 
     let event = agent.lane.read_with_prev(|event, _| event);
-    assert_eq!(event, Some(MapLaneEvent::Remove(K1, Text::new(V1))));
+    assert_eq!(event, Some(MapLaneEvent::Remove(K1, V1.to_owned())));
+}
+
+#[test]
+fn map_lane_with_entry_handler_absent() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::with_init();
+
+    let mut handler = MapLaneWithEntry::new(TestAgent::LANE, ABSENT, |maybe_v: Option<&str>| {
+        maybe_v.map(str::to_owned)
+    });
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(None));
+}
+
+#[test]
+fn map_lane_with_entry_handler_present() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::with_init();
+
+    let mut handler = MapLaneWithEntry::new(TestAgent::LANE, K1, |maybe_v: Option<&str>| {
+        maybe_v.map(str::to_owned)
+    });
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(Some(V1.to_owned())));
 }
diff --git a/server/swimos_agent/src/lanes/value/tests.rs b/server/swimos_agent/src/lanes/value/tests.rs
index 3d1dd1c51..bdb528ff4 100644
--- a/server/swimos_agent/src/lanes/value/tests.rs
+++ b/server/swimos_agent/src/lanes/value/tests.rs
@@ -26,7 +26,7 @@ use crate::{
     event_handler::{EventHandlerError, HandlerAction, Modification, StepResult},
     item::ValueItem,
     lanes::{
-        value::{ValueLaneGet, ValueLaneSync},
+        value::{ValueLaneGet, ValueLaneSync, ValueLaneWithValue},
         LaneItem,
     },
     meta::AgentMetadata,
@@ -265,23 +265,28 @@ fn make_meta<'a>(
 
 struct TestAgent {
     lane: ValueLane<i32>,
+    str_lane: ValueLane<String>,
 }
 
 const LANE_ID: u64 = 9;
+const STR_LANE_ID: u64 = 73;
 
 impl Default for TestAgent {
     fn default() -> Self {
         Self {
             lane: ValueLane::new(LANE_ID, 0),
+            str_lane: ValueLane::new(STR_LANE_ID, "hello".to_string()),
         }
     }
 }
 
 impl TestAgent {
     const LANE: fn(&TestAgent) -> &ValueLane<i32> = |agent| &agent.lane;
+    const STR_LANE: fn(&TestAgent) -> &ValueLane<String> = |agent| &agent.str_lane;
 }
 
-fn check_result<T: Eq + Debug>(
+fn check_result_for<T: Eq + Debug>(
+    lane_id: u64,
     result: StepResult<T>,
     written: bool,
     trigger_handler: bool,
@@ -289,9 +294,9 @@ fn check_result<T: Eq + Debug>(
 ) {
     let expected_mod = if written {
         if trigger_handler {
-            Some(Modification::of(LANE_ID))
+            Some(Modification::of(lane_id))
         } else {
-            Some(Modification::no_trigger(LANE_ID))
+            Some(Modification::no_trigger(lane_id))
         }
     } else {
         None
@@ -316,6 +321,15 @@ fn check_result<T: Eq + Debug>(
     }
 }
 
+fn check_result<T: Eq + Debug>(
+    result: StepResult<T>,
+    written: bool,
+    trigger_handler: bool,
+    complete: Option<T>,
+) {
+    check_result_for(LANE_ID, result, written, trigger_handler, complete)
+}
+
 #[test]
 fn value_lane_set_event_handler() {
     let uri = make_uri();
@@ -426,3 +440,30 @@ fn value_lane_sync_event_handler() {
         }
     }
 }
+
+#[test]
+fn value_lane_with_value_event_handler() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::default();
+
+    let mut handler = ValueLaneWithValue::new(TestAgent::STR_LANE, |s: &str| s.len());
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result_for(STR_LANE_ID, result, false, false, Some(5));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    assert!(matches!(
+        result,
+        StepResult::Fail(EventHandlerError::SteppedAfterComplete)
+    ));
+}
diff --git a/server/swimos_agent/src/stores/map/tests.rs b/server/swimos_agent/src/stores/map/tests.rs
index eff480939..deaf61348 100644
--- a/server/swimos_agent/src/stores/map/tests.rs
+++ b/server/swimos_agent/src/stores/map/tests.rs
@@ -33,8 +33,8 @@ use crate::{
     meta::AgentMetadata,
     stores::{
         map::{
-            MapStoreClear, MapStoreGet, MapStoreGetMap, MapStoreRemove, MapStoreUpdate,
-            MapStoreWithEntry,
+            MapStoreClear, MapStoreGet, MapStoreGetMap, MapStoreRemove, MapStoreTransformEntry,
+            MapStoreUpdate, MapStoreWithEntry,
         },
         MapStore, StoreItem,
     },
@@ -573,7 +573,7 @@ fn map_store_with_event_handler_update() {
     let meta = make_meta(&uri, &route_params);
     let agent = TestAgent::with_init();
 
-    let mut handler = MapStoreWithEntry::new(TestAgent::STORE, K1, |maybe: Option<Text>| {
+    let mut handler = MapStoreTransformEntry::new(TestAgent::STORE, K1, |maybe: Option<&Text>| {
         maybe.map(|v| Text::from(v.as_str().to_uppercase()))
     });
 
@@ -612,7 +612,7 @@ fn map_lane_with_event_handler_remove() {
     let meta = make_meta(&uri, &route_params);
     let agent = TestAgent::with_init();
 
-    let mut handler = MapStoreWithEntry::new(TestAgent::STORE, K1, |_: Option<Text>| None);
+    let mut handler = MapStoreTransformEntry::new(TestAgent::STORE, K1, |_: Option<&Text>| None);
 
     let result = handler.step(
         &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
@@ -640,3 +640,41 @@ fn map_lane_with_event_handler_remove() {
     let event = agent.store.read_with_prev(|event, _| event);
     assert_eq!(event, Some(MapLaneEvent::Remove(K1, Text::new(V1))));
 }
+
+#[test]
+fn map_lane_with_entry_handler_absent() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::with_init();
+
+    let mut handler = MapStoreWithEntry::new(TestAgent::STORE, ABSENT, |maybe_v: Option<&str>| {
+        maybe_v.map(str::to_owned)
+    });
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(None));
+}
+
+#[test]
+fn map_lane_with_entry_handler_present() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::with_init();
+
+    let mut handler = MapStoreWithEntry::new(TestAgent::STORE, K1, |maybe_v: Option<&str>| {
+        maybe_v.map(str::to_owned)
+    });
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(Some(V1.to_owned())));
+}
diff --git a/server/swimos_agent/src/stores/value/tests.rs b/server/swimos_agent/src/stores/value/tests.rs
index ac89b21ae..54fcc1db4 100644
--- a/server/swimos_agent/src/stores/value/tests.rs
+++ b/server/swimos_agent/src/stores/value/tests.rs
@@ -24,7 +24,7 @@ use crate::{
     event_handler::{EventHandlerError, HandlerAction, Modification, StepResult},
     meta::AgentMetadata,
     stores::{
-        value::{ValueStore, ValueStoreGet, ValueStoreSet},
+        value::{ValueStore, ValueStoreGet, ValueStoreSet, ValueStoreWithValue},
         StoreItem,
     },
     test_context::dummy_context,
@@ -114,23 +114,28 @@ fn make_meta<'a>(
 
 struct TestAgent {
     store: ValueStore<i32>,
+    str_store: ValueStore<String>,
 }
 
 const STORE_ID: u64 = 9;
+const STR_STORE_ID: u64 = 3;
 
 impl Default for TestAgent {
     fn default() -> Self {
         Self {
             store: ValueStore::new(STORE_ID, 0),
+            str_store: ValueStore::new(STR_STORE_ID, "world".to_owned()),
         }
     }
 }
 
 impl TestAgent {
     const STORE: fn(&TestAgent) -> &ValueStore<i32> = |agent| &agent.store;
+    const STR_STORE: fn(&TestAgent) -> &ValueStore<String> = |agent| &agent.str_store;
 }
 
-fn check_result<T: Eq + Debug>(
+fn check_result_for<T: Eq + Debug>(
+    store_id: u64,
     result: StepResult<T>,
     written: bool,
     trigger_handler: bool,
@@ -138,9 +143,9 @@ fn check_result<T: Eq + Debug>(
 ) {
     let expected_mod = if written {
         if trigger_handler {
-            Some(Modification::of(STORE_ID))
+            Some(Modification::of(store_id))
         } else {
-            Some(Modification::no_trigger(STORE_ID))
+            Some(Modification::no_trigger(store_id))
         }
     } else {
         None
@@ -165,6 +170,15 @@ fn check_result<T: Eq + Debug>(
     }
 }
 
+fn check_result<T: Eq + Debug>(
+    result: StepResult<T>,
+    written: bool,
+    trigger_handler: bool,
+    complete: Option<T>,
+) {
+    check_result_for(STORE_ID, result, written, trigger_handler, complete)
+}
+
 #[test]
 fn value_store_set_event_handler() {
     let uri = make_uri();
@@ -221,3 +235,30 @@ fn value_store_get_event_handler() {
         StepResult::Fail(EventHandlerError::SteppedAfterComplete)
     ));
 }
+
+#[test]
+fn value_store_with_value_event_handler() {
+    let uri = make_uri();
+    let route_params = HashMap::new();
+    let meta = make_meta(&uri, &route_params);
+    let agent = TestAgent::default();
+
+    let mut handler = ValueStoreWithValue::new(TestAgent::STR_STORE, |s: &str| s.len());
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    check_result(result, false, false, Some(5));
+
+    let result = handler.step(
+        &mut dummy_context(&mut HashMap::new(), &mut BytesMut::new()),
+        meta,
+        &agent,
+    );
+    assert!(matches!(
+        result,
+        StepResult::Fail(EventHandlerError::SteppedAfterComplete)
+    ));
+}