Skip to navigation

Customizing Graph Structure

Define the entity and edge types that Zep extracts for your domain

Zep enables the use of rich, domain-specific data structures in graphs through Entity Types and Edge Types, replacing generic graph nodes and edges with detailed models.

Zep classifies newly created nodes/edges as one of the default or custom types or leaves them unclassified. For example, a node representing a preference is classified as a Preference node, and attributes specific to that type are automatically populated. You may restrict graph queries to nodes/edges of a specific type, such as Preference. To stop Zep from extracting entities and edges outside your configured types altogether, see Strict ontology below.

The default entity and edge types are applied to user graphs (not all graphs) by default, but you may define additional custom types as needed.

Each node/edge is classified as a single type only. Multiple classifications are not supported.

Set the ontology before ingestion. Ontologies affect only data processed after they are set; existing data is not re-extracted automatically. See Prepare Data for Ingestion for identity and ordering guidance.

Why use custom ontology

Custom entity and edge types improve graph construction by focusing extraction on the most important entities and relationships for your domain. When a custom ontology is defined, Zep’s extraction process prioritizes these types, resulting in more relevant and structured data for your use case.

Even if Zep’s default types cover your basic needs, defining a custom ontology tailored to your domain often produces better results because:

  • Extraction focuses on entities and relationships that matter most to your application
  • Graph structure aligns more closely with your domain model
  • Retrieved context becomes more relevant and actionable

Default Entity and Edge Types

Definition

Zep provides default entity and edge types that are automatically applied to user graphs (not all graphs). These types help classify and structure the information extracted from conversations.

You can view the exact definition for the default ontology here.

Default Entity Types

The default entity types are:

  • User: A Zep user specified by role in chat messages. There can only be a single User entity.
  • Assistant: Represents the AI assistant in the conversation. This entity is a singleton.
  • Preference: Entities mentioned in contexts expressing user preferences, choices, opinions, or selections. This classification is prioritized over most other classifications.
  • Location: A physical or virtual place where activities occur or entities exist. Use this classification only after checking if the entity fits other more specific types.
  • Event: A time-bound activity, occurrence, or experience.
  • Object: A physical item, tool, device, or possession. Use this classification only as a last resort after checking other types.
  • Topic: A subject of conversation, interest, or knowledge domain. Use this classification only as a last resort after checking other types.
  • Organization: A company, institution, group, or formal entity.
  • Document: Information content in various forms.

Default Edge Types

The default edge types are:

  • LOCATED_AT: Represents that an entity exists or occurs at a specific location. Connects any entity to a Location.
  • OCCURRED_AT: Represents that an event happened at a specific time or location. Connects an Event to any entity.

Default entity and edge types apply to user graphs. All nodes and edges in any user graph will be classified into one of these types or none.

Adding Data

When we add data to the graph, default entity and edge types are automatically created:

from zep_cloud.types import Message
message = {
"name": "John Doe",
"role": "user",
"content": "I really like pop music, and I don't like metal",
}
client.thread.add_messages(thread_id=thread_id, messages=[Message(**message)])

Searching

When searching nodes in the graph, you may provide a list of types to filter the search by. The provided types are ORed together. Search results will only include nodes that satisfy one of the provided types:

search_results = client.graph.search(
user_id=user_id,
query="the user's music preferences",
scope="nodes",
search_filters={
"node_labels": ["Preference"]
}
)
for i, node in enumerate(search_results.nodes):
preference = node.attributes
print(f"Preference {i+1}:{preference}")
Preference 1: {'category': 'Music', 'description': 'Pop Music is a genre of music characterized by its catchy melodies and widespread appeal.', 'labels': ['Entity', 'Preference']}
Preference 2: {'category': 'Music', 'description': 'Metal Music is a genre of music characterized by its heavy sound and complex compositions.', 'labels': ['Entity', 'Preference']}

Disabling Default Ontology

Set disable_default_ontology when you create or update a user to omit Zep’s default entity and edge types except User. Zep still extracts the User type on that user’s graph so the speaker maps to the user node.

