Skip to navigation

Searching the Graph

Search a user graph or shared Context Graph for task-relevant data
Custom Context Blocks

Use graph search results with Advanced Context Block construction to assemble reference data for your model. Pass the result through the provider’s untrusted-data channel, as described in Memory security best practices.

Custom context blocks let you combine graph results with conversation history and other relevant data.

Graph search in an agent

In an agent, graph search is one tool among several. For the agent loop, read Build an Agent with Zep. For the search tool, read Build Tools for an Agent.

OptionIn an agent, use this when
Auto searchThe application grounds one conversational turn in one call. Do not use auto search as a step in a retrieval plan
A single scopeA plan step needs one result type: facts, entities, or source episodes
Search filtersA plan step needs one entity type, one edge type, a date range, or one source
Breadth-first search (BFS)The agent already has the nodes, and a plan step needs their neighborhood
Cross encoder rerankerThe result order must follow the meaning of the query

Introduction

Zep graph search combines semantic similarity with BM25 full-text search. Semantic search finds conceptual matches, and full-text search finds exact terms.

You can enable breadth-first search to expand results around specified graph nodes.

Each search operation takes the graph_uuid of the graph to search:

  • To search one user’s graph, use the graph_uuid that user.create returns for the user.
  • To search a shared Context Graph for a customer account, project, product, organization, or business domain, use the uuid that graph.create returns.

Zep generates these UUIDs. Store them in your database next to your own identifiers, and pass them on each search. Apply application authorization and Zep access policies before you search a shared graph.

Each result type has its own search operation: graph.search_edges, graph.search_nodes, graph.search_episodes, graph.search_observations, and graph.search_thread_summaries. Each operation returns one page of results of that type. Auto search uses a different operation, graph.get_context, which returns one assembled context block.

How It Works

  • Semantic similarity: Converts queries into embeddings to find conceptually similar content
  • BM25 full-text search: Performs traditional keyword-based search for exact matches
  • Breadth-first search (optional): Biases results toward information connected to specified starting nodes, useful for contextual relevance
  • Hybrid results: Combines and reranks results using reciprocal rank fusion (RRF)

If the graph embedder is unavailable, search continues with BM25 full-text search in that graph’s model tables. The vector leg is omitted. Recall can be lower than hybrid search. Thread context uses the same text-only path.

Graph Concepts

  • Nodes: Connection points representing entities (people, places, concepts) discussed in conversations or added via the Graph API
  • Edges: Relationships between nodes containing specific facts and interactions

The example below demonstrates a simple search:

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_edges(
zep_graph_uuid,
query=query,
)

To search a shared Context Graph, use the same operation with the uuid of the shared graph:

search_results = client.graph.search_edges(
zep_project_graph_uuid,
query="What blocks the release?",
)

Pagination

A search operation returns one page of results. The limit parameter sets the page size. The default and the maximum page size are both 50. Zep ranks the results once and keeps the ranked list for the next pages, so the result order stays the same from page to page. The SDK pager fetches the next pages when you iterate over the results:

search_results = client.graph.search_edges(
zep_graph_uuid,
query="project status",
limit=10,
)
# The results of the first page
first_page = search_results.items or []
# Iterate over all pages
for edge in search_results:
print(edge.fact)
Best Practices

Keep queries short. Long queries can increase latency without improving search quality. Break down complex searches into smaller, targeted queries. Use precise, contextual queries rather than generic ones

Looking for one-call context retrieval?

For most assistant use cases, use graph.get_context and let Zep dynamically compose the most relevant context across edges, nodes, observations, and thread summaries into a single ready-to-use block. See Auto Search below.

Auto search is the recommended entry point to graph retrieval. The graph.get_context operation runs auto search. Instead of asking you to pre-commit to a single result type — facts, entity summaries, observations, or thread summaries — auto search retrieves across all of them in parallel, applies a cross-scope rerank, and dynamically composes the most relevant results into a single context block sized to a character budget you control.

The output is a single string that you can pass to your LLM as data. There is no client-side stitching, scope-selection heuristic, or need for multiple search calls.

