Status

Canonical implementation guide. Version 0.2. Companion to the Memory Runtime Integration Guide — that guide fixes the NAMS/graph-tier binding mechanics (client configuration, bolt mode, Community Edition constraints); this guide is the per-agent use-case and query-pattern catalog built on top of that binding. It does not restate that guide’s §5. Related specs: Synedrix OS Outcome Spec, Synedrix OS Memory Fabric Spec, Synedrix OS - Agent Registry, Synedrix OS ADR-OS-002.

Purpose

The Neo4j platform page establishes what the store is and why the OS uses it. The Memory Fabric Spec and the Memory Runtime Integration Guide fix the graph tier’s lifecycle and its NAMS binding. The Outcome Spec fixes the node and relationship model. None of them walks the Agent Registry end to end and shows what each of the twelve OS agents actually does — or explicitly does not do — with the graph. This guide fills that gap.

A Neo4j or Cypher usage is non-compliant unless it stays inside the three-agent boundary the Agent Registry already defines — os-agt-008 (Audit & Lineage Agent, read-only), os-agt-010 (Memory Curator Agent, sole writer, via NAMS bolt mode), os-agt-011 (Consolidation & Reflection Agent, read-only) — and every graph write is a projection of a prior Postgres write, never the reverse. A pattern that reads well but breaks either rule is not a smaller version of compliance; it is a governance bypass that happens to run.

Scope

  • The per-agent use-case catalog: which of the twelve OS agents touch the graph tier, with worked Cypher and NAMS-client examples for the three that do, and an honest accounting of the nine that do not
  • The Postgres-first write pattern, taught through a real, already-flagged violation in the trading domain pack
  • OS-level indexing guidance for the query shapes os-agt-008 actually issues
  • A compliance checklist for any new Neo4j or Cypher usage added to an OS-owned workflow or agent

Not in scope:

  • NAMS client configuration, bolt-mode setup, and Community Edition constraint handling — owned by the Memory Runtime Integration Guide §5
  • Community Edition licensing, backup, RBAC, and metrics limitations — owned by the Neo4j platform page’s “Deployment and licensing” section
  • Domain-pack-specific Cypher schemas — the trading and art domain packs each own their own node types and query patterns; this guide references one trading example for teaching purposes and does not modify or resolve it
  • Granting any agent Neo4j or NAMS access beyond the three the Agent Registry currently authorizes. This guide documents the existing authorization boundary; it proposes no new access for any of the other nine agents
  • Neo4j installation and deployment mechanics — owned by the synedrix-os-stack-self-hosted reference implementation per DOCS.md’s repository classification

Quick reference: OS concept → Neo4j / Cypher construct

OS conceptNeo4j / Cypher construct
outcome_idA node, labelled by source type (agent or workflow outcome)
agent_lineage_links (Supabase, write-authoritative)The source the graph is projected from; never written to directly from Neo4j
Relation types invoked · contributed-to · escalated-to · governed-byCypher relationship types. Hyphenated names are not valid unquoted identifiers and must be backtick-escaped, e.g. `contributed-to`
correlation_idAn indexed node property used as the traversal scope filter — a partition key, not a graph element
causation_idRealised as a directed edge between two outcome nodes, typed as whichever of the four relation types applies — see §1.1.1 for the MERGE pattern that writes it
Escalation-path reachability (Agent Registry invariant)A variable-length EXISTS { MATCH (a)-[:escalated-to*1..]->(t:Agent) WHERE t.agent_id IN [...] } query — see the platform page for the full form
Audit read path (os-agt-008)A read transaction over the Bolt driver — never a write transaction
Graph-tier write (os-agt-010)A NAMS MemoryClient async call in bolt mode — never hand-written Cypher issued from an agent node
POLE+O entity model (NAMS)Node labels for Person / Organization / Location / Event, plus other typed objects
Reasoning-to-entity lineage (NAMS)AgentStep / ToolCall nodes joined to the entities they affected via :TOUCHED edges
Entity dedup candidates (os-agt-011)A similarity read query — illustrative of what NAMS’s dedupe_entities performs internally, not a query the OS hand-writes in production
Tenant isolation under Community Edition’s one-standard-database limitA tenant_id property on every node and edge, filtered in every query — never a per-tenant database
NAMS’s own vector indexScoped to entity resolution only; Qdrant remains the single semantic retrieval substrate
The NAMS MCP server’s read-only Cypher toolGoverned by mcp_tool_policy; not exposed by default