When disable_default_ontology is set to true:

  • Zep still uses the User entity type on a user graph so the speaker maps to the node that user.add creates
  • Other default entity and edge types (Assistant, Preference, Location, and the rest) are not applied
  • Classification uses the custom entity and edge types you define
  • Other nodes stay unclassified when they do not match a custom type

Use this when the graph must use domain-specific types instead of Zep’s default types.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
# Create a user with default ontology disabled
user = client.user.add(
user_id=user_id,
first_name="John",
last_name="Doe",
email="john.doe@example.com",
disable_default_ontology=True
)
# Or update an existing user to disable default ontology
client.user.update(
user_id=user_id,
disable_default_ontology=True
)

Custom Entity and Edge Types

Definition

In addition to the default entity and edge types, you may specify your own custom entity and custom edge types. You need to provide a description of the type and a description for each of the fields. The syntax for this is different for each language.

The number of custom entity types and custom edge types you may define on one ontology depends on your plan. The pricing page lists those limits.

The entity-type limit and the edge-type limit apply independently. The same limits apply whether you set the ontology for a specific graph or project-wide. The entity-type limit does not include Zep’s default types.

Each model may have up to 10 fields. The type description and each field description can have a maximum of 500 characters. If one description is longer, Zep rejects the whole ontology with HTTP 400.

When creating custom entity or edge types, you may not use the following attribute names (including in Go struct tags), as they conflict with default node attributes: uuid, name, graph_id, name_embedding, summary, and created_at.

Add custom attributes when a Context Block needs structured field values. The custom type example shows this method.

Use node summaries and facts when the agent does not need structured fields.

from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel, EntityBoolean
from pydantic import Field
class Restaurant(EntityModel):
"""
A business that serves prepared meals to customers, such as Green Leaf Cafe, Chipotle, or a local pizza place. Do not use for a cuisine such as Mexican food, a dish such as a burrito, or a grocery store.
"""
cuisine_type: EntityText = Field(description="The cuisine type of the restaurant, for example: American, Mexican, Indian, etc.", default=None)
dietary_accommodation: EntityText = Field(description="The dietary accommodation of the restaurant, if any, for example: vegetarian, vegan, etc.", default=None)
class Audiobook(EntityModel):
"""
A specific audiobook title that a person can listen to, such as 'The 7 Habits of Highly Effective People' or 'Dune'. Do not use for a printed book that the user reads, a podcast, or an author.
"""
genre: EntityText = Field(description="The genre of the audiobook, for example: self-help, fiction, nonfiction, etc.", default=None)
class RestaurantVisit(EdgeModel):
"""
The user goes to, visits, or eats at a restaurant. Examples: 'Let's go to Green Leaf Cafe', 'I had lunch at Chipotle yesterday'. Do not use when the user only asks about a restaurant or gets a recommendation and does not go there.
"""
restaurant_name: EntityText = Field(description="The name of the restaurant the user visited", default=None)
class AudiobookListen(EdgeModel):
"""
The user listens to or plays an audiobook. Examples: 'Play the 7 habits of highly effective people', 'I finished Dune on audiobook last night'. Do not use when the user only mentions, buys, or asks about an audiobook.
"""
audiobook_title: EntityText = Field(description="The title of the audiobook the user listened to or played", default=None)
class DietaryPreference(EdgeModel):
"""
The user follows a diet or has a food preference, a food restriction, or a food allergy. Examples: 'I'm vegetarian', 'I'm allergic to peanuts', 'I don't eat pork'. Do not use for a one-time meal choice such as 'I want pasta tonight', or for a preference about a restaurant.
"""
preference_type: EntityText = Field(description="Preference type of the user: anything, vegetarian, vegan, peanut allergy, etc.", default=None)
allergy: EntityBoolean = Field(description="Whether this dietary preference represents a user allergy: True or false", default=None)

Setting Entity and Edge Types

