Reading Data from the Graph

Zep provides APIs to read Edges, Nodes, and Episodes from the graph. These elements can be retrieved individually using their UUID, or as lists associated with a specific user_id or graph_id. The latter method returns all objects in that user’s or graph’s data.

Examples of each retrieval method are provided below.

Reading Edges

Alongside source_node_uuid and target_node_uuid, edge responses can include source_node_name, target_node_name, source_node_labels, and target_node_labels. These are projections of current node state, so a node rename shows up on the next read. Zep omits the corresponding fields when an endpoint node cannot be resolved, and omits all four when the API key has active attribute constraints. The edge is still returned with its endpoint UUIDs.

1from zep_cloud.client import Zep
2
3client = Zep(
4 api_key=API_KEY,
5)
6
7edge = client.graph.edge.get(edgeUuid)

Reading Nodes

1from zep_cloud.client import Zep
2
3client = Zep(
4 api_key=API_KEY,
5)
6
7node = client.graph.node.get_by_user(userUuid)

Reading Episodes

1from zep_cloud.client import Zep
2
3client = Zep(
4 api_key=API_KEY,
5)
6
7episode = client.graph.episode.get_by_graph_id(graph_uuid)

Listing artifacts in bulk

The methods above return a single artifact by its UUID. To enumerate artifacts of a given type in bulk, use the list methods. Where searching the graph ranks results by relevance to a query, the list methods return everything of a given type in a user or graph, with filtering, sorting, and pagination applied server-side.

Reach for these methods when you are not answering a question but enumerating data: rendering an entity browser or a facts table in a UI, exporting a graph, or auditing what a graph contains. Sorting and cursor pagination let you walk large graphs in stable, predictable pages instead of pulling everything into memory at once.

The same parameters — filters, order_by, direction, limit, and cursor — are shared across four artifact types:

Each type exposes two list methods: get_by_user_id for a user’s graph and get_by_graph_id for a named graph. Both return a flat array of typed objects.

Episodes paginate the same way but take a smaller parameter set of their own — see Listing episodes.

Shared parameters

All four artifact types accept the same parameters on both get_by_user_id and get_by_graph_id.

ParameterTypeDescriptionDefault
filtersSearchFiltersRestrict which artifacts are returned. Same type as graph.search, supporting date filters (created_at, valid_at, expired_at, invalid_at as 2D OR/AND arrays), edge_types/exclude_edge_types, node_labels/exclude_node_labels, and property filters.
order_bystringField to sort by: "created_at" or "uuid"."uuid"
directionstringSort direction: "asc" or "desc"."desc"
limitintegerMaximum number of items returned per call. Explicit values are capped at 50; omitting the parameter uses a page size of 100.100
cursorstringOpaque forward cursor for the next page. Read it from the Zep-Next-Cursor response header (see Pagination).
uuid_cursorstringDeprecated legacy cursor. Pass the UUID of the last item from the previous page. Prefer cursor.

The filters object is the same SearchFilters type documented under Search Filters, so date, type, and property filters behave identically here. Refer to that section for the full filter reference.

Listing nodes

List the entities in a user’s graph, most recently created first.

1from zep_cloud.client import Zep
2
3client = Zep(api_key=API_KEY)
4
5# The 20 most recently created entities in a user's graph.
6nodes = client.graph.node.get_by_user_id(
7 user_id="emily-painter",
8 order_by="created_at",
9 direction="desc",
10 limit=20,
11)
12
13for node in nodes:
14 print(node.name, node.created_at)

To list entities in a named (non-user) graph, use get_by_graph_id with a graph_id. See Entities for per-type detail.

Listing edges with filters

Pass a SearchFilters object to narrow the results. The example below lists facts on a named graph that were created in July 2025 and use specific edge types.

1from zep_cloud.client import Zep
2from zep_cloud.types import SearchFilters, DateFilter
3
4client = Zep(api_key=API_KEY)
5
6edges = client.graph.edge.get_by_graph_id(
7 graph_id="my-graph",
8 filters=SearchFilters(
9 edge_types=["WORKS_WITH", "COLLABORATES_ON"],
10 created_at=[
11 [
12 DateFilter(comparison_operator=">=", date="2025-07-01T00:00:00Z"),
13 DateFilter(comparison_operator="<", date="2025-08-01T00:00:00Z"),
14 ],
15 ],
16 ),
17 order_by="created_at",
18 direction="desc",
19 limit=50,
20)
21
22for edge in edges:
23 print(edge.fact, edge.created_at)

The date filter uses the same 2D OR/AND array structure as graph.search: the outer array is OR, the inner array is AND. See Datetime Filtering for the full semantics.

Pagination

The list methods return a flat array, not a paginated envelope. The forward cursor for the next page comes back in the Zep-Next-Cursor response header. To read it, use the SDK’s raw response accessor — with_raw_response in Python, withRawResponse in TypeScript, and WithRawResponse in Go — which returns both the parsed data and the raw HTTP response.

Pass the cursor from one page as the cursor argument of the next call. When the header is empty, there are no more pages.

1from zep_cloud.client import Zep
2
3client = Zep(api_key=API_KEY)
4
5all_nodes = []
6cursor = None
7
8while True:
9 resp = client.graph.node.with_raw_response.get_by_user_id(
10 user_id="emily-painter",
11 order_by="created_at",
12 direction="desc",
13 limit=100,
14 cursor=cursor,
15 )
16 all_nodes.extend(resp.data)
17
18 cursor = resp.headers.get("Zep-Next-Cursor")
19 if not cursor:
20 break

