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.

NAMS is a hosted backend behind the same MemoryClient. The migration is additive — your existing bolt code keeps working unchanged; 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.5.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 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" — NAMS-only 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

client.graph is being retired in favour of client.query.cypher:

  • Nowclient.graph.execute_read emits a one-time DeprecationWarning per process on bolt.

  • Planned (targeted v0.6.0)client.graph is removed; client.query.cypher becomes the only path for raw Cypher.

See the CHANGELOG for the authoritative schedule.

Need help?