You can set these custom entity and edge types as the graph ontology for your current Zep project. The ontology can be applied either project-wide to all users and graphs, or targeted to specific users and graphs only.

Setting Types Project Wide

When no user IDs or graph IDs are provided, the ontology is set for the entire project. All users and graphs within the project will use this ontology. Note that for custom edge types, you can require the source and destination nodes to be a certain type, or allow them to be any type:

from zep_cloud import EntityEdgeSourceTarget
client.graph.set_ontology(
entities={
"Restaurant": Restaurant,
"Audiobook": Audiobook,
},
edges={
"RESTAURANT_VISIT": (
RestaurantVisit,
[EntityEdgeSourceTarget(source="User", target="Restaurant")]
),
"AUDIOBOOK_LISTEN": (
AudiobookListen,
[EntityEdgeSourceTarget(source="User", target="Audiobook")]
),
"DIETARY_PREFERENCE": (
DietaryPreference,
[EntityEdgeSourceTarget(source="User")]
),
}
)

Setting Types For Specific Graphs

You can also set the ontology for specific users and/or graphs by providing user IDs and graph IDs. When these parameters are provided, the ontology will only apply to the specified users and graphs, while other users and graphs in the project will continue using the previously set ontology (whether that was due to a project-wide setting of ontology or due to a graph-specific setting of ontology):

from zep_cloud import EntityEdgeSourceTarget
client.graph.set_ontology(
user_ids=["user_1234", "user_5678"],
graph_ids=["graph_1234", "graph_5678"],
entities={
"Restaurant": Restaurant,
"Audiobook": Audiobook,
},
edges={
"RESTAURANT_VISIT": (
RestaurantVisit,
[EntityEdgeSourceTarget(source="User", target="Restaurant")]
),
"AUDIOBOOK_LISTEN": (
AudiobookListen,
[EntityEdgeSourceTarget(source="User", target="Audiobook")]
),
"DIETARY_PREFERENCE": (
DietaryPreference,
[EntityEdgeSourceTarget(source="User")]
),
}
)

Adding Data

Now, when you add data to the graph, new nodes and edges are classified into exactly one of the overall set of entity or edge types respectively, or no type:

from zep_cloud import Message
import uuid
messages_thread1 = [
Message(content="Take me to a lunch place", role="user", name="John Doe"),
Message(content="How about Panera Bread, Chipotle, or Green Leaf Cafe, which are nearby?", role="assistant", name="Assistant"),
Message(content="Do any of those have vegetarian options? I'm vegetarian", role="user", name="John Doe"),
Message(content="Yes, Green Leaf Cafe has vegetarian options", role="assistant", name="Assistant"),
Message(content="Let's go to Green Leaf Cafe", role="user", name="John Doe"),
Message(content="Navigating to Green Leaf Cafe", role="assistant", name="Assistant"),
]
messages_thread2 = [
Message(content="Play the 7 habits of highly effective people", role="user", name="John Doe"),
Message(content="Playing the 7 habits of highly effective people", role="assistant", name="Assistant"),
]
user_id = f"user-{uuid.uuid4()}"
client.user.add(user_id=user_id, first_name="John", last_name="Doe", email="john.doe@example.com")
thread1_id = f"thread-{uuid.uuid4()}"
thread2_id = f"thread-{uuid.uuid4()}"
client.thread.create(thread_id=thread1_id, user_id=user_id)
client.thread.create(thread_id=thread2_id, user_id=user_id)
client.thread.add_messages(thread_id=thread1_id, messages=messages_thread1, ignore_roles=["assistant"])
client.thread.add_messages(thread_id=thread2_id, messages=messages_thread2, ignore_roles=["assistant"])

Retrieving Custom Types

Once you’ve created custom entity and edge types in your graph, you’ll typically want to retrieve information filtered by these specific types. There are two primary approaches:

  1. Context Templates (Recommended): Use context templates to create reusable context blocks that automatically filter by type. This is ideal when you want consistent formatting with automatic relevance detection.
  2. Graph Search: Use direct graph search for more granular control over queries and when you need dynamic, query-based retrieval.

