Graph View
The Graph admin page (/graph) renders a force-directed view of your
account's Neo4j subgraph. Labels on the canvas follow the zoom level, so you
see the most useful identity at every scale.
Search and pivot
Type a term in the search box to highlight matching nodes on the canvas. Hits get an amber border so you can pick them out of a busy view. Click any highlighted node to open its side panel and pivot into its neighbourhood — both clicks (hit and non-hit) behave identically.
When a search is active and you click a node, the neighbourhood you pivot into is narrowed to the search-relevant subset. For example: searching "david" with 175 matches and clicking yourself returns the Davids you're connected to, not your entire LinkedIn graph. The narrowing applies once per pivot — clearing the search and pivoting again returns the full neighbourhood.
Searches reach every textual property of every operator-meaningful label, including denormalised fields the platform writes specifically so search can reach them — for example, the current job title of each LinkedIn connection (originally stored on the connection edge, copied to the Person node so the fulltext index can match it).
Conversation label tiers
Conversation nodes carry the most operator-meaningful identity in the subgraph (the conversation name or summary, the date it started, the message count). They render in one of three tiers, switched by canvas scale:
| Zoom | Label shape | Example |
|---|---|---|
| Zoomed out (< 0.7×) | Compact — one line, capped at 24 characters. Preserves the no-overlap contract that matters when nodes are tightly packed. | Maxyfi branding conflict… |
| Mid zoom (0.7× to 1.3×) | Wrapped — up to two lines of 24 characters each, soft-ellipsis on overflow. Full name is visible without hover. | Maxyfi branding conflict / with Rubytech |
| Zoomed in (≥ 1.3×) | Detailed — wrapped name plus a metadata line reading YYYY-MM-DD · N msgs. | Maxyfi branding conflict / with Rubytech / 2026-04-23 · 7 msgs |
Non-Conversation nodes (People, Messages, Tasks, WorkflowRuns, etc.) keep their concise single-line labels at every zoom level — the canvas stays readable when you zoom out to see a large subgraph.
Tier transitions are debounced so spinning the scroll wheel does not cause label flicker; labels only rewrite once zoom settles on a new tier.
Cluster-expand on Conversation/Message clicks (cluster-integrity fix)
Clicking a Conversation node OR any Message node pulls the WHOLE
conversation cluster onto the canvas: the Conversation node itself plus
every Message belonging to it (via PART_OF), capped at 200 messages
for layout reasons. The arrow chain along the conversation (the NEXT
edges) renders for free because the inter-node relationship pass picks
up edges where both endpoints are in the visible window.
Pre-fix, clicking a middle Message expanded only its prev+next
neighbours; the head, tail, and Conversation node dropped off, visually
disintegrating the conversation. The new behaviour keeps the cluster
intact across click navigation. PART_OF edges are now rendered between
visible Conversation/Message pairs (previously suppressed because they
"added no information when the Conversation node wasn't on canvas" — an
assumption that broke the moment the cluster-expand put it there).
The breadcrumb above the canvas tracks each pivot — every entry except the last is clickable to pop the view-stack back to that point.
Tooltips and side panel
Hovering a node still shows the full 5-line tooltip (display name, labels, id, created at, updated at). Clicking a Conversation opens the side panel with the full property table — zoom-tier changes never alter these paths.
The side panel carries a Trash button for live nodes and a Restore button for trashed nodes. Soft-delete is reversible: trashed nodes remain in the graph and reappear when Show trashed is on.
Deleting a node
Two surfaces, same outcome:
- Mouse (desktop): drag a node to the dashed Trash zone in the upper- right corner of the canvas.
- Touch (mobile/tablet): the dashed Trash zone is hidden because vis-network's drag hit-test never fires on touch. Tap the node to open the side panel, then tap Trash.
Both paths POST to the same soft-delete endpoint; the operator-side behaviour is identical.
Mobile layout
Below 640px viewport width the toolbar wraps: the search input claims its own row, the search-result slider claims its own row (full-width with an enlarged thumb for touch), and the Filter button + node count share the bottom row. The "← Back" control collapses to a left-arrow icon to preserve toolbar space at depth.
Trashed conversations
Trashed Conversation nodes are hidden by default. Toggle Show trashed in
the filter popover to surface them; they render with a faded fill and dashed
border, with their zoom-tier labels intact. The N msgs count excludes
trashed Messages, so the detailed-tier label reflects only live turns in the
conversation.
Filtering by channel and message kind
When you select AdminConversation or PublicConversation in the filter popover, two extra rows appear underneath the chip list:
- Channel — Web / WhatsApp. Select one to scope the canvas to conversations that came in over that channel only. Selecting both is the same as selecting neither (all channels). After the migration that ships with this release, every conversation carries an explicit channel value — pre-existing conversations are backfilled to "Web" because only the WhatsApp and Telegram intake paths ever set non-Web values.
- Message — User / Assistant / WhatsApp. When you've also pivoted into a conversation neighbourhood (or your search hits messages directly), this row scopes the messages on canvas to the chosen kind. WhatsApp messages persist with their own sublabel so you can isolate the live-channel cohort from the agent-path cohort within the same conversation.
These sub-facets compose with the chip selection. Searching with the AdminConversation chip selected now also reaches the body text of every admin message — typing a rare word like "ATM" returns every conversation that mentions it, not just conversations with that word in the title.
Sidebar conversations list
The Recents list above the chat sidebar carries a per-row marker: WhatsApp conversations show a small WhatsApp glyph next to the conversation name. The dropdown above the list filters Recents to a specific channel — flipping it to WhatsApp hides web-chat conversations and vice versa.
Agents in the graph
Both admin and public agents appear as :Agent nodes in the graph. Open
the Agents entry from the sidebar to see them all. Each agent
carries a :HANDLED_BY edge from every conversation it has handled, so
you can pivot from an agent to the conversations it ran. The admin
agent's IDENTITY, SOUL, KNOWLEDGE, and KNOWLEDGE-SUMMARY documents
appear as :KnowledgeDocument nodes connected via HAS_* edges, the same
projection shape used for public agents.
Agent-execution telemetry
ToolCall, StepResult, WorkflowStep, and WorkflowRun nodes are
agent-execution telemetry — kept for audit but noisy for day-to-day graph
navigation (they make up roughly 9% of a typical brand's live nodes).
They are hidden from /graph by default. To see them — for audit, debug,
or tracing a specific agent run — open the filter popover and tick
Include agent actions (the checkbox directly below Show trashed).
Flipping it on surfaces the four labels as chips in the popover roster AND
keeps them on the canvas when you pivot into a neighbourhood. The toggle
is session-scoped: every new session starts with agent actions hidden, so
the 90% domain-navigation path stays clean without having to remember to
switch them off. Flipping it off again also drops them from any already-
expanded neighbourhood so a click near a ToolCall does not re-introduce
it.
Direct edge management
memory-edge creates or deletes a typed directed edge between two nodes that already exist in the graph. Both nodes must belong to the same account — mismatched or foreign nodes are rejected with a structured error before any mutation runs.
Create: MERGE is idempotent. First call returns {created: true}; a repeated call with the same endpoints and type returns {created: false}. Properties supplied on the call are stamped onto the relationship on CREATE only; a subsequent idempotent hit does not overwrite them.
Delete: If the edge is present it is deleted and {deleted: true} is returned. If absent, the call is a no-op and returns {deleted: false}.
relationshipType is uppercase-coerced. Types that start with an underscore (e.g. _SOFT_DELETE) are reserved for platform internals and are rejected.
Typical flow: call memory-search for each endpoint to retrieve their elementId values, then call memory-edge action=create relationshipType=RELATES_TO fromId=<id> toId=<id>. The new edge appears when you hop-expand either endpoint on the /graph canvas.