uuid_cursor is the deprecated legacy pagination path: pass the UUID of the last item from the previous page to fetch the next one. Prefer the opaque cursor from the Zep-Next-Cursor header, which encodes the sort field, direction, and continuation position.

Listing observations and thread summaries

Observations and thread summaries use the same shared parameters. The examples below list each for a user, newest first.

1# Observations for a user, newest first.
2observations = client.graph.observation.get_by_user_id(
3 user_id="emily-painter",
4 order_by="created_at",
5 direction="desc",
6 limit=20,
7)
8
9# Thread summaries across a user's threads.
10summaries = client.graph.thread_summary.get_by_user_id(
11 user_id="emily-painter",
12 limit=20,
13)

See Observations and Thread summaries for per-type detail. Use get_by_graph_id for a named graph.

Listing episodes

Episodes list through graph.episode.list_by_user_id and graph.episode.list_by_graph_id, which take the same order_by, direction, limit, and cursor parameters as the other artifact types. Explicit limit values are capped at 50; omitting limit uses a page size of 100. Use these methods to walk every episode in a graph in stable pages — see Pagination.

Episodes do not accept a SearchFilters object. Their one filter is mentioned_node_uuids, which restricts results to episodes mentioning any of the listed entities — up to 256 UUIDs.

1# Episodes that mention a given entity, newest first.
2episodes = client.graph.episode.list_by_user_id(
3 user_id="emily-painter",
4 mentioned_node_uuids=[node_uuid],
5 order_by="created_at",
6 direction="desc",
7 limit=50,
8)
9
10for episode in episodes:
11 print(episode.uuid_, episode.created_at)

graph.episode.get_by_user_id and graph.episode.get_by_graph_id remain the most-recent-lastn convenience reads. They accept only lastn and return a GraphEpisodeResponse envelope rather than a paginated array, so prefer the list methods whenever you need ordering, filtering, or more than one page.

Listing walks a graph by artifact type. Navigation walks it by connection: start from a node and pull what it is attached to. When the API key has no active attribute constraints, both endpoints include the connecting edges’ endpoint names and labels (source_node_name, target_node_name, source_node_labels, target_node_labels), avoiding separate endpoint-node reads. With active attribute constraints, Zep omits these four fields and retains the endpoint UUIDs.

Neighbors of a node

graph.node.get_neighbors returns each distinct node connected to an anchor node, together with every edge that connects it to the anchor. Results paginate by neighbor node with the same cursor parameter as the list methods (see Pagination).

ParameterTypeDescriptionDefault
directionstringOrientation of the connecting edge relative to the anchor: "out", "in", or "both"."both"
filtersSearchFiltersConstrains the connecting edges (edge types, dates, and the node and episode filters) and the neighbor nodes (node_labels, exclude_node_labels).
order_bystringField to sort neighbor nodes by: "created_at" or "uuid"."uuid"
direction_sortstringSort direction for order_by: "asc" or "desc". Named separately so it does not clash with the traversal direction."desc"
limitintegerMaximum neighbor nodes per page. Explicit values are capped at 50; omitting the parameter uses a page size of 100.100
cursorstringOpaque forward cursor from the previous page.
1neighbors = client.graph.node.get_neighbors(
2 node_uuid=node_uuid,
3 direction="both",
4 limit=25,
5)
6
7for neighbor in neighbors:
8 print(neighbor.node.name, len(neighbor.edges))

Bounded subgraphs

graph.get_subgraph expands breadth-first from up to 20 seed nodes and returns the resulting neighborhood as a single {nodes, edges} payload. Every edge’s endpoints are present in nodes, so the response is a self-contained graph you can render directly. It is built for agent exploration and visualization, not for exporting a graph — use the list methods for that.

ParameterTypeDescriptionDefault
user_id / graph_idstringTarget graph. Exactly one is required.
seed_node_uuidsarrayNodes to expand from, 1–20 entries. Seeds are admitted in request order. Seeds that do not exist are ignored.
depthintegerMaximum hops from the seeds, 1–3.1
directionstringEdge orientation followed during expansion: "out", "in", or "both"."both"
search_filtersSearchFiltersConstrains traversed edges and included nodes. A node excluded by a filter is not expanded through. episode_metadata_filters is rejected here, because it cannot be enforced during traversal.
max_nodesintegerNode budget, 1–500. Seeds count against it.100
max_edgesintegerEdge budget, 1–1000.200

When a budget stops the expansion, the response sets truncated to true and names the binding limit in truncation_reason (for example max_nodes or max_edges), so a partial neighborhood is never mistaken for a complete one.

1subgraph = client.graph.get_subgraph(
2 user_id="emily-painter",
3 seed_node_uuids=[node_uuid],
4 depth=2,
5 max_nodes=200,
6)
7
8print(len(subgraph.nodes), len(subgraph.edges))
9if subgraph.truncated:
10 print("truncated by:", subgraph.truncation_reason)

Deprecated by-node reads

Three convenience endpoints predate the filters and endpoints above. They still work with unchanged behavior, but each caps its result set internally, cannot paginate past that cap, and does not indicate truncation in its standard SDK response. Prefer the replacements for any graph beyond toy size:

Deprecated methodReplacement
graph.node.get_edgesEdge listing with connected_node_uuids, or graph.node.get_neighbors.
graph.node.get_episodesEpisode listing with mentioned_node_uuids.
graph.episode.get_nodes_and_edgesNode and edge listing with episode_uuids.