1. Per-agent use-case catalog

1.1 Agents with direct Neo4j or NAMS access

Exactly three agents. Each entry below shows the query shape the agent actually issues and states the benefit in terms of what a relational store would have cost.

1.1.1 os-agt-008 — Audit & Lineage Agent (read-only)

Its registry input contract accepts query_type: lineage_trace | decision_explanation | policy_audit | run_summary, entity_id?, workflow_id?, correlation_id, and time_range? — there is no outcome_id field. For a lineage_trace query the outcome being traced is carried in entity_id; this guide’s Cypher examples name that value $outcome_id purely for readability, and a client built from this guide must still populate the contract’s actual entity_id field, not invent one. Two query shapes cover the great majority of that surface. Both assume outcome nodes and typed edges already exist — the next subsection shows how they get there before the two read shapes are covered.

How the lineage graph is structured before it can be queried

This write is deliberately not one of the three agent-mediated paths in this guide’s scope box. No agent calls this Cypher as a tool. It is the outcome-persistence path itself — the same step that writes agent_lineage_links in Supabase — projecting the fact it just committed into Neo4j immediately afterward, fire-and-forget. This is what makes the Postgres-first rule in §2 a concrete mechanism rather than an aspiration: the projection only ever writes a fact Postgres already has.

// Project one agent_lineage_links row whose endpoints are Outcome nodes.
// Parameters come from the Postgres row: source_id, target_id, relation,
// tenant_id, and optional context.correlation_id — NOT from causation_id
// alone (causation_id may name an event, which must never become an Outcome node).
MERGE (src:Outcome {outcome_id: $source_id})
  ON CREATE SET src.tenant_id = $tenant_id
MERGE (tgt:Outcome {outcome_id: $target_id})
  ON CREATE SET tgt.tenant_id = $tenant_id
SET tgt.correlation_id = coalesce($correlation_id, tgt.correlation_id),
    tgt.tenant_id      = $tenant_id

// Select one of four fixed patterns for relation (Cypher cannot parameterise
// relationship types). Example for relation = 'contributed-to':
MERGE (src)-[r:`contributed-to`]->(tgt)
  ON CREATE SET r.tenant_id = $tenant_id   // every relationship carries tenant_id, from the lineage row
Illustrative example, not a production implementation. Review and test before use.

When projecting a newly written outcome node (not an edge), upsert properties from the outcome row / envelope. Store causation_id as a node property only; never MERGE an Outcome keyed by causation_id unless that value is known to be an outcome_id from a committed lineage endpoint.

The example above is written for relation_type = 'contributed-to'. Cypher cannot bind a relationship type as a query parameter — [:$relation_type] is not valid syntax — so the calling code selects between four fixed query strings, one per canonical type, rather than interpolating a value into the pattern. Building the relationship type from a string that isn’t one of the four fixed literals is a Cypher-injection surface, not a convenience; if a single call site genuinely needs a dynamic type at runtime, APOC core’s apoc.merge.relationship(origin, $relation_type, {}, {}, o, {}) does the same idempotent upsert with the type name passed safely as a parameter, and it ships in Community Edition.

Because both the node upsert and the edge upsert use MERGE, running the same projection twice — the normal behaviour of a fire-and-forget async step that may retry — leaves the graph unchanged rather than duplicating nodes or edges.

Causal-chain reconstruction. Given an outcome_id, walk backward along the four canonical relation types to the outcomes that led to it, scoped to a single flow:

// Reconstruct the causal chain leading to a given outcome, scoped to one
// flow and one tenant. Relation type names follow the Outcome Spec;
// hyphenated names require backtick-escaping.
//
// Collects every matching path rather than picking one: a fan-out/fan-in
// workflow contributes more than one upstream branch to the same target,
// and `ORDER BY length(path) DESC LIMIT 1` would silently keep only the
// longest branch and drop the rest -- exactly the kind of gap a policy
// audit or incident postmortem must not have.
MATCH path = (origin:Outcome)
             -[:`invoked`|`contributed-to`|`escalated-to`|`governed-by`*1..20]->
             (target:Outcome {outcome_id: $outcome_id})