Do not insert the context block into a system or developer message. Retrieved context can contain end-user or third-party content. Follow Memory security best practices for provider-specific placement.

What auto search does

  • Composes across all data shapes in one call. A single query returns the most relevant material whether it lives in graph facts, entity summaries, derived observations, or per-thread summaries.
  • Ranks globally, not per-scope. Auto search applies its own internal cross-scope rerank so results are ordered by overall relevance to the query — a strong observation can outrank a weaker edge, and vice versa.
  • Packs to a character budget. The returned context block is materialized to fit within max_characters, giving you predictable, prompt-window-friendly output.
  • Returns a ready-to-use context block. The context field is the primary output. Send the formatted string through your provider’s untrusted-data channel.
  • Optionally exposes the underlying results. Set include_results=true to also receive the selected items as typed arrays — useful for inspection, citation, or building custom context blocks on top of auto’s selection.

Auto search can interpret relative calendar language as a retrieval constraint. For example:

  • What happened yesterday?
  • Updates from last week
  • What changed this month?

For supported relative requests, Zep uses a machine learning model to determine whether the phrase expresses a temporal retrieval constraint in the context of the query. When it does, Zep resolves the corresponding calendar window and restricts the retrieved facts automatically. You do not need to calculate timestamps or construct explicit date filters.

The window uses the time zone stored for the target user or graph. If that value is unavailable or invalid, Zep uses the project’s default time zone, then UTC.

If Zep is not sufficiently confident that the phrase is a temporal constraint, it runs normal auto search without forcing a time window. When temporal filtering is applied, the returned context block states that evidence was restricted and includes the UTC interval and the time zone used to calculate it.

This interpretation applies only to graph.get_context. It is separate from explicit datetime filters, which let you choose timestamp fields and boundaries for edge searches. A caller-supplied temporal filter takes precedence, so Zep does not also infer a window from the query.

How to use it

Call graph.get_context with the graph UUID and the query. Optionally, set max_characters to bound the size of the returned context block. max_characters defaults to 2500 and is capped at 50000. Zep selects results across scopes, applies its internal cross-scope rerank, and packs the top-ranked results into the context block until the character budget is reached.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
context_result = client.graph.get_context(
zep_graph_uuid,
query="What did we decide about the pricing rollout?",
max_characters=2500,
)
# The materialized context block, ready to drop into a prompt
print(context_result.context)

When to use auto vs. a specific scope

Use auto when…Use a specific scope when…
You want the best available context for an arbitrary user queryYou know exactly which data shape you need (e.g. just facts, just entities)
The right result type varies query-by-queryYou’re driving a UI that renders one result type (e.g. an entity browser)
You want a ready-to-prompt context blockYou need to programmatically merge results with other data
You want Zep to manage cross-scope ranking for youYou need fine-grained control over reranker, filters, or BFS per scope

Auto search is for one-shot grounding. In an agent, use a specific scope for each step of the retrieval plan.

Response format

graph.get_context returns a GraphContextResponse object with these fields:

  • context (string) — A single materialized context block composed from the highest-ranked results across scopes, packed up to max_characters. This is the primary output of auto search and is intended to be passed directly to your LLM.
  • truncated (boolean) — true when the character budget limited the context block.
  • results (object) — Present only when include_results=true. It contains the selected results as typed arrays: edges, nodes, episodes, observations, and thread_summaries. Only the result types Zep chose for this query will be non-empty. Auto search does not select episodes, so episodes is always empty. Each item carries a selection_rank field — the 1-based global cross-scope rank assigned by auto selection — which you can use to reconstruct the order Zep used when building the context block. By default include_results=false and results is absent.
Example response (include_results=true)
{
"context": "Pricing rollout decisions:\n- Approved tiered pricing for Q3, with grandfathering for existing enterprise contracts...\n\nRelated discussion:\n- 2026-04-22: Eng and Finance agreed to delay the enterprise tier by two weeks...\n",
"results": {
"edges": [
{
"uuid": "...",
"fact": "Engineering and Finance agreed to delay the enterprise tier by two weeks",
"selection_rank": 2,
"score": 0.81
}
],
"observations": [
{
"uuid": "...",
"summary": "Pricing rollout decisions",
"selection_rank": 1
}
],
"episodes": [],
"nodes": [],
"thread_summaries": []
},
"truncated": false
}

