Skip to main content

Visualization payloads

visualize_graph() renders a self-contained HTML page. The functions on this page return the same data as plain dictionaries instead, so an external UI or dashboard can render it itself. Both paths run through the same authorized, bounded graph read and the same preprocess() step, so they cannot drift on the data — only on how each packages it.
These builders are not re-exported on the top-level cognee module. Import them from cognee.api.v1.visualize.

visualize_graph_json()

The graph as a JSON-safe dict, without the semantic layout. Authorization, dataset resolution and the bounded fetch are shared with visualize_graph(), so the same arguments give you exactly the subgraph the HTML page would have shown — including the bounded-subgraph defaults.

Parameters

bool
default:"True"
Embed the caller’s search and improve events in the payload as search_events, scoped to dataset. Sessions attributed to no dataset are not included.
list
default:"None"
Restrict embedded session events to these sessions. An explicit list is an intentional override, used as given and not narrowed to dataset.
Optional[User]
default:"None"
User context for dataset access. Falls back to the default user.
Optional[Union[str, UUID]]
default:"'main_dataset'"
Dataset name or id to read.
bool
default:"False"
Return the entire graph instead of a bounded subgraph.
Optional[str]
default:"None"
Query string whose nearest vector hits seed the subgraph.
Optional[List[str]]
default:"None"
Explicit seed node ids for neighborhood expansion.
Optional[Any]
default:"None"
A recall() or search result whose graph provenance seeds the subgraph. Python-only — there is no HTTP equivalent.
int
default:"2"
k-hop expansion depth around the seeds.
int
default:"10"
Maximum number of seed nodes.
int
default:"500"
Hard cap on nodes after expansion.

Returns

dict — every field of the renderer’s PreprocessedGraph snapshot, plus search_events: Semantic positions are deliberately absent — see below.

visualize_semantic_json()

Semantic positions and clusters for the same subgraph, computed on demand. Pass the same dataset/seed/depth/cap arguments you passed to visualize_graph_json() to lay out the same subgraph. This is the one call that fetches embeddings and runs the PCA (or UMAP) projection over them, bounded to SEMANTIC_NODE_CAP = 2000 nodes. The HTML render computes the layout on every render; splitting it out here means a client that never opens a semantic view never pays for it.

Returns

dict with semantic_positions and semantic_clusters. Both are null together — the layout is best-effort, so no embeddings resolving or the projection failing yields {"semantic_positions": null, "semantic_clusters": null} rather than an error.

build_brains_payload()

A small graph preview for every dataset the caller may read — their own, their tenant’s, and anything granted to a role they belong to. There is no dataset argument: this is the overview across brains, not one brain.

Returns

dict keyed by dataset id, each value {"name", "nodes", "links", "node_set_colors"} — not the full visualize_graph_json() shape, since an overview does not need one dataset’s worth of schema/memory/pipeline detail multiplied by every dataset. max_nodes is applied independently per dataset; there is no larger combined cap.

build_brains_summary_payload()

The cheap counterpart of build_brains_payload(): the same datasets — the authorization union is shared, so both list exactly the same brains — but only what an overview shows (name, sources, size, colors), built from relational metadata and the per-cognify-run count cache instead of one bounded graph read per dataset. A cold cache pays one count query per cognify run whose count is not cached yet; every call after that pays no graph reads at all. Reach for build_brains_payload() only when the node and link arrays themselves are needed.

Returns

dict keyed by dataset id (as a string), each value {"name", "source_names", "node_count", "node_set_colors"}:

get_live_events()

The delta of search and improve events since a cursor, for refreshing a timeline without rebuilding the whole graph payload. dataset_id both gates and scopes. It gates with the same read-permission check every other visualization entry point runs, and it scopes the events: only the caller’s own sessions attributed to that dataset contribute, matching the search_events already embedded in visualize_graph_json() for the same dataset. A session attributed to no dataset — the plain global default_session an unscoped or cross-dataset search runs in — contributes to no dataset’s timeline, since it could belong to any dataset the caller has queried. Attribution is per session, not per answered turn: a session id reused across datasets stays with the first dataset it touched, so its later turns appear on that dataset’s timeline. Raises PermissionDeniedError when the dataset does not exist or the caller cannot read it.

Returns

{"events": [...], "cursor": <ISO datetime string or None>}. Omit since on the first call to get everything available, then pass the previous response’s cursor straight back as the next since — the filter is strict (>, not >=), so no event is delivered twice. When nothing new has happened the response echoes back the since you sent, or null if you sent none.

stream_dataset_updates()

The push loop behind WS /api/v1/visualize/subscribe/{dataset_id} — sends a ready frame, then pushes live_events, graph_grew and heartbeat frames until the client disconnects, so a client can stop polling get_live_events() and refetching the graph payload entirely. See Live dataset updates for the frame shapes, cadences, and close codes. Exposed for deployments that mount their own WebSocket route. It expects an already accepted, already authorized connection — the caller owns accept and close, so a rejection can carry a close code the client will actually see — and raises PermissionDeniedError when read access to the dataset is lost while the stream is running (permission is re-checked on every poll).

stream_dataset_graph()

