Summary
EntityNode.save, EntityEdge.save and the two bulk write paths branch on GraphProvider.KUZU to json.dumps(attributes), but every other provider spreads each attribute into its own property. A property graph stores only primitives or arrays of primitives, so a single nested attribute value fails the entire episode:
neo4j.exceptions.CypherTypeError: {code: Neo.ClientError.Statement.TypeError}
Property values can only be of primitive types or arrays thereof.
Encountered: Map{title -> String("..."), description -> String("...")}
The correct handling already exists in the codebase — it is simply not applied to the other providers.
Two distinct causes produce this, and they need different remedies
I collected 83 dead-lettered episodes carrying this error from a production deployment and separated them by whether the payload contains a literal JSON Schema type marker (type -> String("string")):
| Cause |
Count |
What happened |
| Legitimately nested extraction |
40 |
The model was correct; the source data really is an object |
| Schema echo |
43 |
The model returned the JSON Schema field definition instead of a value |
A correct extraction that still fails the write:
Map{title -> String("compose.yml.bak.qworkers.*"),
description -> String("Compose file backup used for rolling back the queue worker patch.")}
A schema echo:
Map{description -> Map{description -> String("Brief description of the object."),
title -> String("Description"),
type -> String("string")}}
This distinction matters. A guard that merely drops non-primitive values would silently discard the 40 legitimate extractions, which is worse than failing loudly. Whatever remedy is chosen, correctly extracted nested data should survive.
Retrying does not help
Because roughly half of these are genuine data rather than a transient model error, replaying the episode reproduces the same failure. In my replay runs these entries failed again with an identical error.
The node and edge paths are not symmetric
_extract_entity_attributes validates shape via entity_type(**merged), so a malformed node attribute raises ValidationError and is rejected before the write. The edge path in resolve_extracted_edge assigns resolved_edge.attributes = merged with no equivalent validation, so the value reaches the driver unchanged.
The cap layer does not catch it either: _check_value_against_cap inspects only str and list, so a dict falls through to return False, 'ok'.
Deterministic reproduction, no database required:
from pydantic import BaseModel
from graphiti_core.utils.maintenance.attribute_utils import apply_capped_attributes
class Document(BaseModel):
title: str | None = None
description: str | None = None
echoed_schema = {
"title": {"description": "The title or identifier of the document",
"title": "Title", "type": "string"},
}
merged, dropped = apply_capped_attributes(
echoed_schema, Document, {}, merge_mode="replace",
)
print(dropped) # set() -> cap dropped nothing
print({k: type(v).__name__ for k, v in merged.items()}) # {'title': 'dict'}
Document(**merged) # node path: ValidationError
# the edge path performs no equivalent validation, so this dict reaches the driver
Affected call sites
graphiti_core/nodes.py — EntityNode.save
graphiti_core/edges.py — EntityEdge.save
graphiti_core/utils/bulk_utils.py — node and edge bulk payload construction
All four share the same if provider == KUZU: json.dumps(...) else: spread shape.
Environment
- graphiti-core 0.29.3; re-verified by source inspection on
main (c64e45c) and v0.30.2
- Neo4j 5 community
- Custom
entity_types / edge_types supplied through the MCP server
Relationship to existing work
I am aware of #440 and #1109 and am not trying to compete with either. #440 carries the identical error text and has been open since July 2025; #1109 takes the serialisation approach but has grown to 9 files including unrelated community-operations changes. I would be glad to see either merged instead.
I am filing this because I could not find the two-cause breakdown recorded anywhere, and it changes what a correct fix must do. Related: #1399, #1153, #705, #1011.
Summary
EntityNode.save,EntityEdge.saveand the two bulk write paths branch onGraphProvider.KUZUtojson.dumps(attributes), but every other provider spreads each attribute into its own property. A property graph stores only primitives or arrays of primitives, so a single nested attribute value fails the entire episode:The correct handling already exists in the codebase — it is simply not applied to the other providers.
Two distinct causes produce this, and they need different remedies
I collected 83 dead-lettered episodes carrying this error from a production deployment and separated them by whether the payload contains a literal JSON Schema type marker (
type -> String("string")):A correct extraction that still fails the write:
A schema echo:
This distinction matters. A guard that merely drops non-primitive values would silently discard the 40 legitimate extractions, which is worse than failing loudly. Whatever remedy is chosen, correctly extracted nested data should survive.
Retrying does not help
Because roughly half of these are genuine data rather than a transient model error, replaying the episode reproduces the same failure. In my replay runs these entries failed again with an identical error.
The node and edge paths are not symmetric
_extract_entity_attributesvalidates shape viaentity_type(**merged), so a malformed node attribute raisesValidationErrorand is rejected before the write. The edge path inresolve_extracted_edgeassignsresolved_edge.attributes = mergedwith no equivalent validation, so the value reaches the driver unchanged.The cap layer does not catch it either:
_check_value_against_capinspects onlystrandlist, so adictfalls through toreturn False, 'ok'.Deterministic reproduction, no database required:
Affected call sites
graphiti_core/nodes.py—EntityNode.savegraphiti_core/edges.py—EntityEdge.savegraphiti_core/utils/bulk_utils.py— node and edge bulk payload constructionAll four share the same
if provider == KUZU: json.dumps(...) else: spreadshape.Environment
main(c64e45c) andv0.30.2entity_types/edge_typessupplied through the MCP serverRelationship to existing work
I am aware of #440 and #1109 and am not trying to compete with either. #440 carries the identical error text and has been open since July 2025; #1109 takes the serialisation approach but has grown to 9 files including unrelated community-operations changes. I would be glad to see either merged instead.
I am filing this because I could not find the two-cause breakdown recorded anywhere, and it changes what a correct fix must do. Related: #1399, #1153, #705, #1011.