graph.get_context has no reranker parameter. Auto search applies its own internal cross-scope rerank to order results before packing the context block.

Configurable Parameters

Zep provides extensive configuration options to fine-tune search behavior and optimize results for your specific use case. The search operations (graph.search_edges, graph.search_nodes, graph.search_episodes, graph.search_observations, and graph.search_thread_summaries) accept these parameters:

ParameterTypeDescriptionDefaultRequired
graph_uuidstringThe UUID of the user graph or shared Context Graph to search-Yes
querystringSearch text-Yes
rerankerstringReranking method: "rrf", "mmr", "node_distance", "episode_mentions", or "cross_encoder". Each scope accepts a subset. See Rerankers"rrf"No
limitintegerPage size: the maximum number of results on one page (max 50)50No
cursorstringOpaque cursor of the next page. The SDK pagers set it for you-No
mmr_lambdafloatMMR diversity vs relevance balance (0.0-1.0)-No†
center_node_uuidstringCenter node for distance-based reranking-No‡
filtersobjectFilter by entity types (node_labels), edge types (edge_types), exclude entity types (exclude_node_labels), exclude edge types (exclude_edge_types), custom properties (property_filters), episode metadata (metadata_filters), connected nodes (connected_node_uuids, source_node_uuids, target_node_uuids), source episodes (episode_uuids), or timestamps (date_filters§)-No
bfs_origin_node_uuidsarrayUp to five node or episode UUIDs that seed breadth-first searches-No

†Required when using mmr reranker ‡Required when using node_distance reranker §Timestamp filtering only applies to edge scope searches

graph.get_context accepts these parameters:

ParameterTypeDescriptionDefaultRequired
graph_uuidstringThe UUID of the user graph or shared Context Graph to search-Yes
querystringSearch text-Yes
max_charactersintegerMaximum total characters in the context block. Limited to 50000.2500No
include_resultsbooleanAlso return the selected results alongside the context block.falseNo
filtersobjectThe same filters as the search operations-No
template_uuidstringThe UUID of a context template that renders the context block-No

Search scopes

To retrieve one result type, use the search operation of one of five result scopes instead of auto search:

Edges

Edges represent individual relationships and facts between entities in your graph. They contain specific interactions, conversations, and detailed information. Use graph.search_edges. Edge search is ideal for:

  • Finding specific details or conversations
  • Retrieving precise facts about relationships
  • Getting granular information about interactions
from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_edges(
zep_graph_uuid,
query="What did John say about the project?",
)

Nodes

Nodes represent entities in the graph. Each node can contain a summary of facts from its edges. Use graph.search_nodes. Node search is useful for:

  • Understanding broader context around entities
  • Getting entity summaries and overviews
  • Finding all information related to a specific person, place, or concept
from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_nodes(
zep_graph_uuid,
query="John Smith",
)

Each node result carries episode_uuids: the UUIDs of the live episodes that mention the entity, newest first. When episode_uuids_truncated is false, the list is the complete set. When the node has more than 100 live mention episodes, the list holds the newest 100 and episode_uuids_truncated is true; read the full set with one episode list call filtered by mentioned_node_uuids.

search_results = client.graph.search_nodes(
zep_graph_uuid,
query="John Smith",
)
for node in search_results.items or []:
print(node.uuid_, node.episode_uuids)
if node.episode_uuids_truncated:
episodes = client.graph.episode.list(
zep_graph_uuid,
filters={"mentioned_node_uuids": [node.uuid_]},
)

Episodes

Episodes represent individual messages or chunks of data sent to Zep. Use graph.search_episodes. Episode search allows you to find relevant episodes based on their content, making it ideal for:

  • Finding specific messages or data chunks related to your query
  • Discovering when certain topics were mentioned
  • Retrieving relevant individual interactions
  • Understanding the context of specific messages
