Migrate to NAMS

How to port an existing direct-bolt project to the hosted Neo4j Agent Memory Service (NAMS) backend, including the full NotSupportedError reference table.

v0.4 introduces NAMS as an alternative backend behind the same MemoryClient. The migration is additive — your existing v0.3.x code keeps working unchanged on bolt; switching to NAMS usually means one config change.

TL;DR

export MEMORY_API_KEY=nams_xxxxxxxxxxxxxxxx

If your code uses MemoryClient() without explicit settings, that’s the whole migration. The _resolve_backend validator picks up MEMORY_API_KEY and switches to NAMS automatically.

If you have explicit MemorySettings(neo4j=Neo4jConfig(…​)), swap it:

# Before (bolt)
settings = MemorySettings(neo4j=Neo4jConfig(password=SecretStr("...")))

# After (NAMS)
settings = MemorySettings(
    backend="nams",
    nams=NamsConfig(api_key=SecretStr("nams_...")),
)

Step-by-step

1. Install the [nams] extra

uv pip install 'neo4j-agent-memory[nams]>=0.4.0'

2. Provision an API key

Get one at memory.neo4jlabs.com. Keys start with nams_.

3. Decide on env-vs-explicit config

Two equally valid patterns:

  • Env-driven (recommended for apps that deploy via env vars): bash export MEMORY_API_KEY=nams_xxx # optional: export MEMORY_ENDPOINT=https://your-private-deployment/v1

  • Explicit (recommended for libraries embedding neo4j-agent-memory): python settings = MemorySettings( backend="nams", nams=NamsConfig(api_key=SecretStr("nams_xxx")), )

4. Audit your code for bolt-only calls

Search your codebase for any of these patterns — they need replacement or guards:

Pattern Replace with

await client.graph.execute_read(…​)

await client.query.cypher(…​)

await client.graph.execute_write(…​)

Use the appropriate higher-level method (add_entity, add_relationship, etc.). NAMS does not expose write-Cypher.

await client.users.upsert_user(…​)

Remove; NAMS scopes per-conversation via user_identifier=.

client.buffered.submit(…​)

Remove; NAMS commits synchronously.

client.consolidation.dedupe_entities()

Remove; NAMS handles deduplication server-side.

client.schema.adopt_existing_graph(…​)

Cannot port; consider keeping that pipeline on bolt.

client.long_term.geocode_locations(), search_locations_near(), etc.

Not available on NAMS; keep on bolt or pre-geocode at ingest.

client.long_term.find_potential_duplicates(), merge_duplicate_entities(), review_duplicate()

Removed by NAMS (server handles dedup automatically).

await client.get_stats() / get_graph() / get_locations()

Use client.query.cypher with a counting / traversal query, or skip.

5. Migrate raw Cypher calls

client.graph.execute_read is deprecated on bolt and raises NotSupportedError on NAMS. The portable replacement is client.query.cypher:

# Before (deprecated on bolt, raises on NAMS):
results = await client.graph.execute_read(
    "MATCH (e:Entity) RETURN e.name AS name LIMIT 10"
)

# After (works on both backends):
results = await client.query.cypher(
    "MATCH (e:Entity) RETURN e.name AS name LIMIT 10"
)

Both validate read-only client-side. Writes (CREATE, MERGE, DELETE, SET, REMOVE, etc.) raise ValueError.

client.graph.execute_write is bolt-only and stays without deprecation — there is no NAMS analog. Use higher-level methods or keep the relevant codepath on bolt.

6. Run your tests

The bolt suite still passes unchanged. For NAMS-mode tests, point your test fixture at a NAMS sandbox key (or use respx to mock — see tests/unit/nams/ in this repo for the pattern).

Full NotSupportedError reference

What raises on each backend, with workarounds:

backend="nams" — bolt-only features raise

Accessor / Method Behavior on NAMS Workaround

client.graph (property access)

Raises immediately

client.query.cypher(query, params)

client.graph.execute_write(…​)

Property access raises first

Use higher-level methods (add_entity, add_relationship, etc.). NAMS has no write-Cypher.

client.users.upsert_user(…​), .get_user(…​), .list_users(…​)

Method call raises

Pass user_identifier= on the calling method (NAMS sends as userId).

client.buffered.submit(…​), .flush(…​), .stop(…​)

Method call raises

Remove the buffered queue — NAMS commits synchronously.

client.consolidation.dedupe_entities()

Method call raises

NAMS deduplicates on add_entity server-side.

client.consolidation.summarize_long_traces()

Method call raises

Server-managed; not exposed via API yet.

client.consolidation.archive_expired_conversations()

Method call raises

Server-managed.

client.schema.adopt_existing_graph(…​)

Method call raises

Not supported — NAMS owns its schema.

client.schema.setup_all() etc.

Method call raises

Server-managed schema; no client-side setup needed.

client.long_term.geocode_locations()

Method call raises

Not exposed by NAMS. Pre-geocode at ingest.