WHERE ALL(n IN nodes(path) WHERE n.correlation_id = $correlation_id
                             AND n.tenant_id = $tenant_id)
WITH collect(path) AS paths
UNWIND paths AS p
UNWIND nodes(p) AS n
WITH paths, collect(DISTINCT {
       outcome_id:  n.outcome_id,
       source_type: n.source_type,
       source_id:   n.source_id,
       status:      n.status
     }) AS subgraph_nodes
RETURN subgraph_nodes,
       [p IN paths | [r IN relationships(p) | type(r)]] AS branch_relation_sequences
Illustrative example, not a production implementation. Review and test before use.

The benefit is concrete, not stylistic. In Supabase, the same reconstruction is a recursive self-join over agent_lineage_links, and its cost grows with chain depth. In Cypher it is one MATCH over a variable-length path, and correlation_id/tenant_id as per-node filters — pushed into the traversal rather than applied to the result set — keep the walk inside one flow, for one tenant, instead of the whole graph. This is the same principle the Memory Fabric Spec §6.3 states for memory recall: filters belong in the query, not after it.

Escalation-path reachability. The Neo4j platform page gives the original illustrative query that checks every agent’s declared escalation_path reaches os-agt-001 or os-agt-004; ADR-OS-007 and the Agent Graph Data Model since fixed the canonical shape as one CAN_ESCALATE_TO relationship carrying declared/observed_count/last_observed_at, rather than overloading the outcome graph’s escalated-to type for both the declared topology and the runtime record. What is worth adding here is when os-agt-008 runs the reachability check: as part of WF-OS-INCIDENT-POSTMORTEM, to distinguish “this agent’s escalation path is broken” from “this agent’s path is sound but nobody walked it” — a question the declared/observed_count split now answers directly rather than by inference.

What Community Edition does not backstop. Community Edition has no role-based access control, so os-agt-008’s read-only guarantee has no database-level enforcement — a point the platform page’s licensing section already makes. It is restated here because it bears directly on this agent’s worked queries: the guarantee holds because the registry invariant ("os-agt-008 must never appear in any agent’s tools_allowed list") and the tool-authorization layer above it hold, not because the database refuses a write from this agent’s driver session.

1.1.2 os-agt-010 — Memory Curator Agent (sole graph writer)

Every graph-tier write is a NAMS MemoryClient call, never hand-written Cypher issued from an agent node — the Memory Runtime Integration Guide §5.3 already states why: a direct NAMS call from an arbitrary agent is a governance bypass even though the data lands in the right store. The worked shape, inside WF-OS-MEMORY-CONSOLIDATE’s commit step, after os-agt-012 has resolved scope:

# os-agt-010 (Memory Curator) — commit a consolidated entity assertion.
# Runs only inside WF-OS-MEMORY-CONSOLIDATE's commit step, after os-agt-012
# has validated scope. Never called from an arbitrary agent node.
result = await memory_client.upsert_entity(
    user_identifier=f"{tenant_id}:{subject_id}",   # tenant_id first, always
    entity_type="Organization",                    # a POLE+O label
    properties={"name": "Acme Logistics BV", "entity_ref": "org-2231"},
    source_step_id=agent_step_id,                  # creates the :TOUCHED edge
)
Illustrative example, not a production implementation. Review and test before use.

The :TOUCHED edge this call creates, from the AgentStep node to the entity node, is the reasoning-to- entity lineage — the graph-tier counterpart of the memory record envelope’s provenance and derived_from fields (Memory Fabric Spec §4). The benefit is that a later question — “which reasoning step asserted this fact about this entity, and what did it touch besides” — is a one-hop traversal from the entity node, rather than a search across every episodic record that happens to mention it.

1.1.3 os-agt-011 — Consolidation & Reflection Agent (read-only entity dedup)