from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_episodes(
zep_graph_uuid,
query="project discussion",
)

Observations

Observations are durable, evidence-backed memories Zep automatically derives from a graph’s recent activity, capturing meaningful changes, decisions, commitments, preferences, and recurring patterns across one or more entities. Use graph.search_observations. Observation search is useful for:

  • Surfacing cross-entity context that spans many facts
  • Retrieving persistent behavioral patterns or stable relationships
  • Grounding responses in higher-level memories rather than granular edges

See Observations for more details on how observations are produced and retrieved.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_observations(
zep_graph_uuid,
query="account suspension and recovery",
)

Thread Summaries

Thread summaries are per-thread, incremental summaries of the messages in a single conversation. Use graph.search_thread_summaries. Thread summary search is useful for:

  • Surfacing the most relevant past conversations across a user’s threads
  • Pulling thread-level recaps into a custom context block
  • Building features that need a different view of a user’s history at the conversation level

See Thread summaries for more details on how thread summaries are produced and retrieved.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_thread_summaries(
zep_graph_uuid,
query="payment failures and account recovery",
)

Rerankers

Zep provides multiple reranking algorithms to optimize search results for different use cases. Each reranker applies a different strategy to prioritize and order results.

Each search scope accepts a subset of the rerankers. If you send a reranker that the scope does not accept, the API rejects the request with an invalid_request error that names the reranker field:

RerankerEdgesNodesEpisodesObservationsThread summaries
rrfYesYesYesYesYes
mmrYesYesYesYesYes
cross_encoderYesYesNoYesYes
node_distanceYesYesNoNoNo
episode_mentionsYesNoNoNoNo

RRF (Reciprocal Rank Fusion)

Reciprocal Rank Fusion is the default reranker that combines results by each result’s rank position in both the semantic similarity and BM25 full-text searches. It merges the two result sets by considering the rank position of each result in both searches, creating a unified ranking that leverages the strengths of both approaches.

When to use: RRF is ideal for most general-purpose search scenarios where you want balanced results combining conceptual understanding with exact keyword matching.

Score interpretation: RRF scores combine semantic similarity and keyword matching by summing reciprocal ranks (1/rank) from both search methods, resulting in higher scores for results that perform well in both approaches. Scores don’t follow a fixed 0-1 scale but rather reflect the combined strength across both search types, with higher values indicating better overall relevance.

MMR (Maximal Marginal Relevance)

Maximal Marginal Relevance addresses a common issue in similarity searches: highly similar top results that don’t add diverse information to your context. MMR reranks results to balance relevance with diversity, promoting varied but still relevant results over redundant similar ones.

When to use: Use MMR when you need varied results for a summary or a complex question.

Required parameter: mmr_lambda (0.0-1.0) - Controls the balance between relevance (1.0) and diversity (0.0). A value of 0.5 provides balanced results. The API rejects an mmr request without mmr_lambda.

Score interpretation: MMR scores balance relevance with diversity based on your mmr_lambda setting, meaning a moderately relevant but diverse result may score higher than a highly relevant but similar result. Interpret scores relative to your lambda value: with lambda=0.5, moderate scores may indicate valuable diversity rather than poor relevance.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_edges(
zep_graph_uuid,
query="project status",
reranker="mmr",
mmr_lambda=0.5, # Balance diversity vs relevance
)

Cross Encoder

cross_encoder uses a specialized neural model that jointly analyzes the query and each search result together, rather than analyzing them separately. This provides more accurate relevance scoring by understanding the relationship between the query and potential results in a single model pass.

When to use: Use cross encoder when you need the highest accuracy in relevance scoring and are willing to trade some performance for better results. Ideal for critical searches where precision is paramount.

Trade-offs: Higher accuracy but slower performance compared to other rerankers.

Score interpretation: Cross encoder scores follow a sigmoid curve (0-1 range) where highly relevant results cluster near the top with scores that decay rapidly as relevance decreases. You’ll typically see a sharp drop-off between truly relevant results (higher scores) and less relevant ones, making it easy to set meaningful relevance thresholds.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_edges(
zep_graph_uuid,
query="critical project decision",
reranker="cross_encoder",
)

