Repository: grapl-security/grapl (language: python)

Issue:
graph-merger to stream Update, not Updates
https://grapl-internal.slack.com/archives/C017F10727P/p1663875891566189


Discussion / hints:

Fix patch:
diff --git a/buf.yaml b/buf.yaml
index 12bc560f14..5f5bedb798 100644
--- a/buf.yaml
+++ b/buf.yaml
@@ -24,6 +24,7 @@ breaking:
     - graplinc/grapl/pipeline/v1beta2
     - graplinc/grapl/api/plugin_work_queue/v1beta1
     - graplinc/grapl/api/plugin_registry/v1beta1
+    - graplinc/grapl/api/plugin_sdk/analyzers/v1beta1
     - graplinc/common/v1beta1
     # Again, we don't care about vendored definitions
     - google/
diff --git a/etc/example_schemas/example.graphql b/etc/example_schemas/example.graphql
index ae03b167a6..2205071d31 100644
--- a/etc/example_schemas/example.graphql
+++ b/etc/example_schemas/example.graphql
@@ -5,7 +5,9 @@ type Process @grapl(identity_algorithm: "session") {
     last_seen_at: UInt! @last_seen_time,
     terminated_at: UInt! @terminate_time,
     binary_file: File! @edge(reverse: "executed_as_processes", reverse_relationship: "ToMany"),
-    created_file: [File!] @edge(reverse: "created_by_process", reverse_relationship: "ToMany"),
+    created_files: [File!] @edge(reverse: "created_by_process", reverse_relationship: "ToOne"),
+    process_asset: Asset! @edge(reverse: "asset_processes", reverse_relationship: "ToMany"),
+    children: [Process!] @edge(reverse: "parent", reverse_relationship: "ToOne"),
 }
 
 type File @grapl(identity_algorithm: "session") {
@@ -15,3 +17,10 @@ type File @grapl(identity_algorithm: "session") {
     terminated_at: UInt! @terminate_time,
 }
 
+type Asset @grapl(identity_algorithm: "static") {
+    asset_id: String! @static_id,
+    hostname: String! @immutable,
+    launch_time: UInt! @immutable,
+    last_seen_time: UInt! @increment_only,
+    files_on_asset: [File!] @edge(reverse: "file_asset", reverse_relationship: "ToOne")
+}
diff --git a/nomad/grapl-core.nomad b/nomad/grapl-core.nomad
index 838921e66a..8b2b296dc0 100644
--- a/nomad/grapl-core.nomad
+++ b/nomad/grapl-core.nomad
@@ -76,6 +76,11 @@ variable "session_table_name" {
   description = "What is the name of the session table?"
 }
 
+variable "static_mapping_table_name" {
+  type        = string
+  description = "The name of the dynamodb table used for storing the ids of static nodes"
+}
+
 variable "plugin_registry_db" {
   type = object({
     hostname = string
@@ -189,19 +194,23 @@ locals {
   dgraph_alpha_grpc_public_port_base  = 9080
 
   # DGraph Alphas (shards * replicas)
-  dgraph_alphas = [for alpha_id in range(0, var.dgraph_replicas * var.dgraph_shards) : {
-    id : alpha_id,
-    grpc_private_port : local.dgraph_alpha_grpc_private_port_base + alpha_id,
-    grpc_public_port : local.dgraph_alpha_grpc_public_port_base + alpha_id,
-    http_port : local.dgraph_alpha_http_private_port_base + alpha_id
-  }]
+  dgraph_alphas = [
+    for alpha_id in range(0, var.dgraph_replicas * var.dgraph_shards) : {
+      id : alpha_id,
+      grpc_private_port : local.dgraph_alpha_grpc_private_port_base + alpha_id,
+      grpc_public_port : local.dgraph_alpha_grpc_public_port_base + alpha_id,
+      http_port : local.dgraph_alpha_http_private_port_base + alpha_id
+    }
+  ]
 
   # DGraph Zeros (replicas)
-  dgraph_zeros = [for zero_id in range(1, var.dgraph_replicas) : {
-    id : zero_id,
-    grpc_private_port : local.dgraph_zero_grpc_private_port_base + zero_id,
-    http_port : local.dgraph_zero_http_private_port_base + zero_id,
-  }]
+  dgraph_zeros = [
+    for zero_id in range(1, var.dgraph_replicas) : {
+      id : zero_id,
+      grpc_private_port : local.dgraph_zero_grpc_private_port_base + zero_id,
+      http_port : local.dgraph_zero_http_private_port_base + zero_id,
+    }
+  ]
 
   # String that contains all of the Zeros for the Alphas to talk to and ensure they don't go down when one dies
   zero_alpha_connect_str = join(",", [for zero_id in range(0, var.dgraph_replicas) : "localhost:${local.dgraph_zero_grpc_private_port_base + zero_id}"])
@@ -751,10 +760,12 @@ job "grapl-core" {
       }
 
       env {
-        AWS_REGION         = var.aws_region
-        RUST_LOG           = var.rust_log
-        RUST_BACKTRACE     = local.rust_backtrace
-        MG_ALPHAS          = local.alpha_grpc_connect_str
+        AWS_REGION     = var.aws_region
+        RUST_LOG       = var.rust_log
+        RUST_BACKTRACE = local.rust_backtrace
+
+        GRAPH_MUTATION_CLIENT_ADDRESS = "http://${NOMAD_UPSTREAM_ADDR_graph-mutation}"
+
         GRAPL_SCHEMA_TABLE = var.schema_table_name
 
         KAFKA_BOOTSTRAP_SERVERS   = var.kafka_bootstrap_servers
@@ -776,14 +787,9 @@ job "grapl-core" {
       connect {
         sidecar_service {
           proxy {
-            dynamic "upstreams" {
-              iterator = alpha
-              for_each = local.dgraph_alphas
-
-              content {
-                destination_name = "dgraph-alpha-${alpha.value.id}-grpc-public"
-                local_bind_port  = alpha.value.grpc_public_port
-              }
+            upstreams {
+              destination_name = "graph-mutation"
+              local_bind_port  = 1001
             }
           }
         }
@@ -833,16 +839,29 @@ job "grapl-core" {
         KAFKA_CONSUMER_TOPIC      = "generated-graphs"
         KAFKA_PRODUCER_TOPIC      = "identified-graphs"
 
+        GRAPH_MUTATION_CLIENT_ADDRESS = "http://${NOMAD_UPSTREAM_ADDR_graph-mutation}"
+
         GRAPL_SCHEMA_TABLE          = var.schema_table_name
         GRAPL_DYNAMIC_SESSION_TABLE = var.session_table_name
+        GRAPL_STATIC_MAPPING_TABLE  = var.static_mapping_table_name
       }
 
       resources {
         cpu = 50
       }
+    }
+    service {
+      name = "node-identifier"
 
-      service {
-        name = "node-identifier"
+      connect {
+        sidecar_service {
+          proxy {
+            upstreams {
+              destination_name = "graph-mutation"
+              local_bind_port  = 1001
+            }
+          }
+        }
       }
     }
   }
diff --git a/pulumi/grapl/__main__.py b/pulumi/grapl/__main__.py
index 6e60a4be40..cdfdb77608 100644
--- a/pulumi/grapl/__main__.py
+++ b/pulumi/grapl/__main__.py
@@ -259,6 +259,7 @@ def main() -> None:
         schema_properties_table_name=dynamodb_tables.schema_properties_table.name,
         schema_table_name=dynamodb_tables.schema_table.name,
         session_table_name=dynamodb_tables.dynamic_session_table.name,
+        static_mapping_table_name=dynamodb_tables.static_mapping_table.name,
         test_user_name=config.GRAPL_TEST_USER_NAME,
         user_auth_table=dynamodb_tables.user_auth_table.name,
         user_session_table=dynamodb_tables.user_session_table.name,
diff --git a/pulumi/infra/dynamodb.py b/pulumi/infra/dynamodb.py
index 7ae48ff1b8..69d027c460 100644
--- a/pulumi/infra/dynamodb.py
+++ b/pulumi/infra/dynamodb.py
@@ -76,8 +76,8 @@ def __init__(self, opts: pulumi.ResourceOptions | None = None) -> None:
 
         self.static_mapping_table = DynamoDBTable(
             f"{STACK_NAME}-static_mapping_table",
-            attrs=[{"name": "pseudo_key", "type": "S"}],
-            hash_key="pseudo_key",
+            attrs=[{"name": "static_id", "type": "S"}],
+            hash_key="static_id",
             opts=pulumi.ResourceOptions(parent=self),
         )
         pulumi.export("static-mapping-table", self.static_mapping_table.name)
diff --git a/pulumi/infra/log_levels.py b/pulumi/infra/log_levels.py
index d14e6155a5..435d665b98 100644
--- a/pulumi/infra/log_levels.py
+++ b/pulumi/infra/log_levels.py
@@ -1,5 +1,7 @@
 # To learn more about this syntax, see
 # https://docs.rs/env_logger/0.9.0/env_logger/#enabling-logging
+# Quick reminder:
+# ERROR > WARN > INFO > DEBUG > TRACE
 RUST_LOG_LEVELS = ",".join(
     [
         "DEBUG",
@@ -7,12 +9,13 @@
         "hyper=WARN",
         "rusoto_core=WARN",
         "rustls=WARN",
-        # noisy, only for debugging
-        "client_executor=TRACE",
         # By default, sqlx outputs an INFO for every query executed.
         # It's very noisy!
         # https://github.com/launchbadge/sqlx/issues/942
         "sqlx::query=WARN",
+        # By default, Tower outputs a DEBUG time you make a gRPC call,
+        # so set it to something less noisy.
+        "tower::buffer::worker=WARN",
     ]
 )
 PY_LOG_LEVEL = "DEBUG"
diff --git a/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto b/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto
index de97d5a759..e1c1392370 100644
--- a/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto
+++ b/src/proto/graplinc/grapl/api/graph/v1beta1/types.proto
@@ -2,6 +2,8 @@ syntax = "proto3";
 
 package graplinc.grapl.api.graph.v1beta1;
 
+import "graplinc/grapl/common/v1beta1/types.proto";
+
 // The Session Identity Strategy is used to derive a canonical identity for any node that has a
 // 'pseudo key', as well as a beginning, middle, and end.
 // The 'pseudo key' can itself be multiple values concatenated together.
@@ -130,24 +132,12 @@ message NodeDescription {
 message IdentifiedNode {
   // Represents the properties of a node
   map<string, NodeProperty> properties = 1;
-  // The node_key, which is now resolved and canonical
-  string node_key = 2;
+  // The uid for this node in the graph
+  graplinc.grapl.common.v1beta1.Uid uid = 2;
   // The node type, as it exists in DGraph
   string node_type = 3;
 }
 
-// A MergedNode represents a node that has already been merged into Grapl.
-message MergedNode {
-  // Represents the properties of a node
-  map<string, NodeProperty> properties = 1;
-  // The 'uid' represents the DGraph node uid
-  uint64 uid = 2;
-  // The node_key, which is now resolved and canonical
-  string node_key = 3;
-  // The node type, as it exists in DGraph
-  string node_type = 4;
-}
-
 // Represents a directed edge between two nodes. The node_key's may be canonical, or they may not be,
 // depending on the pipeline stage
 message Edge {
@@ -159,18 +149,14 @@ message Edge {
   string edge_name = 3;
 }
 
-// Represents a directed edge between two nodes, where both nodes, and this edge, exist in the graph.
-message MergedEdge {
+// Represents a directed edge between two nodes, where both nodes, and this edge, have been identified.
+message IdentifiedEdge {
   // The uid associated with the 'source' of the edge
-  string from_uid = 1;
-  // The node_key associated with the 'source' of the edge
-  string from_node_key = 2;
+  graplinc.grapl.common.v1beta1.Uid from_uid = 1;
   // The uid associated with the 'destination' of the edge
-  string to_uid = 3;
-  // The node_key associated with the 'destination' of the edge
-  string to_node_key = 4;
+  graplinc.grapl.common.v1beta1.Uid to_uid = 2;
   // The name of the edge
-  string edge_name = 5;
+  string edge_name = 3;
 }
 
 // Represents multiple edges - mostly necessary because protobuf can't express a `map` of `repeated` values directly.
@@ -179,11 +165,10 @@ message EdgeList {
   repeated Edge edges = 1;
 }
 
-// Represents multiple edges - mostly necessary because protobuf can't express a `map` of `repeated` values directly.
-// Every edge in the MergedEdgeList has been merged into the graph store.
-message MergedEdgeList {
+// Represents multiple identified edges
+message IdentifiedEdgeList {
   // The internal repeated edges
-  repeated MergedEdge edges = 1;
+  repeated IdentifiedEdge edges = 1;
 }
 
 // GraphDescription represents a graph of nodes that may or may not exist yet in the graph store.
@@ -199,18 +184,9 @@ message GraphDescription {
 // These nodes have canonical identities.
 message IdentifiedGraph {
   // The mapping of node_key to the IdentifiedNode
-  map<string, IdentifiedNode> nodes = 1;
-  // The mapping of node_key to the EdgeLists
-  map<string, EdgeList> edges = 2;
-}
-
-// GraphDescription represents a graph of nodes that exist in the graph store. These nodes
-// have canonical identities.
-message MergedGraph {
-  // The mapping of node_key to the MergedNode
-  map<string, MergedNode> nodes = 1;
-  // The mapping of node_key to the EdgeLists
-  map<string, MergedEdgeList> edges = 2;
+  map<uint64, IdentifiedNode> nodes = 1;
+  // The mapping of node_key to the IdentifiedEdgeLists
+  map<uint64, IdentifiedEdgeList> edges = 2;
 }
 
 // A Lens.
@@ -227,10 +203,10 @@ message Lens {
 
 // Result computed by an analyzer when it makes a detection.
 message ExecutionHit {
-  // The mapping of node_key to the MergedNode
-  map<string, graplinc.grapl.api.graph.v1beta1.MergedNode> nodes = 1;
+  // The mapping of node_key to the identified Node
+  map<uint64, graplinc.grapl.api.graph.v1beta1.IdentifiedNode> nodes = 1;
   // The mapping of node_key to the EdgeLists
-  map<string, graplinc.grapl.api.graph.v1beta1.MergedEdgeList> edges = 2;
+  map<uint64, graplinc.grapl.api.graph.v1beta1.IdentifiedEdgeList> edges = 2;
   // The name of the analyzer which made the detection
   string analyzer_name = 3;
   // The risk score associated with this detection
diff --git a/src/proto/graplinc/grapl/api/graph_query_service/v1beta1/graph_query_service.proto b/src/proto/graplinc/grapl/api/graph_query_service/v1beta1/graph_query_service.proto
index 0f028c0f92..e4185e4494 100644
--- a/src/proto/graplinc/grapl/api/graph_query_service/v1beta1/graph_query_service.proto
+++ b/src/proto/graplinc/grapl/api/graph_query_service/v1beta1/graph_query_service.proto
@@ -331,7 +331,7 @@ message QueryGraphFromUidRequest {
 // The QueryGraphFromUidResponse contains the graph that matched
 // a QueryGraphFromUidResponse
 message QueryGraphFromUidResponse {
-  // A view of the MergedGraph that matched our query
+  // A view of the graph that matched our query
   // or None if query did not match
   GraphView matched_graph = 1;
 }
diff --git a/src/proto/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/analyzers.proto b/src/proto/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/analyzers.proto
index 781df4232e..ca6333c0c4 100644
--- a/src/proto/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/analyzers.proto
+++ b/src/proto/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/analyzers.proto
@@ -96,10 +96,8 @@ message ExecutionMiss {
 
 // A request to execute an Analyzer against a specific update to a tenant's graph
 message RunAnalyzerRequest {
-  // The tenant that the graph belongs to
-  graplinc.common.v1beta1.Uuid tenant_id = 1;
-  // The update that occurred
-  Update update = 2;
+  // The updates that occurred
+  Update update = 1;
 }
 
 // The result of an Analyzer's execution
diff --git a/src/python/python-proto/python_proto/api/graph/v1beta1/messages.py b/src/python/python-proto/python_proto/api/graph/v1beta1/messages.py
index c43ab672e5..aa764d6d02 100644
--- a/src/python/python-proto/python_proto/api/graph/v1beta1/messages.py
+++ b/src/python/python-proto/python_proto/api/graph/v1beta1/messages.py
@@ -4,10 +4,14 @@
 from typing import Mapping, Sequence, cast
 
 from graplinc.grapl.api.graph.v1beta1 import types_pb2 as proto
+from python_proto.grapl.common.v1beta1.messages import Uid
 from python_proto.serde import SerDe
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class Session(SerDe[proto.Session]):
     primary_key_properties: Sequence[str]
     primary_key_requires_asset_id: bool
@@ -37,7 +41,10 @@ def into_proto(self) -> proto.Session:
         return proto_session
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class Static(SerDe[proto.Static]):
     primary_key_properties: Sequence[str]
     primary_key_requires_asset_id: bool
@@ -58,7 +65,10 @@ def into_proto(self) -> proto.Static:
         return proto_static
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class IdStrategy(SerDe[proto.IdStrategy]):
     strategy: Session | Static
     _proto_cls = proto.IdStrategy
@@ -87,7 +97,10 @@ def into_proto(self) -> proto.IdStrategy:
         return proto_id_strategy
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class IncrementOnlyUintProp(SerDe[proto.IncrementOnlyUintProp]):
     prop: int
     _proto_cls = proto.IncrementOnlyUintProp
@@ -105,7 +118,10 @@ def into_proto(self) -> proto.IncrementOnlyUintProp:
         return proto_increment_only_uint_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class ImmutableUintProp(SerDe[proto.ImmutableUintProp]):
     prop: int
     _proto_cls = proto.ImmutableUintProp
@@ -123,7 +139,10 @@ def into_proto(self) -> proto.ImmutableUintProp:
         return proto_immutable_uint_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class DecrementOnlyUintProp(SerDe[proto.DecrementOnlyUintProp]):
     prop: int
     _proto_cls = proto.DecrementOnlyUintProp
@@ -141,7 +160,10 @@ def into_proto(self) -> proto.DecrementOnlyUintProp:
         return proto_decrement_only_uint_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class IncrementOnlyIntProp(SerDe[proto.IncrementOnlyIntProp]):
     prop: int
     _proto_cls = proto.IncrementOnlyIntProp
@@ -159,7 +181,10 @@ def into_proto(self) -> proto.IncrementOnlyIntProp:
         return proto_increment_only_int_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class DecrementOnlyIntProp(SerDe[proto.DecrementOnlyIntProp]):
     prop: int
     _proto_cls = proto.DecrementOnlyIntProp
@@ -177,7 +202,10 @@ def into_proto(self) -> proto.DecrementOnlyIntProp:
         return proto_decrement_only_int_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class ImmutableIntProp(SerDe[proto.ImmutableIntProp]):
     prop: int
     _proto_cls = proto.ImmutableIntProp
@@ -195,7 +223,10 @@ def into_proto(self) -> proto.ImmutableIntProp:
         return proto_immutable_int_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class ImmutableStrProp(SerDe[proto.ImmutableStrProp]):
     prop: str
     _proto_cls = proto.ImmutableStrProp
@@ -213,7 +244,10 @@ def into_proto(self) -> proto.ImmutableStrProp:
         return proto_immutable_str_prop
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class NodeProperty(SerDe[proto.NodeProperty]):
     property_: (
         IncrementOnlyUintProp
@@ -304,7 +338,10 @@ def into_proto(self) -> proto.NodeProperty:
         return proto_node_property
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class NodeDescription(SerDe[proto.NodeDescription]):
     properties: Mapping[str, NodeProperty]
     node_key: str
@@ -339,10 +376,13 @@ def into_proto(self) -> proto.NodeDescription:
         return proto_node_description
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class IdentifiedNode(SerDe[proto.IdentifiedNode]):
     properties: Mapping[str, NodeProperty]
-    node_key: str
+    uid: Uid
     node_type: str
     _proto_cls = proto.IdentifiedNode
 
@@ -353,7 +393,7 @@ def from_proto(cls, proto_identified_node: proto.IdentifiedNode) -> IdentifiedNo
                 k: NodeProperty.from_proto(proto_identified_node.properties[k])
                 for k in proto_identified_node.properties
             },
-            node_key=proto_identified_node.node_key,
+            uid=Uid.from_proto(proto_identified_node.uid),
             node_type=proto_identified_node.node_type,
         )
 
@@ -361,42 +401,15 @@ def into_proto(self) -> proto.IdentifiedNode:
         proto_identified_node = proto.IdentifiedNode()
         for k, v in self.properties.items():
             proto_identified_node.properties[k].CopyFrom(v.into_proto())
-        proto_identified_node.node_key = self.node_key
+        proto_identified_node.uid.CopyFrom(self.uid.into_proto())
         proto_identified_node.node_type = self.node_type
         return proto_identified_node
 
 
-@dataclasses.dataclass(frozen=True)
-class MergedNode(SerDe[proto.MergedNode]):
-    properties: Mapping[str, NodeProperty]
-    uid: int
-    node_key: str
-    node_type: str
-    _proto_cls = proto.MergedNode
-
-    @classmethod
-    def from_proto(cls, proto_merged_node: proto.MergedNode) -> MergedNode:
-        return MergedNode(
-            properties={
-                k: NodeProperty.from_proto(proto_merged_node.properties[k])
-                for k in proto_merged_node.properties
-            },
-            uid=proto_merged_node.uid,
-            node_key=proto_merged_node.node_key,
-            node_type=proto_merged_node.node_type,
-        )
-
-    def into_proto(self) -> proto.MergedNode:
-        proto_merged_node = proto.MergedNode()
-        for k, v in self.properties.items():
-            proto_merged_node.properties[k].CopyFrom(v.into_proto())
-        proto_merged_node.uid = self.uid
-        proto_merged_node.node_key = self.node_key
-        proto_merged_node.node_type = self.node_type
-        return proto_merged_node
-
-
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class Edge(SerDe[proto.Edge]):
     from_node_key: str
     to_node_key: str
@@ -419,7 +432,10 @@ def into_proto(self) -> proto.Edge:
         return proto_edge
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class EdgeList(SerDe[proto.EdgeList]):
     edges: Sequence[Edge]
     _proto_cls = proto.EdgeList
@@ -435,58 +451,61 @@ def into_proto(self) -> proto.EdgeList:
         return proto_edge_list
 
 
-@dataclasses.dataclass(frozen=True)
-class MergedEdge(SerDe[proto.MergedEdge]):
-    from_uid: str
-    from_node_key: str
-    to_uid: str
-    to_node_key: str
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
+class IdentifiedEdge(SerDe[proto.IdentifiedEdge]):
+    from_uid: Uid
+    to_uid: Uid
     edge_name: str
-    _proto_cls = proto.MergedEdge
+    _proto_cls = proto.IdentifiedEdge
 
     @classmethod
-    def from_proto(cls, proto_merged_edge: proto.MergedEdge) -> MergedEdge:
-        return MergedEdge(
-            from_uid=proto_merged_edge.from_uid,
-            from_node_key=proto_merged_edge.from_node_key,
-            to_uid=proto_merged_edge.to_uid,
-            to_node_key=proto_merged_edge.to_node_key,
-            edge_name=proto_merged_edge.edge_name,
+    def from_proto(cls, proto_identified_edge: proto.IdentifiedEdge) -> IdentifiedEdge:
+        return IdentifiedEdge(
+            from_uid=Uid.from_proto(proto_identified_edge.from_uid),
+            to_uid=Uid.from_proto(proto_identified_edge.to_uid),
+            edge_name=proto_identified_edge.edge_name,
         )
 
-    def into_proto(self) -> proto.MergedEdge:
-        proto_merged_edge = proto.MergedEdge()
-        proto_merged_edge.from_uid = self.from_uid
-        proto_merged_edge.from_node_key = self.from_node_key
-        proto_merged_edge.to_uid = self.to_uid
-        proto_merged_edge.to_node_key = self.to_node_key
-        proto_merged_edge.edge_name = self.edge_name
-        return proto_merged_edge
+    def into_proto(self) -> proto.IdentifiedEdge:
+        proto_identified_edge = proto.IdentifiedEdge()
+        proto_identified_edge.from_uid.CopyFrom(self.from_uid.into_proto())
+        proto_identified_edge.to_uid.CopyFrom(self.to_uid.into_proto())
+        proto_identified_edge.edge_name = self.edge_name
+        return proto_identified_edge
 
 
-# TODO: Delete, this is being replaced by Updates
-@dataclasses.dataclass(frozen=True)
-class MergedEdgeList(SerDe[proto.MergedEdgeList]):
+@dataclasses.dataclass(frozen=True, slots=True)
+class IdentifiedEdgeList(SerDe[proto.IdentifiedEdgeList]):
     # TODO: seed to places where this is used:
     # /src/python/grapl_analyzerlib/grapl_analyzerlib/view_from_proto.py
     # /src/python/grapl_analyzerlib/grapl_analyzerlib/subgraph_view.py
-    edges: Sequence[MergedEdge]
-    _proto_cls = proto.MergedEdgeList
+    edges: Sequence[IdentifiedEdge]
+    _proto_cls = proto.IdentifiedEdgeList
 
     @classmethod
-    def from_proto(cls, proto_merged_edge_list: proto.MergedEdgeList) -> MergedEdgeList:
-        return MergedEdgeList(
-            edges=[MergedEdge.from_proto(e) for e in proto_merged_edge_list.edges]
+    def from_proto(
+        cls, proto_identified_edge_list: proto.IdentifiedEdgeList
+    ) -> IdentifiedEdgeList:
+        return IdentifiedEdgeList(
+            edges=[
+                IdentifiedEdge.from_proto(e) for e in proto_identified_edge_list.edges
+            ]
         )
 
-    def into_proto(self) -> proto.MergedEdgeList:
-        proto_merged_edge_list = proto.MergedEdgeList()
+    def into_proto(self) -> proto.IdentifiedEdgeList:
+        proto_identified_edge_list = proto.IdentifiedEdgeList()
         for e in self.edges:
-            proto_merged_edge_list.edges.append(e.into_proto())
-        return proto_merged_edge_list
+            proto_identified_edge_list.edges.append(e.into_proto())
+        return proto_identified_edge_list
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class GraphDescription(SerDe[proto.GraphDescription]):
     nodes: Mapping[str, NodeDescription]
     edges: Mapping[str, EdgeList]
@@ -516,10 +535,13 @@ def into_proto(self) -> proto.GraphDescription:
         return proto_graph_description
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class IdentifiedGraph(SerDe[proto.IdentifiedGraph]):
-    nodes: Mapping[str, IdentifiedNode]
-    edges: Mapping[str, EdgeList]
+    nodes: Mapping[Uid, IdentifiedNode]
+    edges: Mapping[Uid, IdentifiedEdgeList]
     _proto_cls = proto.IdentifiedGraph
 
     @classmethod
@@ -528,11 +550,11 @@ def from_proto(
     ) -> IdentifiedGraph:
         return IdentifiedGraph(
             nodes={
-                k: IdentifiedNode.from_proto(proto_identified_graph.nodes[k])
+                Uid(k): IdentifiedNode.from_proto(proto_identified_graph.nodes[k])
                 for k in proto_identified_graph.nodes
             },
             edges={
-                k: EdgeList.from_proto(proto_identified_graph.edges[k])
+                Uid(k): IdentifiedEdgeList.from_proto(proto_identified_graph.edges[k])
                 for k in proto_identified_graph.edges
             },
         )
@@ -540,41 +562,16 @@ def from_proto(
     def into_proto(self) -> proto.IdentifiedGraph:
         proto_identified_graph = proto.IdentifiedGraph()
         for k1, v1 in self.nodes.items():
-            proto_identified_graph.nodes[k1].CopyFrom(v1.into_proto())
+            proto_identified_graph.nodes[k1.value].CopyFrom(v1.into_proto())
         for k2, v2 in self.edges.items():
-            proto_identified_graph.edges[k2].CopyFrom(v2.into_proto())
+            proto_identified_graph.edges[k2.value].CopyFrom(v2.into_proto())
         return proto_identified_graph
 
 
-@dataclasses.dataclass(frozen=True)
-class MergedGraph(SerDe[proto.MergedGraph]):
-    nodes: Mapping[str, MergedNode]
-    edges: Mapping[str, MergedEdgeList]
-    _proto_cls = proto.MergedGraph
-
-    @classmethod
-    def from_proto(cls, proto_merged_graph: proto.MergedGraph) -> MergedGraph:
-        return MergedGraph(
-            nodes={
-                k: MergedNode.from_proto(proto_merged_graph.nodes[k])
-                for k in proto_merged_graph.nodes
-            },
-            edges={
-                k: MergedEdgeList.from_proto(proto_merged_graph.edges[k])
-                for k in proto_merged_graph.edges
-            },
-        )
-
-    def into_proto(self) -> proto.MergedGraph:
-        proto_merged_graph = proto.MergedGraph()
-        for k1, v1 in self.nodes.items():
-            proto_merged_graph.nodes[k1].CopyFrom(v1.into_proto())
-        for k2, v2 in self.edges.items():
-            proto_merged_graph.edges[k2].CopyFrom(v2.into_proto())
-        return proto_merged_graph
-
-
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class Lens(SerDe[proto.Lens]):
     lens_type: str
     lens_name: str
@@ -602,10 +599,13 @@ def into_proto(self) -> proto.Lens:
         return proto_lens
 
 
-@dataclasses.dataclass(frozen=True)
+@dataclasses.dataclass(
+    frozen=True,
+    slots=True,
+)
 class ExecutionHit(SerDe[proto.ExecutionHit]):
-    nodes: Mapping[str, MergedNode]
-    edges: Mapping[str, MergedEdgeList]
+    nodes: Mapping[Uid, IdentifiedNode]
+    edges: Mapping[Uid, IdentifiedEdgeList]
     analyzer_name: str
     risk_score: int
     lenses: Sequence[Lens]
@@ -616,11 +616,11 @@ class ExecutionHit(SerDe[proto.ExecutionHit]):
     def from_proto(cls, proto_execution_hit: proto.ExecutionHit) -> ExecutionHit:
         return ExecutionHit(
             nodes={
-                k: MergedNode.from_proto(v)
+                Uid(k): IdentifiedNode.from_proto(v)
                 for k, v in proto_execution_hit.nodes.items()
             },
             edges={
-                k: MergedEdgeList.from_proto(v)
+                Uid(k): IdentifiedEdgeList.from_proto(v)
                 for k, v in proto_execution_hit.edges.items()
             },
             analyzer_name=proto_execution_hit.analyzer_name,
@@ -632,9 +632,9 @@ def from_proto(cls, proto_execution_hit: proto.ExecutionHit) -> ExecutionHit:
     def into_proto(self) -> proto.ExecutionHit:
         proto_execution_hit = proto.ExecutionHit()
         for k1, v1 in self.nodes.items():
-            proto_execution_hit.nodes[k1].CopyFrom(v1.into_proto())
+            proto_execution_hit.nodes[k1.value].CopyFrom(v1.into_proto())
         for k2, v2 in self.edges.items():
-            proto_execution_hit.edges[k2].CopyFrom(v2.into_proto())
+            proto_execution_hit.edges[k2.value].CopyFrom(v2.into_proto())
         proto_execution_hit.analyzer_name = self.analyzer_name
         proto_execution_hit.risk_score = self.risk_score
         for lens in self.lenses:
diff --git a/src/python/python-proto/python_proto/api/plugin_sdk/analyzers/v1beta1/messages.py b/src/python/python-proto/python_proto/api/plugin_sdk/analyzers/v1beta1/messages.py
index 2de58ba6c5..f34c549598 100644
--- a/src/python/python-proto/python_proto/api/plugin_sdk/analyzers/v1beta1/messages.py
+++ b/src/python/python-proto/python_proto/api/plugin_sdk/analyzers/v1beta1/messages.py
@@ -10,7 +10,6 @@
 
 @dataclasses.dataclass(frozen=True, slots=True)
 class RunAnalyzerRequest(SerDe[proto.RunAnalyzerRequest]):
-    tenant_id: proto_common_msgs.Uuid
     update: Update
 
     _proto_cls = proto.RunAnalyzerRequest
@@ -21,13 +20,11 @@ def from_proto(
         proto_value: proto.RunAnalyzerRequest,
     ) -> RunAnalyzerRequest:
         return cls(
-            tenant_id=proto_common_msgs.Uuid.from_proto(proto_value.tenant_id),
             update=Update.from_proto(proto_value.update),
         )
 
     def into_proto(self) -> proto.RunAnalyzerRequest:
         proto_value = self.new_proto()
-        proto_value.tenant_id.CopyFrom(self.tenant_id.into_proto())
         proto_value.update.CopyFrom(self.update.into_proto())
         return proto_value
 
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 0be412a2c5..87bca73d30 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -72,7 +72,7 @@ dependencies = [
  "mime",
  "percent-encoding",
  "pin-project-lite",
- "rand 0.8.5",
+ "rand",
  "sha-1",
  "smallvec",
 ]
@@ -310,7 +310,7 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom 0.2.7",
+ "getrandom",
  "once_cell",
  "version_check",
 ]
@@ -361,6 +361,15 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -547,7 +556,7 @@ dependencies = [
  "mime",
  "percent-encoding",
  "pin-project-lite",
- "rand 0.8.5",
+ "rand",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -809,15 +818,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.19"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
 dependencies = [
- "libc",
+ "iana-time-zone",
+ "js-sys",
  "num-integer",
  "num-traits",
  "serde",
  "time 0.1.43",
+ "wasm-bindgen",
  "winapi",
 ]
 
@@ -853,7 +864,7 @@ dependencies = [
  "clap_lex",
  "indexmap",
  "once_cell",
- "strsim 0.10.0",
+ "strsim",
  "termcolor",
  "textwrap 0.15.0",
 ]
@@ -987,7 +998,7 @@ dependencies = [
  "hkdf",
  "hmac 0.12.1",
  "percent-encoding",
- "rand 0.8.5",
+ "rand",
  "sha2 0.10.2",
  "subtle",
  "time 0.3.11",
@@ -1070,7 +1081,7 @@ dependencies = [
  "clap 2.34.0",
  "criterion-plot",
  "csv",
- "itertools 0.10.3",
+ "itertools",
  "lazy_static",
  "num-traits",
  "oorandom",
@@ -1092,7 +1103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876"
 dependencies = [
  "cast",
- "itertools 0.10.3",
+ "itertools",
 ]
 
 [[package]]
@@ -1171,7 +1182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03"
 dependencies = [
  "generic-array",
- "rand_core 0.6.3",
+ "rand_core",
  "subtle",
 ]
 
@@ -1235,41 +1246,6 @@ dependencies = [
  "cipher",
 ]
 
-[[package]]
-name = "darling"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
-dependencies = [
- "fnv",
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim 0.9.3",
- "syn",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
-dependencies = [
- "darling_core",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "dashmap"
 version = "5.3.4"
@@ -1324,31 +1300,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "derive_builder"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
-dependencies = [
- "darling",
- "derive_builder_core",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "derive_builder_core"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
-dependencies = [
- "darling",
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "derive_more"
 version = "0.99.17"
@@ -1362,45 +1313,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "dgraph-tonic"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9e90175d6d7d6b386ed60dd9a4072046ed05a1c0ca39de067b98acb06e122f"
-dependencies = [
- "anyhow",
- "async-stream",
- "async-trait",
- "futures",
- "http",
- "lazy_static",
- "prost 0.9.0",
- "rand 0.8.5",
- "serde",
- "serde_derive",
- "serde_json",
- "thiserror",
- "tokio",
- "tonic 0.6.2",
- "tonic-build 0.6.2",
- "tracing",
- "tracing-attributes",
- "tracing-futures",
-]
-
-[[package]]
-name = "dgraph_query_lib"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f801dd3b159613798898550c6583fe57e977948d07fe9813eb95f3b89953dd6"
-dependencies = [
- "derive_builder",
- "itertools 0.9.0",
- "rand 0.7.3",
- "serde",
- "serde_json",
-]
-
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -1854,17 +1766,6 @@ dependencies = [
  "version_check",
 ]
 
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.2.7"
@@ -1902,12 +1803,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 name = "graph-merger"
 version = "1.0.0"
 dependencies = [
- "anyhow",
  "async-trait",
  "bytes",
  "clap 3.2.13",
- "dgraph-tonic",
- "dgraph_query_lib",
  "e2e-tests",
  "eyre",
  "futures",
@@ -1942,6 +1840,7 @@ dependencies = [
  "grapl-utils",
  "hash_hasher",
  "lazy_static",
+ "moka",
  "rust-proto",
  "scylla",
  "thiserror",
@@ -1960,18 +1859,20 @@ dependencies = [
  "bytes",
  "chrono",
  "clap 3.2.13",
+ "e2e-tests",
  "env_logger 0.9.0",
  "eyre",
  "futures",
  "grapl-tracing",
- "itertools 0.10.3",
+ "itertools",
  "prost 0.9.0",
- "rand 0.8.5",
+ "rand",
  "rust-proto",
  "rustc-hash",
  "scylla",
  "secrecy",
  "serde",
+ "test-context",
  "test-log",
  "thiserror",
  "tokio",
@@ -2084,7 +1985,7 @@ dependencies = [
  "grapl-utils",
  "hmap",
  "jsonwebtoken-google",
- "rand 0.8.5",
+ "rand",
  "reqwest",
  "rusoto_core",
  "rusoto_dynamodb",
@@ -2384,10 +2285,17 @@ dependencies = [
 ]
 
 [[package]]
-name = "ident_case"
-version = "1.0.1"
+name = "iana-time-zone"
+version = "0.1.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
 
 [[package]]
 name = "idna"
@@ -2455,15 +2363,6 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
 
-[[package]]
-name = "itertools"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itertools"
 version = "0.10.3"
@@ -2525,7 +2424,7 @@ checksum = "b91805e5a399ed856b7ea5a87ff9d1eb5486a44700b7ec0fb9ec475b5a4c8f11"
 dependencies = [
  "headers",
  "jsonwebtoken",
- "rand 0.8.5",
+ "rand",
  "reqwest",
  "rsa",
  "rustls 0.20.6",
@@ -2832,6 +2731,7 @@ name = "node-identifier"
 version = "1.0.0"
 dependencies = [
  "async-trait",
+ "blake2",
  "bytes",
  "chrono",
  "clap 3.2.13",
@@ -2845,7 +2745,7 @@ dependencies = [
  "hex",
  "hmap",
  "kafka",
- "rand 0.8.5",
+ "rand",
  "rusoto_core",
  "rusoto_credential",
  "rusoto_dynamodb",
@@ -2858,6 +2758,7 @@ dependencies = [
  "thiserror",
  "tokio",
  "tracing",
+ "uid-allocator",
  "uuid",
 ]
 
@@ -2917,7 +2818,7 @@ dependencies = [
  "num-integer",
  "num-iter",
  "num-traits",
- "rand 0.8.5",
+ "rand",
  "smallvec",
  "zeroize",
 ]
@@ -3062,7 +2963,7 @@ dependencies = [
  "lazy_static",
  "percent-encoding",
  "pin-project",
- "rand 0.8.5",
+ "rand",
  "thiserror",
  "tokio",
  "tokio-stream",
@@ -3083,7 +2984,7 @@ dependencies = [
  "lazy_static",
  "percent-encoding",
  "pin-project",
- "rand 0.8.5",
+ "rand",
  "thiserror",
  "tokio",
  "tokio-stream",
@@ -3297,7 +3198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
 dependencies = [
  "base64ct",
- "rand_core 0.6.3",
+ "rand_core",
  "subtle",
 ]
 
@@ -3633,8 +3534,8 @@ dependencies = [
  "lazy_static",
  "num-traits",
  "quick-error 2.0.1",
- "rand 0.8.5",
- "rand_chacha 0.3.1",
+ "rand",
+ "rand_chacha",
  "rand_xorshift",
  "regex-syntax",
  "rusty-fork",
@@ -3669,7 +3570,7 @@ checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5"
 dependencies = [
  "bytes",
  "heck 0.3.3",
- "itertools 0.10.3",
+ "itertools",
  "lazy_static",
  "log",
  "multimap",
@@ -3691,7 +3592,7 @@ dependencies = [
  "cfg-if",
  "cmake",
  "heck 0.4.0",
- "itertools 0.10.3",
+ "itertools",
  "lazy_static",
  "log",
  "multimap",
@@ -3710,7 +3611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
 dependencies = [
  "anyhow",
- "itertools 0.10.3",
+ "itertools",
  "proc-macro2",
  "quote",
  "syn",
@@ -3723,7 +3624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc"
 dependencies = [
  "anyhow",
- "itertools 0.10.3",
+ "itertools",
  "proc-macro2",
  "quote",
  "syn",
@@ -3814,7 +3715,7 @@ checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
 dependencies = [
  "env_logger 0.8.4",
  "log",
- "rand 0.8.5",
+ "rand",
 ]
 
 [[package]]
@@ -3837,19 +3738,6 @@ dependencies = [
  "proc-macro2",
 ]
 
-[[package]]
-name = "rand"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-dependencies = [
- "getrandom 0.1.16",
- "libc",
- "rand_chacha 0.2.2",
- "rand_core 0.5.1",
- "rand_hc",
-]
-
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -3857,18 +3745,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.3",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.5.1",
+ "rand_chacha",
+ "rand_core",
 ]
 
 [[package]]
@@ -3878,16 +3756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core 0.6.3",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-dependencies = [
- "getrandom 0.1.16",
+ "rand_core",
 ]
 
 [[package]]
@@ -3896,16 +3765,7 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
 dependencies = [
- "getrandom 0.2.7",
-]
-
-[[package]]
-name = "rand_hc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-dependencies = [
- "rand_core 0.5.1",
+ "getrandom",
 ]
 
 [[package]]
@@ -3914,7 +3774,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
 dependencies = [
- "rand_core 0.6.3",
+ "rand_core",
 ]
 
 [[package]]
@@ -4008,7 +3868,7 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
 dependencies = [
- "getrandom 0.2.7",
+ "getrandom",
  "redox_syscall",
  "thiserror",
 ]
@@ -4131,7 +3991,7 @@ dependencies = [
  "num-traits",
  "pkcs1",
  "pkcs8",
- "rand 0.8.5",
+ "rand",
  "subtle",
  "zeroize",
 ]
@@ -4241,12 +4101,13 @@ dependencies = [
  "clap 3.2.13",
  "client-executor",
  "futures",
+ "grapl-utils",
  "proptest",
  "prost 0.10.4",
  "prost-build 0.10.4",
  "quickcheck",
  "quickcheck_macros",
- "rand 0.8.5",
+ "rand",
  "rustc-hash",
  "test-context",
  "thiserror",
@@ -4420,9 +4281,9 @@ dependencies = [
 
 [[package]]
 name = "scylla"
-version = "0.4.7"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16bd82cb3eb8961f45759695eee56f162e73c1e2e30042d9450dfa98d600ac1"
+checksum = "eed40c02a5c835d894044bbd6f454ef54b6937ec8af23efe3cdeaa57005bb00d"
 dependencies = [
  "arc-swap",
  "bigdecimal",
@@ -4432,11 +4293,12 @@ dependencies = [
  "dashmap",
  "futures",
  "histogram",
- "itertools 0.10.3",
+ "itertools",
  "lz4_flex",
  "num-bigint 0.3.3",
  "num_enum",
- "rand 0.8.5",
+ "rand",
+ "scylla-cql",
  "scylla-macros",
  "smallvec",
  "snap",
@@ -4448,6 +4310,26 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "scylla-cql"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e0ac8202660385589dba16123cc5916cf4eb3078e2dac0ab6af6903d7f5467"
+dependencies = [
+ "bigdecimal",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "lz4_flex",
+ "num-bigint 0.3.3",
+ "num_enum",
+ "scylla-macros",
+ "snap",
+ "thiserror",
+ "tokio",
+ "uuid",
+]
+
 [[package]]
 name = "scylla-macros"
 version = "0.1.1"
@@ -4742,7 +4624,7 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4"
 dependencies = [
- "itertools 0.10.3",
+ "itertools",
  "nom",
  "unicode_categories",
 ]
@@ -4792,7 +4674,7 @@ dependencies = [
  "once_cell",
  "paste",
  "percent-encoding",
- "rand 0.8.5",
+ "rand",
  "rustls 0.20.6",
  "rustls-pemfile",
  "serde",
@@ -4861,12 +4743,6 @@ dependencies = [
  "unicode-normalization",
 ]
 
-[[package]]
-name = "strsim"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
-
 [[package]]
 name = "strsim"
 version = "0.10.0"
@@ -5213,7 +5089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f"
 dependencies = [
  "pin-project",
- "rand 0.8.5",
+ "rand",
  "tokio",
 ]
 
@@ -5309,7 +5185,6 @@ dependencies = [
  "prost 0.9.0",
  "prost-derive 0.9.0",
  "tokio",
- "tokio-rustls 0.22.0",
  "tokio-stream",
  "tokio-util 0.6.10",
  "tower",
@@ -5405,7 +5280,7 @@ dependencies = [
  "indexmap",
  "pin-project",
  "pin-project-lite",
- "rand 0.8.5",
+ "rand",
  "slab",
  "tokio",
  "tokio-util 0.7.3",
@@ -5602,7 +5477,7 @@ dependencies = [
  "ipnet",
  "lazy_static",
  "log",
- "rand 0.8.5",
+ "rand",
  "smallvec",
  "thiserror",
  "tinyvec",
@@ -5675,11 +5550,11 @@ dependencies = [
  "futures",
  "grapl-config",
  "grapl-tracing",
- "itertools 0.10.3",
+ "itertools",
  "opentelemetry 0.16.0",
  "opentelemetry-jaeger 0.15.0",
  "prost 0.9.0",
- "rand 0.8.5",
+ "rand",
  "rust-proto",
  "serde",
  "sqlx",
@@ -5795,7 +5670,7 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
 dependencies = [
- "getrandom 0.2.7",
+ "getrandom",
  "serde",
 ]
 
@@ -5859,12 +5734,6 @@ dependencies = [
  "try-lock",
 ]
 
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
 [[package]]
 name = "wasi"
 version = "0.10.2+wasi-snapshot-preview1"
diff --git a/src/rust/analyzer-dispatcher/src/main.rs b/src/rust/analyzer-dispatcher/src/main.rs
index dcf38ea9d7..9fef676ea4 100644
--- a/src/rust/analyzer-dispatcher/src/main.rs
+++ b/src/rust/analyzer-dispatcher/src/main.rs
@@ -29,11 +29,11 @@ use rust_proto::{
     },
     graplinc::grapl::{
         api::{
-            graph::v1beta1::MergedGraph,
             plugin_registry::v1beta1::{
                 GetAnalyzersForTenantRequest,
                 PluginRegistryServiceClient,
             },
+            plugin_sdk::analyzers::v1beta1::messages::Update,
             plugin_work_queue::v1beta1::{
                 ExecutionJob,
                 PluginWorkQueueServiceClient,
@@ -127,8 +127,8 @@ enum AnalyzerDispatcherError {
 
 struct AnalyzerDispatcher {
     plugin_work_queue_client: PluginWorkQueueServiceClient,
-    merged_graphs_consumer: Consumer<MergedGraph>,
-    merged_graphs_retry_producer: RetryProducer<MergedGraph>,
+    graph_update_consumer: Consumer<Update>,
+    graph_update_retry_producer: RetryProducer<Update>,
     analyzer_ids_cache: AsyncCache<Uuid, Vec<Uuid>>,
 }
 
@@ -137,8 +137,8 @@ impl AnalyzerDispatcher {
         config: AnalyzerDispatcherConfig,
         plugin_work_queue_client: PluginWorkQueueServiceClient,
     ) -> Result<Self, ConfigurationError> {
-        let merged_graphs_consumer: Consumer<MergedGraph> = Consumer::new(config.kafka_config)?;
-        let merged_graphs_retry_producer: RetryProducer<MergedGraph> =
+        let graph_update_consumer: Consumer<Update> = Consumer::new(config.kafka_config)?;
+        let graph_update_retry_producer: RetryProducer<Update> =
             RetryProducer::new(config.kafka_retry_producer_config)?;
         let client_config = PluginRegistryClientConfig::parse();
 
@@ -188,8 +188,8 @@ impl AnalyzerDispatcher {
 
         Ok(Self {
             plugin_work_queue_client,
-            merged_graphs_consumer,
-            merged_graphs_retry_producer,
+            graph_update_consumer,
+            graph_update_retry_producer,
             analyzer_ids_cache,
         })
     }
@@ -197,23 +197,23 @@ impl AnalyzerDispatcher {
     pub async fn run(&mut self, pool_size: usize) -> Result<(), AnalyzerDispatcherError> {
         let analyzer_ids_cache = self.analyzer_ids_cache.clone();
         let plugin_work_queue_client = self.plugin_work_queue_client.clone();
-        let merged_graphs_retry_producer = self.merged_graphs_retry_producer.clone();
+        let graph_update_retry_producer = self.graph_update_retry_producer.clone();
 
         loop {
             let analyzer_ids_cache = analyzer_ids_cache.clone();
             let plugin_work_queue_client = plugin_work_queue_client.clone();
-            let merged_graphs_retry_producer = merged_graphs_retry_producer.clone();
+            let graph_update_retry_producer = graph_update_retry_producer.clone();
 
-            let stream = self.merged_graphs_consumer
+            let stream = self.graph_update_consumer
                 .stream()
                 .take(pool_size)
-                .then(move |merged_graphs_result: Result<(tracing::Span, Envelope<MergedGraph>), ConsumerError>| {
+                .then(move |graph_update_result: Result<(tracing::Span, Envelope<Update>), ConsumerError>| {
                     let analyzer_ids_cache = analyzer_ids_cache.clone();
                     let plugin_work_queue_client = plugin_work_queue_client.clone();
-                    let merged_graphs_retry_producer = merged_graphs_retry_producer.clone();
+                    let graph_update_retry_producer = graph_update_retry_producer.clone();
 
                     async move {
-                        match merged_graphs_result {
+                        match graph_update_result {
                             Ok((span, envelope)) => {
                                 match analyzer_ids_cache
                                     .clone()
@@ -250,7 +250,7 @@ impl AnalyzerDispatcher {
                                         drop(_guard);
 
                                         retry_message(
-                                            &merged_graphs_retry_producer,
+                                            &graph_update_retry_producer,
                                             envelope,
                                         )
                                             .instrument(span)
@@ -269,7 +269,7 @@ impl AnalyzerDispatcher {
                                         drop(_guard);
 
                                         retry_message(
-                                            &merged_graphs_retry_producer,
+                                            &graph_update_retry_producer,
                                             envelope,
                                         )
                                             .instrument(span)
@@ -295,7 +295,7 @@ impl AnalyzerDispatcher {
                     }
                 })
                 .then(|result| async {
-                    self.merged_graphs_consumer.commit()?;
+                    self.graph_update_consumer.commit()?;
                     result
                 });
 
@@ -315,18 +315,18 @@ impl AnalyzerDispatcher {
 }
 
 async fn retry_message(
-    merged_graphs_retry_producer: &RetryProducer<MergedGraph>,
-    envelope: Envelope<MergedGraph>,
+    graph_update_retry_producer: &RetryProducer<Update>,
+    envelope: Envelope<Update>,
 ) -> Result<(), ProducerError> {
     // TODO: be a little smarter about handling ProducerError here
-    merged_graphs_retry_producer.send(envelope).await
+    graph_update_retry_producer.send(envelope).await
 }
 
 #[tracing::instrument(skip(plugin_work_queue_client, analyzer_ids, envelope), err)]
 async fn enqueue_plugin_work(
     plugin_work_queue_client: PluginWorkQueueServiceClient,
     analyzer_ids: Vec<Uuid>,
-    envelope: Envelope<MergedGraph>,
+    envelope: Envelope<Update>,
 ) -> Result<(), AnalyzerDispatcherError> {
     let pool_size = analyzer_ids.len();
     let tenant_id = envelope.tenant_id();
diff --git a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs
index bfc58c6930..01757789d1 100644
--- a/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs
+++ b/src/rust/generators/osquery-generator/src/parsers/grapl_pack/processes.rs
@@ -72,7 +72,7 @@ impl From<ProcessEvent> for GraphDescription {
                 .with_last_seen_timestamp(process_start_time);
 
             graph.add_edge(
-                "bin_file",
+                "binary_file",
                 child.clone_node_key(),
                 child_exe.clone_node_key(),
             );
diff --git a/src/rust/generators/sysmon-generator/src/models/process/create.rs b/src/rust/generators/sysmon-generator/src/models/process/create.rs
index 9d5a91fa4b..b213d5287e 100644
--- a/src/rust/generators/sysmon-generator/src/models/process/create.rs
+++ b/src/rust/generators/sysmon-generator/src/models/process/create.rs
@@ -78,7 +78,7 @@ pub(crate) fn generate_process_create_subgraph(
     );
 
     graph.add_edge(
-        "bin_file",
+        "binary_file",
         child.clone_node_key(),
         child_exe.clone_node_key(),
     );
diff --git a/src/rust/graph-merger/Cargo.toml b/src/rust/graph-merger/Cargo.toml
index 08674fbd1a..bf7406dc75 100644
--- a/src/rust/graph-merger/Cargo.toml
+++ b/src/rust/graph-merger/Cargo.toml
@@ -16,12 +16,9 @@ path = "src/main.rs"
 integration_tests = []
 
 [dependencies]
-anyhow = "1.0"
 async-trait = "0.1"
 bytes = "1.1"
 clap = { version = "3.0", default_features = false, features = ["std", "env"] }
-dgraph-tonic = { version = "0.10", features = ["dgraph-1-1"] }
-dgraph_query_lib = "0.1"
 futures = "0.3"
 futures-retry = "0.6"
 grapl-config = { path = "../grapl-config", version = "*" }
@@ -33,10 +30,10 @@ lru = "0.7"
 rusoto_core = { version = "0.47", default_features = false, features = [
   "rustls"
 ] }
-rusoto_credential = "0.47"
 rusoto_dynamodb = { version = "0.47", default_features = false, features = [
   "rustls"
 ] }
+rusoto_credential = "0.47"
 rust-proto = { path = "../rust-proto", version = "*" }
 serde_json = "1.0"
 thiserror = "1.0"
@@ -49,9 +46,9 @@ tokio = { version = "1.17", features = [
 ] }
 tracing = "0.1"
 uuid = { version = "1.0", features = ["v4"] }
+eyre = "0.6.8"
 
 [dev-dependencies]
 e2e-tests = { path = "../e2e-tests" }
-eyre = "0.6"
 kafka = { path = "../kafka", features = ["test-utils"] }
 test-context = "0.1"
diff --git a/src/rust/graph-merger/src/config.rs b/src/rust/graph-merger/src/config.rs
new file mode 100644
index 0000000000..da074690be
--- /dev/null
+++ b/src/rust/graph-merger/src/config.rs
@@ -0,0 +1,7 @@
+use rust_proto::client_factory::services::GraphMutationClientConfig;
+
+#[derive(clap::Parser, Debug)]
+pub struct GraphMergerConfig {
+    #[clap(flatten)]
+    pub graph_mutation_client_config: GraphMutationClientConfig,
+}
diff --git a/src/rust/graph-merger/src/lib.rs b/src/rust/graph-merger/src/lib.rs
index f7ac75bb90..11bd8fe24b 100644
--- a/src/rust/graph-merger/src/lib.rs
+++ b/src/rust/graph-merger/src/lib.rs
@@ -1,4 +1,2 @@
-pub mod reverse_resolver;
+pub mod config;
 pub mod service;
-pub mod upsert_util;
-pub mod upserter;
diff --git a/src/rust/graph-merger/src/main.rs b/src/rust/graph-merger/src/main.rs
index c93cf0599e..bf7293b6de 100644
--- a/src/rust/graph-merger/src/main.rs
+++ b/src/rust/graph-merger/src/main.rs
@@ -1,12 +1,8 @@
-use std::sync::Arc;
-
 use clap::Parser;
-use dgraph_tonic::Client as DgraphClient;
 use futures::{
     FutureExt,
     StreamExt,
 };
-use grapl_config::env_helpers::FromEnv;
 use grapl_tracing::setup_tracing;
 use kafka::{
     config::{
@@ -14,69 +10,52 @@ use kafka::{
         ProducerConfig,
     },
     StreamProcessor,
-    StreamProcessorError,
 };
-use rusoto_dynamodb::DynamoDbClient;
-use rust_proto::graplinc::grapl::{
-    api::graph::v1beta1::{
-        IdentifiedGraph,
-        MergedGraph,
+use rust_proto::{
+    graplinc::grapl::api::{
+        graph::v1beta1::IdentifiedGraph,
+        graph_mutation::v1beta1::client::GraphMutationClient,
+        plugin_sdk::analyzers::v1beta1::messages::Update,
     },
-    pipeline::v1beta1::Envelope,
-};
-use tokio::sync::Mutex;
-use tracing::{
-    instrument::WithSubscriber,
-    Instrument,
+    protocol::service_client::ConnectWithConfig,
 };
+use tracing::instrument::WithSubscriber;
 
 use crate::{
-    reverse_resolver::ReverseEdgeResolver,
+    config::GraphMergerConfig,
     service::{
         GraphMerger,
         GraphMergerError,
     },
 };
 
-pub mod reverse_resolver;
+pub mod config;
 pub mod service;
-pub mod upsert_util;
-pub mod upserter;
 
 const SERVICE_NAME: &'static str = "graph-merger";
 
 #[tokio::main]
-async fn main() -> Result<(), GraphMergerError> {
+async fn main() -> eyre::Result<()> {
     let _guard = setup_tracing(SERVICE_NAME)?;
 
-    let mg_alphas = grapl_config::mg_alphas();
-    let dynamo = DynamoDbClient::from_env();
-    let reverse_edge_resolver = ReverseEdgeResolver::new(dynamo, 1000);
-
-    tracing::debug!(
-        mg_alphas =? &mg_alphas,
-        message = "Connecting to mg_alphas"
-    );
-
-    let graph_merger = GraphMerger::new(DgraphClient::new(mg_alphas)?, reverse_edge_resolver);
+    let service_config = GraphMergerConfig::parse();
+    let graph_mutation_client =
+        GraphMutationClient::connect_with_config(service_config.graph_mutation_client_config)
+            .await?;
+    let graph_merger = GraphMerger::new(graph_mutation_client);
 
     let consumer_config = ConsumerConfig::parse();
     let producer_config = ProducerConfig::parse();
 
-    handler(
-        Arc::new(Mutex::new(graph_merger)),
-        consumer_config,
-        producer_config,
-    )
-    .await
+    handler(graph_merger, consumer_config, producer_config).await
 }
 
-#[tracing::instrument(skip(graph_merger))]
+#[tracing::instrument(skip(graph_merger, consumer_config, producer_config))]
 async fn handler(
-    graph_merger: Arc<Mutex<GraphMerger>>,
+    graph_merger: GraphMerger,
     consumer_config: ConsumerConfig,
     producer_config: ProducerConfig,
-) -> Result<(), GraphMergerError> {
+) -> eyre::Result<()> {
     tracing::info!(
         message = "configuring kafka stream processor",
         bootstrap_servers = %consumer_config.bootstrap_servers,
@@ -87,79 +66,17 @@ async fn handler(
 
     // TODO: also construct a stream processor for retries
 
-    let stream_processor: StreamProcessor<IdentifiedGraph, MergedGraph> =
+    let stream_processor: StreamProcessor<IdentifiedGraph, Update> =
         StreamProcessor::new(consumer_config, producer_config)?;
 
     tracing::info!(message = "kafka stream processor configured successfully",);
 
-    let stream = stream_processor.stream::<_, _, StreamProcessorError>(move |event| {
-        let graph_merger = graph_merger.clone();
-        async move {
-            let (span, envelope) = event?;
-            let handler_span = span.clone();
-            let _guard = span.enter();
-            let tenant_id = envelope.tenant_id();
-            let trace_id = envelope.trace_id();
-            let event_source_id = envelope.event_source_id();
-
-            tracing::debug!("received kafka message");
-
-            match graph_merger
-                .lock()
-                .await
-                .handle_event(envelope.inner_message())
-                .instrument(handler_span)
-                .await
-            {
-                Ok(merged_graph) => Ok(Some(Envelope::new(
-                    tenant_id,
-                    trace_id,
-                    event_source_id,
-                    merged_graph,
-                ))),
-                Err(e) => match e {
-                    Ok((_, e)) => {
-                        match e {
-                            GraphMergerError::Unexpected(ref reason) => {
-                                tracing::error!(
-                                    message = "unexpected error",
-                                    reason = %reason,
-                                    error = %e,
-                                );
-                                // TODO: write message to failed topic here
-                                Err(StreamProcessorError::from(e))
-                            }
-                            _ => {
-                                tracing::error!(
-                                    mesage = "unexpected error",
-                                    error = %e,
-                                );
-                                // TODO: write message to failed topic here
-                                Err(StreamProcessorError::from(e))
-                            }
-                        }
-                    }
-                    Err(e) => match e {
-                        GraphMergerError::Unexpected(reason) => {
-                            tracing::warn!(
-                                message = "unexpected error",
-                                reason = %reason,
-                            );
-                            Ok(None)
-                        }
-                        _ => {
-                            tracing::error!(
-                                message = "unknown error",
-                                error = %e,
-                            );
-                            Err(StreamProcessorError::from(e))
-                        }
-                    },
-                },
-            }
-        }
-        .into_stream()
-        .filter_map(|res| async move { res.transpose() })
+    let stream = stream_processor.stream::<_, _, GraphMergerError>(move |event| {
+        graph_merger
+            .clone()
+            .handle_event(event)
+            .into_stream()
+            .flat_map(|results| futures::stream::iter(results))
     });
 
     stream
diff --git a/src/rust/graph-merger/src/reverse_resolver.rs b/src/rust/graph-merger/src/reverse_resolver.rs
deleted file mode 100644
index 50605636c1..0000000000
--- a/src/rust/graph-merger/src/reverse_resolver.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-use std::collections::HashMap;
-
-use grapl_utils::{
-    future_ext::GraplFutureExt,
-    rusoto_ext::dynamodb::GraplDynamoDbClientExt,
-};
-use lazy_static::lazy_static;
-use rusoto_dynamodb::{
-    AttributeValue,
-    BatchGetItemInput,
-    DynamoDbClient,
-    KeysAndAttributes,
-};
-use rust_proto::graplinc::grapl::api::graph::v1beta1::Edge;
-
-use crate::service::GraphMergerError;
-
-lazy_static! {
-    /// timeout for dynamodb queries
-    static ref DYNAMODB_QUERY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
-}
-
-#[derive(Clone)]
-pub struct ReverseEdgeResolver {
-    dynamo: DynamoDbClient,
-    r_edge_cache: std::sync::Arc<std::sync::Mutex<lru::LruCache<String, String>>>,
-}
-
-impl ReverseEdgeResolver {
-    pub fn new(dynamo: DynamoDbClient, cache_size: usize) -> Self {
-        let r_edge_cache = lru::LruCache::new(cache_size);
-        let r_edge_cache = std::sync::Arc::new(std::sync::Mutex::new(r_edge_cache));
-        Self {
-            dynamo,
-            r_edge_cache,
-        }
-    }
-
-    pub async fn resolve_reverse_edges(
-        &self,
-        edges: Vec<Edge>,
-    ) -> Result<Vec<Edge>, GraphMergerError> {
-        if edges.is_empty() {
-            return Ok(vec![]);
-        }
-        let (mut reversed, remaining) = self.resolve_reverse_edges_from_cache(edges).await;
-        if remaining.is_empty() {
-            return Ok(reversed);
-        }
-        let mut edge_names: Vec<&String> = remaining.iter().map(|e| &e.edge_name).collect();
-        edge_names.sort_unstable();
-        edge_names.dedup();
-
-        let resolved = get_r_edges_from_dynamodb(&self.dynamo, &edge_names).await?;
-
-        let cache = self.r_edge_cache.clone();
-        let mut cache = cache.lock().unwrap();
-        for f_edge in remaining.into_iter() {
-            if let Some(Some(r_edge_name)) = resolved.get(&f_edge.edge_name) {
-                cache.put(f_edge.edge_name.clone(), r_edge_name.clone());
-                cache.put(r_edge_name.clone(), f_edge.edge_name.clone());
-                reversed.push(reverse_edge(&f_edge, r_edge_name.to_owned()));
-            }
-        }
-        reversed.sort_unstable();
-        reversed.dedup();
-        Ok(reversed)
-    }
-
-    pub async fn resolve_reverse_edges_from_cache(
-        &self,
-        edges: Vec<Edge>,
-    ) -> (Vec<Edge>, Vec<Edge>) {
-        let mut reversed = vec![];
-        let mut remaining = vec![];
-        let cache = self.r_edge_cache.clone();
-        let mut cache = cache.lock().unwrap();
-        for edge in edges.into_iter() {
-            match cache.get(&edge.edge_name).map(String::from) {
-                Some(r_edge_name) => {
-                    reversed.push(reverse_edge(&edge, r_edge_name));
-                }
-                None => {
-                    remaining.push(edge);
-                }
-            }
-        }
-        drop(cache);
-
-        (reversed, remaining)
-    }
-}
-
-fn reverse_edge(edge: &Edge, reverse_edge_name: String) -> Edge {
-    Edge {
-        from_node_key: edge.to_node_key.to_owned(),
-        to_node_key: edge.from_node_key.to_owned(),
-        edge_name: reverse_edge_name,
-    }
-}
-
-/// Returns a HashMap of f_edge -> Optional r_edge entries from dynamodb
-pub async fn get_r_edges_from_dynamodb(
-    client: &DynamoDbClient,
-    f_edges: &[&String],
-) -> Result<HashMap<String, Option<String>>, GraphMergerError> {
-    let schema_table_name = std::env::var("GRAPL_SCHEMA_TABLE").expect("GRAPL_SCHEMA_TABLE");
-
-    let keys_and_attributes = make_keys(f_edges);
-    tracing::debug!(
-        message="Querying dynamodb for reverse edges",
-        edge_count=?keys_and_attributes.keys.len(),
-    );
-    let mut request_items = HashMap::with_capacity(1);
-    request_items.insert(schema_table_name.clone(), keys_and_attributes);
-
-    let query = BatchGetItemInput {
-        request_items,
-        return_consumed_capacity: None,
-    };
-
-    /*
-       1. Map timeout error
-       2. Map Rusoto error
-       3. Grab responses (HashMap<String, Vec<HashMap<String, AttributeValue>>>) (or return error)
-       4. Pull the entire set of responses out for the schema table (Vec<HashMap<String, AttributeValue>>) (or return error)
-    */
-    let schema_table_response: Vec<HashMap<String, AttributeValue>> = client
-        .batch_get_item_reliably(query)
-        .timeout(*DYNAMODB_QUERY_TIMEOUT)
-        .await
-        .map_err(|e| GraphMergerError::Unexpected(e.to_string()))?
-        .map_err(|e| GraphMergerError::Unexpected(e.to_string()))?
-        .responses
-        .ok_or(GraphMergerError::Unexpected(
-            "Failed to fetch results from dynamodb".to_string(),
-        ))?
-        .remove(&schema_table_name)
-        .ok_or(GraphMergerError::Unexpected(
-            "Missing data from expected table in dynamodb".to_string(),
-        ))?;
-
-    /*
-       1. Remove entries without f_edge and r_edge
-       2. Grab both f_edge and r_edge properties as strings (`.s`)
-       3. If both are present, return; otherwise error and filter entry out
-    */
-    Ok(schema_table_response
-        .into_iter()
-        .filter(|hashmap| hashmap.contains_key("f_edge") && hashmap.contains_key("r_edge"))
-        .filter_map(|hashmap| {
-            match (
-                hashmap.get("f_edge").map(|item| item.s.clone()),
-                hashmap.get("r_edge").map(|item| item.s.clone()),
-            ) {
-                (Some(Some(f_edge)), Some(r_edge)) => Some((f_edge, r_edge)),
-                (Some(Some(f_edge)), _) => {
-                    tracing::error!(
-                        message="Missing r_edge for f_edge in dynamodb schema.",
-                        f_edge=?f_edge,
-                    );
-                    None
-                }
-                (None, Some(Some(r_edge))) => {
-                    tracing::error!(
-                        message="Failed to associate retrieved r_edge with an f_edge",
-                        r_edge=?r_edge,
-                    );
-                    None
-                }
-                _ => None,
-            }
-        })
-        .collect())
-}
-
-fn make_keys(f_edges: &[&String]) -> KeysAndAttributes {
-    let keys: Vec<HashMap<String, AttributeValue>> = f_edges
-        .iter()
-        .map(|f_edge| {
-            let mut key_map = HashMap::new();
-            key_map.insert(
-                "f_edge".to_string(),
-                AttributeValue {
-                    s: Some(f_edge.to_string()),
-                    ..Default::default()
-                },
-            );
-
-            key_map
-        })
-        .collect();
-
-    KeysAndAttributes {
-        consistent_read: Some(true),
-        keys,
-        ..Default::default()
-    }
-}
diff --git a/src/rust/graph-merger/src/service.rs b/src/rust/graph-merger/src/service.rs
index 781d166686..2d87560578 100644
--- a/src/rust/graph-merger/src/service.rs
+++ b/src/rust/graph-merger/src/service.rs
@@ -1,22 +1,43 @@
 use std::{
+    collections::HashMap,
     fmt::Debug,
-    sync::Arc,
-    time::{
-        SystemTime,
-        UNIX_EPOCH,
-    },
 };
 
-use dgraph_tonic::Client as DgraphClient;
 use grapl_tracing::SetupTracingError;
-use rust_proto::graplinc::grapl::api::graph::v1beta1::{
-    IdentifiedGraph,
-    MergedGraph,
-};
-
-use crate::{
-    reverse_resolver::ReverseEdgeResolver,
-    upserter,
+use kafka::StreamProcessorError;
+use rust_proto::graplinc::grapl::{
+    api::{
+        graph::v1beta1::{
+            IdentifiedEdge,
+            IdentifiedGraph,
+            Property,
+        },
+        graph_mutation::v1beta1::{
+            client::{
+                GraphMutationClient,
+                GraphMutationClientError,
+            },
+            messages::{
+                CreateEdgeRequest,
+                MutationRedundancy,
+                SetNodePropertyRequest,
+            },
+        },
+        plugin_sdk::analyzers::v1beta1::messages::{
+            EdgeUpdate,
+            Int64PropertyUpdate,
+            StringPropertyUpdate,
+            UInt64PropertyUpdate,
+            Update,
+        },
+    },
+    common::v1beta1::types::{
+        EdgeName,
+        NodeType,
+        PropertyName,
+        Uid,
+    },
+    pipeline::v1beta1::Envelope,
 };
 
 #[non_exhaustive]
@@ -37,8 +58,8 @@ pub enum GraphMergerError {
     #[error("failed to configure tracing {0}")]
     SetupTracingError(#[from] SetupTracingError),
 
-    #[error("anyhow error {0}")]
-    AnyhowError(#[from] anyhow::Error),
+    #[error("GraphMutationClientError {0}")]
+    GraphMutationClientError(#[from] GraphMutationClientError),
 }
 
 impl From<GraphMergerError> for kafka::StreamProcessorError {
@@ -55,75 +76,171 @@ impl From<&GraphMergerError> for kafka::StreamProcessorError {
 
 #[derive(Clone)]
 pub struct GraphMerger {
-    mg_client: Arc<DgraphClient>,
-    reverse_edge_resolver: ReverseEdgeResolver,
+    graph_mutation_client: GraphMutationClient,
 }
 
 impl GraphMerger {
-    pub fn new(mg_client: DgraphClient, reverse_edge_resolver: ReverseEdgeResolver) -> Self {
+    pub fn new(graph_mutation_client: GraphMutationClient) -> Self {
         Self {
-            mg_client: Arc::new(mg_client),
-            reverse_edge_resolver,
+            graph_mutation_client,
         }
     }
 
-    #[tracing::instrument(skip(self, subgraph))]
+    #[tracing::instrument(skip(self, event))]
     pub async fn handle_event(
-        &mut self,
-        subgraph: IdentifiedGraph,
-    ) -> Result<MergedGraph, Result<(MergedGraph, GraphMergerError), GraphMergerError>> {
-        if subgraph.is_empty() {
-            tracing::warn!("Attempted to merge empty subgraph. Short circuiting.");
-            return Ok(MergedGraph::default());
-        }
-
-        tracing::info!(
-            message = "handling new subgraph",
-            nodes =? subgraph.nodes.len(),
-            edges =? subgraph.edges.len(),
-        );
-
-        let uncached_nodes = subgraph.nodes.into_iter().map(|(_, n)| n);
-        let mut uncached_edges: Vec<_> =
-            subgraph.edges.into_iter().flat_map(|e| e.1.edges).collect();
-        let reverse = self
-            .reverse_edge_resolver
-            .resolve_reverse_edges(uncached_edges.clone())
-            .await
-            .map_err(Err)?;
-
-        uncached_edges.extend_from_slice(&reverse[..]);
-
-        let mut merged_graph = MergedGraph::new();
-        let mut uncached_subgraph = IdentifiedGraph::new();
-
-        for node in uncached_nodes {
-            uncached_subgraph.add_node(node);
+        mut self,
+        event: Result<(tracing::Span, Envelope<IdentifiedGraph>), StreamProcessorError>,
+    ) -> Vec<Result<Envelope<Update>, GraphMergerError>> {
+        match event {
+            Ok((span, envelope)) => {
+                let _guard = span.enter();
+                let tenant_id = envelope.tenant_id();
+                let trace_id = envelope.trace_id();
+                let event_source_id = envelope.event_source_id();
+                let subgraph = envelope.inner_message();
+
+                tracing::debug!("received kafka message");
+
+                if subgraph.is_empty() {
+                    tracing::warn!("Attempted to merge empty subgraph. Short circuiting.");
+                    return vec![];
+                }
+
+                tracing::info!(
+                    message = "handling new subgraph",
+                    nodes =? subgraph.nodes.len(),
+                    edges =? subgraph.edges.len(),
+                );
+
+                let mut updates = Vec::with_capacity(subgraph.nodes.len() + subgraph.edges.len());
+
+                let node_types: HashMap<Uid, String> = subgraph
+                    .nodes
+                    .iter()
+                    .map(|(uid, n)| (*uid, n.node_type.clone()))
+                    .collect();
+                let nodes = subgraph.nodes;
+                let edges = subgraph.edges;
+
+                for node in nodes.into_values() {
+                    for (prop_name, prop_value) in node.properties {
+                        let update =
+                            property_to_update(node.uid, prop_name.clone(), &prop_value.property);
+
+                        let response = self
+                            .graph_mutation_client
+                            .set_node_property(SetNodePropertyRequest {
+                                tenant_id,
+                                node_type: NodeType {
+                                    value: node.node_type.clone(),
+                                },
+                                uid: node.uid,
+                                property_name: PropertyName { value: prop_name },
+                                property: prop_value,
+                            })
+                            .await;
+
+                        match response {
+                            Ok(set_node_property_response) => {
+                                if let MutationRedundancy::True =
+                                    set_node_property_response.mutation_redundancy
+                                {
+                                    continue;
+                                } else {
+                                    updates.push(Ok(Envelope::new(
+                                        tenant_id,
+                                        trace_id,
+                                        event_source_id,
+                                        update,
+                                    )));
+                                }
+                            }
+                            Err(e) => updates.push(Err(e.into())),
+                        }
+                    }
+                }
+
+                for edge_list in edges.into_values() {
+                    for edge in edge_list.edges {
+                        let IdentifiedEdge {
+                            to_uid,
+                            from_uid,
+                            edge_name,
+                        } = edge;
+                        let response = self
+                            .graph_mutation_client
+                            .create_edge(CreateEdgeRequest {
+                                tenant_id,
+                                edge_name: EdgeName {
+                                    value: edge_name.clone(),
+                                },
+                                from_uid,
+                                to_uid,
+                                source_node_type: NodeType {
+                                    value: node_types[&from_uid].clone(),
+                                },
+                            })
+                            .await;
+
+                        match response {
+                            Ok(create_edge_response) => {
+                                if let MutationRedundancy::True =
+                                    create_edge_response.mutation_redundancy
+                                {
+                                    continue;
+                                } else {
+                                    updates.push(Ok(Envelope::new(
+                                        tenant_id,
+                                        trace_id,
+                                        event_source_id,
+                                        Update::Edge(EdgeUpdate {
+                                            src_uid: from_uid,
+                                            dst_uid: to_uid,
+                                            forward_edge_name: EdgeName {
+                                                value: edge_name.clone(),
+                                            },
+                                            reverse_edge_name: EdgeName {
+                                                value: edge_name.clone(),
+                                            },
+                                        }),
+                                    )));
+                                }
+                            }
+                            Err(e) => updates.push(Err(e.into())),
+                        }
+                    }
+                }
+
+                updates
+            }
+            Err(e) => vec![Err(e.into())],
         }
-
-        for edge in uncached_edges {
-            uncached_subgraph.add_edge(edge.edge_name, edge.from_node_key, edge.to_node_key);
-        }
-
-        upserter::GraphMergeHelper {}
-            .upsert_into(
-                self.mg_client.clone(),
-                &uncached_subgraph,
-                &mut merged_graph,
-            )
-            .await;
-
-        Ok(merged_graph)
     }
 }
 
-pub fn time_based_key_fn(_event: &[u8]) -> String {
-    let cur_ms = match SystemTime::now().duration_since(UNIX_EPOCH) {
-        Ok(n) => n.as_millis(),
-        Err(_) => panic!("SystemTime before UNIX EPOCH!"),
-    };
-
-    let cur_day = cur_ms - (cur_ms % 86400);
-
-    format!("{}/{}-{}", cur_day, cur_ms, uuid::Uuid::new_v4())
+fn property_to_update(uid: Uid, property_name: String, property: &Property) -> Update {
+    match property {
+        Property::IncrementOnlyUintProp(_)
+        | Property::DecrementOnlyUintProp(_)
+        | Property::ImmutableUintProp(_) => Update::Uint64Property(UInt64PropertyUpdate {
+            uid,
+            property_name: PropertyName {
+                value: property_name,
+            },
+        }),
+        Property::IncrementOnlyIntProp(_)
+        | Property::DecrementOnlyIntProp(_)
+        | Property::ImmutableIntProp(_) => Update::Int64Property(Int64PropertyUpdate {
+            uid,
+            property_name: PropertyName {
+                value: property_name,
+            },
+        }),
+        Property::ImmutableStrProp(_) => Update::StringProperty(StringPropertyUpdate {
+            uid,
+            property_name: PropertyName {
+                value: property_name,
+            },
+        }),
+    }
 }
diff --git a/src/rust/graph-merger/src/upsert_util.rs b/src/rust/graph-merger/src/upsert_util.rs
deleted file mode 100644
index a5c035bed8..0000000000
--- a/src/rust/graph-merger/src/upsert_util.rs
+++ /dev/null
@@ -1,186 +0,0 @@
-use std::collections::HashMap;
-
-use rust_proto::graplinc::grapl::api::graph::v1beta1::{
-    NodeProperty,
-    Property,
-};
-
-pub struct Escaped(String);
-
-impl std::ops::Deref for Escaped {
-    type Target = str;
-
-    fn deref(&self) -> &Self::Target {
-        self.0.as_str()
-    }
-}
-
-impl std::fmt::Display for Escaped {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-fn escape_quote(s: &str) -> Escaped {
-    // otherwise we need to double quote it
-
-    let mut output = String::with_capacity(s.len());
-    output.push('"');
-
-    for c in s.chars() {
-        if c == '"' {
-            output += "\\\"";
-        } else if c == '\\' {
-            output += "\\\\";
-        } else {
-            output.push(c);
-        }
-    }
-
-    output.push('"');
-    Escaped(output)
-}
-
-fn escape_prop(node_property: &NodeProperty) -> Escaped {
-    match &node_property.property {
-        Property::IncrementOnlyIntProp(prop) => escape_quote(&prop.to_string()),
-        Property::DecrementOnlyIntProp(prop) => escape_quote(&prop.to_string()),
-        Property::ImmutableIntProp(prop) => escape_quote(&prop.to_string()),
-        Property::IncrementOnlyUintProp(prop) => escape_quote(&prop.to_string()),
-        Property::DecrementOnlyUintProp(prop) => escape_quote(&prop.to_string()),
-        Property::ImmutableUintProp(prop) => escape_quote(&prop.to_string()),
-        Property::ImmutableStrProp(prop) => escape_quote(prop.as_inner()),
-    }
-}
-
-#[tracing::instrument]
-pub(crate) fn build_upserts(
-    query_param: u128,
-    node_key: &str,
-    node_type: &str,
-    properties: &HashMap<String, NodeProperty>,
-    key_query_map: &mut HashMap<String, String>,
-) -> (String, Vec<dgraph_tonic::Mutation>) {
-    let _node_key = node_key.to_string();
-    let node_key = escape_quote(node_key);
-    let mut inner_queries = String::with_capacity(properties.len() * 32);
-    let mut mutations = Vec::with_capacity(properties.len());
-
-    let (creation_var_name, creation_query, creation_quad) =
-        node_creation_quads(query_param, &node_key, node_type);
-    key_query_map.insert(_node_key, creation_var_name.clone());
-
-    mutations.push(creation_quad);
-    inner_queries.push_str(&creation_query);
-    inner_queries.push('\n');
-    for (prop_name, prop) in properties.iter() {
-        if prop_name == "node_key" {
-            continue;
-        }
-        if prop_name == "dgraph.type" {
-            continue;
-        }
-        tracing::debug!(
-            message="generating upsert quads for predicate",
-            predicate_name=?prop_name,
-        );
-        let prop_value = escape_prop(prop);
-        let (next_query, muts) =
-            gen_node_property_upsert_quads(&creation_var_name, prop_name, &prop_value);
-        inner_queries.push_str(&next_query);
-        inner_queries.push('\n');
-        mutations.extend_from_slice(&muts[..]);
-    }
-
-    (inner_queries, mutations)
-}
-
-pub(crate) fn node_creation_quads(
-    query_param: u128,
-    node_key: &Escaped,
-    node_type: &str,
-) -> (String, String, dgraph_tonic::Mutation) {
-    let creation_var_name = format!("node_exists_{}", query_param);
-    let escaped_node_key = node_key;
-    let inner_query = format!(
-        r#"
-            {creation_var_name} as var(func: eq(node_key, {node_key}), first: 1) @cascade
-            q_{creation_var_name}(func: uid({creation_var_name}), first: 1) @cascade
-            {{
-                uid,
-                node_key,
-            }}
-    "#,
-        creation_var_name = creation_var_name,
-        node_key = escaped_node_key,
-    );
-
-    // If the node exists, do nothing, otherwise create it with its type
-    let mut mu_1 = dgraph_tonic::Mutation::new();
-    let mu_1_n_quads = format!(
-        concat!(
-            r#"_:{creation_var_name} <node_key> {node_key} ."#,
-            "\n",
-            r#"_:{creation_var_name} <dgraph.type> "{node_type}" ."#,
-        ),
-        node_key = escaped_node_key,
-        node_type = node_type,
-        creation_var_name = creation_var_name,
-    );
-
-    mu_1.set_set_nquads(mu_1_n_quads);
-    mu_1.set_cond(format!(
-        "@if(eq(len({creation_var_name}), 0))",
-        creation_var_name = creation_var_name
-    ));
-
-    (creation_var_name, inner_query, mu_1)
-}
-
-pub(crate) fn gen_node_property_upsert_quads(
-    creation_var_name: &str,
-    prop_name: &str,
-    prop_value: &Escaped,
-) -> (String, [dgraph_tonic::Mutation; 2]) {
-    // let mut node_query_name = format!("pred_query_{}_{}_{}", prop_name, query_param, predicate_param);
-    let mut mu_0 = dgraph_tonic::Mutation::new();
-
-    let inner_query = format!(
-        r#"
-            var(func: uid({creation_var_name}), first: 1)
-    "#,
-        creation_var_name = creation_var_name,
-    );
-
-    // If the node exists, set the predicate. Currently 'last write wins'.
-    let mu_0_n_quads = format!(
-        r#"uid({creation_var_name}) <{prop_name}> {prop_value} ."#,
-        creation_var_name = creation_var_name,
-        prop_name = prop_name,
-        prop_value = prop_value,
-    );
-
-    mu_0.set_set_nquads(mu_0_n_quads);
-    mu_0.set_cond(format!(
-        "@if(eq(len({creation_var_name}), 1))",
-        creation_var_name = creation_var_name
-    ));
-
-    let mut mu_1 = dgraph_tonic::Mutation::new();
-
-    // condition if the node does not exist
-    let mu_1_n_quads = format!(
-        concat!(r#"_:{creation_var_name} <{prop_name}> {prop_value} ."#,),
-        creation_var_name = creation_var_name,
-        prop_name = prop_name,
-        prop_value = prop_value,
-    );
-
-    mu_1.set_set_nquads(mu_1_n_quads);
-    mu_1.set_cond(format!(
-        "@if(eq(len({creation_var_name}), 0))",
-        creation_var_name = creation_var_name
-    ));
-
-    (inner_query, [mu_0, mu_1])
-}
diff --git a/src/rust/graph-merger/src/upserter.rs b/src/rust/graph-merger/src/upserter.rs
deleted file mode 100644
index 367ce73c99..0000000000
--- a/src/rust/graph-merger/src/upserter.rs
+++ /dev/null
@@ -1,418 +0,0 @@
-use std::{
-    collections::HashMap,
-    sync::Arc,
-};
-
-use dgraph_query_lib::{
-    condition::{
-        Condition,
-        ConditionValue,
-    },
-    mutation::{
-        MutationBuilder,
-        MutationPredicateValue,
-        MutationUID,
-        MutationUnit,
-    },
-    predicate::{
-        Field,
-        Predicate,
-    },
-    query::QueryBuilder,
-    queryblock::{
-        QueryBlock,
-        QueryBlockBuilder,
-        QueryBlockType,
-    },
-    ToQueryString,
-};
-use dgraph_tonic::{
-    Client as DgraphClient,
-    Mutate,
-    Query,
-};
-use futures::StreamExt;
-use futures_retry::{
-    FutureRetry,
-    RetryPolicy,
-};
-use grapl_utils::iter_ext::GraplIterExt;
-use rust_proto::graplinc::grapl::api::graph::v1beta1::{
-    Edge,
-    EdgeList,
-    IdentifiedGraph,
-    IdentifiedNode,
-    MergedGraph,
-    MergedNode,
-};
-
-use crate::upsert_util;
-
-const DGRAPH_CONCURRENCY_UPSERTS: usize = 8;
-// DGraph Live Loader uses a size of 1,000 elements and they claim this has relatively good performance
-const DGRAPH_UPSERT_CHUNK_SIZE: usize = 1024;
-
-pub struct GraphMergeHelper {}
-
-impl GraphMergeHelper {
-    pub async fn upsert_into(
-        &self,
-        dgraph_client: Arc<DgraphClient>,
-        identified_graph: &IdentifiedGraph,
-        merged_graph: &mut MergedGraph,
-    ) {
-        let node_key_map_to_uid = self
-            .upsert_nodes(dgraph_client.clone(), identified_graph, merged_graph)
-            .await;
-        self.upsert_edges(dgraph_client, identified_graph, node_key_map_to_uid)
-            .await;
-    }
-
-    #[tracing::instrument(skip(self, dgraph_client, identified_graph, merged_graph))]
-    async fn upsert_nodes(
-        &self,
-        dgraph_client: Arc<DgraphClient>,
-        identified_graph: &IdentifiedGraph,
-        merged_graph: &mut MergedGraph,
-    ) -> HashMap<String, u64> {
-        let mut key_to_query_name = HashMap::new();
-        let mut node_upserts = Vec::with_capacity(identified_graph.nodes.len());
-        for (unique_id, node) in identified_graph.nodes.values().enumerate() {
-            let (query, upserts) = upsert_util::build_upserts(
-                unique_id as u128,
-                &node.node_key,
-                &node.node_type,
-                &node.properties,
-                &mut key_to_query_name,
-            );
-            node_upserts.push((query, upserts));
-        }
-
-        tracing::info!(message = "Upserting nodes", count = node_upserts.len());
-        let responses: Vec<dgraph_tonic::Response> = futures::stream::iter(
-            node_upserts
-                .into_iter()
-                .chunks_owned(DGRAPH_UPSERT_CHUNK_SIZE),
-        )
-        .map(move |upsert_chunk| {
-            let mut combined_query = String::new();
-            let mut all_mutations = Vec::new();
-            for (query_block, mutations) in upsert_chunk.iter() {
-                combined_query.push_str(query_block);
-                all_mutations.extend_from_slice(mutations);
-            }
-
-            let combined_query = format!(
-                r"
-            {{
-                    {}
-            }}
-            ",
-                combined_query
-            );
-
-            tracing::debug!(message="Generated query for upsert", combined_query=?combined_query);
-
-            let dgraph_client = dgraph_client.clone();
-            Self::enforce_transaction(move || {
-                let txn = dgraph_client.new_mutated_txn();
-                txn.upsert_and_commit_now(combined_query.clone(), all_mutations.clone())
-            })
-        })
-        .buffer_unordered(DGRAPH_CONCURRENCY_UPSERTS)
-        .collect::<Vec<_>>()
-        .await;
-
-        let mut node_key_map_to_uid = HashMap::new();
-        let mut uids = HashMap::new();
-        for response in responses.iter() {
-            let query_responses: serde_json::Value = match serde_json::from_slice(&response.json) {
-                Ok(response) => response,
-                Err(e) => {
-                    tracing::error!(message="Failed to parse JSON response for upsert", error=?e);
-                    continue;
-                }
-            };
-            tracing::debug!(
-                message="Received upsert response",
-                query_response=?query_responses,
-                uids=?response.uids,
-            );
-            uids.extend(response.uids.clone());
-            extract_node_key_map_uid(&query_responses, &mut node_key_map_to_uid);
-        }
-
-        for node in identified_graph.nodes.values() {
-            let IdentifiedNode {
-                node_key,
-                node_type,
-                properties,
-            } = node.to_owned();
-
-            let uid = node_key_map_to_uid
-                .get(&node_key)
-                .copied()
-                .or_else(|| uid_from_uids(&node_key, &key_to_query_name, &uids));
-            let uid = match uid {
-                Some(uid) => uid,
-                None => {
-                    tracing::error!(
-                        message="Failed to retrieve uid associated with node_key",
-                        node_key=?node_key,
-                    );
-                    continue;
-                }
-            };
-
-            merged_graph.add_node(MergedNode {
-                properties,
-                uid,
-                node_key,
-                node_type,
-            });
-        }
-        node_key_map_to_uid
-    }
-
-    #[tracing::instrument(skip(self, dgraph_client, identified_graph, node_key_to_uid))]
-    async fn upsert_edges(
-        &self,
-        dgraph_client: Arc<DgraphClient>,
-        identified_graph: &IdentifiedGraph,
-        mut node_key_to_uid: HashMap<String, u64>,
-    ) {
-        let all_edges: Vec<_> = identified_graph
-            .edges
-            .iter()
-            .flat_map(|(_, EdgeList { edges })| edges)
-            .map(
-                |Edge {
-                     from_node_key,
-                     to_node_key,
-                     edge_name,
-                 }| {
-                    (
-                        from_node_key.clone(),
-                        to_node_key.clone(),
-                        edge_name.clone(),
-                    )
-                },
-            )
-            .collect();
-
-        let mut unresolved = vec![];
-        for (from_node_key, to_node_key, _edge_name) in all_edges.iter() {
-            if !node_key_to_uid.contains_key(from_node_key) {
-                unresolved.push(from_node_key);
-            }
-            if !node_key_to_uid.contains_key(to_node_key) {
-                unresolved.push(to_node_key);
-            }
-        }
-        unresolved.sort_unstable();
-        unresolved.dedup();
-        if !unresolved.is_empty() {
-            let m = query_for_node_keys(dgraph_client.clone(), &unresolved[..]).await;
-            for (node_key, uid) in m.into_iter() {
-                node_key_to_uid.insert(node_key, uid);
-            }
-        }
-
-        let mut mutations = Vec::with_capacity(all_edges.len());
-        for items in all_edges.into_iter().chunks_owned(DGRAPH_UPSERT_CHUNK_SIZE) {
-            let mut mutation_units = vec![];
-
-            for (from_node_key, to_node_key, edge_name) in items.iter() {
-                let from_uid = node_key_to_uid.get(from_node_key);
-                let to_uid = node_key_to_uid.get(to_node_key);
-                let (from_uid, to_uid) = match (from_uid, to_uid) {
-                    (Some(from_uid), Some(to_uid)) => (*from_uid, *to_uid),
-                    (from_uid, to_uid) => {
-                        tracing::error!(
-                            message="Could not retrieve uids",
-                            from_uid=?from_uid,
-                            to_uid=?to_uid,
-                            from_node_key=?from_node_key,
-                            to_node_key=?to_node_key,
-                        );
-                        continue;
-                    }
-                };
-
-                let (from_uid, to_uid) = (from_uid.to_string(), to_uid.to_string());
-                let mutation_unit = MutationUnit::new(MutationUID::uid(&from_uid)).predicate(
-                    edge_name,
-                    MutationPredicateValue::Edges(vec![MutationUID::uid(&to_uid)]),
-                );
-                mutation_units.push(mutation_unit);
-            }
-            let mutation = MutationBuilder::default()
-                .set(mutation_units)
-                .build()
-                .unwrap();
-            mutations.push(mutation);
-        }
-
-        futures::stream::iter(mutations.into_iter())
-            .map(|mutation| {
-                let dgraph_client = dgraph_client.clone();
-                Self::enforce_transaction(move || {
-                    let mut dgraph_mutation = dgraph_tonic::Mutation::new();
-                    dgraph_mutation.set_set_json(&mutation.set).unwrap_or_else(
-                        |e| tracing::error!(message="Failed to set json for mutation", error=?e),
-                    );
-
-                    let txn = dgraph_client.new_mutated_txn();
-                    txn.mutate_and_commit_now(dgraph_mutation.clone())
-                })
-            })
-            .buffer_unordered(DGRAPH_CONCURRENCY_UPSERTS)
-            .collect::<Vec<_>>()
-            .await;
-    }
-
-    #[tracing::instrument(skip(f))]
-    async fn enforce_transaction<Factory, Txn>(f: Factory) -> dgraph_tonic::Response
-    where
-        Factory: FnMut() -> Txn + 'static + Unpin,
-        Txn: std::future::Future<Output = Result<dgraph_tonic::Response, anyhow::Error>>,
-    {
-        let handle_upsert_err = UpsertErrorHandler {};
-        let (response, attempts) = FutureRetry::new(f, handle_upsert_err)
-            .await
-            .expect("Surfaced an error despite retry strategy while performing an upsert.");
-
-        tracing::info!(message = "Performed upsert", attempts = attempts);
-
-        response
-    }
-}
-
-pub struct UpsertErrorHandler {}
-
-impl futures_retry::ErrorHandler<anyhow::Error> for UpsertErrorHandler {
-    type OutError = anyhow::Error;
-
-    #[tracing::instrument(skip(self, attempt, e))]
-    fn handle(&mut self, attempt: usize, e: anyhow::Error) -> RetryPolicy<Self::OutError> {
-        let attempt = attempt as u64;
-        tracing::warn!(
-            message="Failed to enforce transaction",
-            error=?e,
-            attempt=?attempt,
-        );
-        match attempt {
-            0..=5 => RetryPolicy::Repeat,
-            t @ 6..=20 => RetryPolicy::WaitRetry(std::time::Duration::from_millis(10 * t as u64)),
-            21..=u64::MAX => RetryPolicy::ForwardError(e),
-        }
-    }
-}
-
-#[tracing::instrument(skip(dgraph_client, node_keys))]
-async fn query_for_node_keys(
-    dgraph_client: Arc<DgraphClient>,
-    node_keys: &[&String],
-) -> HashMap<String, u64> {
-    let mut resolved_nodes = HashMap::new();
-
-    let mut query_blocks = Vec::with_capacity(node_keys.len());
-    for node_key in node_keys.iter() {
-        let query_block = gen_node_key_query(node_key);
-        query_blocks.push(query_block);
-    }
-
-    let query = QueryBuilder::default()
-        .query_blocks(query_blocks)
-        .build()
-        .unwrap();
-
-    let mut txn = dgraph_client.new_read_only_txn();
-    let query_responses = txn
-        .query(query.to_query_string())
-        .await
-        .expect("query failed");
-
-    let query_responses: HashMap<String, Vec<HashMap<String, String>>> =
-        serde_json::from_slice(&query_responses.json).expect("response failed to parse");
-
-    for (_, query_response) in query_responses.into_iter() {
-        let query_response = match query_response.as_slice() {
-            [query_response] => query_response,
-            [] => {
-                tracing::error!(message = "Empty response for node_key");
-                continue;
-            }
-            res => {
-                tracing::error!(message = "Too many responses for node_key", count=?res.len());
-                continue;
-            }
-        };
-        let node_key = query_response.get("node_key");
-        let uid = query_response.get("uid");
-        let (node_key, uid) = match (node_key, uid) {
-            (Some(node_key), Some(uid)) => (node_key, uid),
-            (missing_key, missing_uid) => {
-                tracing::error!(message="Unable to retrieve node_key and uid", node_key=?missing_key, uid=?missing_uid);
-                continue;
-            }
-        };
-        let uid = u64::from_str_radix(&uid[2..], 16).expect("uid is not valid hex");
-        resolved_nodes.insert(node_key.to_owned(), uid);
-    }
-
-    resolved_nodes
-}
-
-fn gen_node_key_query(node_key: &str) -> QueryBlock {
-    QueryBlockBuilder::default()
-        .query_type(QueryBlockType::query())
-        .root_filter(Condition::EQ(
-            "node_key".to_string(),
-            ConditionValue::string(node_key),
-        ))
-        .predicates(vec![
-            Predicate::Field(Field::new("uid")),
-            Predicate::Field(Field::new("node_key")),
-        ])
-        .first(1)
-        .build()
-        .unwrap()
-}
-
-fn uid_from_uids(
-    node_key: &str,
-    key_to_query_name: &HashMap<String, String>,
-    uids: &HashMap<String, String>,
-) -> Option<u64> {
-    let query_name = key_to_query_name.get(node_key)?;
-    let uid = uids.get(query_name)?;
-    Some(u64::from_str_radix(&uid[2..], 16).expect("uid is not valid hex"))
-}
-
-fn extract_node_key_map_uid(
-    dgraph_response: &serde_json::Value,
-    node_key_map_to_uid: &mut HashMap<String, u64>,
-) {
-    let query_responses = dgraph_response.as_object().expect("Invalid response");
-
-    for query_response in query_responses.values() {
-        let query_response = query_response.as_array().expect("Invalid response");
-        for query_response in query_response {
-            let uid = query_response
-                .get("uid")
-                .expect("uid")
-                .as_str()
-                .expect("uid");
-            let node_key = query_response
-                .get("node_key")
-                .expect("node_key")
-                .as_str()
-                .expect("node_key");
-
-            // dgraph uids are hex encoded as '0x1b'
-            let uid = u64::from_str_radix(&uid[2..], 16).expect("uid is not valid hex");
-            node_key_map_to_uid.insert(node_key.to_owned(), uid);
-        }
-    }
-}
diff --git a/src/rust/graph-mutation/Cargo.toml b/src/rust/graph-mutation/Cargo.toml
index 51aa4888ee..d96243d789 100644
--- a/src/rust/graph-mutation/Cargo.toml
+++ b/src/rust/graph-mutation/Cargo.toml
@@ -17,8 +17,9 @@ grapl-tracing = { path = "../grapl-tracing" }
 grapl-utils = { path = "../grapl-utils" }
 hash_hasher = "2.0.3"
 lazy_static = "1.4.0"
+moka = { version = "0.9", features = ["future"] }
 rust-proto = { path = "../rust-proto" }
-scylla = "0.4.4"
+scylla = "0.5"
 thiserror = "1.0.30"
 tokio = { version = "1.17.0", features = ["full"] }
 tracing = "0.1.34"
diff --git a/src/rust/graph-mutation/src/graph_mutation.rs b/src/rust/graph-mutation/src/graph_mutation.rs
index c5b9d45fb6..fbbf953882 100644
--- a/src/rust/graph-mutation/src/graph_mutation.rs
+++ b/src/rust/graph-mutation/src/graph_mutation.rs
@@ -1,5 +1,6 @@
 use std::sync::Arc;
 
+use grapl_utils::future_ext::GraplFutureExt;
 use rust_proto::{
     graplinc::grapl::{
         api::{
@@ -31,6 +32,7 @@ use scylla::{
     query::Query,
     CachingSession,
 };
+use tracing::Instrument;
 use uid_allocator::client::CachingUidAllocatorServiceClient as UidAllocatorClient;
 
 use crate::{
@@ -61,6 +63,11 @@ pub enum GraphMutationManagerError {
     ScyllaError(#[from] scylla::transport::errors::QueryError),
     #[error("ReverseEdgeResolverError: {0}")]
     ReverseEdgeResolverError(#[from] ReverseEdgeResolverError),
+    #[error("Scylla Insert Timeout: {tenant_id:?} {insert_type:?}")]
+    ScyllaInsertTimeout {
+        tenant_id: uuid::Uuid,
+        insert_type: &'static str,
+    },
 }
 
 impl From<GraphMutationManagerError> for Status {
@@ -96,7 +103,7 @@ impl GraphMutationManager {
         scylla_client: Arc<CachingSession>,
         uid_allocator_client: UidAllocatorClient,
         reverse_edge_resolver: ReverseEdgeResolver,
-        max_write_drop_size: usize,
+        max_write_drop_size: u64,
     ) -> Self {
         Self {
             scylla_client,
@@ -157,23 +164,31 @@ impl GraphMutationManager {
                 node_type.clone(),
                 property_name.clone(),
                 property_value,
-                || async move {
-                    let property_value = property_value as i64;
-                    let tenant_ks = tenant_keyspace_name(tenant_id);
-                    let mut query = Query::new(format!(
-                        r"
+                || {
+                    async move {
+                        let property_value = property_value as i64;
+                        let tenant_ks = tenant_keyspace_name(tenant_id);
+                        let mut query = Query::new(format!(
+                            r"
                         INSERT INTO {tenant_ks}.{MIN_U_64_TABLE_NAME} 
                         (uid, populated_field, value)
                         VALUES (?, ?, ?)
                     "
-                    ));
-
-                    query.set_timestamp(Some(-property_value));
-
-                    self.scylla_client
-                        .execute(query, &(uid.as_i64(), property_name.value, property_value))
-                        .await?;
-                    Ok(())
+                        ));
+
+                        query.set_timestamp(Some(-property_value));
+
+                        self.scylla_client
+                            .execute(query, &(uid.as_i64(), property_name.value, property_value))
+                            .timeout(std::time::Duration::from_secs(3))
+                            .await
+                            .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                                tenant_id,
+                                insert_type: "MIN_U_64",
+                            })??;
+                        Ok(())
+                    }
+                    .instrument(tracing::info_span!("upsert_min_u64"))
                 },
             )
             .await
@@ -189,11 +204,8 @@ impl GraphMutationManager {
         property_value: u64,
     ) -> Result<(), GraphMutationManagerError> {
         self.write_dropper
-            .check_imm_u64(
-                tenant_id,
-                node_type.clone(),
-                property_name.clone(),
-                || async move {
+            .check_imm_u64(tenant_id, node_type.clone(), property_name.clone(), || {
+                async move {
                     let property_value = property_value as i64;
                     let tenant_ks = tenant_keyspace_name(tenant_id);
                     let query = Query::new(format!(
@@ -206,10 +218,16 @@ impl GraphMutationManager {
 
                     self.scylla_client
                         .execute(query, &(uid.as_i64(), property_name.value, property_value))
-                        .await?;
+                        .timeout(std::time::Duration::from_secs(3))
+                        .await
+                        .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                            tenant_id,
+                            insert_type: "MAX_U_64",
+                        })??;
                     Ok(())
-                },
-            )
+                }
+                .instrument(tracing::info_span!("upsert_max_u64"))
+            })
             .await
     }
 
@@ -228,21 +246,29 @@ impl GraphMutationManager {
                 node_type.clone(),
                 property_name.clone(),
                 property_value,
-                || async move {
-                    let tenant_ks = tenant_keyspace_name(tenant_id);
-                    let mut query = Query::new(format!(
-                        r"
+                || {
+                    async move {
+                        let tenant_ks = tenant_keyspace_name(tenant_id);
+                        let mut query = Query::new(format!(
+                            r"
                         INSERT INTO {tenant_ks}.{MAX_I_64_TABLE_NAME} 
                         (uid, populated_field, value)
                         VALUES (?, ?, ?)
                     "
-                    ));
-                    query.set_timestamp(Some(property_value));
-
-                    self.scylla_client
-                        .execute(query, &(uid.as_i64(), property_name.value, property_value))
-                        .await?;
-                    Ok(())
+                        ));
+                        query.set_timestamp(Some(property_value));
+
+                        self.scylla_client
+                            .execute(query, &(uid.as_i64(), property_name.value, property_value))
+                            .timeout(std::time::Duration::from_secs(3))
+                            .await
+                            .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                                tenant_id,
+                                insert_type: "MAX_I_64",
+                            })??;
+                        Ok(())
+                    }
+                    .instrument(tracing::info_span!("upsert_max_i64"))
                 },
             )
             .await
@@ -263,22 +289,29 @@ impl GraphMutationManager {
                 node_type.clone(),
                 property_name.clone(),
                 property_value,
-                || async move {
-                    let tenant_ks = tenant_keyspace_name(tenant_id);
-                    let mut query = Query::new(format!(
-                        r"
+                || {
+                    async move {
+                        let tenant_ks = tenant_keyspace_name(tenant_id);
+                        let mut query = Query::new(format!(
+                            r"
                         INSERT INTO {tenant_ks}.{MIN_I_64_TABLE_NAME} 
                         (uid, populated_field, value)
                         VALUES (?, ?, ?)
                     "
-                    ));
-
-                    query.set_timestamp(Some(-property_value));
-
-                    self.scylla_client
-                        .execute(query, &(uid.as_i64(), property_name.value, property_value))
-                        .await?;
-                    Ok(())
+                        ));
+                        query.set_timestamp(Some(-property_value));
+
+                        self.scylla_client
+                            .execute(query, &(uid.as_i64(), property_name.value, property_value))
+                            .timeout(std::time::Duration::from_secs(3))
+                            .await
+                            .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                                tenant_id,
+                                insert_type: "MIN_I_64",
+                            })??;
+                        Ok(())
+                    }
+                    .instrument(tracing::info_span!("upsert_min_i64"))
                 },
             )
             .await
@@ -294,11 +327,8 @@ impl GraphMutationManager {
         property_value: i64,
     ) -> Result<(), GraphMutationManagerError> {
         self.write_dropper
-            .check_imm_i64(
-                tenant_id,
-                node_type.clone(),
-                property_name.clone(),
-                || async move {
+            .check_imm_i64(tenant_id, node_type.clone(), property_name.clone(), || {
+                async move {
                     let tenant_ks = tenant_keyspace_name(tenant_id);
                     let query = Query::new(format!(
                         r"
@@ -310,10 +340,16 @@ impl GraphMutationManager {
 
                     self.scylla_client
                         .execute(query, &(uid.as_i64(), property_name.value, property_value))
-                        .await?;
+                        .timeout(std::time::Duration::from_secs(3))
+                        .await
+                        .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                            tenant_id,
+                            insert_type: "IMM_I_64",
+                        })??;
                     Ok(())
-                },
-            )
+                }
+                .instrument(tracing::info_span!("upsert_imm_i64"))
+            })
             .await
     }
 
@@ -325,19 +361,27 @@ impl GraphMutationManager {
         node_type: NodeType,
     ) -> Result<(), GraphMutationManagerError> {
         self.write_dropper
-            .check_node_type(tenant_id, uid, || async move {
-                let tenant_ks = tenant_keyspace_name(tenant_id);
-                let query = Query::new(format!(
-                    r"
+            .check_node_type(tenant_id, uid, || {
+                async move {
+                    let tenant_ks = tenant_keyspace_name(tenant_id);
+                    let query = Query::new(format!(
+                        r"
                         INSERT INTO {tenant_ks}.node_type (uid, node_type)
                         VALUES (?, ?)
                     "
-                ));
+                    ));
 
-                self.scylla_client
-                    .execute(query, &(uid.as_i64(), node_type.value))
-                    .await?;
-                Ok(())
+                    self.scylla_client
+                        .execute(query, &(uid.as_i64(), node_type.value))
+                        .timeout(std::time::Duration::from_secs(3))
+                        .await
+                        .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                            tenant_id,
+                            insert_type: "NODE_TYPE",
+                        })??;
+                    Ok(())
+                }
+                .instrument(tracing::info_span!("set_node_type"))
             })
             .await
     }
@@ -352,11 +396,8 @@ impl GraphMutationManager {
         property_value: String,
     ) -> Result<(), GraphMutationManagerError> {
         self.write_dropper
-            .check_imm_string(
-                tenant_id,
-                node_type.clone(),
-                property_name.clone(),
-                || async move {
+            .check_imm_string(tenant_id, node_type.clone(), property_name.clone(), || {
+                async move {
                     let tenant_ks = tenant_keyspace_name(tenant_id);
                     let query = Query::new(format!(
                         r"
@@ -368,10 +409,16 @@ impl GraphMutationManager {
 
                     self.scylla_client
                         .execute(query, &(uid.as_i64(), property_name.value, property_value))
-                        .await?;
+                        .timeout(std::time::Duration::from_secs(3))
+                        .await
+                        .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                            tenant_id,
+                            insert_type: "IMM_STRING",
+                        })??;
                     Ok(())
-                },
-            )
+                }
+                .instrument(tracing::info_span!("upsert_imm_string"))
+            })
             .await
     }
 
@@ -391,13 +438,14 @@ impl GraphMutationManager {
                 to_uid,
                 f_edge_name,
                 r_edge_name,
-                |f_edge_name, r_edge_name| async move {
-                    // todo: Batch statements are currently not supported by the Scylla rust client
-                    //       https://github.com/scylladb/scylla-rust-driver/issues/469
-                    let tenant_ks = tenant_keyspace_name(tenant_id);
-
-                    let f_statement = format!(
-                        r"
+                |f_edge_name, r_edge_name| {
+                    async move {
+                        // todo: Batch statements are currently not supported by the Scylla rust client
+                        //       https://github.com/scylladb/scylla-rust-driver/issues/469
+                        let tenant_ks = tenant_keyspace_name(tenant_id);
+
+                        let f_statement = format!(
+                            r"
                         INSERT INTO {tenant_ks}.edges (
                             source_uid,
                             destination_uid,
@@ -406,36 +454,43 @@ impl GraphMutationManager {
                         )
                         VALUES (?, ?, ?, ?)
                         ",
-                    );
-                    let r_statement = f_statement.clone();
-
-                    let mut batch: scylla::batch::Batch = Default::default();
-                    batch.statements.reserve(2);
-                    batch.append_statement(Query::from(f_statement));
-                    batch.append_statement(Query::from(r_statement));
-                    batch.set_is_idempotent(true);
-
-                    self.scylla_client
-                        .session
-                        .batch(
-                            &batch,
-                            (
-                                (
-                                    from_uid.as_i64(),
-                                    to_uid.as_i64(),
-                                    &f_edge_name.value,
-                                    &r_edge_name.value,
-                                ),
+                        );
+                        let r_statement = f_statement.clone();
+
+                        let mut batch: scylla::batch::Batch = Default::default();
+                        batch.statements.reserve(2);
+                        batch.append_statement(Query::from(f_statement));
+                        batch.append_statement(Query::from(r_statement));
+                        batch.set_is_idempotent(true);
+
+                        self.scylla_client
+                            .session
+                            .batch(
+                                &batch,
                                 (
-                                    to_uid.as_i64(),
-                                    from_uid.as_i64(),
-                                    &r_edge_name.value,
-                                    &f_edge_name.value,
+                                    (
+                                        from_uid.as_i64(),
+                                        to_uid.as_i64(),
+                                        &f_edge_name.value,
+                                        &r_edge_name.value,
+                                    ),
+                                    (
+                                        to_uid.as_i64(),
+                                        from_uid.as_i64(),
+                                        &r_edge_name.value,
+                                        &f_edge_name.value,
+                                    ),
                                 ),
-                            ),
-                        )
-                        .await?;
-                    Ok(())
+                            )
+                            .timeout(std::time::Duration::from_secs(3))
+                            .await
+                            .map_err(|_| GraphMutationManagerError::ScyllaInsertTimeout {
+                                tenant_id,
+                                insert_type: "EDGES",
+                            })??;
+                        Ok(())
+                    }
+                    .instrument(tracing::info_span!("upsert_edges"))
                 },
             )
             .await
@@ -452,21 +507,34 @@ impl GraphMutationApi for GraphMutationManager {
         &self,
         request: CreateNodeRequest,
     ) -> Result<CreateNodeResponse, Self::Error> {
+        tracing::debug!(message = "Creating node",);
         let uid = self
             .uid_allocator_client
             .allocate_id(request.tenant_id)
             .await?;
         let uid = Uid::from_u64(uid).ok_or_else(|| GraphMutationManagerError::ZeroUid)?;
-
+        tracing::debug!(
+            message="Allocated uid",
+            uid=?uid,
+        );
         self.set_node_type(request.tenant_id, uid, request.node_type)
             .await?;
+        tracing::debug!(
+            message="Set node type",
+            uid=?uid,
+        );
 
         Ok(CreateNodeResponse { uid })
     }
 
     /// SetNodeProperty will update the property of the node with the given uid.
     /// If the node does not exist it will be created.
-    #[tracing::instrument(skip(self), err)]
+    #[tracing::instrument(
+    skip(self),
+    fields(
+        tenant_id=?request.tenant_id,
+        property_name=?request.property_name,
+    ), err)]
     async fn set_node_property(
         &self,
         request: SetNodePropertyRequest,
@@ -478,6 +546,11 @@ impl GraphMutationApi for GraphMutationManager {
             property_name,
             property,
         } = request;
+        tracing::debug!(
+            message="Setting node property",
+            uid=?uid,
+            property_name=?property_name,
+        );
         match property.property {
             Property::IncrementOnlyUintProp(property) => {
                 self.upsert_max_u64(tenant_id, uid, node_type, property_name, property.prop)
diff --git a/src/rust/graph-mutation/src/main.rs b/src/rust/graph-mutation/src/main.rs
index aa32ac1359..0fb0b1f722 100644
--- a/src/rust/graph-mutation/src/main.rs
+++ b/src/rust/graph-mutation/src/main.rs
@@ -32,6 +32,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
 
     let config = GraphMutationServiceConfig::parse();
     let mut scylla_config = scylla::SessionConfig::new();
+    scylla_config.request_timeout = Some(Duration::from_secs(3));
     scylla_config.add_known_nodes_addr(&config.graph_db_config.graph_db_addresses[..]);
     scylla_config.auth_username = Some(config.graph_db_config.graph_db_auth_username.to_owned());
     scylla_config.auth_password = Some(config.graph_db_config.graph_db_auth_password.to_owned());
diff --git a/src/rust/graph-mutation/src/reverse_edge_resolver.rs b/src/rust/graph-mutation/src/reverse_edge_resolver.rs
index dcba13c948..217e315e4c 100644
--- a/src/rust/graph-mutation/src/reverse_edge_resolver.rs
+++ b/src/rust/graph-mutation/src/reverse_edge_resolver.rs
@@ -24,6 +24,7 @@ pub enum ReverseEdgeResolverError {
 #[derive(Clone)]
 pub struct ReverseEdgeResolver {
     schema_client: GraphSchemaManagerClient,
+    #[allow(dead_code)] // https://github.com/grapl-security/issue-tracker/issues/1028
     r_edge_cache: dashmap::DashMap<(uuid::Uuid, EdgeName, NodeType), GetEdgeSchemaResponse>,
 }
 
@@ -36,33 +37,50 @@ impl ReverseEdgeResolver {
         }
     }
 
+    #[tracing::instrument(skip(self), err)]
     pub async fn resolve_reverse_edge(
         &self,
         tenant_id: uuid::Uuid,
         node_type: NodeType,
         edge_name: EdgeName,
     ) -> Result<EdgeName, ReverseEdgeResolverError> {
-        match self
-            .r_edge_cache
-            .entry((tenant_id, edge_name.clone(), node_type.clone()))
-        {
-            dashmap::mapref::entry::Entry::Occupied(entry) => {
-                Ok(entry.get().reverse_edge_name.clone())
-            }
-            dashmap::mapref::entry::Entry::Vacant(entry) => {
-                let mut schema_client = self.schema_client.clone();
-                let response = schema_client
-                    .get_edge_schema(GetEdgeSchemaRequest {
-                        tenant_id,
-                        node_type: node_type.clone(),
-                        edge_name: edge_name.clone(),
-                    })
-                    .await?;
+        let mut schema_client = self.schema_client.clone();
+        let response = schema_client
+            .get_edge_schema(GetEdgeSchemaRequest {
+                tenant_id,
+                node_type: node_type.clone(),
+                edge_name: edge_name.clone(),
+            })
+            .await?;
 
-                let reverse_name = response.reverse_edge_name.clone();
-                entry.insert(response);
-                Ok(reverse_name)
-            }
-        }
+        let reverse_name = response.reverse_edge_name;
+        Ok(reverse_name)
+
+        // TODO: Resurrect the below once we figure out caching for Graph Mutation
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // entry.insert(response);
+        // match self
+        //     .r_edge_cache
+        //     .entry((tenant_id, edge_name.clone(), node_type.clone()))
+        // {
+        //     dashmap::mapref::entry::Entry::Occupied(entry) => {
+        //         Ok(entry.get().reverse_edge_name.clone())
+        //     }
+        //     dashmap::mapref::entry::Entry::Vacant(entry) => {
+        //         let mut schema_client = self.schema_client.clone();
+        //         let response = schema_client
+        //             .get_edge_schema(GetEdgeSchemaRequest {
+        //                 tenant_id,
+        //                 node_type: node_type.clone(),
+        //                 edge_name: edge_name.clone(),
+        //             })
+        //             .await?;
+        //
+        //         let reverse_name = response.reverse_edge_name.clone();
+        //         entry.insert(response);
+        //         Ok(reverse_name)
+        //     }
+        // }
     }
 }
diff --git a/src/rust/graph-mutation/src/write_dropper.rs b/src/rust/graph-mutation/src/write_dropper.rs
index 8d2c1e610d..98f8c7f485 100644
--- a/src/rust/graph-mutation/src/write_dropper.rs
+++ b/src/rust/graph-mutation/src/write_dropper.rs
@@ -1,11 +1,6 @@
 use std::future::Future;
 
-use blake2::{
-    digest::consts::U16,
-    Blake2b,
-    Digest,
-};
-use dashmap::mapref::entry::Entry;
+use moka::future::Cache;
 use rust_proto::graplinc::grapl::common::v1beta1::types::{
     EdgeName,
     NodeType,
@@ -13,20 +8,6 @@ use rust_proto::graplinc::grapl::common::v1beta1::types::{
     Uid,
 };
 
-type Blake2b16 = Blake2b<U16>;
-
-macro_rules! handle_full {
-    ($self:ident, $cache:ident) => {{
-        if $self.$cache.len() >= $self.max_size {
-            // Do not 'inline' the `if let Some` or you may cause a deadlock
-            let entry = $self.$cache.iter().next().map(|i| i.key().clone());
-            if let Some(entry) = entry {
-                $self.$cache.remove(&entry);
-            }
-        }
-    }};
-}
-
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 struct NodeTypeKey {
     tenant_id: uuid::Uuid,
@@ -40,50 +21,44 @@ struct PropertyKey {
     property_name: PropertyName,
 }
 
-fn edge_key(
-    tenant_id: uuid::Uuid,
-    source_uid: Uid,
-    dst_uid: Uid,
-    edge_name: &EdgeName,
-) -> [u8; 16] {
-    let mut hasher = Blake2b16::new();
-    hasher.update(tenant_id.as_bytes());
-    hasher.update(source_uid.as_u64().to_le_bytes());
-    hasher.update(dst_uid.as_u64().to_le_bytes());
-    hasher.update(edge_name.value.as_bytes());
-    hasher.update(b"my_input");
-    hasher.finalize().into()
-}
-
+/// WriteDropper lets us save database IO by proactively "dropping" db writes
+/// that wouldn't change eventual outcome.
+/// EXAMPLE 1: Immutable String
+///   An immutable string can't change, so if we receive two instructions to
+///   write an immutable string, we only have to write one of them.
+/// EXAMPLE 2: Max u64/i64 (aka IncrOnly)
+///   If you have a property that can only increment, and we've previously
+///   written 5 to the DB, there's no reason to write a 4 if we encounter it.
+#[allow(dead_code)] // TODO https://github.com/grapl-security/issue-tracker/issues/1028
+#[derive(Clone, Debug)]
 pub struct WriteDropper {
-    max_i64: dashmap::DashMap<PropertyKey, i64>,
-    min_i64: dashmap::DashMap<PropertyKey, i64>,
-    imm_i64: dashmap::DashSet<PropertyKey>,
-    max_u64: dashmap::DashMap<PropertyKey, u64>,
-    min_u64: dashmap::DashMap<PropertyKey, u64>,
-    imm_u64: dashmap::DashSet<PropertyKey>,
-    imm_string: dashmap::DashSet<PropertyKey>,
-    node_type: dashmap::DashSet<NodeTypeKey>,
-    edges: dashmap::DashSet<[u8; 16], hash_hasher::HashBuildHasher>,
-    max_size: usize,
+    max_i64: Cache<PropertyKey, i64>,
+    min_i64: Cache<PropertyKey, i64>,
+    imm_i64: Cache<PropertyKey, ()>,
+    max_u64: Cache<PropertyKey, u64>,
+    min_u64: Cache<PropertyKey, u64>,
+    imm_u64: Cache<PropertyKey, ()>,
+    imm_string: Cache<PropertyKey, ()>,
+    node_type: Cache<NodeTypeKey, ()>,
+    edges: Cache<[u8; 16], ()>,
 }
 
 impl WriteDropper {
-    pub fn new(max_size: usize) -> Self {
+    pub fn new(max_size: u64) -> Self {
         Self {
-            max_i64: Default::default(),
-            min_i64: Default::default(),
-            imm_i64: Default::default(),
-            max_u64: Default::default(),
-            min_u64: Default::default(),
-            imm_u64: Default::default(),
-            imm_string: Default::default(),
-            node_type: Default::default(),
-            edges: Default::default(),
-            max_size,
+            max_i64: Cache::new(max_size),
+            min_i64: Cache::new(max_size),
+            imm_i64: Cache::new(max_size),
+            max_u64: Cache::new(max_size),
+            min_u64: Cache::new(max_size),
+            imm_u64: Cache::new(max_size),
+            imm_string: Cache::new(max_size),
+            node_type: Cache::new(max_size),
+            edges: Cache::new(max_size),
         }
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_max_i64<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -94,28 +69,37 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        match self.max_i64.entry(PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        }) {
-            Entry::Vacant(entry) => {
-                callback().await?;
-                handle_full!(self, max_i64);
-                entry.insert(value);
-            }
-            Entry::Occupied(mut entry) => {
-                if value > *entry.get() {
-                    callback().await?;
-                    handle_full!(self, max_i64);
-                    entry.insert(value);
-                }
-            }
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // match self.max_i64.entry(PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // }) {
+        //     Entry::Vacant(entry) => {
+        //         callback().await?;
+        //         handle_full!(self, max_i64);
+        //         entry.insert(value);
+        //     }
+        //     Entry::Occupied(mut entry) => {
+        //         if value > *entry.get() {
+        //             callback().await?;
+        //             handle_full!(self, max_i64);
+        //             entry.insert(value);
+        //         }
+        //     }
+        // }
         Ok(())
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_min_i64<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -126,27 +110,32 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        match self.min_i64.entry(PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        }) {
-            Entry::Vacant(entry) => {
-                callback().await?;
-                handle_full!(self, min_i64);
-                entry.insert(value);
-            }
-            Entry::Occupied(mut entry) => {
-                if value < *entry.get() {
-                    callback().await?;
-                    handle_full!(self, min_i64);
-                    entry.insert(value);
-                }
-            }
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+        // match self.min_i64.entry(PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // }) {
+        //     Entry::Vacant(entry) => {
+        //         callback().await?;
+        //         handle_full!(self, min_i64);
+        //         entry.insert(value);
+        //     }
+        //     Entry::Occupied(mut entry) => {
+        //         if value < *entry.get() {
+        //             callback().await?;
+        //             handle_full!(self, min_i64);
+        //             entry.insert(value);
+        //         }
+        //     }
+        // }
         Ok(())
     }
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_imm_i64<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -156,20 +145,29 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        let key = PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        };
-        if !self.imm_i64.contains(&key) {
-            callback().await?;
-            handle_full!(self, imm_i64);
-            self.imm_i64.insert(key);
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // let key = PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // };
+        // if !self.imm_i64.contains(&key) {
+        //     callback().await?;
+        //     handle_full!(self, imm_i64);
+        //     self.imm_i64.insert(key);
+        // }
         Ok(())
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_max_u64<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -180,28 +178,37 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        match self.max_u64.entry(PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        }) {
-            Entry::Vacant(entry) => {
-                callback().await?;
-                handle_full!(self, max_u64);
-                entry.insert(value);
-            }
-            Entry::Occupied(mut entry) => {
-                if value > *entry.get() {
-                    callback().await?;
-                    handle_full!(self, max_u64);
-                    entry.insert(value);
-                }
-            }
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // match self.max_u64.entry(PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // }) {
+        //     Entry::Vacant(entry) => {
+        //         callback().await?;
+        //         handle_full!(self, max_u64);
+        //         entry.insert(value);
+        //     }
+        //     Entry::Occupied(mut entry) => {
+        //         if value > *entry.get() {
+        //             callback().await?;
+        //             handle_full!(self, max_u64);
+        //             entry.insert(value);
+        //         }
+        //     }
+        // }
         Ok(())
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_min_u64<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -212,27 +219,36 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        match self.min_u64.entry(PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        }) {
-            Entry::Vacant(entry) => {
-                callback().await?;
-                handle_full!(self, min_u64);
-                entry.insert(value);
-            }
-            Entry::Occupied(mut entry) => {
-                if value < *entry.get() {
-                    callback().await?;
-                    handle_full!(self, min_u64);
-                    entry.insert(value);
-                }
-            }
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // match self.min_u64.entry(PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // }) {
+        //     Entry::Vacant(entry) => {
+        //         callback().await?;
+        //         handle_full!(self, min_u64);
+        //         entry.insert(value);
+        //     }
+        //     Entry::Occupied(mut entry) => {
+        //         if value < *entry.get() {
+        //             callback().await?;
+        //             handle_full!(self, min_u64);
+        //             entry.insert(value);
+        //         }
+        //     }
+        // }
         Ok(())
     }
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_imm_u64<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -242,20 +258,29 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        let key = PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        };
-        if !self.imm_u64.contains(&key) {
-            callback().await?;
-            handle_full!(self, imm_u64);
-            self.imm_u64.insert(key);
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // let key = PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // };
+        // if !self.imm_u64.contains(&key) {
+        //     callback().await?;
+        //     handle_full!(self, imm_u64);
+        //     self.imm_u64.insert(key);
+        // }
         Ok(())
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_imm_string<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -265,20 +290,29 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        let key = PropertyKey {
-            tenant_id,
-            node_type,
-            property_name,
-        };
-        if !self.imm_string.contains(&key) {
-            callback().await?;
-            handle_full!(self, imm_string);
-            self.imm_string.insert(key);
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // let key = PropertyKey {
+        //     tenant_id,
+        //     node_type,
+        //     property_name,
+        // };
+        // if !self.imm_string.contains(&key) {
+        //     callback().await?;
+        //     handle_full!(self, imm_string);
+        //     self.imm_string.insert(key);
+        // }
         Ok(())
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_node_type<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -287,16 +321,25 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        let key = NodeTypeKey { tenant_id, uid };
-        if !self.node_type.contains(&key) {
-            callback().await?;
-            handle_full!(self, node_type);
-            self.node_type.insert(key);
-        }
+        tracing::debug!(message = "Performing insert",);
+        callback().await?;
+        tracing::debug!(message = "Insert performed");
+
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
+
+        // let key = NodeTypeKey { tenant_id, uid };
+        // if !self.node_type.contains(&key) {
+        //     callback().await?;
+        //     handle_full!(self, node_type);
+        //     self.node_type.insert(key);
+        // }
         Ok(())
     }
 
+    #[tracing::instrument(skip(self, callback), err)]
     pub async fn check_edges<T, E, Fut>(
         &self,
         tenant_id: uuid::Uuid,
@@ -308,19 +351,27 @@ impl WriteDropper {
     ) -> Result<(), E>
     where
         Fut: Future<Output = Result<T, E>>,
+        E: std::error::Error,
     {
-        let fkey = edge_key(tenant_id, source_uid, dest_uid, &f_edge_name);
+        tracing::debug!(message = "Performing insert",);
+        callback(f_edge_name, r_edge_name).await?;
+        tracing::debug!(message = "Insert performed");
 
-        // We always insert both the forward and reverse edges in a batch insert
-        if !self.edges.contains(&fkey) {
-            handle_full!(self, edges);
-            let rkey = edge_key(tenant_id, dest_uid, source_uid, &r_edge_name);
+        // TODO: Resurrect the below, delete the above
+        // https://github.com/grapl-security/issue-tracker/issues/1028
 
-            callback(f_edge_name, r_edge_name).await?;
-
-            self.edges.insert(fkey);
-            self.edges.insert(rkey);
-        }
+        // let fkey = edge_key(tenant_id, source_uid, dest_uid, &f_edge_name);
+        //
+        // // We always insert both the forward and reverse edges in a batch insert
+        // if !self.edges.contains(&fkey) {
+        //     handle_full!(self, edges);
+        //     let rkey = edge_key(tenant_id, dest_uid, source_uid, &r_edge_name);
+        //
+        //     callback(f_edge_name, r_edge_name).await?;
+        //
+        //     self.edges.insert(fkey);
+        //     self.edges.insert(rkey);
+        // }
         Ok(())
     }
 }
diff --git a/src/rust/graph-query-service/Cargo.toml b/src/rust/graph-query-service/Cargo.toml
index 2a87fd534b..6cd11ef71b 100644
--- a/src/rust/graph-query-service/Cargo.toml
+++ b/src/rust/graph-query-service/Cargo.toml
@@ -19,7 +19,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [
 ] }
 test-log = { version = "0.2.8", features = ["trace"] }
 env_logger = "0.9.0"
-scylla = "0.4.3"
+scylla = "0.5"
 rand = "0.8.5"
 futures = "0.3.21"
 itertools = "0.10.3"
@@ -37,7 +37,9 @@ tonic-build = { version = "0.6.0", features = ["prost"] }
 
 [dev-dependencies]
 test-log = { version = "0.2", default-features = false, features = ["trace"] }
+e2e-tests = { path = "../e2e-tests" }
 eyre = "0.6"
+test-context = "0.1.4"
 
 [features]
 integration_tests = []
diff --git a/src/rust/graph-query-service/src/server.rs b/src/rust/graph-query-service/src/server.rs
index f00ee539a4..5b3042d65b 100644
--- a/src/rust/graph-query-service/src/server.rs
+++ b/src/rust/graph-query-service/src/server.rs
@@ -1,11 +1,9 @@
-#![allow(warnings)]
 use std::sync::Arc;
 
 use rust_proto::{
     graplinc::grapl::api::graph_query_service::v1beta1::{
         messages::{
             GraphQuery,
-            GraphView,
             MatchedGraphWithUid,
             MaybeMatchWithUid,
             NoMatchWithUid,
@@ -116,7 +114,7 @@ impl GraphQueryApi for GraphQueryService {
         let visited = Visited::new();
         let x_short_circuit = ShortCircuit::new();
         let graph = fetch_node_with_edges(
-            &node_query,
+            node_query,
             &graph_query,
             node_uid,
             request.tenant_id,
diff --git a/src/rust/graph-schema-manager/src/server.rs b/src/rust/graph-schema-manager/src/server.rs
index 39b600891f..d63fc8f68d 100644
--- a/src/rust/graph-schema-manager/src/server.rs
+++ b/src/rust/graph-schema-manager/src/server.rs
@@ -26,34 +26,36 @@ use crate::{
 
 #[derive(thiserror::Error, Debug)]
 pub enum GraphSchemaManagerServiceError {
-    #[error("NonUtf8 GraphQL Schema {0}")]
+    #[error("NonUtf8 GraphQL Schema: '{0}'")]
     NonUtf8GraphQLSchema(std::string::FromUtf8Error),
-    #[error("DeployGraphqlError {0}")]
+    #[error("DeployGraphqlError: '{0}'")]
     DeployGraphqlError(#[from] DeployGraphqlError),
-    #[error("GetEdgeSchema sqlx error {0}")]
+    #[error("GetEdgeSchema sqlx error: '{0}'")]
     GetEdgeSchemaSqlxError(sqlx::Error),
-    #[error("Invalid ReverseEdgeName: {0}")]
+    #[error("Invalid ReverseEdgeName: '{0}'")]
     InvalidReverseEdgeName(SerDeError),
+    #[error("GetEdgeSchema: Edge not found for tenant_id={tenant_id}, node_type={node_type}, edge_name={edge_name}")]
+    EdgeSchemaNotFound {
+        tenant_id: uuid::Uuid,
+        node_type: String,
+        edge_name: String,
+    },
 }
 
 impl From<GraphSchemaManagerServiceError> for Status {
     fn from(error: GraphSchemaManagerServiceError) -> Self {
+        let msg = error.to_string();
         match error {
-            GraphSchemaManagerServiceError::NonUtf8GraphQLSchema(e) => {
-                Status::invalid_argument(format!("NonUtf8GraphQLSchema - {}", e))
+            GraphSchemaManagerServiceError::NonUtf8GraphQLSchema(_) => {
+                Status::invalid_argument(msg)
             }
             GraphSchemaManagerServiceError::DeployGraphqlError(DeployGraphqlError::SqlxError(
-                e,
-            )) => Status::internal(format!("SqlError during deployment - {}", e)),
-            GraphSchemaManagerServiceError::DeployGraphqlError(e) => {
-                Status::invalid_argument(format!("DeployGraphqlError - {}", e))
-            }
-            GraphSchemaManagerServiceError::GetEdgeSchemaSqlxError(e) => {
-                Status::internal(format!("SqlError during deployment - {}", e))
-            }
-            GraphSchemaManagerServiceError::InvalidReverseEdgeName(name) => {
-                Status::internal(format!("InvalidReverseEdgeName - {}", name))
-            }
+                _,
+            )) => Status::internal(msg),
+            GraphSchemaManagerServiceError::DeployGraphqlError(_) => Status::invalid_argument(msg),
+            GraphSchemaManagerServiceError::EdgeSchemaNotFound { .. } => Status::internal(msg),
+            GraphSchemaManagerServiceError::GetEdgeSchemaSqlxError(_) => Status::internal(msg),
+            GraphSchemaManagerServiceError::InvalidReverseEdgeName(_) => Status::internal(msg),
         }
     }
 }
@@ -66,6 +68,7 @@ pub struct GraphSchemaManager {
 impl GraphSchemaManagerApi for GraphSchemaManager {
     type Error = GraphSchemaManagerServiceError;
 
+    #[tracing::instrument(skip(self, request), err)]
     async fn deploy_schema(
         &self,
         request: DeploySchemaRequest,
@@ -87,6 +90,7 @@ impl GraphSchemaManagerApi for GraphSchemaManager {
         }
     }
 
+    #[tracing::instrument(skip(self), err)]
     async fn get_edge_schema(
         &self,
         request: GetEdgeSchemaRequest,
@@ -99,9 +103,16 @@ impl GraphSchemaManagerApi for GraphSchemaManager {
 
         let response = self
             .db_client
-            .get_edge_schema(tenant_id, node_type, edge_name)
+            .get_edge_schema(tenant_id, node_type.clone(), edge_name.clone())
             .await
-            .map_err(GraphSchemaManagerServiceError::GetEdgeSchemaSqlxError)?;
+            .map_err(|e| match e {
+                sqlx::Error::RowNotFound => Self::Error::EdgeSchemaNotFound {
+                    tenant_id,
+                    node_type: node_type.value,
+                    edge_name: edge_name.value,
+                },
+                _ => Self::Error::GetEdgeSchemaSqlxError(e),
+            })?;
 
         Ok(GetEdgeSchemaResponse {
             reverse_edge_name: EdgeName::try_from(response.reverse_edge_name)
diff --git a/src/rust/grapl-tracing/src/setup_tracing.rs b/src/rust/grapl-tracing/src/setup_tracing.rs
index fe98668abc..067c7187fa 100644
--- a/src/rust/grapl-tracing/src/setup_tracing.rs
+++ b/src/rust/grapl-tracing/src/setup_tracing.rs
@@ -24,6 +24,8 @@ pub fn setup_tracing(service_name: &str) -> Result<WorkerGuard, SetupTracingErro
 
     // initialize json logging layer
     let log_layer = tracing_subscriber::fmt::layer()
+        .with_file(true)
+        .with_line_number(true)
         .json()
         .with_writer(non_blocking);
 
diff --git a/src/rust/node-identifier/Cargo.toml b/src/rust/node-identifier/Cargo.toml
index 8c6c6773ad..ec3994fd55 100644
--- a/src/rust/node-identifier/Cargo.toml
+++ b/src/rust/node-identifier/Cargo.toml
@@ -25,20 +25,21 @@ clap = { version = "3.0", default_features = false, features = [
 ] }
 failure = "0.1"
 futures = "0.3"
+grapl-utils = { path = "../grapl-utils" }
 grapl-config = { path = "../grapl-config" }
 grapl-tracing = { path = "../grapl-tracing" }
-grapl-utils = { path = "../grapl-utils" }
 hex = "0.4"
 hmap = "0.1"
-kafka = { path = "../kafka" }
+kafka = { path = "../kafka", version = "*" }
 rusoto_core = { version = "0.47", default_features = false, features = [
   "rustls"
 ] }
-rusoto_credential = "0.47"
 rusoto_dynamodb = { version = "0.47", default_features = false, features = [
   "rustls"
 ] }
+rusoto_credential = "0.47"
 rust-proto = { path = "../rust-proto", version = "*" }
+uid-allocator = { path = "../uid-allocator" }
 serde = "1.0"
 serde_dynamodb = { version = "0.9", default_features = false, features = [
   "rustls"
@@ -54,11 +55,12 @@ tokio = { version = "1.17", features = [
 ] }
 tracing = "0.1"
 uuid = { version = "1.0", features = ["v4"] }
+blake2 = "0.10.4"
+eyre = "0.6.8"
 
 [dev-dependencies]
 async-trait = "0.1"
-eyre = "0.6"
+test-context = "0.1"
 e2e-tests = { path = "../e2e-tests" }
 kafka = { path = "../kafka", features = ["test-utils"] }
 rand = "0.8"
-test-context = "0.1"
diff --git a/src/rust/node-identifier/src/config.rs b/src/rust/node-identifier/src/config.rs
new file mode 100644
index 0000000000..e5c3b24246
--- /dev/null
+++ b/src/rust/node-identifier/src/config.rs
@@ -0,0 +1,12 @@
+use rust_proto::client_factory::services::GraphMutationClientConfig;
+
+#[derive(clap::Parser, Debug)]
+#[clap(name = "node-identifier", about = "Node Identifier Service")]
+pub struct NodeIdentifierConfig {
+    #[clap(long, env)]
+    pub grapl_static_mapping_table: String,
+    #[clap(long, env)]
+    pub grapl_dynamic_session_table: String,
+    #[clap(flatten)]
+    pub graph_mutation_client_config: GraphMutationClientConfig,
+}
diff --git a/src/rust/node-identifier/src/dynamic_sessiondb.rs b/src/rust/node-identifier/src/dynamic_sessiondb.rs
index 174c080fa5..9b4d23763d 100644
--- a/src/rust/node-identifier/src/dynamic_sessiondb.rs
+++ b/src/rust/node-identifier/src/dynamic_sessiondb.rs
@@ -3,43 +3,31 @@ use failure::{
     Error,
 };
 use rusoto_dynamodb::DynamoDb;
-use rust_proto::graplinc::grapl::api::graph::v1beta1::{
-    NodeDescription,
-    Session,
-    Static,
-    Strategy,
-};
-use serde::{
-    Deserialize,
-    Serialize,
-};
-use sha2::{
-    Digest,
-    Sha256,
+use rust_proto::graplinc::grapl::api::{
+    graph::v1beta1::{
+        IdentifiedNode,
+        NodeDescription,
+        Session,
+        Static,
+        Strategy,
+    },
+    graph_mutation::v1beta1::client::GraphMutationClient,
 };
 
 use crate::{
     sessiondb::SessionDb,
     sessions::UnidSession,
+    StaticMappingDb,
 };
 
-#[derive(Debug, Serialize, Deserialize)]
-pub(crate) struct ResolvedMapping {
-    pub mapping: String,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub(crate) struct DirectMapping {
-    pub pseudo_key: String,
-    pub mapping: String,
-}
-
 #[derive(Debug, Clone)]
 pub(crate) struct NodeDescriptionIdentifier<D>
 where
     D: DynamoDb,
 {
     dyn_session_db: SessionDb<D>,
+    graph_mutation_client: GraphMutationClient,
+    static_mapping_db: StaticMappingDb<D>,
     should_guess: bool,
 }
 
@@ -47,31 +35,37 @@ impl<D> NodeDescriptionIdentifier<D>
 where
     D: DynamoDb,
 {
-    pub fn new(dyn_session_db: SessionDb<D>, should_guess: bool) -> Self {
+    pub fn new(
+        dyn_session_db: SessionDb<D>,
+        graph_mutation_client: GraphMutationClient,
+        static_mapping_db: StaticMappingDb<D>,
+        should_guess: bool,
+    ) -> Self {
         Self {
             dyn_session_db,
+            graph_mutation_client,
+            static_mapping_db,
             should_guess,
         }
     }
 
     #[tracing::instrument(skip(self, node, strategy), err)]
-    async fn primary_session_key(
+    async fn pseudo_key(
         &self,
-        tenant_id: uuid::Uuid,
         node: &mut NodeDescription,
         strategy: &Session,
     ) -> Result<String, Error> {
-        let mut primary_key = tenant_id.urn().to_string();
-        primary_key.reserve(32);
+        let mut pseudo_key = String::with_capacity(32);
 
         if strategy.primary_key_requires_asset_id {
             panic!("asset_id resolution is currently not supported")
         }
+
         for prop_name in &strategy.primary_key_properties {
             let prop_val = node.properties.get(prop_name);
 
             match prop_val {
-                Some(val) => primary_key.push_str(&val.to_string()),
+                Some(val) => pseudo_key.push_str(&val.to_string()),
                 None => bail!(format!(
                     "Node is missing required property {} for identity",
                     prop_name
@@ -80,67 +74,40 @@ where
         }
 
         // Push node type, as a natural partition
-        primary_key.push_str(&node.node_key);
-        Ok(primary_key)
-    }
-
-    /// Because statically identified nodes are uniquely identifiable based on their static properties
-    /// we can avoid fetching from dynamodb and calculate a node key by hashing the properties deterministically
-    #[tracing::instrument(skip(self, node, strategy), err)]
-    fn get_static_node_key(
-        &self,
-        tenant_id: uuid::Uuid,
-        node: &NodeDescription,
-        strategy: &Static,
-    ) -> Result<String, Error> {
-        let mut hasher = Sha256::new();
-
-        // first, let's sort the properties, so we get a consistent ordering for hashing
-        let mut sorted_key_properties = strategy.primary_key_properties.clone();
-        sorted_key_properties.sort();
+        pseudo_key.push_str(&node.node_type);
 
-        for prop_name in sorted_key_properties {
-            match node.properties.get(&prop_name) {
-                Some(prop_val) => hasher.update(prop_val.to_string().as_bytes()),
-                None => bail!(format!(
-                    "Node is missing required property {} for identity",
-                    prop_name
-                )),
-            }
-        }
-
-        hasher.update(node.node_type.as_bytes());
-        hasher.update(tenant_id.as_bytes());
-        Ok(hex::encode(hasher.finalize()))
+        Ok(pseudo_key)
     }
 
     #[tracing::instrument(skip(self, strategy), err)]
     pub(crate) async fn attribute_dynamic_session(
         &self,
         tenant_id: uuid::Uuid,
-        node: NodeDescription,
+        node: &NodeDescription,
         strategy: &Session,
-    ) -> Result<NodeDescription, Error> {
+    ) -> Result<IdentifiedNode, Error> {
         let mut attributed_node = node.clone();
 
-        let primary_key = self
-            .primary_session_key(tenant_id, &mut attributed_node, strategy)
-            .await?;
+        let pseudo_key = self.pseudo_key(&mut attributed_node, strategy).await?;
 
         let created_time = strategy.create_time;
         let last_seen_time = strategy.last_seen_time;
 
         let unid = match (created_time != 0, last_seen_time != 0) {
-            (true, _) => UnidSession {
-                pseudo_key: primary_key,
-                timestamp: created_time,
-                is_creation: true,
-            },
-            (_, true) => UnidSession {
-                pseudo_key: primary_key,
-                timestamp: last_seen_time,
-                is_creation: false,
-            },
+            (true, _) => UnidSession::new(
+                tenant_id,
+                attributed_node.node_type,
+                &pseudo_key,
+                created_time,
+                true,
+            ),
+            (_, true) => UnidSession::new(
+                tenant_id,
+                attributed_node.node_type,
+                &pseudo_key,
+                last_seen_time,
+                false,
+            ),
             _ => bail!(
                 "Terminating sessions not yet supported: {:?} {:?}",
                 node.properties,
@@ -150,25 +117,38 @@ where
 
         let session_id = self
             .dyn_session_db
-            .handle_unid_session(unid, self.should_guess)
+            .handle_unid_session(
+                tenant_id,
+                unid,
+                &self.graph_mutation_client,
+                self.should_guess,
+            )
             .await?;
 
-        attributed_node.node_key = session_id;
-
-        Ok(attributed_node)
+        Ok(IdentifiedNode {
+            properties: node.properties.clone(),
+            uid: session_id,
+            node_type: node.node_type.to_string(),
+        })
     }
 
     #[tracing::instrument(skip(self, node, strategy), err)]
     pub(crate) async fn attribute_static_mapping(
         &self,
         tenant_id: uuid::Uuid,
-        mut node: NodeDescription,
+        node: &NodeDescription,
         strategy: &Static,
-    ) -> Result<NodeDescription, Error> {
-        let static_node_key = self.get_static_node_key(tenant_id, &node, strategy)?;
-        node.set_key(static_node_key);
+    ) -> Result<IdentifiedNode, Error> {
+        let uid = self
+            .static_mapping_db
+            .map_unid(tenant_id, node, strategy)
+            .await?;
 
-        Ok(node)
+        Ok(IdentifiedNode {
+            properties: node.properties.clone(),
+            uid,
+            node_type: node.node_type.to_string(),
+        })
     }
 
     #[tracing::instrument(skip(self, node), err)]
@@ -176,25 +156,20 @@ where
         &self,
         tenant_id: uuid::Uuid,
         node: &NodeDescription,
-    ) -> Result<NodeDescription, Error> {
-        let mut attributed_node = node.clone();
+    ) -> Result<IdentifiedNode, Error> {
         let strategy = &node.id_strategy[0];
 
         match strategy.strategy {
             Strategy::Session(ref strategy) => {
                 tracing::info!("Attributing dynamic node via session");
-                attributed_node = self
-                    .attribute_dynamic_session(tenant_id, attributed_node, strategy)
-                    .await?;
+                self.attribute_dynamic_session(tenant_id, node, strategy)
+                    .await
             }
             Strategy::Static(ref strategy) => {
                 tracing::info!("Attributing dynamic node via static mapping");
-                attributed_node = self
-                    .attribute_static_mapping(tenant_id, attributed_node, strategy)
-                    .await?;
+                self.attribute_static_mapping(tenant_id, node, strategy)
+                    .await
             }
         }
-
-        Ok(attributed_node)
     }
 }
diff --git a/src/rust/node-identifier/src/error.rs b/src/rust/node-identifier/src/error.rs
index 83066ad2b8..df30806146 100644
--- a/src/rust/node-identifier/src/error.rs
+++ b/src/rust/node-identifier/src/error.rs
@@ -1,4 +1,5 @@
 use grapl_tracing::SetupTracingError;
+use rust_proto::graplinc::grapl::api::uid_allocator::v1beta1::client::UidAllocatorServiceClientError;
 use thiserror::Error;
 
 #[non_exhaustive]
@@ -19,6 +20,9 @@ pub(crate) enum NodeIdentifierError {
     #[error("kafka configuration error {0}")]
     KafkaConfigurationError(#[from] kafka::ConfigurationError),
 
+    #[error("UidAllocatorServiceClientError {0}")]
+    UidAllocatorServiceClientError(#[from] UidAllocatorServiceClientError),
+
     #[error("failed to configure tracing {0}")]
     SetupTracingError(#[from] SetupTracingError),
 }
diff --git a/src/rust/node-identifier/src/lib.rs b/src/rust/node-identifier/src/lib.rs
index b02a45dd12..8b5a013c7f 100644
--- a/src/rust/node-identifier/src/lib.rs
+++ b/src/rust/node-identifier/src/lib.rs
@@ -1,2 +1,6 @@
+#![recursion_limit = "256"]
+
+pub mod config;
 pub mod sessiondb;
 pub mod sessions;
+pub mod static_mapping_db;
diff --git a/src/rust/node-identifier/src/main.rs b/src/rust/node-identifier/src/main.rs
index f523cb2378..56b70d152e 100644
--- a/src/rust/node-identifier/src/main.rs
+++ b/src/rust/node-identifier/src/main.rs
@@ -16,49 +16,71 @@ use kafka::{
     StreamProcessorError,
 };
 use rusoto_dynamodb::DynamoDbClient;
-use rust_proto::graplinc::grapl::{
-    api::graph::v1beta1::{
-        GraphDescription,
-        IdentifiedGraph,
+use rust_proto::{
+    graplinc::grapl::{
+        api::{
+            graph::v1beta1::{
+                GraphDescription,
+                IdentifiedGraph,
+            },
+            graph_mutation::v1beta1::client::GraphMutationClient,
+        },
+        pipeline::v1beta1::Envelope,
     },
-    pipeline::v1beta1::Envelope,
+    protocol::service_client::ConnectWithConfig,
 };
 use tracing::{
     instrument::WithSubscriber,
     Instrument,
 };
 
-mod dynamic_sessiondb;
-mod error;
-mod node_identifier;
-mod sessiondb;
-mod sessions;
-
 use crate::{
+    config::NodeIdentifierConfig,
     dynamic_sessiondb::NodeDescriptionIdentifier,
     error::NodeIdentifierError,
     node_identifier::NodeIdentifier,
     sessiondb::SessionDb,
+    static_mapping_db::StaticMappingDb,
 };
 
+mod config;
+mod dynamic_sessiondb;
+mod error;
+mod node_identifier;
+mod sessiondb;
+mod sessions;
+mod static_mapping_db;
+
 const SERVICE_NAME: &'static str = "node-identifier";
 
 #[tokio::main]
-async fn main() -> Result<(), NodeIdentifierError> {
+async fn main() -> eyre::Result<()> {
     let _guard = setup_tracing(SERVICE_NAME)?;
 
     handler().await
 }
 
 #[tracing::instrument]
-async fn handler() -> Result<(), NodeIdentifierError> {
+async fn handler() -> eyre::Result<()> {
+    let service_config = NodeIdentifierConfig::parse();
     let dynamo = DynamoDbClient::from_env();
-    let dyn_session_db = SessionDb::new(
+    let dyn_session_db = SessionDb::new(dynamo.clone(), service_config.grapl_dynamic_session_table);
+    let graph_mutation_client =
+        GraphMutationClient::connect_with_config(service_config.graph_mutation_client_config)
+            .await?;
+
+    let static_mapping_db = StaticMappingDb::new(
         dynamo.clone(),
-        std::env::var("GRAPL_DYNAMIC_SESSION_TABLE")?,
+        graph_mutation_client.clone(),
+        service_config.grapl_static_mapping_table,
     );
-    let node_identifier = NodeIdentifier::new(NodeDescriptionIdentifier::new(dyn_session_db, true));
 
+    let node_identifier = NodeIdentifier::new(NodeDescriptionIdentifier::new(
+        dyn_session_db,
+        graph_mutation_client,
+        static_mapping_db,
+        true,
+    ));
     let consumer_config = ConsumerConfig::parse();
     let producer_config = ProducerConfig::parse();
 
diff --git a/src/rust/node-identifier/src/node_identifier.rs b/src/rust/node-identifier/src/node_identifier.rs
index 860c380e86..039b41c102 100644
--- a/src/rust/node-identifier/src/node_identifier.rs
+++ b/src/rust/node-identifier/src/node_identifier.rs
@@ -2,11 +2,14 @@ use std::collections::HashMap;
 
 use failure::Error;
 use grapl_utils::rusoto_ext::dynamodb::GraplDynamoDbClientExt;
-use rust_proto::graplinc::grapl::api::graph::v1beta1::{
-    GraphDescription,
-    IdentifiedGraph,
-    IdentifiedNode,
-    NodeDescription,
+use rust_proto::graplinc::grapl::{
+    api::graph::v1beta1::{
+        GraphDescription,
+        IdentifiedGraph,
+        IdentifiedNode,
+        NodeDescription,
+    },
+    common::v1beta1::types::Uid,
 };
 use tap::tap::TapOptional;
 
@@ -51,11 +54,9 @@ where
         tenant_id: uuid::Uuid,
         node: &NodeDescription,
     ) -> Result<IdentifiedNode, Error> {
-        let new_node = self
-            .dynamic_identifier
+        self.dynamic_identifier
             .attribute_dynamic_node(tenant_id, node)
-            .await?;
-        Ok(new_node.into())
+            .await
     }
 
     /// Performs batch identification of unidentified nodes into identified
@@ -70,7 +71,7 @@ where
         tenant_id: uuid::Uuid,
         unidentified_subgraph: &GraphDescription,
         identified_graph: &mut IdentifiedGraph,
-    ) -> (HashMap<String, String>, Option<failure::Error>) {
+    ) -> (HashMap<String, Uid>, Option<failure::Error>) {
         let mut identified_nodekey_map = HashMap::new();
         let mut attribution_failure = None;
 
@@ -90,10 +91,7 @@ where
                 }
             };
 
-            identified_nodekey_map.insert(
-                unidentified_node_key.to_owned(),
-                identified_node.clone_node_key(),
-            );
+            identified_nodekey_map.insert(unidentified_node_key.to_owned(), identified_node.uid);
             identified_graph.add_node(identified_node);
         }
 
@@ -114,7 +112,7 @@ where
         &self,
         unidentified_subgraph: &GraphDescription,
         identified_graph: &mut IdentifiedGraph,
-        identified_nodekey_map: HashMap<String, String>,
+        identified_nodekey_map: HashMap<String, Uid>,
     ) {
         let identified_node_edges = unidentified_subgraph.edges.iter()
             // filter out all edges for nodes that were not identified (also gets our from_key)
diff --git a/src/rust/node-identifier/src/sessiondb.rs b/src/rust/node-identifier/src/sessiondb.rs
index d51c8c1879..9d6c3b1137 100644
--- a/src/rust/node-identifier/src/sessiondb.rs
+++ b/src/rust/node-identifier/src/sessiondb.rs
@@ -19,6 +19,16 @@ use rusoto_dynamodb::{
     TransactWriteItemsInput,
     UpdateItemInput,
 };
+use rust_proto::graplinc::grapl::{
+    api::graph_mutation::v1beta1::{
+        client::GraphMutationClient,
+        messages::CreateNodeRequest,
+    },
+    common::v1beta1::types::{
+        NodeType,
+        Uid,
+    },
+};
 use tracing::{
     info,
     warn,
@@ -340,8 +350,13 @@ where
         Ok(())
     }
 
-    #[tracing::instrument(skip(self, unid), err)]
-    pub(crate) async fn handle_creation_event(&self, unid: UnidSession) -> Result<String, Error> {
+    #[tracing::instrument(skip(self, unid, graph_mutation_client), err)]
+    pub(crate) async fn handle_creation_event(
+        &self,
+        tenant_id: Uuid,
+        unid: UnidSession,
+        graph_mutation_client: &GraphMutationClient,
+    ) -> Result<Uid, Error> {
         info!(
             message="Handling unid session creation",
             pseudo_key=?unid.pseudo_key, timestamp=?unid.timestamp
@@ -351,6 +366,10 @@ where
         let session = self.find_first_session_after(&unid).await?;
 
         if let Some(session) = session {
+            let session_id = match Uid::from_u64(session.session_id) {
+                Some(uid) => uid,
+                None => bail!("Invalid session_id: {}", session.session_id),
+            };
             // If session.is_create_canon is false,
             // This means that there is a 'Guessed' session in the future,
             // and we should consider this the canonical ID for that session
@@ -358,7 +377,7 @@ where
                 info!(message = "Extending session create_time");
                 self.update_session_create_time(&session, unid.timestamp, true)
                     .await?;
-                return Ok(session.session_id);
+                return Ok(session_id);
             }
 
             tracing::debug!(
@@ -373,7 +392,7 @@ where
             // with an accurate timestamp
             if skewed_cmp(unid.timestamp, session.create_time) {
                 info!(message = "Found existing session with exact create time");
-                return Ok(session.session_id);
+                return Ok(session_id);
             }
 
             // We should never be looking at a case where the query returned
@@ -406,9 +425,20 @@ where
             }
         }
 
+        let mut graph_mutation_client = graph_mutation_client.clone();
+        let uid = graph_mutation_client
+            .create_node(CreateNodeRequest {
+                tenant_id,
+                node_type: NodeType {
+                    value: unid.node_type.clone(),
+                },
+            })
+            .await?
+            .uid;
+
         // Create new session, return new session id
         let session = Session {
-            session_id: Uuid::new_v4().to_string(),
+            session_id: uid.as_u64(),
             create_time: unid.timestamp,
             end_time: unid.timestamp + 101,
             is_create_canon: true,
@@ -419,15 +449,17 @@ where
 
         info!(message = "Creating session");
         self.create_session(&session).await?;
-        Ok(session.session_id)
+        Ok(uid)
     }
 
-    #[tracing::instrument(skip(self, unid), err)]
+    #[tracing::instrument(skip(self, unid, graph_mutation_client), err)]
     pub(crate) async fn handle_last_seen(
         &self,
+        tenant_id: Uuid,
         unid: UnidSession,
+        graph_mutation_client: &GraphMutationClient,
         should_default: bool,
-    ) -> Result<String, Error> {
+    ) -> Result<Uid, Error> {
         info!(
             message="Handling unid session",
             pseudo_key=?unid.pseudo_key, timestamp=?unid.timestamp
@@ -437,9 +469,13 @@ where
         // Look for last session where session.create_time <= unid.create_time
         let session = self.find_last_session_before(&unid).await?;
         if let Some(mut session) = session {
+            let session_id = match Uid::from_u64(session.session_id) {
+                Some(uid) => uid,
+                None => bail!("Invalid session_id: {}", session.session_id),
+            };
             if unid.timestamp < session.end_time || skewed_cmp(unid.timestamp, session.end_time) {
                 info!(message = "Identified session because it fell within a timeline.");
-                return Ok(session.session_id);
+                return Ok(session_id);
             }
 
             if !session.is_end_canon {
@@ -447,7 +483,7 @@ where
                 info!(message = "Updating session end_time.");
                 //                self.update_session_end_time(&session, unid.timestamp, false)?;
 
-                return Ok(session.session_id);
+                return Ok(session_id);
             }
         }
 
@@ -455,18 +491,33 @@ where
         if let Some(session) = session {
             if !session.is_create_canon {
                 info!(message = "Found a later, non canonical session. Extending create_time.");
+                let session_id = match Uid::from_u64(session.session_id) {
+                    Some(uid) => uid,
+                    None => bail!("Invalid session_id: {}", session.session_id),
+                };
 
                 self.update_session_create_time(&session, unid.timestamp, false)
                     .await?;
-                return Ok(session.session_id);
+                return Ok(session_id);
             }
         }
 
         if should_default {
             info!(message = "Defaulting and creating new session.");
-            let session_id = Uuid::new_v4().to_string();
+
+            let mut graph_mutation_client = graph_mutation_client.clone();
+            let uid = graph_mutation_client
+                .create_node(CreateNodeRequest {
+                    tenant_id,
+                    node_type: NodeType {
+                        value: unid.node_type.clone(),
+                    },
+                })
+                .await?
+                .uid;
+
             let session = Session {
-                session_id: session_id.clone(),
+                session_id: uid.as_u64(),
                 create_time: unid.timestamp,
                 end_time: unid.timestamp + 101,
                 is_create_canon: false,
@@ -476,7 +527,7 @@ where
             };
             self.create_session(&session).await?;
 
-            Ok(session_id)
+            Ok(uid)
         } else {
             warn!(message = "Could not attribute session. Not defaulting.");
             bail!(
@@ -487,17 +538,21 @@ where
     }
 
     // exposed for integration tests
-    #[tracing::instrument(skip(self), err)]
+    #[tracing::instrument(skip(self, graph_mutation_client), err)]
     pub async fn handle_unid_session(
         &self,
+        tenant_id: Uuid,
         mut unid: UnidSession,
+        graph_mutation_client: &GraphMutationClient,
         should_default: bool,
-    ) -> Result<String, Error> {
+    ) -> Result<Uid, Error> {
         unid.timestamp = shave_int(unid.timestamp, 1);
         if unid.is_creation {
-            self.handle_creation_event(unid).await
+            self.handle_creation_event(tenant_id, unid, graph_mutation_client)
+                .await
         } else {
-            self.handle_last_seen(unid, should_default).await
+            self.handle_last_seen(tenant_id, unid, graph_mutation_client, should_default)
+                .await
         }
     }
 }
diff --git a/src/rust/node-identifier/src/sessions.rs b/src/rust/node-identifier/src/sessions.rs
index 23aba45c01..c8ccad9bf7 100644
--- a/src/rust/node-identifier/src/sessions.rs
+++ b/src/rust/node-identifier/src/sessions.rs
@@ -3,6 +3,11 @@ use std::{
     convert::TryFrom,
 };
 
+use blake2::{
+    digest::consts::U16,
+    Blake2b,
+    Digest,
+};
 use failure::Error;
 use rusoto_dynamodb::AttributeValue;
 use serde::{
@@ -10,17 +15,42 @@ use serde::{
     Serialize,
 };
 
-#[derive(Debug, Serialize, Deserialize)]
+type Blake2b16 = Blake2b<U16>;
+
+#[derive(Debug)]
 pub struct UnidSession {
     pub pseudo_key: String,
+    pub node_type: String,
     pub timestamp: u64,
     pub is_creation: bool, // Is this a creation event
 }
 
+impl UnidSession {
+    pub fn new(
+        tenant_id: uuid::Uuid,
+        node_type: String,
+        pseudo_key: &str,
+        timestamp: u64,
+        is_creation: bool,
+    ) -> Self {
+        let mut hasher = Blake2b16::new();
+        hasher.update(tenant_id.as_bytes());
+        hasher.update(node_type.as_bytes());
+        hasher.update(pseudo_key.as_bytes());
+        let pseudo_key = hex::encode(hasher.finalize());
+        Self {
+            pseudo_key,
+            node_type,
+            timestamp,
+            is_creation,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Session {
-    pub session_id: String,
-    pub pseudo_key: String, // hostname-pid
+    pub session_id: u64,
+    pub pseudo_key: String,
     pub create_time: u64,
     pub end_time: u64,
     pub is_create_canon: bool,
diff --git a/src/rust/node-identifier/src/static_mapping_db.rs b/src/rust/node-identifier/src/static_mapping_db.rs
new file mode 100644
index 0000000000..a5bb896147
--- /dev/null
+++ b/src/rust/node-identifier/src/static_mapping_db.rs
@@ -0,0 +1,198 @@
+use std::{
+    collections::HashMap,
+    fmt::Debug,
+};
+
+use blake2::{
+    digest::consts::U16,
+    Blake2b,
+    Digest,
+};
+use rusoto_dynamodb::{
+    AttributeValue,
+    DynamoDb,
+    GetItemInput,
+    GetItemOutput,
+    PutItemInput,
+    PutItemOutput,
+};
+use rust_proto::graplinc::grapl::{
+    api::{
+        graph::v1beta1::{
+            NodeDescription,
+            Static,
+        },
+        graph_mutation::v1beta1::{
+            client::GraphMutationClient,
+            messages::CreateNodeRequest,
+        },
+    },
+    common::v1beta1::types::{
+        NodeType,
+        Uid,
+    },
+};
+
+type Blake2b16 = Blake2b<U16>;
+
+#[derive(Clone)]
+pub struct StaticMappingDb<D> {
+    static_mapping_db: D,
+    graph_mutation_client: GraphMutationClient,
+    table_name: String,
+}
+
+impl<D> Debug for StaticMappingDb<D> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("StaticMappingDb")
+            .field("graph_mutation_client", &self.graph_mutation_client)
+            .field("table_name", &self.table_name)
+            .finish()
+    }
+}
+
+impl<D> StaticMappingDb<D>
+where
+    D: DynamoDb,
+{
+    pub fn new(
+        static_mapping_db: D,
+        uid_allocator_client: GraphMutationClient,
+        table_name: String,
+    ) -> Self {
+        Self {
+            static_mapping_db,
+            graph_mutation_client: uid_allocator_client,
+            table_name,
+        }
+    }
+
+    pub async fn map_unid(
+        &self,
+        tenant_id: uuid::Uuid,
+        node: &NodeDescription,
+        strategy: &Static,
+    ) -> Result<Uid, failure::Error> {
+        let static_id = get_static_id(tenant_id, node, strategy)?;
+
+        if let Some(uid) = self.retrieve_uid_from_dynamodb(static_id.clone()).await? {
+            return Ok(uid);
+        }
+
+        let mut graph_mutation_client = self.graph_mutation_client.clone();
+        let uid = graph_mutation_client
+            .create_node(CreateNodeRequest {
+                tenant_id,
+                node_type: NodeType {
+                    value: node.node_type.clone(),
+                },
+            })
+            .await?
+            .uid;
+
+        // todo: Retry this operation if it fails.
+        self.store_uid_in_dynamodb(static_id, uid).await?;
+
+        Ok(uid)
+    }
+
+    pub async fn store_uid_in_dynamodb(
+        &self,
+        static_id: String,
+        uid: Uid,
+    ) -> Result<(), failure::Error> {
+        let key = HashMap::from([
+            (
+                "static_id".to_string(),
+                AttributeValue {
+                    s: Some(static_id),
+                    ..Default::default()
+                },
+            ),
+            (
+                "uid".to_string(),
+                AttributeValue {
+                    n: Some(uid.as_u64().to_string()),
+                    ..Default::default()
+                },
+            ),
+        ]);
+
+        // todo: Consider a `condition_expression` here to ensure the uid is not already set
+        //       and if it is, retrieve it from dynamodb instead
+        let _response: PutItemOutput = self
+            .static_mapping_db
+            .put_item(PutItemInput {
+                table_name: self.table_name.clone(),
+                item: key,
+                ..Default::default()
+            })
+            .await?;
+
+        Ok(())
+    }
+
+    pub async fn retrieve_uid_from_dynamodb(
+        &self,
+        static_id: String,
+    ) -> Result<Option<Uid>, failure::Error> {
+        let key = HashMap::from([(
+            "static_id".to_string(),
+            AttributeValue {
+                s: Some(static_id),
+                ..Default::default()
+            },
+        )]);
+        let response: GetItemOutput = self
+            .static_mapping_db
+            .get_item(GetItemInput {
+                attributes_to_get: Some(vec!["uid".to_string()]),
+                consistent_read: Some(true),
+                key,
+                table_name: self.table_name.clone(),
+                ..GetItemInput::default()
+            })
+            .await?;
+
+        let mut item = match response.item {
+            Some(item) => item,
+            None => return Ok(None),
+        };
+
+        Ok(item
+            .remove("uid")
+            .and_then(|v| v.n)
+            .map(|uid| uid.parse::<u64>().map(|uid| Uid::from_u64(uid).unwrap()))
+            .transpose()?)
+    }
+}
+
+/// Because statically identified nodes are uniquely identifiable based on their static properties
+/// we can avoid fetching from dynamodb and calculate a node key by hashing the properties deterministically
+#[tracing::instrument(skip(node, strategy), err)]
+fn get_static_id(
+    tenant_id: uuid::Uuid,
+    node: &NodeDescription,
+    strategy: &Static,
+) -> Result<String, failure::Error> {
+    let mut hasher = Blake2b16::new();
+    hasher.update(tenant_id.as_bytes());
+
+    // first, let's sort the properties, so we get a consistent ordering for hashing
+    let mut sorted_key_properties = strategy.primary_key_properties.clone();
+    sorted_key_properties.sort();
+
+    for prop_name in sorted_key_properties {
+        match node.properties.get(&prop_name) {
+            Some(prop_val) => hasher.update(prop_val.to_string().as_bytes()),
+            None => failure::bail!(format!(
+                "Node is missing required property {} for identity",
+                prop_name
+            )),
+        }
+    }
+
+    hasher.update(node.node_type.as_bytes());
+
+    Ok(hex::encode(hasher.finalize()))
+}
diff --git a/src/rust/plugin-execution-sidecar/analyzer-execution-sidecar.rs b/src/rust/plugin-execution-sidecar/analyzer-execution-sidecar.rs
index 488342065b..9aa50ed0f3 100644
--- a/src/rust/plugin-execution-sidecar/analyzer-execution-sidecar.rs
+++ b/src/rust/plugin-execution-sidecar/analyzer-execution-sidecar.rs
@@ -1,4 +1,28 @@
+use clap::Parser;
+use grapl_tracing::setup_tracing;
+use plugin_execution_sidecar::{
+    config::PluginExecutorConfig,
+    plugin_executor::PluginExecutor,
+    work::AnalyzerWorkProcessor,
+};
+
+const SERVICE_NAME: &'static str = "analyzer-execution-sidecar";
+
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
-    todo!();
+    let _guard = setup_tracing(SERVICE_NAME)?;
+    let plugin_executor_config = PluginExecutorConfig::parse();
+
+    tracing::info!("logging configured successfully");
+
+    // Give the plugin a little time to become available.
+    tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+
+    let analyzer_work_processor = AnalyzerWorkProcessor::new(&plugin_executor_config).await?;
+    let mut plugin_executor =
+        PluginExecutor::new(plugin_executor_config, analyzer_work_processor).await?;
+
+    tracing::info!("starting analyzer executor");
+
+    plugin_executor.main_loop().await
 }
diff --git a/src/rust/plugin-execution-sidecar/src/work.rs b/src/rust/plugin-execution-sidecar/src/work.rs
index d16d8df223..6043208102 100644
--- a/src/rust/plugin-execution-sidecar/src/work.rs
+++ b/src/rust/plugin-execution-sidecar/src/work.rs
@@ -4,5 +4,8 @@ pub use plugin_work_processor::{
     Workload,
 };
 
+mod analyzer_work_processor;
+pub use analyzer_work_processor::AnalyzerWorkProcessor;
+
 mod generator_work_processor;
 pub use generator_work_processor::GeneratorWorkProcessor;
diff --git a/src/rust/plugin-execution-sidecar/src/work/analyzer_work_processor.rs b/src/rust/plugin-execution-sidecar/src/work/analyzer_work_processor.rs
new file mode 100644
index 0000000000..d8aafec1f2
--- /dev/null
+++ b/src/rust/plugin-execution-sidecar/src/work/analyzer_work_processor.rs
@@ -0,0 +1,160 @@
+use rust_proto::{
+    client_factory::{
+        build_grpc_client,
+        services::AnalyzerClientConfig,
+    },
+    graplinc::grapl::api::{
+        plugin_sdk::analyzers::v1beta1::{
+            client::AnalyzerServiceClient,
+            messages::{
+                ExecutionResult,
+                RunAnalyzerRequest,
+                Update,
+            },
+        },
+        plugin_work_queue::v1beta1::{
+            AcknowledgeAnalyzerRequest,
+            ExecutionJob,
+            GetExecuteAnalyzerRequest,
+            GetExecuteAnalyzerResponse,
+            PluginWorkQueueServiceClient,
+        },
+    },
+    SerDe,
+};
+use uuid::Uuid;
+
+use super::{
+    plugin_work_processor::{
+        PluginWorkProcessorError,
+        RequestId,
+        Workload,
+    },
+    PluginWorkProcessor,
+};
+use crate::config::PluginExecutorConfig;
+
+impl Workload for GetExecuteAnalyzerResponse {
+    fn request_id(&self) -> i64 {
+        self.request_id()
+    }
+
+    fn maybe_job(self) -> Option<ExecutionJob> {
+        self.execution_job()
+    }
+}
+
+pub struct AnalyzerWorkProcessor {
+    analyzer_service_client: AnalyzerServiceClient,
+}
+
+impl AnalyzerWorkProcessor {
+    pub async fn new(config: &PluginExecutorConfig) -> Result<Self, Box<dyn std::error::Error>> {
+        let plugin_id = config.plugin_id;
+        let upstream_addr_env_var = format!("NOMAD_UPSTREAM_ADDR_plugin-{plugin_id}");
+        let upstream_addr = std::env::var(&upstream_addr_env_var).expect(&upstream_addr_env_var);
+        let address = format!("http://{upstream_addr}");
+
+        tracing::info!(message = "connecting to analyzer plugin", address = address);
+
+        let client_config = AnalyzerClientConfig {
+            analyzer_client_address: address.parse().expect("analyzer client address"),
+        };
+
+        let analyzer_service_client = build_grpc_client(client_config).await?;
+
+        Ok(AnalyzerWorkProcessor {
+            analyzer_service_client,
+        })
+    }
+}
+
+#[async_trait::async_trait]
+impl PluginWorkProcessor for AnalyzerWorkProcessor {
+    type Work = GetExecuteAnalyzerResponse;
+    type ProducedMessage = ExecutionResult;
+
+    async fn get_work(
+        &self,
+        config: &PluginExecutorConfig,
+        pwq_client: &mut PluginWorkQueueServiceClient,
+    ) -> Result<Self::Work, PluginWorkProcessorError> {
+        let plugin_id = config.plugin_id;
+        let response = pwq_client
+            .get_execute_analyzer(GetExecuteAnalyzerRequest::new(plugin_id))
+            .await?;
+        let request_id = response.request_id();
+        let response_retval = response.clone();
+
+        if let Some(execution_job) = response.execution_job() {
+            let tenant_id = execution_job.tenant_id();
+            let trace_id = execution_job.trace_id();
+            let event_source_id = execution_job.event_source_id();
+
+            tracing::debug!(
+                message = "retrieved execution job",
+                tenant_id =% tenant_id,
+                trace_id =% trace_id,
+                event_source_id =% event_source_id,
+                plugin_id =% plugin_id,
+                request_id =? request_id,
+            );
+        } else {
+            tracing::debug!(
+                message = "found no execution jobs",
+                plugin_id =% plugin_id,
+            )
+        }
+
+        Ok(response_retval)
+    }
+
+    async fn ack_work(
+        &self,
+        config: &PluginExecutorConfig,
+        pwq_client: &mut PluginWorkQueueServiceClient,
+        process_result: Result<Self::ProducedMessage, PluginWorkProcessorError>,
+        request_id: RequestId,
+        tenant_id: Uuid,
+        trace_id: Uuid,
+        event_source_id: Uuid,
+    ) -> Result<(), PluginWorkProcessorError> {
+        let plugin_id = config.plugin_id;
+
+        tracing::debug!(
+            message = "acknowledging analyzer work",
+            tenant_id =% tenant_id,
+            trace_id =% trace_id,
+            event_source_id =% event_source_id,
+            plugin_id =% plugin_id,
+            request_id =? request_id,
+        );
+
+        let execution_hit = process_result.ok();
+        let ack_request = AcknowledgeAnalyzerRequest::new(
+            request_id,
+            execution_hit.is_some(),
+            plugin_id,
+            tenant_id,
+            trace_id,
+            event_source_id,
+        );
+
+        pwq_client.acknowledge_analyzer(ack_request).await?;
+
+        Ok(())
+    }
+
+    async fn process_job(
+        &mut self,
+        _config: &PluginExecutorConfig,
+        job: ExecutionJob,
+    ) -> Result<Self::ProducedMessage, PluginWorkProcessorError> {
+        let run_analyzer_response = self
+            .analyzer_service_client
+            .run_analyzer(RunAnalyzerRequest::new(Update::deserialize(job.data())?))
+            .await?;
+
+        Ok(run_analyzer_response.execution_result)
+    }
+}
diff --git a/src/rust/plugin-execution-sidecar/src/work/plugin_work_processor.rs b/src/rust/plugin-execution-sidecar/src/work/plugin_work_processor.rs
index 0b53d190ae..4f99537b0c 100644
--- a/src/rust/plugin-execution-sidecar/src/work/plugin_work_processor.rs
+++ b/src/rust/plugin-execution-sidecar/src/work/plugin_work_processor.rs
@@ -5,6 +5,7 @@ use rust_proto::{
     },
     protocol::error::GrpcClientError,
     SerDe,
+    SerDeError,
 };
 use uuid::Uuid;
 
@@ -14,12 +15,17 @@ pub type RequestId = i64;
 
 #[derive(thiserror::Error, Debug)]
 pub enum PluginWorkProcessorError {
-    #[error("GrpcClientError {0}")]
+    #[error("Grpc call failed {0}")]
     GrpcClientError(#[from] GrpcClientError),
+
     #[error("Processing job failed, marking failed: {0}")]
     ProcessingJobFailed(String),
+
     #[error("Processing job failed, PWQ will retry: {0}")]
     ProcessingJobFailedRetriable(String),
+
+    #[error("Error serializing/deserializing protocol buffer: {0}")]
+    SerDeError(#[from] SerDeError),
 }
 
 impl PluginWorkProcessorError {
@@ -28,6 +34,7 @@ impl PluginWorkProcessorError {
             PluginWorkProcessorError::GrpcClientError(_) => true,
             PluginWorkProcessorError::ProcessingJobFailedRetriable(_) => true,
             PluginWorkProcessorError::ProcessingJobFailed(_) => false,
+            PluginWorkProcessorError::SerDeError(_) => false,
         }
     }
 }
diff --git a/src/rust/plugin-work-queue/src/server.rs b/src/rust/plugin-work-queue/src/server.rs
index a6129129ef..4d2dc4d873 100644
--- a/src/rust/plugin-work-queue/src/server.rs
+++ b/src/rust/plugin-work-queue/src/server.rs
@@ -187,13 +187,13 @@ impl PluginWorkQueueApi for PluginWorkQueue {
         &self,
         request: v1beta1::GetExecuteAnalyzerRequest,
     ) -> Result<v1beta1::GetExecuteAnalyzerResponse, PluginWorkQueueError> {
-        let message = self.queue.get_analyzer_message(request.plugin_id).await?;
+        let message = self.queue.get_analyzer_message(request.plugin_id()).await?;
         let message = match message {
             Some(message) => message,
             None => {
                 tracing::warn!(
                     message = "found no analyzer executions",
-                    plugin_id =% request.plugin_id,
+                    plugin_id =% request.plugin_id(),
                 );
                 return Ok(v1beta1::GetExecuteAnalyzerResponse::new(None, 0));
             }
@@ -208,7 +208,7 @@ impl PluginWorkQueueApi for PluginWorkQueue {
             tenant_id =% tenant_id,
             trace_id =% trace_id,
             event_source_id =% event_source_id,
-            plugin_id =% request.plugin_id,
+            plugin_id =% request.plugin_id(),
         );
 
         let execution_job = v1beta1::ExecutionJob::new(
diff --git a/src/rust/rust-proto/Cargo.toml b/src/rust/rust-proto/Cargo.toml
index 8cf9e6f624..21befe3056 100644
--- a/src/rust/rust-proto/Cargo.toml
+++ b/src/rust/rust-proto/Cargo.toml
@@ -16,6 +16,7 @@ clap = { version = "3.0", default_features = false, features = [
 ], optional = true }
 client-executor = { path = "../client-executor" }
 futures = "0.3"
+grapl-utils = { path = "../grapl-utils" }
 prost = "0.10"
 rand = "0.8.5"
 rustc-hash = "1.1.0"
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/graph/v1beta1.rs b/src/rust/rust-proto/src/graplinc/grapl/api/graph/v1beta1.rs
index 61ec9cd861..cb5697b2fb 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/graph/v1beta1.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/graph/v1beta1.rs
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 
 use crate::{
+    graplinc::grapl::common::v1beta1::types::Uid,
     protobufs::graplinc::grapl::api::graph::v1beta1::{
         DecrementOnlyIntProp as DecrementOnlyIntPropProto,
         DecrementOnlyUintProp as DecrementOnlyUintPropProto,
@@ -9,6 +10,8 @@ use crate::{
         ExecutionHit as ExecutionHitProto,
         GraphDescription as GraphDescriptionProto,
         IdStrategy as IdStrategyProto,
+        IdentifiedEdge as IdentifiedEdgeProto,
+        IdentifiedEdgeList as IdentifiedEdgeListProto,
         IdentifiedGraph as IdentifiedGraphProto,
         IdentifiedNode as IdentifiedNodeProto,
         ImmutableIntProp as ImmutableIntPropProto,
@@ -17,10 +20,6 @@ use crate::{
         IncrementOnlyIntProp as IncrementOnlyIntPropProto,
         IncrementOnlyUintProp as IncrementOnlyUintPropProto,
         Lens as LensProto,
-        MergedEdge as MergedEdgeProto,
-        MergedEdgeList as MergedEdgeListProto,
-        MergedGraph as MergedGraphProto,
-        MergedNode as MergedNodeProto,
         NodeDescription as NodeDescriptionProto,
         NodeProperty as NodePropertyProto,
         Session as SessionProto,
@@ -1120,14 +1119,21 @@ impl serde_impl::ProtobufSerializable for NodeDescription {
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub struct IdentifiedNode {
     pub properties: HashMap<String, NodeProperty>,
-    pub node_key: String,
+    pub uid: Uid,
     pub node_type: String,
 }
 
 impl IdentifiedNode {
+    pub fn from(n: IdentifiedNode, uid: Uid) -> Self {
+        Self {
+            uid,
+            properties: n.properties,
+            node_type: n.node_type,
+        }
+    }
+
     pub fn merge(&mut self, other: &Self) {
         debug_assert_eq!(self.node_type, other.node_type);
-        debug_assert_eq!(self.node_key, other.node_key);
         for (prop_name, prop_value) in other.properties.iter() {
             match self.properties.get_mut(prop_name) {
                 Some(self_prop) => self_prop.merge(prop_value),
@@ -1138,28 +1144,17 @@ impl IdentifiedNode {
             }
         }
     }
-
-    pub fn into(self, uid: u64) -> MergedNode {
-        MergedNode {
-            uid,
-            properties: self.properties,
-            node_key: self.node_key,
-            node_type: self.node_type,
-        }
-    }
-    pub fn get_node_key(&self) -> &str {
-        self.node_key.as_str()
-    }
-
-    pub fn clone_node_key(&self) -> String {
-        self.node_key.clone()
-    }
 }
 
 impl TryFrom<IdentifiedNodeProto> for IdentifiedNode {
     type Error = SerDeError;
 
     fn try_from(identified_node_proto: IdentifiedNodeProto) -> Result<Self, Self::Error> {
+        let uid = identified_node_proto
+            .uid
+            .ok_or(SerDeError::MissingField("IdentifiedNode.uid"))?
+            .try_into()?;
+
         let mut properties = HashMap::with_capacity(identified_node_proto.properties.len());
         for (key, property) in identified_node_proto.properties {
             properties.insert(key, NodeProperty::try_from(property)?);
@@ -1167,7 +1162,7 @@ impl TryFrom<IdentifiedNodeProto> for IdentifiedNode {
 
         Ok(IdentifiedNode {
             properties,
-            node_key: identified_node_proto.node_key,
+            uid,
             node_type: identified_node_proto.node_type,
         })
     }
@@ -1182,7 +1177,7 @@ impl From<IdentifiedNode> for IdentifiedNodeProto {
 
         IdentifiedNodeProto {
             properties,
-            node_key: identified_node.node_key,
+            uid: Some(identified_node.uid.into()),
             node_type: identified_node.node_type,
         }
     }
@@ -1197,103 +1192,6 @@ impl serde_impl::ProtobufSerializable for IdentifiedNode {
     type ProtobufMessage = IdentifiedNodeProto;
 }
 
-impl From<NodeDescription> for IdentifiedNode {
-    fn from(n: NodeDescription) -> Self {
-        IdentifiedNode {
-            properties: n.properties,
-            node_key: n.node_key,
-            node_type: n.node_type,
-        }
-    }
-}
-
-//
-// MergedNode
-//
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct MergedNode {
-    pub properties: HashMap<String, NodeProperty>,
-    pub uid: u64,
-    pub node_key: String,
-    pub node_type: String,
-}
-
-impl MergedNode {
-    pub fn from(n: IdentifiedNode, uid: u64) -> Self {
-        Self {
-            uid,
-            properties: n.properties,
-            node_key: n.node_key,
-            node_type: n.node_type,
-        }
-    }
-
-    pub fn merge(&mut self, other: &Self) {
-        debug_assert_eq!(self.node_type, other.node_type);
-        debug_assert_eq!(self.node_key, other.node_key);
-        for (prop_name, prop_value) in other.properties.iter() {
-            match self.properties.get_mut(prop_name) {
-                Some(self_prop) => self_prop.merge(prop_value),
-                None => {
-                    self.properties
-                        .insert(prop_name.clone(), prop_value.clone());
-                }
-            }
-        }
-    }
-
-    pub fn get_node_key(&self) -> &str {
-        self.node_key.as_str()
-    }
-
-    pub fn clone_node_key(&self) -> String {
-        self.node_key.clone()
-    }
-}
-
-impl TryFrom<MergedNodeProto> for MergedNode {
-    type Error = SerDeError;
-
-    fn try_from(merged_node_proto: MergedNodeProto) -> Result<Self, Self::Error> {
-        let mut properties = HashMap::with_capacity(merged_node_proto.properties.len());
-        for (key, property) in merged_node_proto.properties {
-            properties.insert(key, NodeProperty::try_from(property)?);
-        }
-
-        Ok(MergedNode {
-            properties,
-            uid: merged_node_proto.uid,
-            node_key: merged_node_proto.node_key,
-            node_type: merged_node_proto.node_type,
-        })
-    }
-}
-
-impl From<MergedNode> for MergedNodeProto {
-    fn from(merged_node: MergedNode) -> Self {
-        let mut properties = HashMap::with_capacity(merged_node.properties.len());
-        for (key, property) in merged_node.properties {
-            properties.insert(key, NodePropertyProto::from(property));
-        }
-
-        MergedNodeProto {
-            properties,
-            uid: merged_node.uid,
-            node_key: merged_node.node_key,
-            node_type: merged_node.node_type,
-        }
-    }
-}
-
-impl type_url::TypeUrl for MergedNode {
-    const TYPE_URL: &'static str = "graplsecurity.com/graplinc.grapl.api.graph.v1beta1.MergedNode";
-}
-
-impl serde_impl::ProtobufSerializable for MergedNode {
-    type ProtobufMessage = MergedNodeProto;
-}
-
 //
 // Edge
 //
@@ -1333,6 +1231,56 @@ impl serde_impl::ProtobufSerializable for Edge {
     type ProtobufMessage = EdgeProto;
 }
 
+//
+// IdentifiedEdge
+//
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct IdentifiedEdge {
+    pub from_uid: Uid,
+    pub to_uid: Uid,
+    pub edge_name: String,
+}
+
+impl TryFrom<IdentifiedEdgeProto> for IdentifiedEdge {
+    type Error = SerDeError;
+    fn try_from(identified_edge_proto: IdentifiedEdgeProto) -> Result<Self, Self::Error> {
+        let from_uid = identified_edge_proto
+            .from_uid
+            .ok_or(SerDeError::MissingField("IdentifiedEdge.from_uid"))?
+            .try_into()?;
+        let to_uid = identified_edge_proto
+            .to_uid
+            .ok_or(SerDeError::MissingField("IdentifiedEdge.to_uid"))?
+            .try_into()?;
+
+        Ok(IdentifiedEdge {
+            from_uid,
+            to_uid,
+            edge_name: identified_edge_proto.edge_name,
+        })
+    }
+}
+
+impl From<IdentifiedEdge> for IdentifiedEdgeProto {
+    fn from(identified_edge: IdentifiedEdge) -> Self {
+        IdentifiedEdgeProto {
+            from_uid: Some(identified_edge.from_uid.into()),
+            to_uid: Some(identified_edge.to_uid.into()),
+            edge_name: identified_edge.edge_name,
+        }
+    }
+}
+
+impl type_url::TypeUrl for IdentifiedEdge {
+    const TYPE_URL: &'static str =
+        "graplsecurity.com/graplinc.grapl.api.graph.v1beta1.IdentifiedEdge";
+}
+
+impl serde_impl::ProtobufSerializable for IdentifiedEdge {
+    type ProtobufMessage = IdentifiedEdgeProto;
+}
+
 //
 // Lens
 //
@@ -1375,51 +1323,6 @@ impl serde_impl::ProtobufSerializable for Lens {
     type ProtobufMessage = LensProto;
 }
 
-//
-// MergedEdge
-//
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct MergedEdge {
-    pub from_uid: String,
-    pub from_node_key: String,
-    pub to_uid: String,
-    pub to_node_key: String,
-    pub edge_name: String,
-}
-
-impl From<MergedEdgeProto> for MergedEdge {
-    fn from(merged_edge_proto: MergedEdgeProto) -> Self {
-        MergedEdge {
-            from_uid: merged_edge_proto.from_uid,
-            from_node_key: merged_edge_proto.from_node_key,
-            to_uid: merged_edge_proto.to_uid,
-            to_node_key: merged_edge_proto.to_node_key,
-            edge_name: merged_edge_proto.edge_name,
-        }
-    }
-}
-
-impl From<MergedEdge> for MergedEdgeProto {
-    fn from(merged_edge: MergedEdge) -> Self {
-        MergedEdgeProto {
-            from_uid: merged_edge.from_uid,
-            from_node_key: merged_edge.from_node_key,
-            to_uid: merged_edge.to_uid,
-            to_node_key: merged_edge.to_node_key,
-            edge_name: merged_edge.edge_name,
-        }
-    }
-}
-
-impl type_url::TypeUrl for MergedEdge {
-    const TYPE_URL: &'static str = "graplsecurity.com/graplinc.grapl.api.graph.v1beta1.MergedEdge";
-}
-
-impl serde_impl::ProtobufSerializable for MergedEdge {
-    type ProtobufMessage = MergedEdgeProto;
-}
-
 //
 // EdgeList
 //
@@ -1465,8 +1368,8 @@ impl serde_impl::ProtobufSerializable for EdgeList {
 
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub struct ExecutionHit {
-    pub nodes: HashMap<String, MergedNode>,
-    pub edges: HashMap<String, MergedEdgeList>,
+    pub nodes: HashMap<Uid, IdentifiedNode>,
+    pub edges: HashMap<Uid, IdentifiedEdgeList>,
     pub analyzer_name: String,
     pub risk_score: u64,
     pub lenses: Vec<Lens>,
@@ -1475,16 +1378,21 @@ pub struct ExecutionHit {
 
 impl TryFrom<ExecutionHitProto> for ExecutionHit {
     type Error = SerDeError;
-
     fn try_from(execution_hit_proto: ExecutionHitProto) -> Result<Self, Self::Error> {
         let mut nodes = HashMap::with_capacity(execution_hit_proto.nodes.len());
-        for (key, merged_node) in execution_hit_proto.nodes {
-            nodes.insert(key, MergedNode::try_from(merged_node)?);
+        for (key, identified_node) in execution_hit_proto.nodes {
+            nodes.insert(
+                Uid::from_u64(key).unwrap(),
+                IdentifiedNode::try_from(identified_node)?,
+            );
         }
 
         let mut edges = HashMap::with_capacity(execution_hit_proto.edges.len());
-        for (key, merged_edge_list) in execution_hit_proto.edges {
-            edges.insert(key, MergedEdgeList::from(merged_edge_list));
+        for (key, identified_edge_list) in execution_hit_proto.edges {
+            edges.insert(
+                Uid::from_u64(key).unwrap(),
+                IdentifiedEdgeList::try_from(identified_edge_list)?,
+            );
         }
 
         let mut lenses = Vec::with_capacity(execution_hit_proto.lenses.len());
@@ -1506,13 +1414,16 @@ impl TryFrom<ExecutionHitProto> for ExecutionHit {
 impl From<ExecutionHit> for ExecutionHitProto {
     fn from(execution_hit: ExecutionHit) -> Self {
         let mut nodes = HashMap::with_capacity(execution_hit.nodes.len());
-        for (key, merged_node) in execution_hit.nodes {
-            nodes.insert(key, MergedNodeProto::from(merged_node));
+        for (key, identified_node) in execution_hit.nodes {
+            nodes.insert(key.as_u64(), IdentifiedNodeProto::from(identified_node));
         }
 
         let mut edges = HashMap::with_capacity(execution_hit.edges.len());
-        for (key, merged_edge_list) in execution_hit.edges {
-            edges.insert(key, MergedEdgeListProto::from(merged_edge_list));
+        for (key, identified_edge_list) in execution_hit.edges {
+            edges.insert(
+                key.as_u64(),
+                IdentifiedEdgeListProto::from(identified_edge_list),
+            );
         }
 
         let mut lenses = Vec::with_capacity(execution_hit.lenses.len());
@@ -1541,43 +1452,44 @@ impl serde_impl::ProtobufSerializable for ExecutionHit {
 }
 
 //
-// MergedEdgeList
+// IdentifiedEdgeList
 //
 
 #[derive(Debug, PartialEq, Eq, Clone)]
-pub struct MergedEdgeList {
-    pub edges: Vec<MergedEdge>,
+pub struct IdentifiedEdgeList {
+    pub edges: Vec<IdentifiedEdge>,
 }
 
-impl From<MergedEdgeListProto> for MergedEdgeList {
-    fn from(merged_edge_list_proto: MergedEdgeListProto) -> Self {
-        let mut edges = Vec::with_capacity(merged_edge_list_proto.edges.len());
-        for edge in merged_edge_list_proto.edges {
-            edges.push(MergedEdge::from(edge));
+impl TryFrom<IdentifiedEdgeListProto> for IdentifiedEdgeList {
+    type Error = SerDeError;
+    fn try_from(identified_edge_list_proto: IdentifiedEdgeListProto) -> Result<Self, Self::Error> {
+        let mut edges = Vec::with_capacity(identified_edge_list_proto.edges.len());
+        for edge in identified_edge_list_proto.edges {
+            edges.push(IdentifiedEdge::try_from(edge)?);
         }
 
-        MergedEdgeList { edges }
+        Ok(IdentifiedEdgeList { edges })
     }
 }
 
-impl From<MergedEdgeList> for MergedEdgeListProto {
-    fn from(merged_edge_list: MergedEdgeList) -> Self {
-        let mut edges = Vec::with_capacity(merged_edge_list.edges.len());
-        for edge in merged_edge_list.edges {
-            edges.push(MergedEdgeProto::from(edge));
+impl From<IdentifiedEdgeList> for IdentifiedEdgeListProto {
+    fn from(identified_edge_list: IdentifiedEdgeList) -> Self {
+        let mut edges = Vec::with_capacity(identified_edge_list.edges.len());
+        for edge in identified_edge_list.edges {
+            edges.push(IdentifiedEdgeProto::from(edge));
         }
 
-        MergedEdgeListProto { edges }
+        IdentifiedEdgeListProto { edges }
     }
 }
 
-impl type_url::TypeUrl for MergedEdgeList {
+impl type_url::TypeUrl for IdentifiedEdgeList {
     const TYPE_URL: &'static str =
-        "graplsecurity.com/graplinc.grapl.api.graph.v1beta1.MergedEdgeList";
+        "graplsecurity.com/graplinc.grapl.api.graph.v1beta1.IdentifiedEdgeList";
 }
 
-impl serde_impl::ProtobufSerializable for MergedEdgeList {
-    type ProtobufMessage = MergedEdgeListProto;
+impl serde_impl::ProtobufSerializable for IdentifiedEdgeList {
+    type ProtobufMessage = IdentifiedEdgeListProto;
 }
 
 //
@@ -1711,8 +1623,8 @@ impl serde_impl::ProtobufSerializable for GraphDescription {
 
 #[derive(Debug, Default, PartialEq, Eq, Clone)]
 pub struct IdentifiedGraph {
-    pub nodes: HashMap<String, IdentifiedNode>,
-    pub edges: HashMap<String, EdgeList>,
+    pub nodes: HashMap<Uid, IdentifiedNode>,
+    pub edges: HashMap<Uid, IdentifiedEdgeList>,
 }
 
 impl IdentifiedGraph {
@@ -1724,35 +1636,28 @@ impl IdentifiedGraph {
     }
 
     pub fn add_node(&mut self, node: IdentifiedNode) {
-        match self.nodes.get_mut(&node.node_key) {
+        match self.nodes.get_mut(&node.uid) {
             Some(n) => n.merge(&node),
             None => {
-                self.nodes.insert(node.clone_node_key(), node);
+                self.nodes.insert(node.uid, node);
             }
         };
     }
 
-    pub fn add_edge(
-        &mut self,
-        edge_name: impl Into<String>,
-        from_node_key: impl Into<String>,
-        to_node_key: impl Into<String>,
-    ) {
-        let from_node_key = from_node_key.into();
-        let to_node_key = to_node_key.into();
-        assert_ne!(from_node_key, to_node_key);
+    pub fn add_edge(&mut self, edge_name: impl Into<String>, from_uid: Uid, to_uid: Uid) {
+        assert_ne!(from_uid, to_uid);
 
         let edge_name = edge_name.into();
-        let edge = Edge {
-            from_node_key: from_node_key.clone(),
-            to_node_key,
+        let edge = IdentifiedEdge {
+            from_uid,
+            to_uid,
             edge_name,
         };
 
-        let edge_list: &mut Vec<Edge> = &mut self
+        let edge_list: &mut Vec<_> = &mut self
             .edges
-            .entry(from_node_key)
-            .or_insert_with(|| EdgeList {
+            .entry(from_uid)
+            .or_insert_with(|| IdentifiedEdgeList {
                 edges: Vec::with_capacity(1),
             })
             .edges;
@@ -1771,11 +1676,7 @@ impl IdentifiedGraph {
 
         for edge_list in other.edges.values() {
             for edge in edge_list.edges.iter() {
-                self.add_edge(
-                    edge.edge_name.clone(),
-                    edge.from_node_key.clone(),
-                    edge.to_node_key.clone(),
-                );
+                self.add_edge(edge.edge_name.clone(), edge.from_uid, edge.to_uid);
             }
         }
     }
@@ -1791,12 +1692,18 @@ impl TryFrom<IdentifiedGraphProto> for IdentifiedGraph {
     fn try_from(identified_graph_proto: IdentifiedGraphProto) -> Result<Self, Self::Error> {
         let mut nodes = HashMap::with_capacity(identified_graph_proto.nodes.len());
         for (key, identified_node) in identified_graph_proto.nodes {
-            nodes.insert(key, IdentifiedNode::try_from(identified_node)?);
+            nodes.insert(
+                Uid::from_u64(key).unwrap(),
+                IdentifiedNode::try_from(identified_node)?,
+            );
         }
 
         let mut edges = HashMap::with_capacity(identified_graph_proto.edges.len());
         for (key, edge_list) in identified_graph_proto.edges {
-            edges.insert(key, EdgeList::from(edge_list));
+            edges.insert(
+                Uid::from_u64(key).unwrap(),
+                IdentifiedEdgeList::try_from(edge_list)?,
+            );
         }
 
         Ok(IdentifiedGraph { nodes, edges })
@@ -1807,12 +1714,12 @@ impl From<IdentifiedGraph> for IdentifiedGraphProto {
     fn from(identified_graph: IdentifiedGraph) -> Self {
         let mut nodes = HashMap::with_capacity(identified_graph.nodes.len());
         for (key, identified_node) in identified_graph.nodes {
-            nodes.insert(key, IdentifiedNodeProto::from(identified_node));
+            nodes.insert(key.as_u64(), IdentifiedNodeProto::from(identified_node));
         }
 
         let mut edges = HashMap::with_capacity(identified_graph.edges.len());
         for (key, edge_list) in identified_graph.edges {
-            edges.insert(key, EdgeListProto::from(edge_list));
+            edges.insert(key.as_u64(), IdentifiedEdgeListProto::from(edge_list));
         }
 
         IdentifiedGraphProto { nodes, edges }
@@ -1828,148 +1735,6 @@ impl serde_impl::ProtobufSerializable for IdentifiedGraph {
     type ProtobufMessage = IdentifiedGraphProto;
 }
 
-//
-// MergedGraph
-//
-
-#[derive(Debug, Default, PartialEq, Eq, Clone)]
-pub struct MergedGraph {
-    pub nodes: HashMap<String, MergedNode>,
-    pub edges: HashMap<String, MergedEdgeList>,
-}
-
-impl MergedGraph {
-    pub fn new() -> Self {
-        Self {
-            nodes: Default::default(),
-            edges: Default::default(),
-        }
-    }
-
-    pub fn add_node(&mut self, node: MergedNode) {
-        match self.nodes.get_mut(&node.node_key) {
-            Some(n) => n.merge(&node),
-            None => {
-                self.nodes.insert(node.clone_node_key(), node);
-            }
-        };
-    }
-
-    pub fn add_merged_edge(&mut self, edge: MergedEdge) {
-        let from_node_key = edge.from_node_key.clone();
-        let edge_list: &mut Vec<MergedEdge> = &mut self
-            .edges
-            .entry(from_node_key)
-            .or_insert_with(|| MergedEdgeList {
-                edges: Vec::with_capacity(1),
-            })
-            .edges;
-        edge_list.push(edge);
-    }
-
-    pub fn add_edge(
-        &mut self,
-        edge_name: impl Into<String>,
-        from_node_key: impl Into<String>,
-        from_uid: impl Into<String>,
-        to_node_key: impl Into<String>,
-        to_uid: impl Into<String>,
-    ) {
-        let edge_name = edge_name.into();
-        let from_node_key = from_node_key.into();
-        let from_uid = from_uid.into();
-        let to_node_key = to_node_key.into();
-        let to_uid = to_uid.into();
-        assert_ne!(from_node_key, to_node_key);
-        assert_ne!(from_uid, to_uid);
-        let edge = MergedEdge {
-            from_node_key: from_node_key.clone(),
-            from_uid,
-            to_node_key,
-            to_uid,
-            edge_name,
-        };
-
-        let edge_list: &mut Vec<MergedEdge> = &mut self
-            .edges
-            .entry(from_node_key)
-            .or_insert_with(|| MergedEdgeList {
-                edges: Vec::with_capacity(1),
-            })
-            .edges;
-        edge_list.push(edge);
-    }
-
-    pub fn merge(&mut self, other: &Self) {
-        for (node_key, other_node) in other.nodes.iter() {
-            match self.nodes.get_mut(node_key) {
-                Some(n) => n.merge(other_node),
-                None => {
-                    self.nodes.insert(node_key.clone(), other_node.clone());
-                }
-            };
-        }
-
-        for edge_list in other.edges.values() {
-            for edge in edge_list.edges.iter() {
-                self.add_edge(
-                    edge.edge_name.clone(),
-                    edge.from_node_key.clone(),
-                    edge.from_uid.clone(),
-                    edge.to_node_key.clone(),
-                    edge.to_uid.clone(),
-                );
-            }
-        }
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.nodes.is_empty() && self.edges.is_empty()
-    }
-}
-
-impl TryFrom<MergedGraphProto> for MergedGraph {
-    type Error = SerDeError;
-
-    fn try_from(merged_graph_proto: MergedGraphProto) -> Result<Self, Self::Error> {
-        let mut nodes = HashMap::with_capacity(merged_graph_proto.nodes.len());
-        for (key, merged_node) in merged_graph_proto.nodes {
-            nodes.insert(key, MergedNode::try_from(merged_node)?);
-        }
-
-        let mut edges = HashMap::with_capacity(merged_graph_proto.edges.len());
-        for (key, merged_edge_list) in merged_graph_proto.edges {
-            edges.insert(key, MergedEdgeList::from(merged_edge_list));
-        }
-
-        Ok(MergedGraph { nodes, edges })
-    }
-}
-
-impl From<MergedGraph> for MergedGraphProto {
-    fn from(merged_graph: MergedGraph) -> Self {
-        let mut nodes = HashMap::with_capacity(merged_graph.nodes.len());
-        for (key, merged_node) in merged_graph.nodes {
-            nodes.insert(key, MergedNodeProto::from(merged_node));
-        }
-
-        let mut edges = HashMap::with_capacity(merged_graph.edges.len());
-        for (key, merged_edge_list) in merged_graph.edges {
-            edges.insert(key, MergedEdgeListProto::from(merged_edge_list));
-        }
-
-        MergedGraphProto { nodes, edges }
-    }
-}
-
-impl type_url::TypeUrl for MergedGraph {
-    const TYPE_URL: &'static str = "graplsecurity.com/graplinc.grapl.api.graph.v1beta1.MergedGraph";
-}
-
-impl serde_impl::ProtobufSerializable for MergedGraph {
-    type ProtobufMessage = MergedGraphProto;
-}
-
 #[cfg(test)]
 pub mod test {
     // TODO: refactor these tests to use proptest instead of quickcheck
@@ -2085,46 +1850,48 @@ pub mod test {
         choices[choice_index].clone()
     }
 
-    fn choose_property(node_key: &str, property_name: &str, g: &mut Gen) -> NodeProperty {
-        let s = format!("{}{}", node_key, property_name);
+    fn choose_property(uid: Uid, property_name: &str, g: &mut Gen) -> NodeProperty {
+        let s = format!("{}{}", uid.as_u64(), property_name);
 
         let props = &[
             Property::IncrementOnlyIntProp(IncrementOnlyIntProp::arbitrary(g)),
             Property::DecrementOnlyIntProp(DecrementOnlyIntProp::arbitrary(g)),
             Property::IncrementOnlyUintProp(IncrementOnlyUintProp::arbitrary(g)),
             Property::DecrementOnlyUintProp(DecrementOnlyUintProp::arbitrary(g)),
-            Property::ImmutableIntProp(ImmutableIntProp::from(
-                hash(&[node_key, property_name]) as i64
-            )),
-            Property::ImmutableUintProp(ImmutableUintProp::from(hash(&[node_key, property_name]))),
+            Property::ImmutableIntProp(ImmutableIntProp::from(hash(
+                &[&uid.as_u64().to_string(), property_name][..],
+            ) as i64)),
+            Property::ImmutableUintProp(ImmutableUintProp::from(hash(
+                &[&uid.as_u64().to_string(), property_name][..],
+            ))),
             Property::ImmutableStrProp(ImmutableStrProp::from(s)),
         ];
-        let p: Property = choice(node_key, props);
+        let p: Property = choice(&uid.as_u64().to_string(), props);
         p.into()
     }
 
     impl Arbitrary for IdentifiedNode {
         fn arbitrary(g: &mut Gen) -> Self {
-            let node_keys = &[
-                "c413e25e-9c50-4faf-8e61-f8bfb0e0d18e".to_string(),
-                "0d5c9261-2b6e-4094-8de3-b349cb0aa310".to_string(),
-                "ed1f73df-f38d-43c0-87b0-5aff06e1f68b".to_string(),
-                "6328e956-117e-4f7f-8a5b-c56be1111f43".to_string(),
+            let uids = &[
+                Uid::from_u64(123).unwrap(),
+                Uid::from_u64(124).unwrap(),
+                Uid::from_u64(125).unwrap(),
+                Uid::from_u64(126).unwrap(),
             ];
-            let node_key = g.choose(node_keys).unwrap().clone();
+            let uid = g.choose(uids).unwrap().clone();
 
             let node_types = &["Process", "File", "IpAddress"];
-            let node_type = choice(&node_key, node_types);
+            let node_type = choice(&uid.as_u64().to_string(), node_types);
             let mut properties = HashMap::new();
             let property_names: Vec<String> = Vec::arbitrary(g);
 
             for property_name in property_names {
-                let property = choose_property(&node_key, &property_name, g);
+                let property = choose_property(uid, &property_name, g);
                 properties.insert(property_name.to_owned(), property);
             }
 
             IdentifiedNode {
-                node_key: node_key.to_owned(),
+                uid,
                 node_type: node_type.to_owned(),
                 properties,
             }
@@ -2215,7 +1982,7 @@ pub mod test {
 
     #[quickcheck]
     fn test_merge_identified_node(mut node_0: IdentifiedNode, node_1: IdentifiedNode) {
-        if node_0.node_key != node_1.node_key {
+        if node_0.uid != node_1.uid {
             return;
         }
         // let original = node_0.clone();
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/client.rs b/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/client.rs
index 8fce0b7932..e98601ae46 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/client.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/client.rs
@@ -1,4 +1,7 @@
-use std::time::Duration;
+use std::{
+    fmt::Debug,
+    time::Duration,
+};
 
 use client_executor::{
     Executor,
@@ -32,6 +35,14 @@ pub struct GraphMutationClient {
     executor: Executor,
 }
 
+impl Debug for GraphMutationClient {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("GraphMutationClient")
+            .field("proto_client", &self.proto_client)
+            .finish()
+    }
+}
+
 #[async_trait::async_trait]
 impl Connectable for GraphMutationClient {
     type Config = GraphMutationClientConfig;
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/server.rs b/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/server.rs
index 5410287ed2..615a8b21ee 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/server.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/graph_mutation/v1beta1/server.rs
@@ -162,6 +162,7 @@ where
 
         // TODO: add tower tracing, tls_config, concurrency limits
         Ok(Server::builder()
+            .timeout(std::time::Duration::from_secs(5))
             .add_service(health_service)
             .add_service(GraphMutationServiceServerProto::new(GrpcApi::new(
                 self.api_server,
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/graph_schema_manager/v1beta1/server.rs b/src/rust/rust-proto/src/graplinc/grapl/api/graph_schema_manager/v1beta1/server.rs
index 3b6c4bdeb5..b327f543e3 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/graph_schema_manager/v1beta1/server.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/graph_schema_manager/v1beta1/server.rs
@@ -159,7 +159,6 @@ where
         let mut server_builder = Server::builder().trace_fn(|request| {
             tracing::info_span!(
                 "exec_service",
-                headers = ?request.headers(),
                 method = ?request.method(),
                 uri = %request.uri(),
                 extensions = ?request.extensions(),
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/messages.rs b/src/rust/rust-proto/src/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/messages.rs
index 3e074fb3be..51d29ab543 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/messages.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/plugin_sdk/analyzers/v1beta1/messages.rs
@@ -212,6 +212,15 @@ impl From<Update> for UpdateProto {
     }
 }
 
+impl type_url::TypeUrl for Update {
+    const TYPE_URL: &'static str =
+        "graplsecurity.com/graplinc.grapl.api.plugin_sdk.analyzers.v1beta1.Update";
+}
+
+impl ProtobufSerializable for Update {
+    type ProtobufMessage = UpdateProto;
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct LensRef {
     pub lens_namespace: String,
@@ -364,20 +373,34 @@ impl From<ExecutionResult> for ExecutionResultProto {
     }
 }
 
+impl type_url::TypeUrl for ExecutionResult {
+    const TYPE_URL: &'static str =
+        "graplsecurity.com/graplinc.grapl.api.plugin_sdk.analyzers.v1beta1.ExecutionResult";
+}
+
+impl ProtobufSerializable for ExecutionResult {
+    type ProtobufMessage = ExecutionResultProto;
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct RunAnalyzerRequest {
-    pub tenant_id: uuid::Uuid,
-    pub update: Update,
+    update: Update,
+}
+
+impl RunAnalyzerRequest {
+    pub fn new(update: Update) -> Self {
+        Self { update }
+    }
+
+    pub fn update(self) -> Update {
+        self.update
+    }
 }
 
 impl TryFrom<RunAnalyzerRequestProto> for RunAnalyzerRequest {
     type Error = SerDeError;
     fn try_from(value: RunAnalyzerRequestProto) -> Result<Self, Self::Error> {
         Ok(Self {
-            tenant_id: value
-                .tenant_id
-                .ok_or(SerDeError::MissingField("RunAnalyzerRequest.tenant_id"))?
-                .try_into()?,
             update: value
                 .update
                 .ok_or(SerDeError::MissingField("RunAnalyzerRequest.update"))?
@@ -389,7 +412,6 @@ impl TryFrom<RunAnalyzerRequestProto> for RunAnalyzerRequest {
 impl From<RunAnalyzerRequest> for RunAnalyzerRequestProto {
     fn from(value: RunAnalyzerRequest) -> Self {
         Self {
-            tenant_id: Some(value.tenant_id.into()),
             update: Some(value.update.into()),
         }
     }
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/plugin_work_queue/v1beta1.rs b/src/rust/rust-proto/src/graplinc/grapl/api/plugin_work_queue/v1beta1.rs
index b8aaee214a..603b5edf8c 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/plugin_work_queue/v1beta1.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/plugin_work_queue/v1beta1.rs
@@ -389,7 +389,17 @@ impl type_url::TypeUrl for AcknowledgeAnalyzerResponse {
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct GetExecuteAnalyzerRequest {
-    pub plugin_id: Uuid,
+    plugin_id: Uuid,
+}
+
+impl GetExecuteAnalyzerRequest {
+    pub fn new(plugin_id: Uuid) -> Self {
+        Self { plugin_id }
+    }
+
+    pub fn plugin_id(&self) -> Uuid {
+        self.plugin_id
+    }
 }
 
 impl TryFrom<proto::GetExecuteAnalyzerRequest> for GetExecuteAnalyzerRequest {
diff --git a/src/rust/rust-proto/src/graplinc/grapl/api/scylla_provisioner/v1beta1/server.rs b/src/rust/rust-proto/src/graplinc/grapl/api/scylla_provisioner/v1beta1/server.rs
index 4e8f08c7b8..2f232dee41 100644
--- a/src/rust/rust-proto/src/graplinc/grapl/api/scylla_provisioner/v1beta1/server.rs
+++ b/src/rust/rust-proto/src/graplinc/grapl/api/scylla_provisioner/v1beta1/server.rs
@@ -1,4 +1,3 @@
-#![allow(warnings)]
 use std::time::Duration;
 
 use futures::{
@@ -9,8 +8,6 @@ use futures::{
     },
     Future,
     FutureExt,
-    SinkExt,
-    StreamExt,
 };
 use proto::scylla_provisioner_service_server::ScyllaProvisionerService;
 use tokio::net::TcpListener;
@@ -41,7 +38,6 @@ use crate::{
         status::Status,
     },
     server_internals::GrpcApi,
-    SerDeError,
 };
 
 /// Implement this trait to define the API business logic
diff --git a/src/rust/rust-proto/src/server_internals.rs b/src/rust/rust-proto/src/server_internals.rs
index addac2fcb3..4824f40513 100644
--- a/src/rust/rust-proto/src/server_internals.rs
+++ b/src/rust/rust-proto/src/server_internals.rs
@@ -39,6 +39,9 @@ impl From<crate::SerDeError> for Status {
 macro_rules! execute_rpc {
     ($self: ident, $request: ident, $rpc_name: ident) => {{
         {
+            let rpc_name = stringify!($rpc_name);
+            tracing::debug!("Executing RPC {rpc_name}");
+
             let proto_request = $request.into_inner();
 
             let native_request = proto_request.try_into()?;
@@ -53,6 +56,7 @@ macro_rules! execute_rpc {
                 .try_into()
                 .map_err($crate::SerDeError::from)?;
 
+            tracing::debug!("Executing RPC {rpc_name} - COMPLETE");
             Ok(tonic::Response::new(proto_response))
         }
     }};
diff --git a/src/rust/scylla-provisioner/Cargo.toml b/src/rust/scylla-provisioner/Cargo.toml
index e821a218dd..d4aded39d1 100644
--- a/src/rust/scylla-provisioner/Cargo.toml
+++ b/src/rust/scylla-provisioner/Cargo.toml
@@ -9,7 +9,7 @@ async-trait = "0.1.56"
 clap = { version = "3.0", default_features = false, features = ["std", "env"] }
 grapl-tracing = { path = "../grapl-tracing" }
 rust-proto = { path = "../rust-proto" }
-scylla = "0.4.7"
+scylla = "0.5"
 secrecy = "0.8.0"
 thiserror = "1.0.31"
 tokio = { version = "1.20.0", features = ["full"] }
diff --git a/src/rust/scylla-provisioner/src/server.rs b/src/rust/scylla-provisioner/src/server.rs
index a5468e3db6..6058e13a12 100644
--- a/src/rust/scylla-provisioner/src/server.rs
+++ b/src/rust/scylla-provisioner/src/server.rs
@@ -27,7 +27,13 @@ use crate::{
     config::ScyllaProvisionerServiceConfig,
     table_names::{
         tenant_keyspace_name,
+        IMM_I_64_TABLE_NAME,
         IMM_STRING_TABLE_NAME,
+        IMM_U_64_TABLE_NAME,
+        MAX_I_64_TABLE_NAME,
+        MAX_U_64_TABLE_NAME,
+        MIN_I_64_TABLE_NAME,
+        MIN_U_64_TABLE_NAME,
     },
 };
 
@@ -70,7 +76,15 @@ impl ScyllaProvisionerApi for ScyllaProvisioner {
             &[]
         ).await?;
 
-        let property_table_names = [(IMM_STRING_TABLE_NAME, "text")];
+        let property_table_names = [
+            (IMM_STRING_TABLE_NAME, "text"),
+            (MAX_I_64_TABLE_NAME, "bigint"),
+            (MIN_I_64_TABLE_NAME, "bigint"),
+            (IMM_I_64_TABLE_NAME, "bigint"),
+            (MAX_U_64_TABLE_NAME, "bigint"),
+            (MIN_U_64_TABLE_NAME, "bigint"),
+            (IMM_U_64_TABLE_NAME, "bigint"),
+        ];
 
         for (table_name, value_type) in property_table_names.into_iter() {
             session


Test patch:
diff --git a/src/python/python-proto/python_proto/tests/strategies.py b/src/python/python-proto/python_proto/tests/strategies.py
index 45e70e8e25..4dff594396 100644
--- a/src/python/python-proto/python_proto/tests/strategies.py
+++ b/src/python/python-proto/python_proto/tests/strategies.py
@@ -10,6 +10,8 @@
     EdgeList,
     ExecutionHit,
     GraphDescription,
+    IdentifiedEdge,
+    IdentifiedEdgeList,
     IdentifiedGraph,
     IdentifiedNode,
     IdStrategy,
@@ -19,16 +21,14 @@
     IncrementOnlyIntProp,
     IncrementOnlyUintProp,
     Lens,
-    MergedEdge,
-    MergedEdgeList,
-    MergedGraph,
-    MergedNode,
     NodeDescription,
     NodeProperty,
     Session,
     Static,
 )
 from python_proto.common import Duration, Timestamp, Uuid
+from python_proto.grapl.common.v1beta1 import messages as grapl_common_messages
+from python_proto.grapl.common.v1beta1.messages import Uid
 from python_proto.metrics import (
     Counter,
     Gauge,
@@ -86,6 +86,12 @@
 small_text = st.text(max_size=4)
 
 
+def uids(
+    value: st.SearchStrategy[int] = uint64s,
+) -> st.SearchStrategy[grapl_common_messages.Uid]:
+    return st.builds(grapl_common_messages.Uid, value=value)
+
+
 def uuids(
     lsbs: st.SearchStrategy[int] = uint64s,
     msbs: st.SearchStrategy[int] = uint64s,
@@ -290,30 +296,13 @@ def identified_nodes(
     properties: st.SearchStrategy[Mapping[str, NodeProperty]] = st.dictionaries(
         keys=st.text(), values=node_properties(), max_size=MAX_DICT_SIZE
     ),
-    node_keys: st.SearchStrategy[str] = st.text(),
+    uids: st.SearchStrategy[Uid] = uids(),
     node_types: st.SearchStrategy[str] = st.text(),
 ) -> st.SearchStrategy[IdentifiedNode]:
     return st.builds(
         IdentifiedNode,
         properties=properties,
-        node_key=node_keys,
-        node_type=node_types,
-    )
-
-
-def merged_nodes(
-    properties: st.SearchStrategy[Mapping[str, NodeProperty]] = st.dictionaries(
-        keys=st.text(), values=node_properties(), max_size=MAX_DICT_SIZE
-    ),
-    uids: st.SearchStrategy[int] = uint64s,
-    node_keys: st.SearchStrategy[str] = st.text(),
-    node_types: st.SearchStrategy[str] = st.text(),
-) -> st.SearchStrategy[MergedNode]:
-    return st.builds(
-        MergedNode,
-        properties=properties,
         uid=uids,
-        node_key=node_keys,
         node_type=node_types,
     )
 
@@ -342,30 +331,26 @@ def edge_lists(
     )
 
 
-def merged_edges(
-    from_uids: st.SearchStrategy[str] = st.text(),
-    from_node_keys: st.SearchStrategy[str] = st.text(),
-    to_uids: st.SearchStrategy[str] = st.text(),
-    to_node_keys: st.SearchStrategy[str] = st.text(),
+def identified_edges(
+    from_uids: st.SearchStrategy[Uid] = uids(),
+    to_uids: st.SearchStrategy[Uid] = uids(),
     edge_names: st.SearchStrategy[str] = st.text(),
-) -> st.SearchStrategy[MergedEdge]:
+) -> st.SearchStrategy[IdentifiedEdge]:
     return st.builds(
-        MergedEdge,
+        IdentifiedEdge,
         from_uid=from_uids,
-        from_node_key=from_node_keys,
         to_uid=to_uids,
-        to_node_key=to_node_keys,
         edge_name=edge_names,
     )
 
 
-def merged_edge_lists(
-    edges: st.SearchStrategy[Sequence[MergedEdge]] = st.lists(
-        merged_edges(), max_size=MAX_LIST_SIZE
+def identified_edge_lists(
+    edges: st.SearchStrategy[Sequence[IdentifiedEdge]] = st.lists(
+        identified_edges(), max_size=MAX_LIST_SIZE
     ),
-) -> st.SearchStrategy[MergedEdgeList]:
+) -> st.SearchStrategy[IdentifiedEdgeList]:
     return st.builds(
-        MergedEdgeList,
+        IdentifiedEdgeList,
         edges=edges,
     )
 
@@ -386,11 +371,11 @@ def graph_descriptions(
 
 
 def identified_graphs(
-    nodes: st.SearchStrategy[Mapping[str, IdentifiedNode]] = st.dictionaries(
-        keys=st.text(), values=identified_nodes(), max_size=MAX_DICT_SIZE
+    nodes: st.SearchStrategy[Mapping[Uid, IdentifiedNode]] = st.dictionaries(
+        keys=uids(), values=identified_nodes(), max_size=MAX_DICT_SIZE
     ),
-    edges: st.SearchStrategy[Mapping[str, EdgeList]] = st.dictionaries(
-        keys=st.text(), values=edge_lists(), max_size=MAX_DICT_SIZE
+    edges: st.SearchStrategy[Mapping[Uid, IdentifiedEdgeList]] = st.dictionaries(
+        keys=uids(), values=identified_edge_lists(), max_size=MAX_DICT_SIZE
     ),
 ) -> st.SearchStrategy[IdentifiedGraph]:
     return st.builds(
@@ -400,21 +385,6 @@ def identified_graphs(
     )
 
 
-def merged_graphs(
-    nodes: st.SearchStrategy[Mapping[str, MergedNode]] = st.dictionaries(
-        keys=st.text(), values=merged_nodes(), max_size=MAX_DICT_SIZE
-    ),
-    edges: st.SearchStrategy[Mapping[str, MergedEdgeList]] = st.dictionaries(
-        keys=st.text(), values=merged_edge_lists(), max_size=MAX_DICT_SIZE
-    ),
-) -> st.SearchStrategy[MergedGraph]:
-    return st.builds(
-        MergedGraph,
-        nodes=nodes,
-        edges=edges,
-    )
-
-
 def lenses(
     lens_types: st.SearchStrategy[str] = st.text(),
     lens_names: st.SearchStrategy[str] = st.text(),
@@ -431,11 +401,11 @@ def lenses(
 
 
 def execution_hits(
-    nodes: st.SearchStrategy[Mapping[str, MergedNode]] = st.dictionaries(
-        keys=st.text(), values=merged_nodes(), max_size=MAX_DICT_SIZE
+    nodes: st.SearchStrategy[Mapping[Uid, IdentifiedNode]] = st.dictionaries(
+        keys=uids(), values=identified_nodes(), max_size=MAX_DICT_SIZE
     ),
-    edges: st.SearchStrategy[Mapping[str, MergedEdgeList]] = st.dictionaries(
-        keys=st.text(), values=merged_edge_lists(), max_size=MAX_DICT_SIZE
+    edges: st.SearchStrategy[Mapping[Uid, IdentifiedEdgeList]] = st.dictionaries(
+        keys=uids(), values=identified_edge_lists(), max_size=MAX_DICT_SIZE
     ),
     analyzer_names: st.SearchStrategy[str] = st.text(),
     risk_scores: st.SearchStrategy[int] = uint64s,
diff --git a/src/python/python-proto/python_proto/tests/test_api_analyzers.py b/src/python/python-proto/python_proto/tests/test_api_analyzers.py
index bcd2fb9411..242dcfcaa9 100644
--- a/src/python/python-proto/python_proto/tests/test_api_analyzers.py
+++ b/src/python/python-proto/python_proto/tests/test_api_analyzers.py
@@ -8,7 +8,7 @@
 from python_proto.grapl.common.v1beta1 import messages as grapl_common_msgs
 from python_proto.tests import strategies
 from python_proto.tests.helpers import check_encode_decode_invariant
-from python_proto.tests.test_grapl_common import edge_names, property_names, uids
+from python_proto.tests.test_grapl_common import edge_names, property_names
 
 ################################################################################
 # Strategies
@@ -16,7 +16,7 @@
 
 
 def string_property_updates(
-    uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     property_name: st.SearchStrategy[grapl_common_msgs.PropertyName] = property_names(),
 ) -> st.SearchStrategy[analyzer_msgs.StringPropertyUpdate]:
     return st.builds(
@@ -25,7 +25,7 @@ def string_property_updates(
 
 
 def uint64_property_updates(
-    uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     property_name: st.SearchStrategy[grapl_common_msgs.PropertyName] = property_names(),
 ) -> st.SearchStrategy[analyzer_msgs.UInt64PropertyUpdate]:
     return st.builds(
@@ -34,7 +34,7 @@ def uint64_property_updates(
 
 
 def int64_property_updates(
-    uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     property_name: st.SearchStrategy[grapl_common_msgs.PropertyName] = property_names(),
 ) -> st.SearchStrategy[analyzer_msgs.Int64PropertyUpdate]:
     return st.builds(
@@ -43,8 +43,8 @@ def int64_property_updates(
 
 
 def edge_updates(
-    src_uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
-    dst_uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    src_uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
+    dst_uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     forward_edge_name: st.SearchStrategy[grapl_common_msgs.EdgeName] = edge_names(),
     reverse_edge_name: st.SearchStrategy[grapl_common_msgs.EdgeName] = edge_names(),
 ) -> st.SearchStrategy[analyzer_msgs.EdgeUpdate]:
@@ -69,12 +69,9 @@ def updates(
 
 
 def run_analyzer_requests(
-    tenant_id: st.SearchStrategy[proto_common_msgs.Uuid] = strategies.uuids(),
     update: st.SearchStrategy[analyzer_msgs.Update] = updates(),
 ) -> st.SearchStrategy[analyzer_msgs.RunAnalyzerRequest]:
-    return st.builds(
-        analyzer_msgs.RunAnalyzerRequest, tenant_id=tenant_id, update=update
-    )
+    return st.builds(analyzer_msgs.RunAnalyzerRequest, update=update)
 
 
 def analyzer_names(
diff --git a/src/python/python-proto/python_proto/tests/test_api_graph.py b/src/python/python-proto/python_proto/tests/test_api_graph.py
index 98cc2f27a1..71477de2fe 100644
--- a/src/python/python-proto/python_proto/tests/test_api_graph.py
+++ b/src/python/python-proto/python_proto/tests/test_api_graph.py
@@ -19,10 +19,6 @@
     increment_only_int_props,
     increment_only_uint_props,
     lenses,
-    merged_edge_lists,
-    merged_edges,
-    merged_graphs,
-    merged_nodes,
     node_descriptions,
     node_properties,
     sessions,
@@ -90,22 +86,6 @@ def test_lens_encode_decode() -> None:
     check_encode_decode_invariant(lenses())
 
 
-def test_merged_edge_list_encode_decode() -> None:
-    check_encode_decode_invariant(merged_edge_lists())
-
-
-def test_merged_edge_encode_decode() -> None:
-    check_encode_decode_invariant(merged_edges())
-
-
-def test_merged_graph_encode_decode() -> None:
-    check_encode_decode_invariant(merged_graphs())
-
-
-def test_merged_node_encode_decode() -> None:
-    check_encode_decode_invariant(merged_nodes())
-
-
 def test_node_description_encode_decode() -> None:
     check_encode_decode_invariant(node_descriptions())
 
diff --git a/src/python/python-proto/python_proto/tests/test_api_graph_query.py b/src/python/python-proto/python_proto/tests/test_api_graph_query.py
index a96dd98f5c..5462f54b4b 100644
--- a/src/python/python-proto/python_proto/tests/test_api_graph_query.py
+++ b/src/python/python-proto/python_proto/tests/test_api_graph_query.py
@@ -8,12 +8,7 @@
 from python_proto.grapl.common.v1beta1 import messages as grapl_common_msgs
 from python_proto.tests import strategies
 from python_proto.tests.helpers import check_encode_decode_invariant
-from python_proto.tests.test_grapl_common import (
-    edge_names,
-    node_types,
-    property_names,
-    uids,
-)
+from python_proto.tests.test_grapl_common import edge_names, node_types, property_names
 
 ################################################################################
 # Strategies
@@ -115,7 +110,7 @@ def edge_query_entries(
 
 def uid_filters(
     operation: st.SearchStrategy[graph_query_msgs.UidOperation] = uid_operations,
-    value: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    value: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
 ) -> st.SearchStrategy[graph_query_msgs.UidFilter]:
     return st.builds(graph_query_msgs.UidFilter, operation=operation, value=value)
 
@@ -228,7 +223,7 @@ def string_properties(
 
 
 def node_properties_views(
-    uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     node_type: st.SearchStrategy[grapl_common_msgs.NodeType] = node_types(),
     string_properties: st.SearchStrategy[
         graph_query_msgs.StringProperties
@@ -246,7 +241,7 @@ def node_properties_view_maps(
     entries: st.SearchStrategy[
         dict[grapl_common_msgs.Uid, graph_query_msgs.NodePropertiesView]
     ] = st.dictionaries(
-        keys=uids(),
+        keys=strategies.uids(),
         values=node_properties_views(),
         max_size=hypothesis_collections_max_size,
     )
@@ -259,11 +254,11 @@ def edge_view_maps(
         dict[graph_query_msgs.EdgeViewMapK, set[grapl_common_msgs.Uid]]
     ] = st.dictionaries(
         keys=st.tuples(
-            uids(),
+            strategies.uids(),
             edge_names(),
         ),
         values=st.sets(
-            uids(),
+            strategies.uids(),
             max_size=hypothesis_collections_max_size,
         ),
         max_size=hypothesis_collections_max_size,
@@ -283,7 +278,7 @@ def graph_views(
 
 def matched_graph_with_uids(
     matched_graph: st.SearchStrategy[graph_query_msgs.GraphView] = graph_views(),
-    root_uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    root_uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
 ) -> st.SearchStrategy[graph_query_msgs.MatchedGraphWithUid]:
     return st.builds(
         graph_query_msgs.MatchedGraphWithUid,
@@ -312,7 +307,7 @@ def maybe_match_with_uids(
 
 def query_graph_with_uid_requests(
     tenant_id: st.SearchStrategy[proto_common_msgs.Uuid] = strategies.uuids(),
-    node_uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    node_uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     graph_query: st.SearchStrategy[graph_query_msgs.GraphQuery] = graph_queries(),
 ) -> st.SearchStrategy[graph_query_msgs.QueryGraphWithUidRequest]:
     return st.builds(
@@ -325,7 +320,7 @@ def query_graph_with_uid_requests(
 
 def query_graph_from_uid_requests(
     tenant_id: st.SearchStrategy[proto_common_msgs.Uuid] = strategies.uuids(),
-    node_uid: st.SearchStrategy[grapl_common_msgs.Uid] = uids(),
+    node_uid: st.SearchStrategy[grapl_common_msgs.Uid] = strategies.uids(),
     graph_query: st.SearchStrategy[graph_query_msgs.GraphQuery] = graph_queries(),
 ) -> st.SearchStrategy[graph_query_msgs.QueryGraphFromUidRequest]:
     return st.builds(
diff --git a/src/python/python-proto/python_proto/tests/test_grapl_common.py b/src/python/python-proto/python_proto/tests/test_grapl_common.py
index e3a732dd5b..a056deff77 100644
--- a/src/python/python-proto/python_proto/tests/test_grapl_common.py
+++ b/src/python/python-proto/python_proto/tests/test_grapl_common.py
@@ -8,12 +8,6 @@
 from python_proto.tests.helpers import check_encode_decode_invariant
 
 
-def uids(
-    value: st.SearchStrategy[int] = strategies.uint64s,
-) -> st.SearchStrategy[messages.Uid]:
-    return st.builds(messages.Uid, value=value)
-
-
 def property_names(
     value: st.SearchStrategy[str] = strategies.small_text,
 ) -> st.SearchStrategy[messages.PropertyName]:
@@ -33,7 +27,7 @@ def node_types(
 
 
 def test_uids() -> None:
-    check_encode_decode_invariant(uids())
+    check_encode_decode_invariant(strategies.uids())
 
 
 def test_property_names() -> None:
diff --git a/src/rust/e2e-tests/src/test_utils/context.rs b/src/rust/e2e-tests/src/test_utils/context.rs
index 0247d148f3..c68d7e3de5 100644
--- a/src/rust/e2e-tests/src/test_utils/context.rs
+++ b/src/rust/e2e-tests/src/test_utils/context.rs
@@ -12,14 +12,24 @@ use plugin_work_queue::{
 use rust_proto::{
     client_factory::services::{
         EventSourceClientConfig,
+        GraphSchemaManagerClientConfig,
         PipelineIngressClientConfig,
         PluginRegistryClientConfig,
+        ScyllaProvisionerClientConfig,
+        UidAllocatorClientConfig,
     },
     graplinc::grapl::api::{
         event_source::v1beta1::{
             client::EventSourceServiceClient,
             CreateEventSourceRequest,
         },
+        graph_schema_manager::v1beta1::{
+            client::GraphSchemaManagerClient,
+            messages::{
+                DeploySchemaRequest,
+                SchemaType,
+            },
+        },
         pipeline_ingress::v1beta1::client::PipelineIngressClient,
         plugin_registry::v1beta1::{
             DeployPluginRequest,
@@ -27,6 +37,14 @@ use rust_proto::{
             PluginRegistryServiceClient,
             PluginType,
         },
+        scylla_provisioner::v1beta1::{
+            client::ScyllaProvisionerClient,
+            messages::ProvisionGraphForTenantRequest,
+        },
+        uid_allocator::v1beta1::{
+            client::UidAllocatorServiceClient,
+            messages::CreateTenantKeyspaceRequest,
+        },
     },
     protocol::service_client::ConnectWithConfig,
 };
@@ -37,9 +55,12 @@ use crate::test_fixtures;
 
 pub struct E2eTestContext {
     pub event_source_client: EventSourceServiceClient,
+    pub graph_schema_manager_client: GraphSchemaManagerClient,
     pub plugin_registry_client: PluginRegistryServiceClient,
     pub pipeline_ingress_client: PipelineIngressClient,
     pub plugin_work_queue_psql_client: PsqlQueue,
+    pub uid_allocator_client: UidAllocatorServiceClient,
+    pub scylla_provisioner_client: ScyllaProvisionerClient,
     pub _guard: WorkerGuard,
 }
 
@@ -60,6 +81,11 @@ impl AsyncTestContext for E2eTestContext {
                 .await
                 .expect("plugin_registry_client");
 
+        let graph_schema_manager_client =
+            GraphSchemaManagerClient::connect_with_config(GraphSchemaManagerClientConfig::parse())
+                .await
+                .expect("graph_schema_manager_client");
+
         let pipeline_ingress_client =
             PipelineIngressClient::connect_with_config(PipelineIngressClientConfig::parse())
                 .await
@@ -70,11 +96,24 @@ impl AsyncTestContext for E2eTestContext {
                 .await
                 .expect("plugin_work_queue");
 
+        let uid_allocator_client =
+            UidAllocatorServiceClient::connect_with_config(UidAllocatorClientConfig::parse())
+                .await
+                .expect("uid_allocator_client");
+
+        let scylla_provisioner_client =
+            ScyllaProvisionerClient::connect_with_config(ScyllaProvisionerClientConfig::parse())
+                .await
+                .expect("scylla_provisioner_client");
+
         Self {
             event_source_client,
+            graph_schema_manager_client,
             plugin_registry_client,
             pipeline_ingress_client,
             plugin_work_queue_psql_client,
+            uid_allocator_client,
+            scylla_provisioner_client,
             _guard,
         }
     }
@@ -89,15 +128,45 @@ pub struct SetupResult {
 
 pub struct SetupGeneratorOptions {
     pub test_name: String,
+    pub tenant_id: Uuid,
     pub generator_artifact: Bytes,
     pub should_deploy_generator: bool,
 }
 
 impl E2eTestContext {
+    #[tracing::instrument(skip(self), err)]
     pub async fn create_tenant(&mut self) -> eyre::Result<Uuid> {
         tracing::info!("creating tenant");
-        // TODO: actually create a real tenant
-        Ok(Uuid::new_v4())
+        let tenant_id = Uuid::new_v4();
+        self.uid_allocator_client
+            .create_tenant_keyspace(CreateTenantKeyspaceRequest { tenant_id })
+            .await?;
+        tracing::info!("provisioning graph");
+        self.scylla_provisioner_client
+            .provision_graph_for_tenant(ProvisionGraphForTenantRequest { tenant_id })
+            .await?;
+        tracing::info!("created tenant");
+        Ok(tenant_id)
+    }
+
+    async fn provision_example_graph_schema(&mut self, tenant_id: uuid::Uuid) -> eyre::Result<()> {
+        let mut graph_schema_manager_client = self.graph_schema_manager_client.clone();
+
+        fn get_example_graphql_schema() -> Result<Bytes, std::io::Error> {
+            // This path is created in rust/Dockerfile
+            let path = "/test-fixtures/example_schemas/example.graphql";
+            std::fs::read(path).map(Bytes::from)
+        }
+
+        graph_schema_manager_client
+            .deploy_schema(DeploySchemaRequest {
+                tenant_id,
+                schema: get_example_graphql_schema().unwrap(),
+                schema_type: SchemaType::GraphqlV0,
+                schema_version: 0,
+            })
+            .await?;
+        Ok(())
     }
 
     pub async fn create_event_source(
@@ -138,6 +207,8 @@ impl E2eTestContext {
             )
             .await?;
 
+        self.provision_example_graph_schema(tenant_id).await?;
+
         Ok(generator.plugin_id())
     }
 
@@ -184,9 +255,14 @@ impl E2eTestContext {
         Ok(())
     }
 
-    pub async fn setup_sysmon_generator(&mut self, test_name: &str) -> eyre::Result<SetupResult> {
+    pub async fn setup_sysmon_generator(
+        &mut self,
+        tenant_id: uuid::Uuid,
+        test_name: &str,
+    ) -> eyre::Result<SetupResult> {
         let generator_artifact = test_fixtures::get_sysmon_generator()?;
         self.setup_generator(SetupGeneratorOptions {
+            tenant_id,
             test_name: test_name.to_owned(),
             generator_artifact,
             should_deploy_generator: true,
@@ -200,7 +276,7 @@ impl E2eTestContext {
     ) -> eyre::Result<SetupResult> {
         tracing::info!(">> Generator Setup for {}", options.test_name);
 
-        let tenant_id = self.create_tenant().await?;
+        let tenant_id = options.tenant_id;
 
         // Register an Event Source
         let event_source_id = self
diff --git a/src/rust/e2e-tests/src/test_utils/find_node.rs b/src/rust/e2e-tests/src/test_utils/find_node.rs
index dfc9ae4263..bff8f021f1 100644
--- a/src/rust/e2e-tests/src/test_utils/find_node.rs
+++ b/src/rust/e2e-tests/src/test_utils/find_node.rs
@@ -1,8 +1,6 @@
 use rust_proto::graplinc::grapl::api::graph::v1beta1::{
     IdentifiedGraph,
     IdentifiedNode,
-    MergedGraph,
-    MergedNode,
     Property,
 };
 
@@ -19,13 +17,3 @@ impl FindNode<IdentifiedNode> for IdentifiedGraph {
         })
     }
 }
-
-impl FindNode<MergedNode> for MergedGraph {
-    fn find_node<'a>(&'a self, o_p_name: &str, o_p_value: Property) -> Option<&'a MergedNode> {
-        self.nodes.values().find(|n| {
-            n.properties.iter().any(|(p_name, p_value)| {
-                p_name.as_str() == o_p_name && p_value.property.clone() == o_p_value
-            })
-        })
-    }
-}
diff --git a/src/rust/e2e-tests/src/test_utils/predicates.rs b/src/rust/e2e-tests/src/test_utils/predicates.rs
index 07902f1943..7927b3e8be 100644
--- a/src/rust/e2e-tests/src/test_utils/predicates.rs
+++ b/src/rust/e2e-tests/src/test_utils/predicates.rs
@@ -1,7 +1,6 @@
 use rust_proto::graplinc::grapl::api::graph::v1beta1::{
     IdentifiedGraph,
     ImmutableUintProp,
-    MergedGraph,
 };
 
 use crate::test_utils::find_node::FindNode;
@@ -18,10 +17,10 @@ pub fn events_36lines_node_identity_predicate(identified_graph: IdentifiedGraph)
         (Some(parent_process), Some(child_process)) => {
             let parent_to_child_edge = identified_graph
                 .edges
-                .get(parent_process.get_node_key())
+                .get(&parent_process.uid)
                 .iter()
                 .flat_map(|edge_list| edge_list.edges.iter())
-                .find(|edge| edge.to_node_key == child_process.get_node_key())
+                .find(|edge| edge.to_uid == child_process.uid)
                 .expect("missing edge from parent to child");
 
             parent_to_child_edge.edge_name == "children"
@@ -29,31 +28,3 @@ pub fn events_36lines_node_identity_predicate(identified_graph: IdentifiedGraph)
         _ => false,
     }
 }
-
-pub fn events_36lines_merged_graph_predicate(merged_graph: MergedGraph) -> bool {
-    let parent_process =
-        merged_graph.find_node("process_id", ImmutableUintProp { prop: 6132 }.into());
-
-    let child_process =
-        merged_graph.find_node("process_id", ImmutableUintProp { prop: 5752 }.into());
-
-    // NOTE: here, unlike node-identifier, we expect the edge
-    // connecting the parent and child proceses to be *absent*
-    // in the message emitted to the merged-graphs topic. The
-    // reason for this is that downstream services (analyzers)
-    // don't operate on edges, just nodes. So the view of the
-    // graph diverges at the graph-merger--we now tell one story
-    // in our Kafka messages and a totally different story in
-    // Dgraph. This is confusing and we should fix it:
-    //
-    // https://app.zenhub.com/workspaces/grapl-6036cbd36bacff000ef314f2/issues/grapl-security/issue-tracker/950
-    match (parent_process, child_process) {
-        (Some(parent_process), Some(child_process)) => !merged_graph
-            .edges
-            .get(parent_process.get_node_key())
-            .iter()
-            .flat_map(|edge_list| edge_list.edges.iter())
-            .any(|edge| edge.to_node_key == child_process.get_node_key()),
-        _ => false,
-    }
-}
diff --git a/src/rust/e2e-tests/tests/sysmon_log_e2e_test.rs b/src/rust/e2e-tests/tests/sysmon_log_e2e_test.rs
index 0d7e6f1d8d..5f2d6dbbe7 100644
--- a/src/rust/e2e-tests/tests/sysmon_log_e2e_test.rs
+++ b/src/rust/e2e-tests/tests/sysmon_log_e2e_test.rs
@@ -10,10 +10,7 @@ use e2e_tests::{
             E2eTestContext,
             SetupResult,
         },
-        predicates::{
-            events_36lines_merged_graph_predicate,
-            events_36lines_node_identity_predicate,
-        },
+        predicates::events_36lines_node_identity_predicate,
     },
 };
 use kafka::{
@@ -26,9 +23,17 @@ use rust_proto::graplinc::grapl::{
         graph::v1beta1::{
             GraphDescription,
             IdentifiedGraph,
-            MergedGraph,
         },
         pipeline_ingress::v1beta1::PublishRawLogRequest,
+        plugin_sdk::analyzers::v1beta1::messages::{
+            StringPropertyUpdate,
+            UInt64PropertyUpdate,
+            Update,
+        },
+    },
+    common::v1beta1::types::{
+        PropertyName,
+        Uid,
     },
     pipeline::v1beta1::{
         Envelope,
@@ -41,15 +46,15 @@ use uuid::Uuid;
 #[tracing::instrument(skip(ctx))]
 #[test_context(E2eTestContext)]
 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
-async fn test_sysmon_log_e2e(ctx: &mut E2eTestContext) {
+async fn test_sysmon_log_e2e(ctx: &mut E2eTestContext) -> eyre::Result<()> {
     let test_name = "test_sysmon_log_e2e";
-
+    let tenant_id = ctx.create_tenant().await?;
     let SetupResult {
         tenant_id,
         plugin_id,
         event_source_id,
     } = ctx
-        .setup_sysmon_generator(test_name)
+        .setup_sysmon_generator(tenant_id, test_name)
         .await
         .expect("failed to setup the sysmon-generator");
 
@@ -98,6 +103,10 @@ async fn test_sysmon_log_e2e(ctx: &mut E2eTestContext) {
     // time out instead of cutting it off early at 36 or 40.
     // Why does it not equal 36? Not really sure! But graph-merger is being
     // heavily rewritten soon, so let's explore that _after_ the rewrite.
+    let graph_merger_priming_message = StringPropertyUpdate {
+        uid: Uid::from_u64(1).unwrap(),
+        property_name: PropertyName::new_unchecked("arbitrary value".to_string()),
+    };
     let graph_merger_upper_bound = 45;
     let graph_merger_scanner_handle = KafkaTopicScanner::new(
         ConsumerConfig::with_topic("merged-graphs"),
@@ -106,14 +115,10 @@ async fn test_sysmon_log_e2e(ctx: &mut E2eTestContext) {
             Uuid::new_v4(),
             Uuid::new_v4(),
             Uuid::new_v4(),
-            MergedGraph::new(),
+            Update::StringProperty(graph_merger_priming_message),
         ),
     )
-    .scan_for_tenant(
-        tenant_id,
-        graph_merger_upper_bound,
-        |_graph: MergedGraph| true,
-    )
+    .scan_for_tenant(tenant_id, graph_merger_upper_bound, |_update: Update| true)
     .await;
 
     tracing::info!(">> Inserting logs into pipeline-ingress!");
@@ -195,27 +200,28 @@ async fn test_sysmon_log_e2e(ctx: &mut E2eTestContext) {
     assert_eq!(filtered_identified_graphs.len(), 1);
 
     tracing::info!(">> Test: graph-merger wrote these identified nodes to our graph database, then write to 'merged-graphs'");
-    let merged_graphs = graph_merger_scanner_handle
+    let graph_updates = graph_merger_scanner_handle
         .await
         .expect("failed to configure merged_graphs scanner");
-    assert!(merged_graphs.len() >= input_log_lines.len());
-
-    let filtered_merged_graphs = merged_graphs
-        .iter()
-        .cloned()
-        .filter(move |envelope| {
-            let envelope = envelope.clone();
-            let merged_graph = envelope.inner_message();
-            events_36lines_merged_graph_predicate(merged_graph)
+    let graph_updates: Vec<Update> = graph_updates
+        .into_iter()
+        .map(|env| env.inner_message())
+        .collect();
+    assert!(graph_updates.len() >= input_log_lines.len());
+
+    // Make sure we're getting at least one reasonable-seeming update
+    let filtered_graph_updates = graph_updates
+        .into_iter()
+        .filter(move |update| {
+            matches!(update, Update::Uint64Property(UInt64PropertyUpdate {property_name, ..}) if {
+                property_name.value == "process_id"
+            })
         })
-        .collect::<Vec<Envelope<MergedGraph>>>();
-
-    assert!(!filtered_merged_graphs.is_empty());
-    assert!(!filtered_merged_graphs.is_empty()); // shushes a really annoying clippy lint
-
-    // should actually be just one, but we're seeing repeated matching graphs.
-    // graph merger is being rewritten very soon, so revisit in sept/oct 2022!
-    // assert_eq!(filtered_merged_graphs.len(), 1);
+        .collect::<Vec<Update>>();
+    assert!(!filtered_graph_updates.is_empty()); // yes, it's doubled, because it
+    assert!(!filtered_graph_updates.is_empty()); // shushes a really annoying clippy lint
 
     // TODO: Perhaps add a test here that looks in dgraph/scylla for those identified nodes
+
+    Ok(())
 }
diff --git a/src/rust/generator-dispatcher/tests/generator_dispatcher_integration_tests.rs b/src/rust/generator-dispatcher/tests/generator_dispatcher_integration_tests.rs
index a6a89484f9..ecb0eb544b 100644
--- a/src/rust/generator-dispatcher/tests/generator_dispatcher_integration_tests.rs
+++ b/src/rust/generator-dispatcher/tests/generator_dispatcher_integration_tests.rs
@@ -15,14 +15,18 @@ use test_context::test_context;
 async fn test_dispatcher_inserts_job_into_plugin_work_queue(
     ctx: &mut E2eTestContext,
 ) -> eyre::Result<()> {
+    let _span = tracing::info_span!("test_dispatcher_inserts_job_into_plugin_work_queue").entered();
     let test_name = "test_dispatcher_inserts_job_into_plugin_work_queue";
     let generator_artifact = Bytes::from("arbitrary binary");
+
+    let tenant_id = ctx.create_tenant().await?;
     let SetupResult {
         tenant_id,
         plugin_id,
         event_source_id,
     } = ctx
         .setup_generator(SetupGeneratorOptions {
+            tenant_id,
             test_name: test_name.to_owned(),
             generator_artifact,
             should_deploy_generator: false,
diff --git a/src/rust/graph-merger/tests/integration_test.rs b/src/rust/graph-merger/tests/integration_test.rs
index f5e3b33617..bf039b51dd 100644
--- a/src/rust/graph-merger/tests/integration_test.rs
+++ b/src/rust/graph-merger/tests/integration_test.rs
@@ -13,43 +13,39 @@ use kafka::{
 };
 use rust_proto::graplinc::grapl::{
     api::{
-        graph::v1beta1::{
-            ImmutableUintProp,
-            MergedGraph,
-            MergedNode,
-            Property,
-        },
         pipeline_ingress::v1beta1::PublishRawLogRequest,
+        plugin_sdk::analyzers::v1beta1::messages::{
+            StringPropertyUpdate,
+            UInt64PropertyUpdate,
+            Update,
+        },
+    },
+    common::v1beta1::types::{
+        PropertyName,
+        Uid,
     },
     pipeline::v1beta1::Envelope,
 };
 use test_context::test_context;
 use uuid::Uuid;
 
-fn find_node<'a>(
-    graph: &'a MergedGraph,
-    o_p_name: &str,
-    o_p_value: Property,
-) -> Option<&'a MergedNode> {
-    graph.nodes.values().find(|n| {
-        n.properties.iter().any(|(p_name, p_value)| {
-            p_name.as_str() == o_p_name && p_value.property.clone() == o_p_value
-        })
-    })
-}
-
 const CONSUMER_TOPIC: &'static str = "merged-graphs";
 
 #[test_context(E2eTestContext)]
 #[tokio::test]
 async fn test_sysmon_event_produces_merged_graph(ctx: &mut E2eTestContext) -> eyre::Result<()> {
     let test_name = "test_sysmon_event_produces_merged_graph";
+    let tenant_id = ctx.create_tenant().await?;
     let SetupResult {
         tenant_id,
         plugin_id: _,
         event_source_id,
-    } = ctx.setup_sysmon_generator(test_name).await?;
+    } = ctx.setup_sysmon_generator(tenant_id, test_name).await?;
 
+    let priming_message = StringPropertyUpdate {
+        uid: Uid::from_u64(1).unwrap(),
+        property_name: PropertyName::new_unchecked("arbitrary value".to_string()),
+    };
     let kafka_scanner = KafkaTopicScanner::new(
         ConsumerConfig::with_topic(CONSUMER_TOPIC),
         Duration::from_secs(60),
@@ -57,12 +53,13 @@ async fn test_sysmon_event_produces_merged_graph(ctx: &mut E2eTestContext) -> ey
             Uuid::new_v4(),
             Uuid::new_v4(),
             Uuid::new_v4(),
-            MergedGraph::new(),
+            Update::StringProperty(priming_message),
         ),
     );
 
+    let expected_num_messages = 22;
     let handle = kafka_scanner
-        .scan_for_tenant(tenant_id, 1, |_: MergedGraph| true)
+        .scan_for_tenant(tenant_id, expected_num_messages, |_: Update| true)
         .await;
 
     let log_event: Bytes = r#"
@@ -128,43 +125,39 @@ async fn test_sysmon_event_produces_merged_graph(ctx: &mut E2eTestContext) -> ey
     tracing::info!("waiting for kafka_scanner to complete");
 
     let envelopes = handle.await?;
-    assert_eq!(envelopes.len(), 1);
-
-    let envelope = envelopes[0].clone();
-    assert_eq!(envelope.tenant_id(), tenant_id);
-    assert_eq!(envelope.event_source_id(), event_source_id);
-
-    let merged_graph = envelope.inner_message();
-
-    let parent_process = find_node(
-        &merged_graph,
-        "process_id",
-        ImmutableUintProp { prop: 6132 }.into(),
-    )
-    .expect("parent process missing");
-
-    let child_process = find_node(
-        &merged_graph,
-        "process_id",
-        ImmutableUintProp { prop: 5752 }.into(),
-    )
-    .expect("child process missing");
-
-    // NOTE: here, unlike node-identifier, we expect the edge connecting the
-    // parent and child proceses to be *absent* in the message emitted to the
-    // merged-graphs topic. The reason for this is that downstream services
-    // (analyzers) don't operate on edges, just nodes. So the view of the graph
-    // diverges at the graph-merger--we now tell one story in our Kafka messages
-    // and a totally different story in Dgraph. This is confusing and we should
-    // fix it:
-    //
-    // https://app.zenhub.com/workspaces/grapl-6036cbd36bacff000ef314f2/issues/grapl-security/issue-tracker/950
-    assert!(!merged_graph
-        .edges
-        .get(parent_process.get_node_key())
-        .iter()
-        .flat_map(|edge_list| edge_list.edges.iter())
-        .any(|edge| edge.to_node_key == child_process.get_node_key()));
+    assert_eq!(envelopes.len(), expected_num_messages);
+
+    let updates: Vec<Update> = envelopes
+        .into_iter()
+        .map(|envelope| {
+            assert_eq!(envelope.tenant_id(), tenant_id);
+            assert_eq!(envelope.event_source_id(), event_source_id);
+
+            envelope.inner_message()
+        })
+        .collect();
+
+    tracing::info!(message="updates", updates=?updates);
+
+    let process_id_update = updates.iter().find(|update| {
+        matches!(update.clone(), Update::Uint64Property(UInt64PropertyUpdate {property_name, ..}) if {
+            property_name.value == "process_id"
+        })
+    });
+    assert!(
+        process_id_update.is_some(),
+        "Expected process_id update: {process_id_update:?}"
+    );
+
+    let process_name_update = updates.iter().find(|update| {
+        matches!(update.clone(), Update::StringProperty(StringPropertyUpdate {property_name, ..}) if {
+            property_name.value == "process_name"
+        })
+    });
+    assert!(
+        process_name_update.is_some(),
+        "Expected process_name update: {process_name_update:?}"
+    );
 
     Ok(())
 }
diff --git a/src/rust/graph-merger/tests/test_merging.rs b/src/rust/graph-merger/tests/test_merging.rs
deleted file mode 100644
index 65e80491e8..0000000000
--- a/src/rust/graph-merger/tests/test_merging.rs
+++ /dev/null
@@ -1,431 +0,0 @@
-#[cfg(feature = "integration_tests")]
-pub mod test {
-    use std::{
-        collections::HashMap,
-        sync::{
-            Arc,
-            Once,
-        },
-    };
-
-    use dgraph_query_lib::{
-        condition::{
-            Condition,
-            ConditionValue,
-        },
-        predicate::{
-            Field,
-            Predicate,
-        },
-        queryblock::QueryBlockType,
-        schema::{
-            Indexing,
-            PredicateDefinition,
-            PredicateType,
-            Schema,
-            SchemaDefinition,
-        },
-        EdgeBuilder,
-        QueryBlockBuilder,
-        QueryBuilder,
-        ToQueryString,
-    };
-    use dgraph_tonic::{
-        Client as DgraphClient,
-        Query,
-    };
-    use graph_merger::upserter::GraphMergeHelper;
-    use grapl_tracing::setup_tracing;
-    use rust_proto::graplinc::grapl::api::graph::v1beta1::{
-        IdentifiedGraph,
-        IdentifiedNode,
-        ImmutableStrProp,
-        MergedGraph,
-    };
-
-    async fn query_for_uid(dgraph_client: Arc<DgraphClient>, node_key: &str) -> u64 {
-        let query_block = QueryBlockBuilder::default()
-            .query_type(QueryBlockType::query())
-            .root_filter(Condition::EQ(
-                "node_key".to_string(),
-                ConditionValue::string(node_key),
-            ))
-            .predicates(vec![Predicate::Field(Field::new("uid"))])
-            .first(1)
-            .build()
-            .unwrap();
-
-        let query = QueryBuilder::default()
-            .query_blocks(vec![query_block])
-            .build()
-            .unwrap();
-
-        let mut txn = dgraph_client.new_read_only_txn();
-        let response = txn
-            .query(query.to_query_string())
-            .await
-            .expect("query failed");
-
-        let m: HashMap<String, Vec<HashMap<String, String>>> =
-            serde_json::from_slice(&response.json).expect("response failed to parse");
-        let m = m.into_iter().next().unwrap().1;
-        debug_assert!((m.len() == 1) || (m.is_empty()));
-
-        let uid = &m[0]["uid"][2..];
-        let uid = u64::from_str_radix(uid, 16).expect("uid is not valid hex");
-        uid
-    }
-
-    async fn query_for_edge(
-        dgraph_client: Arc<DgraphClient>,
-        from_uid: u64,
-        edge_name: &str,
-    ) -> serde_json::Value {
-        let edge = Predicate::Edge(
-            EdgeBuilder::default()
-                .name(edge_name.to_string())
-                .predicates(vec![
-                    Predicate::Field(Field::new("uid")),
-                    Predicate::Field(Field::new("dgraph.type").alias("dgraph_type")),
-                ])
-                .build()
-                .unwrap(),
-        );
-
-        let query_block = QueryBlockBuilder::default()
-            .query_type(QueryBlockType::query())
-            .root_filter(Condition::uid(&from_uid.to_string()))
-            .predicates(vec![Predicate::Field(Field::new("uid")), edge])
-            .first(1)
-            .build()
-            .unwrap();
-
-        let query = QueryBuilder::default()
-            .query_blocks(vec![query_block])
-            .build()
-            .unwrap();
-
-        let mut txn = dgraph_client.new_read_only_txn();
-        let response = txn
-            .query(query.to_query_string())
-            .await
-            .expect("query failed");
-
-        serde_json::from_slice(&response.json).expect("response failed to parse")
-    }
-
-    const SERVICE_NAME: &'static str = "graph-merger-test-merging";
-
-    fn init_test_env() {
-        static START: Once = Once::new();
-        START.call_once(|| {
-            let _guard = setup_tracing(SERVICE_NAME).expect("setup_tracing");
-
-            let schema = Schema::new()
-                .add_definition(
-                    SchemaDefinition::new("ExampleNode")
-                        .add_predicate(
-                            PredicateDefinition::new("example_id", PredicateType::INT)
-                                .add_index(Indexing::INT),
-                        )
-                        .add_predicate(
-                            PredicateDefinition::new("node_key", PredicateType::String)
-                                .add_index(Indexing::EXACT)
-                                .upsert(),
-                        )
-                        .add_predicate(
-                            PredicateDefinition::new("example_name", PredicateType::String)
-                                .add_index(Indexing::TRIGRAM),
-                        )
-                        .add_predicate(PredicateDefinition::new(
-                            "to_many_edge",
-                            PredicateType::UIDArray,
-                        ))
-                        .add_predicate(PredicateDefinition::new(
-                            "to_single_edge",
-                            PredicateType::UID,
-                        )),
-                )
-                .to_string();
-
-            std::thread::spawn(move || {
-                let rt = tokio::runtime::Runtime::new().expect("failed to init runtime");
-                let mg_alpha = grapl_config::mg_alphas()
-                    .pop()
-                    .expect("Dgraph Alpha not specified.");
-
-                rt.block_on(async {
-                    let dgraph_client =
-                        DgraphClient::new(mg_alpha).expect("Failed to create dgraph client.");
-
-                    dgraph_client
-                        .alter(dgraph_tonic::Operation {
-                            drop_all: true,
-                            ..Default::default()
-                        })
-                        .await
-                        .expect("alter failed");
-
-                    dgraph_client
-                        .alter(dgraph_tonic::Operation {
-                            schema,
-                            ..Default::default()
-                        })
-                        .await
-                        .expect("alter failed");
-                });
-            })
-            .join()
-            .expect("provision failed");
-        });
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_upsert_edge_and_retrieve() -> eyre::Result<()> {
-        init_test_env();
-        let mg_alpha = grapl_config::mg_alphas()
-            .pop()
-            .expect("Dgraph Alpha not specified.");
-
-        let mut identified_graph = IdentifiedGraph::new();
-        let mut merged_graph = MergedGraph::new();
-        let dgraph_client = DgraphClient::new(mg_alpha).expect("Failed to create dgraph client.");
-        let dgraph_client = std::sync::Arc::new(dgraph_client);
-        let mut properties = HashMap::new();
-        properties.insert(
-            "example_name".to_string(),
-            ImmutableStrProp {
-                prop: "foobar".to_string(),
-            }
-            .into(),
-        );
-        let n0 = IdentifiedNode {
-            node_key: "example-node-key".to_string(),
-            node_type: "ExampleNode".to_string(),
-            properties,
-        };
-
-        let mut properties = HashMap::new();
-        properties.insert(
-            "example_name".to_string(),
-            ImmutableStrProp {
-                prop: "baz".to_string(),
-            }
-            .into(),
-        );
-
-        let n1 = IdentifiedNode {
-            node_key: "someother-node-key".to_string(),
-            node_type: "Process".to_string(),
-            properties,
-        };
-
-        identified_graph.add_node(n0);
-        identified_graph.add_node(n1);
-
-        identified_graph.add_edge(
-            "to_many_edge".to_string(),
-            "example-node-key".to_string(),
-            "someother-node-key".to_string(),
-        );
-
-        identified_graph.add_edge(
-            "to_single_edge".to_string(),
-            "someother-node-key".to_string(),
-            "example-node-key".to_string(),
-        );
-
-        GraphMergeHelper {}
-            .upsert_into(dgraph_client.clone(), &identified_graph, &mut merged_graph)
-            .await;
-
-        let node_uid_0 = query_for_uid(dgraph_client.clone(), "example-node-key").await;
-        let node_uid_1 = query_for_uid(dgraph_client.clone(), "someother-node-key").await;
-        assert_ne!(node_uid_0, node_uid_1);
-        assert_ne!(node_uid_0, 0);
-        assert_ne!(node_uid_1, 0);
-
-        let to_many_res = query_for_edge(dgraph_client.clone(), node_uid_0, "to_many_edge").await;
-
-        let to_single_res =
-            query_for_edge(dgraph_client.clone(), node_uid_1, "to_single_edge").await;
-
-        let to_many_res = to_many_res
-            .as_object()
-            .expect("to_many_res.as_object")
-            .values()
-            .next()
-            .expect("to_many_res empty array");
-        let to_single_res = to_single_res
-            .as_object()
-            .expect("to_single_res.as_object")
-            .values()
-            .next()
-            .expect("to_single_res empty array");
-
-        let tm_from = to_many_res[0]["uid"].as_str().expect("tm_from");
-        let tm_to = to_many_res[0]["to_many_edge"][0]["uid"]
-            .as_str()
-            .expect("tm_to");
-
-        let ts_from = to_single_res[0]["uid"].as_str().expect("ts_from");
-        let ts_to = to_single_res[0]["to_single_edge"]["uid"]
-            .as_str()
-            .expect("ts_to");
-
-        assert_eq!(tm_from, ts_to);
-        assert_eq!(tm_to, ts_from);
-        Ok(())
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_upsert_idempotency() -> eyre::Result<()> {
-        init_test_env();
-        let mg_alpha = grapl_config::mg_alphas()
-            .pop()
-            .expect("Dgraph Alpha not specified.");
-
-        let dgraph_client = DgraphClient::new(mg_alpha).expect("Failed to create dgraph client.");
-        let dgraph_client = std::sync::Arc::new(dgraph_client);
-
-        let node_key = "test_upsert_idempotency-example-node-key";
-        let mut properties = HashMap::new();
-        properties.insert(
-            "example_name".to_string(),
-            ImmutableStrProp {
-                prop: "foobar".to_string(),
-            }
-            .into(),
-        );
-        let n0 = IdentifiedNode {
-            node_key: node_key.to_string(),
-            node_type: "ExampleNode".to_string(),
-            properties,
-        };
-
-        let upsert_futs: Vec<_> = (0..10)
-            .map(|_| {
-                let dgraph_client = dgraph_client.clone();
-                let n0 = n0.clone();
-                async move {
-                    let mut identified_graph = IdentifiedGraph::new();
-                    identified_graph.add_node(n0);
-                    let mut merged_graph = MergedGraph::new();
-
-                    GraphMergeHelper {}
-                        .upsert_into(dgraph_client.clone(), &identified_graph, &mut merged_graph)
-                        .await;
-                    merged_graph
-                }
-            })
-            .collect();
-
-        let mut merged_graphs = Vec::with_capacity(upsert_futs.len());
-        for upsert_fut in upsert_futs.into_iter() {
-            merged_graphs.push(upsert_fut.await);
-        }
-
-        for merged_graph in merged_graphs {
-            assert_eq!(merged_graph.nodes.len(), 1);
-        }
-
-        // If we query for multiple nodes by node_key we should only ever receive one
-        let query_block = QueryBlockBuilder::default()
-            .query_type(QueryBlockType::query())
-            .root_filter(Condition::EQ(
-                "node_key".to_string(),
-                ConditionValue::string(node_key),
-            ))
-            .predicates(vec![Predicate::Field(Field::new("uid"))])
-            .first(2)
-            .build()
-            .unwrap();
-
-        let query = QueryBuilder::default()
-            .query_blocks(vec![query_block])
-            .build()
-            .unwrap();
-
-        let mut txn = dgraph_client.new_read_only_txn();
-        let response = txn
-            .query(query.to_query_string())
-            .await
-            .expect("query failed");
-
-        let m: HashMap<String, Vec<HashMap<String, String>>> =
-            serde_json::from_slice(&response.json).expect("response failed to parse");
-        let m = m.into_iter().next().unwrap().1;
-        debug_assert_eq!(m.len(), 1);
-        Ok(())
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_upsert_multifield() -> eyre::Result<()> {
-        init_test_env();
-        let mg_alpha = grapl_config::mg_alphas()
-            .pop()
-            .expect("Dgraph Alpha not specified.");
-
-        let dgraph_client = DgraphClient::new(mg_alpha).expect("Failed to create dgraph client.");
-        let dgraph_client = std::sync::Arc::new(dgraph_client);
-
-        let node_key = "test_upsert_multifield-example-node-key";
-        let mut properties = HashMap::new();
-        properties.insert(
-            "example_name".to_string(),
-            ImmutableStrProp {
-                prop: "test_upsert_multifield".to_string(),
-            }
-            .into(),
-        );
-        let n0 = IdentifiedNode {
-            node_key: node_key.to_string(),
-            node_type: "ExampleNode".to_string(),
-            properties,
-        };
-        let mut identified_graph = IdentifiedGraph::new();
-        identified_graph.add_node(n0);
-        let mut merged_graph = MergedGraph::new();
-
-        GraphMergeHelper {}
-            .upsert_into(dgraph_client.clone(), &identified_graph, &mut merged_graph)
-            .await;
-
-        // If we query for multiple nodes by node_key we should only ever receive one
-        let query_block = QueryBlockBuilder::default()
-            .query_type(QueryBlockType::query())
-            .root_filter(Condition::EQ(
-                "node_key".to_string(),
-                ConditionValue::string(node_key),
-            ))
-            .predicates(vec![
-                Predicate::Field(Field::new("uid")),
-                Predicate::Field(Field::new("example_name")),
-            ])
-            // .first(2)
-            .build()
-            .unwrap();
-
-        let query = QueryBuilder::default()
-            .query_blocks(vec![query_block])
-            .build()
-            .unwrap();
-
-        let mut txn = dgraph_client.new_read_only_txn();
-        let response = txn
-            .query(query.to_query_string())
-            .await
-            .expect("query failed");
-
-        let m: HashMap<String, Vec<HashMap<String, String>>> =
-            serde_json::from_slice(&response.json).expect("response failed to parse");
-        let mut m = m.into_iter().next().unwrap().1;
-        debug_assert_eq!(m.len(), 1);
-        let mut m = m.remove(0);
-        let _uid = m.remove("uid").expect("uid");
-        let example_name = m.remove("example_name").expect("example_name");
-        assert!(m.is_empty());
-        assert_eq!(example_name, "test_upsert_multifield");
-        Ok(())
-    }
-}
diff --git a/src/rust/graph-query-service/tests/integration_test.rs b/src/rust/graph-query-service/tests/integration_test.rs
index b196e7b7c5..e6cbeabd36 100644
--- a/src/rust/graph-query-service/tests/integration_test.rs
+++ b/src/rust/graph-query-service/tests/integration_test.rs
@@ -14,6 +14,7 @@ use rust_proto::{
         api::{
             graph::v1beta1::{
                 ImmutableStrProp,
+                IncrementOnlyUintProp,
                 NodeProperty,
                 Property,
             },
@@ -75,43 +76,71 @@ async fn provision_example_graph_schema(tenant_id: uuid::Uuid) -> eyre::Result<(
     Ok(())
 }
 
-#[test_log::test(tokio::test)]
-async fn test_query_two_attached_nodes() -> eyre::Result<()> {
-    let _span = tracing::info_span!(
-        "tenant_id", tenant_id=?tracing::field::Empty,
-    );
-
-    let query_client_config = GraphQueryClientConfig::parse();
-    let mut graph_query_client = GraphQueryClient::connect_with_config(query_client_config).await?;
-
-    let mutation_client_config = GraphMutationClientConfig::parse();
-    let mut graph_mutation_client =
-        GraphMutationClient::connect_with_config(mutation_client_config).await?;
-
-    let provisioner_client_config = ScyllaProvisionerClientConfig::parse();
-    let mut provisioner_client =
-        ScyllaProvisionerClient::connect_with_config(provisioner_client_config).await?;
-
-    let tenant_id = uuid::Uuid::new_v4();
-    _span.record("tenant_id", &format!("{tenant_id}"));
+// This should probably replaced by a refactored E2eTestContext at some point.
+struct GraphQueryIntegTestSetup {
+    graph_query_client: GraphQueryClient,
+    graph_mutation_client: GraphMutationClient,
+    tenant_id: uuid::Uuid,
+    _span: tracing::Span,
+}
 
-    provisioner_client
-        .provision_graph_for_tenant(scylla_provisioner_msgs::ProvisionGraphForTenantRequest {
+impl GraphQueryIntegTestSetup {
+    pub async fn setup() -> eyre::Result<GraphQueryIntegTestSetup> {
+        // <setup>
+        let _span = tracing::info_span!(
+            "tenant_id", tenant_id=?tracing::field::Empty,
+        );
+
+        let query_client_config = GraphQueryClientConfig::parse();
+        let graph_query_client = GraphQueryClient::connect_with_config(query_client_config).await?;
+
+        let mutation_client_config = GraphMutationClientConfig::parse();
+        let graph_mutation_client =
+            GraphMutationClient::connect_with_config(mutation_client_config).await?;
+
+        let provisioner_client_config = ScyllaProvisionerClientConfig::parse();
+        let mut provisioner_client =
+            ScyllaProvisionerClient::connect_with_config(provisioner_client_config).await?;
+
+        let tenant_id = uuid::Uuid::new_v4();
+        _span.record("tenant_id", &format!("{tenant_id}"));
+
+        provisioner_client
+            .provision_graph_for_tenant(scylla_provisioner_msgs::ProvisionGraphForTenantRequest {
+                tenant_id,
+            })
+            .await?;
+
+        // Only used to provision the keyspace. It's okay here to use the
+        // otherwise-unrecommended non-caching UidAllocator client.
+
+        let mut uid_allocator_client =
+            UidAllocatorServiceClient::connect_with_config(UidAllocatorClientConfig::parse())
+                .await?;
+        uid_allocator_client
+            .create_tenant_keyspace(CreateTenantKeyspaceRequest { tenant_id })
+            .await?;
+
+        // Provision a Schema so we can test how edges work
+        provision_example_graph_schema(tenant_id).await?;
+
+        Ok(Self {
+            graph_query_client,
+            graph_mutation_client,
             tenant_id,
+            _span,
         })
-        .await?;
-
-    // Only used to provision the keyspace. It's okay here to use the
-    // otherwise-unrecommended non-caching UidAllocator client.
-
-    let mut uid_allocator_client =
-        UidAllocatorServiceClient::connect_with_config(UidAllocatorClientConfig::parse()).await?;
-    uid_allocator_client
-        .create_tenant_keyspace(CreateTenantKeyspaceRequest { tenant_id })
-        .await?;
+    }
+}
 
-    // Provision a Schema so we can test how edges work
-    provision_example_graph_schema(tenant_id).await?;
+#[test_log::test(tokio::test)]
+async fn test_query_two_attached_nodes() -> eyre::Result<()> {
+    let GraphQueryIntegTestSetup {
+        mut graph_query_client,
+        mut graph_mutation_client,
+        tenant_id,
+        _span,
+    } = GraphQueryIntegTestSetup::setup().await?;
 
     let process_node_type = NodeType::try_from("Process").unwrap();
     let file_node_type = NodeType::try_from("File").unwrap();
@@ -265,4 +294,56 @@ async fn test_query_two_attached_nodes() -> eyre::Result<()> {
     Ok(())
 }
 
+#[test_log::test(tokio::test)]
+async fn test_set_node_property() -> eyre::Result<()> {
+    let GraphQueryIntegTestSetup {
+        graph_query_client: _,
+        mut graph_mutation_client,
+        tenant_id,
+        _span,
+    } = GraphQueryIntegTestSetup::setup().await?;
+
+    let process_node_type = NodeType::try_from("Process").unwrap();
+
+    // Make a node. Do lots of SetNodePropertys on it
+    let mutation::CreateNodeResponse { uid } = graph_mutation_client
+        .create_node(mutation::CreateNodeRequest {
+            tenant_id,
+            node_type: process_node_type.clone(),
+        })
+        .await?;
+
+    graph_mutation_client
+        .set_node_property(mutation::SetNodePropertyRequest {
+            tenant_id,
+            uid,
+            node_type: process_node_type.clone(),
+            property_name: "process_name".try_into()?,
+            property: NodeProperty {
+                property: Property::ImmutableStrProp(ImmutableStrProp {
+                    prop: "chrome.exe".into(),
+                }),
+            },
+        })
+        .await?;
+
+    graph_mutation_client
+        .set_node_property(mutation::SetNodePropertyRequest {
+            tenant_id,
+            uid,
+            node_type: process_node_type.clone(),
+            property_name: "last_seen_time".try_into()?,
+            property: NodeProperty {
+                property: Property::IncrementOnlyUintProp(IncrementOnlyUintProp {
+                    // arbitrary date - millis since July 2019
+                    prop: 1563991514399,
+                }),
+            },
+        })
+        .await?;
+
+    drop(_span);
+    Ok(())
+}
+
 // TODO: test `with_edge_to`
diff --git a/src/rust/node-identifier/tests/integration_test.rs b/src/rust/node-identifier/tests/integration_test.rs
index ee6b7d0a37..793a458c32 100644
--- a/src/rust/node-identifier/tests/integration_test.rs
+++ b/src/rust/node-identifier/tests/integration_test.rs
@@ -44,11 +44,12 @@ fn find_node<'a>(
 #[tokio::test]
 async fn test_sysmon_event_produces_identified_graph(ctx: &mut E2eTestContext) -> eyre::Result<()> {
     let test_name = "test_sysmon_event_produces_identified_graph";
+    let tenant_id = ctx.create_tenant().await?;
     let SetupResult {
         tenant_id,
         plugin_id: _,
         event_source_id,
-    } = ctx.setup_sysmon_generator(test_name).await?;
+    } = ctx.setup_sysmon_generator(tenant_id, test_name).await?;
 
     let kafka_scanner = KafkaTopicScanner::new(
         ConsumerConfig::with_topic(CONSUMER_TOPIC),
@@ -152,10 +153,10 @@ async fn test_sysmon_event_produces_identified_graph(ctx: &mut E2eTestContext) -
 
     let parent_to_child_edge = identified_graph
         .edges
-        .get(parent_process.get_node_key())
+        .get(&parent_process.uid)
         .iter()
         .flat_map(|edge_list| edge_list.edges.iter())
-        .find(|edge| edge.to_node_key == child_process.get_node_key())
+        .find(|edge| edge.to_uid == child_process.uid)
         .expect("missing edge from parent to child");
 
     assert_eq!(parent_to_child_edge.edge_name, "children");
diff --git a/src/rust/node-identifier/tests/sessiondb_integration_tests.rs b/src/rust/node-identifier/tests/sessiondb_integration_tests.rs
deleted file mode 100644
index d1ec22cd66..0000000000
--- a/src/rust/node-identifier/tests/sessiondb_integration_tests.rs
+++ /dev/null
@@ -1,297 +0,0 @@
-#![cfg(feature = "integration_tests")]
-
-use std::time::Duration;
-
-use grapl_config::env_helpers::FromEnv;
-use node_identifier::{
-    sessiondb::SessionDb,
-    sessions::{
-        Session,
-        UnidSession,
-    },
-};
-use rusoto_core::RusotoError;
-use rusoto_dynamodb::{
-    AttributeDefinition,
-    CreateTableError,
-    CreateTableInput,
-    CreateTableOutput,
-    DeleteTableInput,
-    DynamoDb,
-    DynamoDbClient,
-    KeySchemaElement,
-    ProvisionedThroughput,
-};
-use tokio::runtime::Runtime;
-use uuid::Uuid;
-
-async fn try_create_table(
-    dynamo: &impl DynamoDb,
-    table_name: String,
-) -> Result<CreateTableOutput, RusotoError<CreateTableError>> {
-    dynamo
-        .create_table(CreateTableInput {
-            table_name,
-            attribute_definitions: vec![
-                AttributeDefinition {
-                    attribute_name: "pseudo_key".into(),
-                    attribute_type: "S".into(),
-                },
-                AttributeDefinition {
-                    attribute_name: "create_time".into(),
-                    attribute_type: "N".into(),
-                },
-            ],
-            key_schema: vec![
-                KeySchemaElement {
-                    attribute_name: "pseudo_key".into(),
-                    key_type: "HASH".into(),
-                },
-                KeySchemaElement {
-                    attribute_name: "create_time".into(),
-                    key_type: "RANGE".into(),
-                },
-            ],
-            provisioned_throughput: Some(ProvisionedThroughput {
-                read_capacity_units: 3,
-                write_capacity_units: 3,
-            }),
-            ..Default::default()
-        })
-        .await
-}
-
-fn create_or_empty_table(dynamo: &impl DynamoDb, table_name: impl Into<String>) {
-    let runtime = Runtime::new().unwrap();
-    let table_name = table_name.into();
-
-    let _ = runtime.block_on(dynamo.delete_table(DeleteTableInput {
-        table_name: table_name.clone(),
-    }));
-
-    std::thread::sleep(Duration::from_millis(250));
-
-    while let Err(_e) = runtime.block_on(try_create_table(dynamo, table_name.clone())) {
-        std::thread::sleep(Duration::from_millis(250));
-    }
-}
-
-// Given an empty timeline
-// When a canonical creation event comes in
-// Then the newly created session should be in the timeline
-#[test]
-fn canon_create_on_empty_timeline() {
-    let runtime = Runtime::new().unwrap();
-    let table_name = "process_history_canon_create_on_empty_timeline";
-    let dynamo = DynamoDbClient::from_env();
-
-    create_or_empty_table(&dynamo, table_name);
-
-    let session_db = SessionDb::new(dynamo, table_name);
-
-    let unid = UnidSession {
-        pseudo_key: format!("{}{}", Uuid::new_v4(), rand::random::<u64>(),),
-        timestamp: 1544301484600,
-        is_creation: true,
-    };
-
-    let session_id = runtime
-        .block_on(session_db.handle_unid_session(unid, false))
-        .expect("Failed to create session");
-
-    assert!(!session_id.is_empty());
-}
-
-// Given a timeline with a single session, where that session has a non canon
-//      creation time 'X'
-// When a canonical creation event comes in with a creation time of 'Y'
-//      where 'Y' < 'X'
-// Then the session should be updated to have 'Y' as its canonical create time
-#[test]
-fn canon_create_update_existing_non_canon_create() {
-    let runtime = Runtime::new().unwrap();
-    let table_name = "process_history_canon_create_update_existing_non_canon_create";
-    let dynamo = DynamoDbClient::from_env();
-
-    create_or_empty_table(&dynamo, table_name);
-
-    let session_db = SessionDb::new(dynamo, table_name);
-
-    let asset_id = Uuid::new_v4();
-    let pid: u64 = rand::random();
-    // Given a timeline with a single session, where that session has a non canon
-    //      creation time 'X'
-    let session = Session {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        create_time: 1_544_301_484_600,
-        is_create_canon: false,
-        session_id: "SessionId".into(),
-        is_end_canon: false,
-        end_time: 1_544_301_484_700,
-        version: 0,
-    };
-
-    runtime
-        .block_on(session_db.create_session(&session))
-        .expect("Failed to create session");
-
-    // When a canonical creation event comes in with a creation time of 'Y'
-    //      where 'Y' < 'X'
-    let unid = UnidSession {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        timestamp: 1_544_301_484_500,
-        is_creation: true,
-    };
-
-    let session_id = runtime
-        .block_on(session_db.handle_unid_session(unid, false))
-        .expect("Failed to handle unid");
-
-    assert_eq!(session_id, "SessionId");
-}
-
-// Given a timeline with a single session, where that session has a non canon
-//      creation time 'X'
-// When a noncanonical creation event comes in with a creation time of 'Y'
-//      where 'Y' < 'X'
-// Then the session should be updated to have 'Y' as its noncanonical create time
-#[test]
-fn noncanon_create_update_existing_non_canon_create() {
-    let runtime = Runtime::new().unwrap();
-    let table_name = "process_history_noncanon_create_update_existing_non_canon_create";
-    let dynamo = DynamoDbClient::from_env();
-
-    create_or_empty_table(&dynamo, table_name);
-
-    let session_db = SessionDb::new(dynamo, table_name);
-
-    let asset_id = Uuid::new_v4();
-    let pid: u64 = rand::random();
-
-    // Given a timeline with a single session, where that session has a non canon
-    //      creation time 'X'
-    let session = Session {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        create_time: 1_544_301_484_600,
-        is_create_canon: false,
-        session_id: "SessionId".into(),
-        is_end_canon: false,
-        end_time: 1_544_301_484_700,
-        version: 0,
-    };
-
-    runtime
-        .block_on(session_db.create_session(&session))
-        .expect("Failed to create session");
-
-    // When a noncanonical creation event comes in with a creation time of 'Y'
-    //      where 'Y' < 'X'
-    let unid = UnidSession {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        timestamp: 1_544_301_484_500,
-        is_creation: false,
-    };
-
-    let session_id = runtime
-        .block_on(session_db.handle_unid_session(unid, false))
-        .expect("Failed to handle unid");
-
-    // TODO: Assert that the create time was updated correctly
-    assert_eq!(session_id, "SessionId");
-}
-
-// Given an empty timeline
-// When a noncanon create event comes in and 'should_default' is true
-// Then Create the new noncanon session
-#[test]
-fn noncanon_create_on_empty_timeline_with_default() {
-    let runtime = Runtime::new().unwrap();
-    let table_name = "process_history_noncanon_create_on_empty_timeline_with_default";
-    let dynamo = DynamoDbClient::from_env();
-
-    create_or_empty_table(&dynamo, table_name);
-
-    let session_db = SessionDb::new(dynamo, table_name);
-
-    let asset_id = Uuid::new_v4();
-    let pid: u64 = rand::random();
-
-    let unid = UnidSession {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        timestamp: 1_544_301_484_500,
-        is_creation: false,
-    };
-
-    let session_id = runtime
-        .block_on(session_db.handle_unid_session(unid, true))
-        .expect("Failed to create session");
-
-    assert!(!session_id.is_empty());
-}
-
-// Given an empty timeline
-// When a noncanon create event comes in and 'should_default' is false
-// Then return an error
-#[test]
-fn noncanon_create_on_empty_timeline_without_default() {
-    let runtime = Runtime::new().unwrap();
-    let table_name = "process_history_noncanon_create_on_empty_timeline_without_default";
-    let dynamo = DynamoDbClient::from_env();
-
-    create_or_empty_table(&dynamo, table_name);
-
-    let session_db = SessionDb::new(dynamo, table_name);
-
-    let unid = UnidSession {
-        pseudo_key: "asset_id_a1234".into(),
-        timestamp: 1_544_301_484_500,
-        is_creation: false,
-    };
-
-    let session_id = runtime.block_on(session_db.handle_unid_session(unid, false));
-    assert!(session_id.is_err());
-}
-
-#[test]
-fn update_end_time() {
-    let runtime = Runtime::new().unwrap();
-    let table_name = "process_history_update_end_time";
-    let dynamo = DynamoDbClient::from_env();
-
-    create_or_empty_table(&dynamo, table_name);
-
-    let session_db = SessionDb::new(dynamo, table_name);
-
-    let asset_id = Uuid::new_v4();
-    let pid: u64 = rand::random();
-
-    // Given a timeline with a single session, where that session has a non canon
-    //      end time 'X'
-    let session = Session {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        create_time: 1_544_301_484_600,
-        is_create_canon: false,
-        session_id: "SessionId".into(),
-        is_end_canon: false,
-        end_time: 1_544_301_484_700,
-        version: 0,
-    };
-
-    runtime
-        .block_on(session_db.create_session(&session))
-        .expect("Failed to create session");
-
-    // When a canonical creation event comes in with an end time of 'Y'
-    //      where 'Y' < 'X'
-    let unid = UnidSession {
-        pseudo_key: format!("{}{}", asset_id, pid),
-        timestamp: 1_544_301_484_800,
-        is_creation: false,
-    };
-
-    let session_id = runtime
-        .block_on(session_db.handle_unid_session(unid, false))
-        .expect("Failed to handle unid");
-
-    assert_eq!(session_id, "SessionId");
-}
diff --git a/src/rust/rust-proto/tests/proto_serde_tests.rs b/src/rust/rust-proto/tests/proto_serde_tests.rs
index 59ab5691d0..39544b45c3 100644
--- a/src/rust/rust-proto/tests/proto_serde_tests.rs
+++ b/src/rust/rust-proto/tests/proto_serde_tests.rs
@@ -206,30 +206,6 @@ mod graph {
             check_encode_decode_invariant(identified_graph)
         }
 
-        #[test]
-        fn test_merged_edge_encode_decode(merged_edge in st::merged_edges()) {
-            check_encode_decode_invariant(merged_edge)
-        }
-
-        #[test]
-        fn test_merged_edge_list_encode_decode(
-            merged_edge_list in st::merged_edge_lists()
-        ) {
-            check_encode_decode_invariant(merged_edge_list)
-        }
-
-        #[test]
-        fn test_merged_node_encode_decode(merged_node in st::merged_nodes()) {
-            check_encode_decode_invariant(merged_node)
-        }
-
-        #[test]
-        fn test_merged_graph_encode_decode(
-            merged_graph in st::merged_graphs()
-        ) {
-            check_encode_decode_invariant(merged_graph)
-        }
-
         #[test]
         fn test_lens_encode_decode(
             lens in st::lenses()
@@ -545,4 +521,11 @@ mod analyzer_sdk {
             check_encode_decode_invariant(value)
         }
     }
+
+    proptest! {
+        #[test]
+        fn test_update(update in as_strats::updates()) {
+            check_encode_decode_invariant(update)
+        }
+    }
 }
diff --git a/src/rust/rust-proto/tests/test_utils/strategies.rs b/src/rust/rust-proto/tests/test_utils/strategies.rs
index c4f397a580..3689cd9c95 100644
--- a/src/rust/rust-proto/tests/test_utils/strategies.rs
+++ b/src/rust/rust-proto/tests/test_utils/strategies.rs
@@ -132,6 +132,8 @@ pub mod graph {
         ExecutionHit,
         GraphDescription,
         IdStrategy,
+        IdentifiedEdge,
+        IdentifiedEdgeList,
         IdentifiedGraph,
         IdentifiedNode,
         ImmutableIntProp,
@@ -140,10 +142,6 @@ pub mod graph {
         IncrementOnlyIntProp,
         IncrementOnlyUintProp,
         Lens,
-        MergedEdge,
-        MergedEdgeList,
-        MergedGraph,
-        MergedNode,
         NodeDescription,
         NodeProperty,
         Property,
@@ -153,6 +151,7 @@ pub mod graph {
     };
 
     use super::*;
+    use crate::strategies::common::uids;
 
     //
     // DecrementOnlyIntProp
@@ -310,8 +309,8 @@ pub mod graph {
 
     prop_compose! {
         pub fn execution_hits()(
-            nodes in collection::hash_map(any::<String>(), merged_nodes(), 10),
-            edges in collection::hash_map(any::<String>(), merged_edge_lists(), 10),
+            nodes in collection::hash_map(uids(), identified_nodes(), 10),
+            edges in collection::hash_map(uids(), identified_edge_lists(), 10),
             analyzer_name in any::<String>(),
             risk_score in any::<u64>(),
             lenses in collection::vec(lenses(), 10),
@@ -456,16 +455,15 @@ pub mod graph {
     //
     // IdentifiedNode
     //
-
     prop_compose! {
         pub fn identified_nodes()(
             properties in collection::hash_map(any::<String>(), node_properties(), 10),
-            node_key in any::<String>(),
+            uid in common::uids(),
             node_type in any::<String>(),
         ) -> IdentifiedNode {
             IdentifiedNode {
                 properties,
-                node_key,
+                uid,
                 node_type
             }
         }
@@ -477,8 +475,8 @@ pub mod graph {
 
     prop_compose! {
         pub fn identified_graphs()(
-            nodes in collection::hash_map(any::<String>(), identified_nodes(), 10),
-            edges in collection::hash_map(any::<String>(), edge_lists(), 10),
+            nodes in collection::hash_map(common::uids(), identified_nodes(), 10),
+            edges in collection::hash_map(common::uids(), identified_edge_lists(), 10),
         ) -> IdentifiedGraph {
             IdentifiedGraph {
                 nodes,
@@ -488,72 +486,32 @@ pub mod graph {
     }
 
     //
-    // MergedEdge
+    // IdentifiedEdge
     //
 
     prop_compose! {
-        pub fn merged_edges()(
-            from_uid in any::<String>(),
-            from_node_key in any::<String>(),
-            to_uid in any::<String>(),
-            to_node_key in any::<String>(),
+        pub fn identified_edges()(
+            from_uid in common::uids(),
+            to_uid in common::uids(),
             edge_name in any::<String>(),
-        ) -> MergedEdge {
-            MergedEdge {
+        ) -> IdentifiedEdge {
+            IdentifiedEdge {
                 from_uid,
-                from_node_key,
                 to_uid,
-                to_node_key,
                 edge_name
             }
         }
     }
 
     //
-    // MergedEdgeList
-    //
-
-    prop_compose! {
-        pub fn merged_edge_lists()(
-            edges in collection::vec(merged_edges(), 10),
-        ) -> MergedEdgeList {
-            MergedEdgeList { edges }
-        }
-    }
-
-    //
-    // MergedNode
-    //
-
-    prop_compose! {
-        pub fn merged_nodes()(
-            properties in collection::hash_map(any::<String>(), node_properties(), 10),
-            uid in any::<u64>(),
-            node_key in any::<String>(),
-            node_type in any::<String>(),
-        ) -> MergedNode {
-            MergedNode {
-                properties,
-                uid,
-                node_key,
-                node_type
-            }
-        }
-    }
-
-    //
-    // MergedGraph
+    // IdentifiedEdgeList
     //
 
     prop_compose! {
-        pub fn merged_graphs()(
-            nodes in collection::hash_map(any::<String>(), merged_nodes(), 10),
-            edges in collection::hash_map(any::<String>(), merged_edge_lists(), 10),
-        ) -> MergedGraph {
-            MergedGraph {
-                nodes,
-                edges,
-            }
+        pub fn identified_edge_lists()(
+            edges in collection::vec(identified_edges(), 10),
+        ) -> IdentifiedEdgeList {
+            IdentifiedEdgeList { edges }
         }
     }
 }
@@ -1042,9 +1000,7 @@ pub mod plugin_work_queue {
         pub fn get_execute_analyzer_requests()(
             plugin_id in uuids(),
         ) -> native::GetExecuteAnalyzerRequest {
-            native::GetExecuteAnalyzerRequest {
-                plugin_id,
-            }
+            native::GetExecuteAnalyzerRequest::new(plugin_id)
         }
     }
 
@@ -1247,13 +1203,9 @@ pub mod analyzer_sdk {
 
     prop_compose! {
         pub fn run_analyzer_requests()(
-            tenant_id in uuids(),
             update in updates(),
         ) -> native::RunAnalyzerRequest {
-            native::RunAnalyzerRequest {
-                tenant_id,
-                update,
-            }
+            native::RunAnalyzerRequest::new(update)
         }
     }
 }