Using Context Templates

Context templates provide a convenient way to create context blocks that filter by your custom entity and edge types. You define a template once and Zep automatically retrieves and formats the relevant information.

Context templates support the types parameter on %{entities} and %{edges} variables to filter by your custom types:

# Create a template that filters by custom types
client.context.create_context_template(
template_id="restaurant-audiobook-context",
template="""# USER PREFERENCES
%{edges types=[DIETARY_PREFERENCE] limit=5}
# RESTAURANT VISITS
%{edges types=[RESTAURANT_VISIT] limit=10}
# RESTAURANTS
%{entities types=[Restaurant] limit=5}
# AUDIOBOOKS
%{entities types=[Audiobook] limit=5}
# AUDIOBOOK LISTENING HISTORY
%{edges types=[AUDIOBOOK_LISTEN] limit=10}"""
)
# Use the template to get context
results = client.thread.get_user_context(
thread_id=thread1_id,
template_id="restaurant-audiobook-context"
)
print(results.context)

The resulting context block will automatically include only the specified types, formatted according to your template:

# USER PREFERENCES
- (2024-11-20 10:30:00 - present) [DIETARY_PREFERENCE] <User states 'I'm vegetarian' indicating a dietary preference> { preference_type: vegetarian, allergy: false }
# RESTAURANT VISITS
- (2024-11-20 10:35:00 - present) [RESTAURANT_VISIT] <User John Doe is going to Green Leaf Cafe> { restaurant_name: Green Leaf Cafe }
# RESTAURANTS
- { name: Green Leaf Cafe, types: [Entity,Restaurant], summary: Green Leaf Cafe is a restaurant that offers vegetarian options, attributes: { dietary_accommodation: vegetarian } }
# AUDIOBOOKS
- { name: 7 habits of highly effective people, types: [Entity,Audiobook], summary: '7 habits of highly effective people' is an audiobook }
# AUDIOBOOK LISTENING HISTORY
- (2024-11-20 10:40:00 - present) [AUDIOBOOK_LISTEN] <John Doe requested to play the audiobook '7 habits of highly effective people'> { audiobook_title: 7 habits of highly effective people }

Learn more about context templates in the Context Templates documentation.

For more granular control or dynamic queries, you can use graph search to filter by entity or edge types. Graph search is ideal when you need to specify custom search queries, want direct access to individual nodes or edges, or are building dynamic filtering based on runtime conditions.

Search for entities (nodes) by filtering on custom entity types using the node_labels parameter:

search_results_restaurants = client.graph.search(
user_id=user_id,
query="Take me to a restaurant",
scope="nodes",
search_filters={
"node_labels": ["Restaurant"]
},
limit=1,
)
node = search_results_restaurants.nodes[0]
print(f"Node name: {node.name}")
print(f"Node labels: {node.labels}")
print(f"Cuisine type: {node.attributes.get('cuisine_type')}")
print(f"Dietary accommodation: {node.attributes.get('dietary_accommodation')}")
Node name: Green Leaf Cafe
Node labels: Entity,Restaurant
Cuisine type: undefined
Dietary accommodation: vegetarian

You can provide multiple types in search filters, and the types will be ORed together. For example, searching with edge_types: ["DIETARY_PREFERENCE", "RESTAURANT_VISIT"] will return edges matching either type.

Behavior of custom types

Zep applies custom entity and edge types with these rules:

  • Each set_ontology call replaces the custom entity and edge types at its scope. The custom types are always the types of the last set_ontology call.
  • The full set of entity and edge types for a project contains the custom types and the default types.
  • A custom type with the same name as a default type replaces that default type.
  • A change to the custom types does not change existing nodes or edges. Existing nodes and edges keep their classification and attributes. Only new data that has new information can change them.
  • Each custom entity type and each custom edge type must have at least one custom property.

Ontology design best practices