Episode Mentions

episode_mentions reranks edge candidates by how many of the episodes listed in filters.episode_uuids mention them, from most to least mentioned. Only edge search accepts this reranker.

Required parameter: filters.episode_uuids - the episode UUIDs to count mentions against. The API rejects an episode_mentions request without a non-empty filters.episode_uuids.

When to use: Use episode mentions when you already have a set of episode UUIDs (for example, from an episode search or a specific conversation) and want to prioritize graph results that those episodes reference most.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters
search_results = client.graph.search_edges(
zep_graph_uuid,
query="team feedback",
reranker="episode_mentions",
filters=SearchFilters(episode_uuids=[episode_uuid_1, episode_uuid_2]),
)

Node Distance

node_distance reranks search results based on graph proximity, prioritizing results that are closer (fewer hops) to a specified center node. This spatial approach to relevance is useful for finding information contextually related to a specific entity or concept. Edge search and node search accept this reranker.

When to use: Use node distance when you want to find information specifically related to a particular entity, person, or concept in your graph. Ideal for exploring the immediate context around a known entity.

Required parameter: center_node_uuid - The UUID of the node to use as the center point for distance calculations. The API rejects a node_distance request without center_node_uuid.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
search_results = client.graph.search_edges(
zep_graph_uuid,
query="recent activities",
reranker="node_distance",
center_node_uuid=center_node_uuid,
)

Reranker Score

Graph search results include a reranker score that provides a measure of relevance for each returned result. The score field is present on every result of a search operation, with any reranker. The reranker score can be used to manually filter results to only include those above a certain relevance threshold, allowing for more precise control over search result quality.

The interpretation of the score depends on which reranker is used. For example, when using the cross_encoder reranker, the score follows a sigmoid curve with the score decaying rapidly as relevance decreases.

Relevance Score

When the cross_encoder reranker scores the results, search results include an additional relevance field alongside the score field. The relevance field is a rank-aligned score in the range [0, 1] derived from the existing sigmoid-distributed score to improve interpretability and thresholding.

Key characteristics:

  • Range: [0, 1]
  • Only populated when the cross_encoder reranker scored the result
  • Preserves the ranking order produced by Zep’s reranker
  • Not a probability; it is a monotonic transform of score to reduce saturation near 1
  • Use relevance for sorting, filtering, and analytics

If the cross encoder cannot score the results, Zep returns the results in the retrieved order without relevance. Do not read a missing relevance field as an error.

The relevance field provides a more intuitive metric for evaluating search result quality compared to the raw score, making it easier to set meaningful thresholds and analyze results.

Search Filters

Zep allows you to filter search results by specific entity types or edge types, enabling more targeted searches within your graph. Put the filters in the filters parameter of a search operation.

Entity Type Filtering

Filter search results to only include nodes of specific entity types. This is useful when you want to focus on particular kinds of entities (e.g., only people, only companies, only locations).

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters
search_results = client.graph.search_nodes(
zep_graph_uuid,
query="software engineers",
filters=SearchFilters(node_labels=["Person", "Company"]),
)

Edge Type Filtering

Filter search results to only include edges of specific relationship types. This helps you find particular kinds of relationships or interactions between entities.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters
search_results = client.graph.search_edges(
zep_graph_uuid,
query="project collaboration",
filters=SearchFilters(edge_types=["WORKS_WITH", "COLLABORATES_ON"]),
)

Exclusion Filters

Exclusion filters allow you to exclude specific entity types or edge types from your search results. This is useful when you want to filter out certain types of information while keeping all others.

Excluding Node Labels

Exclude specific entity types from node or edge search results. When searching edges, nodes connected to the edges are also checked against exclusion filters.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters
# Exclude certain entity types from results
search_results = client.graph.search_nodes(
zep_graph_uuid,
query="project information",
filters=SearchFilters(exclude_node_labels=["Assistant", "Document"]),
)

Excluding Edge Types