os-agt-011 recommends consolidations; it never writes. The dedup step it draws on is NAMS’s own dedupe_entities primitive. The shape below is illustrative of what that primitive performs internally — it is not Cypher the OS hand-writes in production, because the entity-resolution path belongs to NAMS, not to OS code:

// Illustrative only. NAMS performs entity resolution internally via
// dedupe_entities(); os-agt-011 does not issue hand-written Cypher
// against the graph tier in production.
MATCH (e1:Organization), (e2:Organization)
WHERE e1.entity_ref <> e2.entity_ref
  AND e1.tenant_id = e2.tenant_id
  AND apoc.text.jaroWinklerDistance(e1.name, e2.name) > 0.92
RETURN e1, e2, apoc.text.jaroWinklerDistance(e1.name, e2.name) AS similarity
ORDER BY similarity DESC
LIMIT 20
Illustrative example, not a production implementation. Review and test before use.

(APOC core, which ships the string-similarity functions, is available in Community Edition.) The benefit over a purely relational fuzzy-match is that entity resolution here can also weigh relationship shape — two organization nodes with near-identical names and overlapping :TOUCHED neighbourhoods are a stronger dedup signal than name similarity alone, and that signal only exists because the relationships are first-class.

1.2 Agents without direct access — why not, and what relationship (if any) remains

The other nine agents have no Neo4j or NAMS entry in tools_allowed, systems_allowed, or memory_scope. That is a deliberate governance boundary, not an oversight, and this table does not propose closing it. Each row cites the agent’s actual systems_allowed from the Agent Registry.

AgentWhy no direct accessIndirect relationship to the graph, if any
Orchestrator Agent (os-agt-001)systems_allowed: n8n, Redis, Supabase (read), agent registry (read) — no Neo4jMay trigger WF-OS-INCIDENT-POSTMORTEM, which invokes os-agt-008, but never reads the graph itself
Policy Guard Agent (os-agt-002)systems_allowed: Supabase policy store and audit trail, Redis — no Neo4jThe escalation-reachability query is a registry/CI validation check, not a runtime tool this agent calls
Registry & Capability Agent (os-agt-003)systems_allowed: Supabase registries (read), Redis capability cache — no Neo4jNone. Capability resolution is a registry lookup, not a graph traversal
HITL Gateway Agent (os-agt-004)systems_allowed: Supabase approval state, Hookdeck Outpost, Redis — no Neo4jNone
Analytics Agent (os-agt-005)systems_allowed: QuestDB, Supabase, PostHog, Langfuse (all read) — no Neo4jNone directly. A cross-domain correlation question that looks graph-shaped stays inside QuestDB/Supabase aggregation, or becomes a request to os-agt-008 for lineage evidence rather than a new tool binding
Forecasting Agent (os-agt-006)systems_allowed: QuestDB, Qdrant (read), Langfuse — no Neo4jNone
Anomaly Detection Agent (os-agt-007)systems_allowed: QuestDB, Redis, Prometheus, Supabase (write) — no Neo4jNone
General Chat Agent (os-agt-009)systems_allowed: Qdrant (read-only), LiteLLM, Langfuse — explicitly “no access to operational, conversation, or canonical relational memory”None. Deliberately the smallest blast radius available for a standalone chat surface
Memory Access Broker Agent (os-agt-012)systems_allowed: Supabase (read), agent registry (read) — explicitly “no access to memory record content in any tier,” including graph contentThe governance chokepoint: every graph-tier recall and write proposal is resolved through it before the store query runs, without it ever holding a Neo4j connection itself

The registry invariant that os-agt-008 “must never appear in any agent’s tools_allowed list” is worth stating plainly here: it means these nine agents have no indirect data path into the graph either, not through the audit agent and not through any other route. Where a workflow needs graph-derived context for one of these agents’ decisions, the correct shape is a governed recall or an explicit os-agt-008 query surfaced into the workflow’s own outcome — never a new binding invented for the consuming agent.


2. The compliant-write pattern, taught through a real violation

The general rule, stated in the Agent DB Schema and repeated in the Memory Fabric Spec §10: lineage links are written to Postgres first, then synced to Neo4j. Postgres is write-authoritative; the graph is a rebuildable projection. Nothing blocks on the Neo4j half of that write.

