Skip to main content
A minimal guide to defining a custom graph model without writing model classes. Use it when the schema arrives as data — a config file, an API payload, or a document produced by the cognee UI graph-model editor — and you want the same extraction control that a hand-written DataPoint model gives you.

Before You Start

  • Complete Quickstart to understand basic operations
  • Ensure you have LLM Providers configured — the cognify step extracts the graph with an LLM
  • Read Custom Graph Model for the Python-class equivalent of what the JSON compiles to
  • Read DataPoints for how graph nodes are identified and indexed

Code in Action

What Just Happened

Step 1: Declare the Schema as JSON

A spec lists the entities in the graph and names one of them as the root — the entity the generated top-level model is built from (the first entity, when root is omitted). Each field is either a primitive value, an enum, or a relation pointing at another declared entity, and a relation’s cardinality decides whether it holds one target or a list. collaborates_with targets Person itself, so an entity may relate to its own type. Every description is compiled into the schema and onto the generated model’s fields, but cognee rebuilds that model into a plain extraction schema before the LLM call and drops them along the way — a description documents the spec, it does not steer the LLM. Use custom_prompt for that.

Step 2: Compile the Spec into a Model

graph_model_from_spec validates the spec, compiles it to the JSON Schema cognee’s model generator accepts, and returns a DataPoint-derived Pydantic class. An invalid spec — an unknown relation target, a duplicate entity name, a field colliding with a DataPoint infrastructure field — raises ValidationError here, before any LLM call is made. graph_spec_to_json_schema, also exported from cognee.low_level, stops one step earlier and returns that JSON Schema, which is useful for inspecting what the spec compiled into.

Step 3: Extract with the Generated Model

The compiled class is an ordinary graph_model argument: pass it to cognify() (or remember()) and it becomes the structured-output schema the LLM must fill, so the graph can only contain the entities and relations the JSON declared.

Step 4: Search the Resulting Graph

Nothing downstream changes because the model came from JSON — the graph is queried exactly like one built from hand-written classes.

Advanced Usage

Each entity compiles with a metadata default of index_fields and identity_fields, both defaulting to ["name"]. identity_fields is what makes nodes extracted from different chunks and different runs merge into one graph node when their identity values match, so Ada Lovelace mentioned twice stays a single node. Set "identity_fields": [] on an entity to opt out and give every extracted node a random id.
Both lists may only reference name or a declared primitive/enum field — never a relation, and so may primary_label_field, a third entity-level key the UI editor writes. primary_label_field is validated for that parity but never compiled into the generated model.
identity_fields is a Python-side extension of the DSL. The cognee UI graph-model editor produces the same JSON shape but never emits it, so models built in the frontend do not merge nodes.
  • primitive — a scalar value, with primitive_type one of "string" (the default), "number", "boolean", or "date" (an ISO date string).
  • enum — a string restricted to the non-empty enum_values list.
  • relation — an edge named after the field, pointing at relation.target_entity_name with relation.cardinality of "one" or "many".
primitive and enum fields accept "required": true to force the LLM to supply a value; a relation accepts required too, for frontend parity, but it is never compiled. Field names are snake_case with camelCase aliases accepted (primitiveType, targetEntityName), so one document works for both the UI editor and Python.
Validation is also the safety gate — the generated model is built by executing generated code, so names are restricted and size is capped:
  • Entity and field names must be plain identifiers: letters, digits, and underscores, starting with a letter.
  • At most 50 entities per spec, and 40 fields per entity.
  • Entity names must be unique, and may not collide with the {Name}Type marker the compiler generates for another entity.
  • Field names must be unique within their entity.
  • root, when given, must name a declared entity, and every relation must target one.
  • A declared name field must be a string primitive — it is the node’s primary identifier.
  • Unknown keys are rejected rather than ignored, so a typo in a key name surfaces immediately.
Extraction with any model other than the default KnowledgeGraph — whether written in Python or compiled from JSON — bypasses ontology grounding and the extra node/edge dedup passes cognee runs on the default path, and functional_relationships is not supported here: nothing raises if you pass it, but that pass is built for the default path’s entity nodes and will not reliably act on a custom model’s graph. Stay on the default path when you need those, and use a custom model when you need a predictable, domain-specific shape instead.

Custom Graph Model

Write the same schema as Python DataPoint classes

DataPoints

How graph nodes are identified, indexed, and merged

Custom Prompts

Tell the LLM what to look for inside the shape you declared