Exclude specific edge types from search results. This helps you filter out certain kinds of relationships while keeping all others.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters
# Exclude certain edge types from results
search_results = client.graph.search_edges(
zep_graph_uuid,
query="user activities",
filters=SearchFilters(exclude_edge_types=["LOCATED_AT", "OCCURRED_AT"]),
)

Exclusion filters can be combined with inclusion filters (node_labels and edge_types). When both are specified, results must match the inclusion criteria AND not match any exclusion criteria.

Node and Episode Filtering

Anchor results to graph structure instead of to text. These four filters restrict results by which nodes an edge connects, or by which episode a fact or entity came from — so you can ask “the facts on this node” or “the entities this episode mentioned” without fetching everything and discarding the rest client-side.

FilterApplies toSemantics
connected_node_uuidsEdgesEdge matches if its source or target node is in the list.
source_node_uuidsEdgesEdge matches if its source node is in the list.
target_node_uuidsEdgesEdge matches if its target node is in the list.
episode_uuidsEdges and nodesEdge matches if it was derived from a listed episode; node matches if a listed episode mentions it.

UUIDs within one list are combined with OR, and the filters are combined with each other — and with every other filter in the same filters object — using AND. So source_node_uuids: [A] together with target_node_uuids: [B] selects the edges directed from A to B. For the undirected set between two nodes, use connected_node_uuids: [A] and read each result’s source_node_uuid and target_node_uuid.

Each list accepts at most 256 UUIDs, and every entry must be a valid UUID. A UUID that does not exist in the target graph matches nothing rather than erroring.

The three node-anchoring filters constrain edges only. Supplying one on a request that returns no edges — node listing, or a search scope with no edge results — is rejected with a validation error naming the field, rather than silently ignored.

episode_uuids applies when results include nodes or edges. Use it with graph.search_edges or graph.search_nodes. The API rejects it on episode, observation, and thread summary search. Edge listing accepts all four filters, while node listing accepts only episode_uuids. Observation and thread-summary listing reject all four.

The neighbors and subgraph endpoints also accept all four filters. The example below lists the facts on one node.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
edges = client.graph.edge.list(
zep_graph_uuid,
filters={"connected_node_uuids": [node_uuid]},
)

episode_uuids also drives the episode_mentions reranker, which orders edge results by how many of the listed episodes mention them.

Property Filtering

Filter search results based on custom attributes stored on nodes and edges. Property filters apply to both node attributes and edge attributes, enabling flexible querying across your graph.

Supported comparison operators:

OperatorDescriptionRequires Value
eqEqual toYes
neNot equal toYes
gtGreater thanYes
ltLess thanYes
gteGreater than or equalYes
lteLess than or equalYes
inValue equals one item of an arrayYes
is_nullProperty is null or not setNo
is_not_nullProperty exists and is not nullNo
from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters, PropertyFilter
# Filter by property values
search_results = client.graph.search_edges(
zep_graph_uuid,
query="team members",
filters=SearchFilters(
property_filters=[
PropertyFilter(
operator="eq",
property_name="department",
value="Engineering",
),
PropertyFilter(
operator="gt",
property_name="level",
value=3,
),
]
),
)

Checking for Null Values

The is_null and is_not_null operators allow you to filter based on whether a property exists. When using these operators, omit the value parameter.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters, PropertyFilter
# Find edges where a property is NOT set
search_results = client.graph.search_edges(
zep_graph_uuid,
query="incomplete records",
filters=SearchFilters(
property_filters=[
PropertyFilter(
operator="is_null",
property_name="end_date",
# value is omitted for is_null
)
]
),
)
# Find edges where a property IS set
search_results = client.graph.search_edges(
zep_graph_uuid,
query="active employees",
filters=SearchFilters(
property_filters=[
PropertyFilter(
operator="is_not_null",
property_name="manager_id",
# value is omitted for is_not_null
)
]
),
)

For standard comparison operators (eq, ne, gt, lt, gte, lte), the value parameter is required. For in, the value is an array. For is_null and is_not_null operators, omit value.

Datetime Filtering