2.1 Postgres → Neo4j lineage mapping (normative)

Agent DB Schema v1.4 and Outcome Spec v0.3 share one Outcome lineage vocabulary. Writers insert Postgres first; the async projector emits Cypher.

Postgres agent_lineage_linksNeo4j projection
source_type = 'outcome', source_id = outcome_id(:Outcome {outcome_id})
target_type = 'outcome', target_id = outcome_id(:Outcome {outcome_id})
relation = 'invoked'-[:`invoked`]->
relation = 'contributed-to'-[:`contributed-to`]->
relation = 'escalated-to'-[:`escalated-to`]->
relation = 'governed-by'-[:`governed-by`]->
tenant_id, context.correlation_id (when present)Node properties tenant_id, correlation_id

Structural registry edges (depends-on, replaced-by with agent/workflow/tool/policy endpoints) project to the Agent Graph model (Agent Graph Data Model), not to :Outcome nodes. Do not invent a fifth Outcome relation type.

The trading domain pack documented the same failure shape (alignment C5). Remediation is now Specified as FILLED Step 5a/5b in Synedrix Trading — TRD DDL Spec §6 and recorded in Trading DB Schema Alignment v2.5; workflow-contract and synedrix-n8n skill updates remain build-phase. What it illustrates, generically, is the failure shape worth recognising anywhere in the OS:

NON-COMPLIANT
  Step 5: write Neo4j causal-graph edge synchronously
          (no prior Postgres write of the same fact — the graph
           is now the only record, and it cannot be rebuilt from
           Postgres because Postgres never had it)

COMPLIANT (the proposed remediation shape)
  Step 5a: write the fact to Postgres (agent_lineage_links)
           — synchronous, write-authoritative
  Step 5b: project the same fact into Neo4j
           — asynchronous, fire-and-forget, never on the hot path
Illustrative example, not a production implementation. Review and test before use.

The test this protects is the one the Memory Runtime Integration Guide §2.3 already states for the memory fabric generally: wiping Neo4j and rebuilding it from Postgres must produce identical behaviour. A direct, synchronous Neo4j write with no Postgres counterpart fails that test by construction — there is nothing to rebuild from.


3. Indexing and performance guidance (OS level)

os-agt-008 queries by outcome_id, correlation_id, and causation_id (per the Outcome Spec); the escalation-reachability query filters by agent_id. All four should be indexed before any of the queries in §1.1.1 run against production volume:

outcome_id is a uniqueness constraint, not a plain index: the Outcome Spec requires outcome_id to be globally unique, and a plain index does not enforce that — two concurrent, retrying fire-and-forget projections can both find no matching node and each create one, MERGE notwithstanding, without a constraint backing the lookup. correlation_id, causation_id, and agent_id remain plain property indexes, available in Community Edition without restriction. The uniqueness constraint is the only constraint type Community Edition offers, per the platform page’s licensing section; a constraint supplies its own lookup index automatically, so outcome_id needs no separate index alongside it.

The trading domain’s own Neo4j price-prediction doc applies the same principle one level down — indexing its PriceCandle nodes on (asset, timestamp, regime_version) — for a domain-specific node type this guide does not otherwise cover. The principle generalises; the schema does not.


4. Governance bridge (mandatory)

ConcernImplementation
Read/write role separationAmong agents, only os-agt-008 and os-agt-011 hold read access; only os-agt-010 holds write access, and only via NAMS bolt mode. No other agent has any Neo4j entry in tools_allowed or systems_allowed. The outcome-lineage projection in §1.1.1 is a separate, non-agent-mediated write: it is system-level plumbing that only ever projects a fact Postgres already committed, not a fourth agent write path.
Commit authorityos-agt-010 is the only committer of graph-tier content, gated by os-agt-012’s scope resolution — the same rule the Memory Fabric Spec states for every other tier (§10).
Postgres-first lineageEvery graph write is a projection of a prior Postgres write (Agent DB Schema; see §2 above for the failure mode when this is skipped).
Audit read-onlyos-agt-008 never appears in any agent’s tools_allowed, including as a memory tool — it is a query target, not a callable tool.
Tenant isolationEnforced by a tenant_id property on every node and edge, filtered in every query — never by a per-tenant database, which Community Edition does not offer.
MCP governanceThe NAMS MCP server’s read-only Cypher tool is not exposed by default; any exposure passes mcp_tool_policy.