client.long_term.search_locations_near(), search_locations_in_bounding_box(), get_location_coordinates()

Method call raises

Same — no NAMS analog.

client.long_term.find_potential_duplicates(), merge_duplicate_entities(), review_duplicate(), get_same_as_cluster(), get_deduplication_stats()

Method call raises

NAMS handles dedup server-side; the surface isn’t exposed.

client.long_term.register_extractor(), link_entity_to_message(), link_entity_to_extractor(), list_extractors()

Method call raises

Provenance is server-managed; query via get_entity_provenance(entity_id).

client.short_term.add_messages_batch(…​)

Not on NamsShortTermMemory

Use Platinum client.short_term.bulk_add_messages(session_id, messages).

client.short_term.extract_entities_from_session(…​)

Not on NamsShortTermMemory

NAMS extracts server-side. Just call add_message and the entities appear in subsequent searches.

client.short_term.migrate_message_links()

Not on NamsShortTermMemory

Bolt schema migration; not applicable on NAMS.

client.short_term.generate_embeddings_batch(…​)

Not on NamsShortTermMemory

NAMS embeds server-side.

client.reasoning.on_tool_call_recorded (hook decorator)

Not on NamsReasoningMemory

Bolt schema-edge mechanism; no NAMS equivalent.

client.reasoning.migrate_tool_stats()

Not on NamsReasoningMemory

Bolt schema migration.

client.get_stats()

Method call raises

client.query.cypher("MATCH (n) RETURN labels(n), count(n)").

client.get_graph()

Method call raises

client.query.cypher with a custom MATCH; or use client.long_term.get_entity_provenance for citations.

client.get_locations()

Method call raises

No NAMS equivalent — geospatial features are bolt-only.

backend="bolt" — Platinum-tier methods raise

These methods exist on NamsLongTermMemory / NamsShortTermMemory but not on the bolt classes:

Method Workaround on bolt

client.long_term.set_entity_feedback(entity_id, feedback)

Store feedback yourself as a relationship: (:Entity)-[:HAS_FEEDBACK]→(:Feedback).

client.long_term.get_entity_history(entity_id)

Derive from MENTIONS edges: MATCH (m:Message)-[:MENTIONS]→(e:Entity {id: $id}) RETURN m.session_id, count(*).

client.short_term.bulk_add_messages(session_id, messages)

Use client.short_term.add_messages_batch(session_id, messages) (bolt-only — different name, same idea).

client.short_term.get_observations(session_id)

Use the bolt MemoryObserver from mcp._observer. Or extract observations client-side from client.short_term.get_conversation(session_id).

client.short_term.get_reflections(session_id)

Use the bolt MemoryObserver.get_reflections(session_id).

client.short_term.create_conversation(session_id)

Bolt creates conversations implicitly on first add_message. Just call add_message.

client.short_term.list_conversations()

client.short_term.list_sessions() is the bolt equivalent.

Behavior differences (subtle but important)

Deduplication

  • Bolt: client-side deduplication runs on every add_entity (configurable via DeduplicationConfig). add_entity returns (Entity, DeduplicationResult) — the tuple carries info about whether a merge happened.

  • NAMS: dedup is server-side and opaque to the client. add_entity returns just Entity. Existing code that unpacks the tuple (entity, dedup = await client.long_term.add_entity(…​)) will break on NAMS — the result isn’t iterable as a 2-tuple. Migrate to: + python result = await client.long_term.add_entity(…​) if isinstance(result, tuple): entity, dedup = result # bolt else: entity = result # NAMS

Embeddings and vector indexes

  • Bolt: you configure an embedder (OpenAI, sentence-transformers, etc.) on MemorySettings.embedding; the client embeds text before storage; vector indexes are created at connect() sized to the embedder’s dimensions.

  • NAMS: server-managed. The MemorySettings.embedding config emits a single UserWarning at connect() time and is ignored.

Entity resolution

  • Bolt: configurable via ResolutionConfig (exact / fuzzy / semantic / composite).

  • NAMS: server-managed; MemorySettings.resolution is ignored with a warning.

Geocoding and enrichment

  • Bolt: opt-in via GeocodingConfig.enabled=True / EnrichmentConfig.enabled=True.

  • NAMS: not available. Configs ignored at connect().

Audit edges (:TOUCHED)

  • Bolt: client.reasoning.record_tool_call(…​, touched_entities=[…​]) writes (:ReasoningStep)-[:TOUCHED]→(:Entity) edges for auditability.

  • NAMS: touched_entities= kwarg is ignored. Audit dimension of client.eval returns empty results on NAMS.

Deprecation timeline

  • v0.4 (this release) — client.graph.execute_read emits one-time DeprecationWarning per process on bolt.

  • v0.5 (planned) — More NAMS Platinum surface (additional tools, batched operations).

  • v0.6 (planned) — Remove client.graph entirely. client.query.cypher becomes the only path for raw Cypher.

Need help?