Filter search results based on timestamps, enabling temporal queries that find information from specific time periods. Each leaf predicate names a timestamp field, an operator, and a value. For supported relative requests, auto search can instead infer the calendar window from the query.

Edge Scope Only

Datetime filtering only applies to edge searches. On node and episode searches, datetime filter values are ignored and have no effect on search results.

Available timestamp fields:

FieldDescriptionExample Use Case
created_atThe time when Zep learned the fact was trueFinding when information was first added to the system
valid_atThe real world time that the fact started being trueIdentifying when a relationship or state began
invalid_atThe real world time that the fact stopped being trueFinding when a relationship or state ended
expired_atThe time that Zep learned that the fact was falseTracking when information was marked as outdated

For example, for the fact “Alice is married to Bob”:

  • valid_at: The time they got married
  • invalid_at: The time they got divorced
  • created_at: The time Zep learned they were married
  • expired_at: The time Zep learned they were divorced

The date_filters object holds an any_of list. Each entry is a group with an all_of list of leaf predicates. Predicates inside one group are ANDed; groups are ORed.

In the example below, results are returned if they match:

  • (created_at >= 2025-07-01 AND created_at < 2025-08-01) OR (created_at < 2025-05-01)

Timestamp format: all values are RFC 3339 timestamps (e.g., “2025-07-01T20:57:56Z”).

Comparison operators:

OperatorDescriptionRequires Value
eqEqual toYes
neNot equal toYes
gtAfterYes
ltBeforeYes
gteOn or afterYes
lteOn or beforeYes
is_nullTimestamp is not setNo
is_not_nullTimestamp is setNo
from datetime import datetime, timezone
from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters, DateFilters, DateFilterGroup, DateFilter
# Search for edges created in July 2025 OR before May 2025
search_results = client.graph.search_edges(
zep_graph_uuid,
query="project discussions",
filters=SearchFilters(
date_filters=DateFilters(
any_of=[
# First group: predicates are ANDed
DateFilterGroup(
all_of=[
DateFilter(
field="created_at",
operator="gte",
value=datetime(2025, 7, 1, tzinfo=timezone.utc),
),
DateFilter(
field="created_at",
operator="lt",
value=datetime(2025, 8, 1, tzinfo=timezone.utc),
),
]
),
# Second group: ORed with the first
DateFilterGroup(
all_of=[
DateFilter(
field="created_at",
operator="lt",
value=datetime(2025, 5, 1, tzinfo=timezone.utc),
)
]
),
]
)
),
)

Checking for Null Timestamps

The is_null and is_not_null operators filter edges on whether a timestamp field is set. Omit value for these operators.

# Find edges that have never been invalidated (invalid_at is not set)
search_results = client.graph.search_edges(
zep_graph_uuid,
query="current facts",
filters=SearchFilters(
date_filters=DateFilters(
any_of=[
DateFilterGroup(
all_of=[DateFilter(field="invalid_at", operator="is_null")]
)
]
)
),
)
# Find edges that have an expiration date set
search_results = client.graph.search_edges(
zep_graph_uuid,
query="temporary facts",
filters=SearchFilters(
date_filters=DateFilters(
any_of=[
DateFilterGroup(
all_of=[DateFilter(field="expired_at", operator="is_not_null")]
)
]
)
),
)

For standard comparison operators (eq, ne, gt, lt, gte, lte), the value is required. For is_null and is_not_null operators, omit value.

Common Use Cases:

  • Date Range Filtering: Find facts from specific time periods using any timestamp type
  • Recent Activity: Search for edges created or expired after a certain date using the gte operator
  • Historical Data: Find older information using the lt or lte operators on any timestamp
  • Validity Period Analysis: Use valid_at and invalid_at together to find facts that were true during specific periods
  • Audit Trail: Use created_at and expired_at to track when your system learned about changes
  • Find current/valid facts: Filter for edges where invalid_at is null to find facts that are still valid
  • Find temporary facts: Filter for edges where expired_at is not null to find facts with expiration dates
  • Find facts without validity periods: Filter for edges where valid_at is null to find facts without explicit start dates