5. Observability

Graph-tier calls are measured with the metric families the Memory Runtime Integration Guide §7 already defines for NAMS calls generally — no new vocabulary is introduced here. os-agt-008’s read path adds two signals specific to this guide’s queries:

SignalSource
Lineage-query latencyLangfuse span on the os-agt-008 read transaction
Reconstructed chain length (length(path))Recorded alongside the query result; a chain that hits the *1..20 bound without reaching a terminal node is itself a signal worth alerting on

6. Compliance checklist

Before shipping any new Neo4j or Cypher usage in an OS-owned workflow or agent:

  • Only os-agt-008, os-agt-010, and os-agt-011 have Neo4j in tools_allowed/systems_allowed; no other agent’s registry entry was changed.
  • os-agt-008’s access is read-only, and it does not appear in any other agent’s tools_allowed.
  • os-agt-010 is the only writer; every write is a NAMS bolt-mode call, never hand-written Cypher from an agent node; MEMORY_API_KEY is unset.
  • os-agt-011’s access is read-only and scoped to entity deduplication.
  • Every Neo4j write is preceded by a Postgres write of the same fact — no direct-write pattern matching the flagged trading FILLED step-5 violation (§2) exists anywhere in an OS-owned path.
  • The outcome-lineage projection (§1.1.1) stays system-level plumbing, never wrapped as a callable tool bound to any agent’s tools_allowed — doing so would create a fourth, ungoverned write path into the graph.
  • Relation types used are exactly invoked / contributed-to / escalated-to / governed-by, correctly backtick-escaped.
  • A uniqueness constraint exists on outcome_id, and plain indexes exist on correlation_id, causation_id, and agent_id, before the query ships against production volume.
  • Tenant isolation is enforced by a tenant_id property and application-layer filtering, not a per-tenant database.
  • The NAMS MCP server’s read-only Cypher tool remains unexposed by default, and is exposed only when mcp_tool_policy explicitly authorizes it.

Versioning


Core specs

  • Synedrix OS Outcome Spec — The node and relationship model this guide’s Cypher examples implement.
  • Synedrix OS Memory Fabric Spec — The graph tier’s lifecycle, record envelope, and governance invariants.
  • Synedrix OS - Agent Registry — The three-agent access boundary this guide catalogs and does not extend.
  • Synedrix OS ADR-OS-002 — The decision adopting NAMS as a library over Neo4j.
  • Synedrix OS Agent Graph Data Model and Synedrix OS ADR-OS-007 — The Agent/Workflow/Tool node types and the CAN_ESCALATE_TO relationship that formalize the escalation-reachability query in §1.1.1; not yet reflected in this guide’s own worked examples (a follow-up, per ADR-OS-007 §6.3).

Orchestration and runtime

Platform reference

  • Neo4j — Primary platform reference; the escalation-reachability query and the Community Edition licensing analysis this guide cross-links rather than repeats.
  • Qdrant — The boundary this guide’s NAMS-vector-index note clarifies.
  • Supabase PG17 — Write-authoritative source for every projection into Neo4j.

Domain reference (informative only, not normative for OS canon)

  • Synedrix Trading Domain Pack, Trading DB Schema Alignment, and TRD DDL Spec — Teaching example in §2; C5 Step 5a/5b is Specified in the TRD DDL companion.
  • Predicting Market Price Movement with Neo4j Pattern Matching — A domain-specific worked example of the indexing principle in §3.

Teaching material

  • Synedrix OS Academy — Chapter 04.8 teaches the hands-on Cypher this guide documents normatively; chapters 02.8 and 04.4 cover the memory taxonomy and the store/state distinction it builds on. Teaching material, not normative: where a chapter disagrees with this guide, this guide wins.

Synedrix OS · August 2026