Zep classifies each extracted entity and edge into one type, or leaves it unclassified when no type matches. Zep selects the type from the description of the type, not from its name. For an edge type, Zep also uses the source and target entity types. The quality of the graph depends on these descriptions.

  • Avoid category overlap. Make the types mutually exclusive, so that each entity or fact has one correct type. When two descriptions overlap, no single type is correct, and Zep classifies the same kind of item inconsistently. Two types can have similar names when their descriptions make them distinct.
  • State a priority when two types must overlap. For example, if you need both Hobby and Hiking, write in the description of Hiking that it takes priority over Hobby.
  • Start with a small number of generic types. Use few types, few fields, and short definitions. Add types and fields one at a time, and examine the graph after each change. Type and field descriptions are prompts, and they need iteration.
  • Model entity types as nouns and edge types as relationships. An entity type names a kind of thing, such as a person, a restaurant, or a product. An edge type names a relationship or an event between two entities, such as “visits”, “works for”, or “is allergic to”. If an entity type describes a relationship, Zep often stores the information as an edge, not as a node of that type. If an edge type describes a thing, Zep often stores the information as a node, not as an edge of that type.
  • Write clear and unambiguous definitions. State the membership rule of the type: what an item must be to belong to the type. Do not use vague words such as “relevant” or “important” as the rule.
  • Give positive and negative examples in each definition. The extraction model uses examples in the description to find the boundary of the type. Give one or more examples that belong to the type. Give one or more similar items that do not belong to the type, and name the type that they belong to instead, if one exists. A field description also benefits from examples of valid values.

The examples on this page use this pattern. Each type description gives a membership rule, positive examples, and negative examples.

Strict Ontology

By default, Zep’s extraction favors recall over strict adherence to your schema: entities and edges that don’t clearly match one of your configured types are still added to the graph, classified generically rather than dropped. Set strict_ontology: true when you need the graph to conform strictly to your declared types — for example, when downstream code or context templates assume every node and edge belongs to a type you defined.

What Strict Ontology Does

When strict_ontology is true on an add-data request:

  • Entities are limited to your configured entity types — the custom entity types you’ve defined, plus Zep’s default entity types unless you’ve also disabled the default ontology. When the default ontology is disabled, a user graph still includes the User type. The generic Entity type is excluded from the candidates, so an entity that doesn’t match a declared type is dropped instead of added as an untyped node.
  • Edges are limited to your configured edge types, when you have any configured. An edge type the extraction proposes that isn’t in your configured set is discarded instead of saved under a derived relation name.

Setting Strict Ontology

The strict_ontology field defaults to false. Set it to true on graph.add, on thread.add_messages, on batch.create, or on the deprecated /memory endpoint:

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
new_episode = client.graph.add(
user_id="user123",
type="text",
data="The user is an avid fan of Eric Clapton",
strict_ontology=True,
)

The same field is available on thread.add_messages:

from zep_cloud.types import Message
message = Message(
name="John Doe",
role="user",
content="I really like pop music, and I don't like metal",
)
client.thread.add_messages(
thread_id="thread-abc",
messages=[message],
strict_ontology=True,
)

The same field is available on batch.create. Processing applies it to every graph episode and every thread message in that batch:

batch = client.batch.create(
strict_ontology=True,
metadata={"description": "Customer support backfill"},
)

Strict ontology has two edge cases worth checking before you rely on it:

  • No edge types configured: edge enforcement only takes effect if you have edge types configured at all — custom or default. If you have none, strict ontology has no effect on edges, and edge extraction stays open-vocabulary.
  • No entity types available: if strict_ontology is true and you have no custom entity types and the default ontology is disabled, a user graph still has the User type. A group graph in that state has no entity types left to match against. Extraction then returns zero entities rather than an error, so this misconfiguration fails silently.

Strict ontology restricts extraction to your project’s configured ontology, which includes Zep’s default entity and edge types unless you’ve separately disabled the default ontology — turning on strict_ontology doesn’t disable the default ontology by itself.