Episode Metadata Filtering

Filter search results based on metadata attached to episodes, including metadata attached to messages added through the Threads API. Because that metadata is projected onto every artifact derived from the episode, the filter restricts results to edges, nodes, or episodes whose associated episodes’ metadata matches the given predicates. For edge and node scopes, a result matches if at least one of its associated episodes satisfies the filter.

Episode metadata filters use explicit AND/OR groups via the metadata_filters field in filters. A filter group contains a type ("and" or "or"), a filters array of leaf predicates (each with property_name, operator, and optionally value), and an optional groups array for nested sub-expressions.

Supported comparison operators:

OperatorDescriptionRequires Value
eqEqual toYes
neNot equal toYes
gtGreater thanYes
ltLess thanYes
gteGreater than or equalYes
lteLess than or equalYes
containsStored value contains the substring (case-sensitive)Yes
inValue equals one item of an arrayYes
is_nullKey is null or absentNo
is_not_nullKey exists and is not nullNo

Metadata value types: filter values must be scalars — string, number (int/float), or boolean — except for in, which takes an array of scalars. You cannot pass a nested object as a filter value. Metadata keys whose stored value is an array of scalars are matched element-wise: eq matches when any element equals the value, and contains matches when any element matches.

contains and in: contains is a case-sensitive substring match, for example "gam" matches a stored gam,ma. in takes an array of up to 100 items and matches when the stored value equals one item exactly, for example ["gam,ma", " water"].

Limits: a filter can hold at most 10 leaf predicates in total across all groups, at most 3 levels of nesting, and at most 5 sub-groups per group. A request over a limit returns HTTP 400.

Numeric operators (gt, lt, gte, lte) compare values lexicographically because episode metadata is stored as strings — for example, "9" > "10" evaluates to true. Zero-pad numeric values (such as "009") if you need correct numeric ordering.

Simple filter

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters, MetadataFilterGroup, MetadataFilter
search_results = client.graph.search_edges(
zep_graph_uuid,
query="lab results",
filters=SearchFilters(
metadata_filters=MetadataFilterGroup(
type="and",
filters=[
MetadataFilter(
property_name="source",
value="lab_report",
operator="eq",
)
],
)
),
)

Nested filter groups

Groups can be nested to express complex logic. A MetadataFilterGroup contains filters for leaf predicates and groups for nested sub-expressions. The example below finds results from episodes where source is "lab_report" AND the department is either "endocrinology" or "general".

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
from zep_cloud.types import SearchFilters, MetadataFilterGroup, MetadataFilter
search_results = client.graph.search_edges(
zep_graph_uuid,
query="lab results",
filters=SearchFilters(
metadata_filters=MetadataFilterGroup(
type="and",
filters=[
MetadataFilter(
property_name="source",
value="lab_report",
operator="eq",
),
],
groups=[
MetadataFilterGroup(
type="or",
filters=[
MetadataFilter(
property_name="department",
value="endocrinology",
operator="eq",
),
MetadataFilter(
property_name="department",
value="general",
operator="eq",
),
],
),
],
)
),
)

Episode metadata filters can be combined with other search filters such as node_labels, edge_types, property_filters, and date_filters. When multiple filter types are specified, results must satisfy all of them.

Breadth-first search (BFS)

The bfs_origin_node_uuids parameter starts breadth-first searches from specified nodes or episodes. You can provide up to five UUIDs. Each search operation accepts this parameter.

When to use: Use BFS when you want to find information that’s contextually connected to specific starting points in your graph, such as recent episodes or important entities.

from zep_cloud.client import Zep
client = Zep(
api_key=API_KEY,
)
# Get recent episodes to use as BFS origin points. The list is newest first.
episodes = client.graph.episode.list(zep_graph_uuid, limit=5)
episode_uuids = [episode.uuid_ for episode in episodes.items or []]
# Search with BFS starting from recent episodes
search_results = client.graph.search_edges(
zep_graph_uuid,
query="project updates",
bfs_origin_node_uuids=episode_uuids,
limit=10,
)