The streaming counterpart of visualize_graph_json(): the same authorized, bounded read, yielded as (event, data) pairs — meta, one chunk per read, summary, done — instead of one payload. See Streaming below for what each event carries. Exposed for deployments that mount their own route. Like stream_dataset_updates(), it takes an already-authorized dataset — a dataset object (its id and owner_id scope the database context), or None for the process-wide default context — so the caller owns the permission check. The whole read runs in the task that iterates the generator, and the dataset context and its queue slot are held until the generator is exhausted or closed. The limits described under Streaming — the stream cap, the 300-second lifetime and keepalives — are applied by the HTTP route, not by this generator: if you serve it yourself, read it to the end promptly rather than at the pace of a slow client, or the dataset’s queue slot stays held for that long. There is no full, recall_result or include_session_events here: streaming reads the bounded subgraph only, and the graph-wide side payloads (schema_graph, memory_map, search_events, and the rest) are not streamed.

get_memory_provenance_payload()

The memory-provenance graph — the ownership and data-flow story read purely from the relational database — packaged as a dict. Same scoping rules as get_memory_provenance_graph(): in multi-tenant deployments you must pass a scope, or the read spans every tenant. scope_dataset_ids narrows that tenant or user scope to an explicit set of datasets — and narrows only, so an empty list is a caller who may read nothing and yields no dataset nodes. The files, ACL grants, agents, sessions and memory follow the datasets, as do the roles and users: a role survives only with a grant on an in-scope dataset, a user only by owning one, holding a direct grant on one, or belonging to a role that does. Pass the caller’s readable datasets for anyone who does not administer the tenant. Given without a tenant scope, it narrows the tenant layer as well, to just the tenants the in-scope datasets belong to.

Returns

dict in the same shape visualize_graph_json() returns (nodes, links, color_maps, schema_graph, memory_map, and the rest) — not the raw (nodes, edges) tuple get_memory_provenance_graph() returns.

Over HTTP

Each builder above has an HTTP endpoint in front of it. GET /api/v1/visualize/json, GET /api/v1/visualize/semantic and GET /api/v1/schema/provenance/json are JSON siblings of existing HTML endpoints; /brains, /brains-summary and /live-events have no HTML equivalent. POST /api/v1/visualize/multi remains HTML-only, and stream_dataset_updates() fronts a WebSocket route rather than a GET. All of them require authentication. The routes that take a dataset_id enforce the same read permission as GET /api/v1/visualize; /brains and /brains-summary take none and simply return the datasets the caller can read. The WebSocket route also accepts the API key or bearer token as a ?token= query parameter, since a browser cannot set headers on a WebSocket handshake — mind that the handshake URL, query string included, shows up in the default access logs of common reverse proxies. /json and /semantic accept the same query params as GET /api/v1/visualize — dataset_id (required), full, query, seed_node_ids, neighborhood_depth, neighborhood_seed_top_k and max_nodes — with the defaults listed above. Pass identical arguments to both to get a graph and its semantic layout for the same subgraph. /json takes one more, stream, and its max_nodes ceiling depends on which path answers: 5000 as JSON, 20000 when streamed.

Streaming

GET /api/v1/visualize/json can send the graph as server-sent events instead of one response body, for graphs too large to hold whole on either side. It is opt-in and negotiated per request, so callers that do nothing keep the JSON payload above unchanged. Opt in with stream=true, or with an Accept header that ranks text/event-stream above application/json. The query parameter decides outright when present (stream=false forces JSON even under an SSE Accept); otherwise a tie stays on the JSON path, so Accept: */* — what fetch, httpx and requests send unless told otherwise — behaves exactly as it did before streaming existed.
The events arrive in this order: A chunk’s nodes and links use a compact shape, not the full objects the JSON payload carries: each node has id, name, type, entity_type, stage and is_unnamed, plus belongs_to_set and source_node_set when the node has them; each link has source, target, relation and edge_class. The values are computed with the same helpers the JSON path uses, so they mean the same thing. Two ordering guarantees make the stream renderable as it arrives:
  • A link’s endpoints were always sent in the same chunk or an earlier one, so there is never a dangling edge to defer.
  • Every node in a summary entry was already sent in a chunk. Each entry carries importance and label_priority; an entry may also carry entity_type, and only when it corrects what its chunk sent. Chunks already carry each entity’s type as resolved from the store, so a correction only appears when that lookup failed for the read: an entity whose is_a link arrived in a later chunk is then sent as "Entity" and typed properly here.
Quiet stretches are padded with SSE keepalive comments every 15 seconds. Two limits are specific to this path. full=true cannot be streamed — it reads the whole graph, which is what streaming exists to avoid. And a server holds at most 8 streams at once; beyond that, new ones are refused rather than queued.

Errors

JSON error bodies carry a fixed message and never the exception text — full detail is server-logged instead.
  • 403 — /live-events only, when the caller lacks read permission on the dataset (or it does not exist).
  • 409 — the payload could not be built. Note that a semantic layout that cannot be computed is not an error: /semantic answers 200 with both fields null, and only returns 409 if the underlying graph fetch itself fails.
Streaming adds three more error cases to /json:
  • Request validation error (400 on the cognee server, like any invalid query parameter) — max_nodes above 5000 without streaming, or full=true with it.
  • 503 — this server already has its maximum number of graph streams open (GraphStreamCapacityError). Retry shortly; one finishing frees a slot.
  • An error event rather than a status code, when the failure happens after the response has already started. It carries message and status — the status the JSON path would have returned — and ends the stream. Failures before the first chunk still come back as ordinary HTTP errors with their real status, because nothing is sent until the read produces its first chunk or fails. A stream the client has not finished reading within 300 seconds is dropped with an error event of status 408.
The WebSocket route signals failure with close codes instead: 1008 when the caller is not authenticated or lacks read permission on the dataset (a retry replays the same rejection), 1011 when the stream failed server-side (reconnecting is reasonable).

See also