Operational evidence. Graph evidence defines the attribution contract for runtime overlays, profiles and Shadow runs. Structural links, live observations and fixture values retain separate truth/coverage. Historical evidence resolves to its recorded graph/artifact/activation, and missing mappings remain visible. See Operations & Reliability.

Reaktor Graph

Graph is the workbench pane that shows how a running app is put together, and what it is doing right now. It reads the app's graph while the app runs: every service, repository, interactor and screen it mounted, the ports each one needs and provides, and the wires that connect them. It draws them as one left-to-right map in the visual language of Unreal Engine's Blueprints, and paints the running app onto the same map: the screen on display glows, calls pulse along the wires, and a pin you watch shows the last value that crossed it. It can read the app running inside Reaktor Desktop or on a phone.

Status: rebuilt on 30 September 2026 and checked against the BestBuds app, in the desktop and on an Android emulator. On 1 October 2026 it gained the Features layout, which groups the map by the app's features and fills the window's shape (built for a 5120×2160 ultrawide), and it started showing the signed-in part of the app while nobody is signed in. It replaces the earlier graph workspace (a tree, a preview and a generic canvas). The design direction for whole-system navigation stays in Graph Navigation and Layers.

At a glance

  • The whole app, grouped by feature. Screens are grouped by the first part of their route (Start, Chats, Circles, Plans, You, Settings and so on). Each group keeps the logic and data only its screens use, and what several features share sits in a bottom row of shared services, data and logic. The groups are packed to the window's shape, so a wide screen gets a wide map. Each group's header names the graphs its nodes live in, and picking a graph in the outline narrows the map to it.
  • A Blueprint map. Each node has a coloured header with an icon for its kind: services blue, data amber, logic violet, screens green. Pins sit on the node's edges: what it needs on the left, what it provides on the right. A pin's colour says what flows through it, it is filled when wired and hollow when not, and wires take the colour of what they carry. A screen that opens another is joined to it by a dashed link.
  • One renderer. The Cloud pane draws its resources with the same kit, so both maps share frames, cards, pins, wires, the camera and the legend.
  • The system at a glance. A summary card on the map shows the app, where it runs, the screen on display and the back stack, the size of the graph, a bar of node kinds, health by severity and live calls a minute with a sparkline. An outline beside the map lists every graph, screen, interactor, repository and service, with dots for what is on screen and what has a finding.
  • Live, like a game in Play-In-Editor. When a phone is attached the map gets a live border. Each call pulses along its wire, green or red, and nodes glow while they run. An execution trace under the map lists the latest calls in order, and a click shows the call's node.
  • Watch a pin. Watching a port makes the phone report the last value that crossed it, masked for secrets. The value shows on the pin, in the inspector and in the trace.
  • Errors on the nodes. Findings show as tags on the node headers (UNWIRED, DUPLICATE, LAYERING, CYCLE), and the wires behind them are drawn dashed in the rule colour.
  • Findings and Ports tabs. Every finding with its evidence and fix, and every port in one sortable table.
  • Agents. Four read-only MCP tools answer the same questions for coding agents.

1. How it works

The pane reads a description of the graph, not the live objects. The same description comes from the desktop process or from a phone, so everything after the first step is shared.

  1. Describe. Graph.shape() in reaktor-graph walks a running graph and every graph nested in its containers. It records each scope, node (kind, type, route, lifecycle), port (key, type, direction and whether it is wired, injected, unmatched or unused), wire, route and back stack, and which scopes are active. Graph.shapes() emits a new description whenever navigation changes a back stack, the selected tab or the session slot. A session slot that was previewed remembers the previewed session's description, its blueprint. While nobody is signed in, the description includes that blueprint with every node marked Not running. The signed-in part of the app stays on the map, and nothing of it runs.
  2. Carry. On a phone, the BestBuds DevTools build publishes those descriptions through the reaktor-devtools agent as a graph.shape stream. The stream keeps only the latest description and replays it to a desktop that connects later. The app encodes off the main thread and sends at most four descriptions a second.
  3. Model. The desktop turns a description into an AppGraph: each node gets a role (service, data, logic, screen, container) and each pin a family (service, data, logic, screen, HTTP endpoint or value). Internal plumbing ports that every route has are left out.
  4. Lay out. The Eclipse Layout Kernel (ELK layered, 0.12.0) places the cards in role lanes, pins each port to its side of the card and routes wires at right angles. In Features, each feature is laid out on its own. Shared groups are packed as grids, and the groups are placed in rows. The row width is chosen so the whole map comes closest to the shape of the map area, not counting the outline and the summary card. Wires between features are drawn as curves, faint until something is selected. Routes between screens are laid out with the wires. Nodes and wires go in sorted by name, so the map is the same on every run. Layout runs off the UI thread and is published on the next frame.
  5. Draw. The Blueprint kit, the reaktor-blueprint module, holds the layout engine and the frame, card, pin, legend and camera pieces; every Hangar map draws through it. Its GraphCanvas draws on compose-flow over a grid that scales with zoom, draws pulses in graph space so they follow pan and zoom, and keeps the camera when the data changes.
  6. Paint live data. For a phone, the agent's port events give each call's node, port, time and outcome. For this desktop, the workbench's own trace spans give the same. The screen on display comes from the active scopes and back stacks in the description.
  7. Watch. The agent offers a port.watch command on builds that capture values. Watching a port adds it to the agent's watch list, and from then on each call through it reports its result, summarised (collections as their size and first items) and masked with the same rules as logs and HTTP bodies. Pins nobody watches cost nothing extra.
PieceWhere it lives
Graph descriptionreaktor-graph · GraphShape.kt
Phone stream and watchreaktor-devtools · DevToolsProtocol.kt, DevToolsAgent.kt, GraphTap.kt, PortWatch.kt
BestBuds publishermodules/app · devtools/BestBudsDevTools.kt
Blueprint kit, layout and the Graph.shape() conversionreaktor-blueprint
Shared canvasreaktor-blueprint · canvas/GraphCanvas.kt
Routed wires, pinch fixcompose-flow · Edges.kt, Nodes.kt, viewport bridge
Findings, live overlays, panemodules/engine · workbench/appgraph/
MCP toolstargets/reaktorDesktop · ReaktorDesktopMcp.kt

The layout libraries are org.eclipse.elk:org.eclipse.elk.alg.layered and the Xtend runtime ELK needs at run time (org.eclipse.xtext.xbase.lib), both versioned in dependeasy's Version.kt.

2. The pane

Header. The source switch (This desktop or a connected phone), the tabs Map, Findings and Ports, a live badge when the source is streaming, and the counts of nodes, wires, routes and graphs.

Map toolbar.

  • One view: every feature as a frame, every node with its wires, and dashed links for the routes each screen can open, so the app's flows and its wiring read on the same map.
  • Compact shows names only. Ports in use shows ports with a wire or live traffic. Every port shows them all; HTTP endpoints fold into a "+N endpoints" line.
  • Search dims everything that does not match and lists the matches; a match reveals the card.
  • Focus keeps the whole graph, the selected node's neighbours, what feeds it, what it feeds, or the path between two nodes.
  • Outline shows or hides the outline; it opens by itself on wide windows. Trace shows or hides the execution trace when a source is live.

Summary card. App, source and a LIVE, THIS PROCESS or NOT RUNNING badge; the screens on display (click to reveal); the back stack across the active graphs; graphs, nodes, wires and routes; a bar of node kinds; health by severity (click for the Findings tab); calls a minute, failures, median time and a one-minute sparkline. It folds to a single line.

Outline. Graphs as a tree with node counts, then screens, logic, data, services and containers, each row with its colour, its graph, a green dot when on screen, a severity dot when it has a finding and its live call count. Click a graph to narrow the map to it; click a node to reveal it.

Map. Pins show a tooltip with key, type, family, wiring, peers, live calls and the last watched value. Zoomed out, cards show only their names, sized to stay readable. The watermark in the corner names the app and says whether it is live.

Camera. Zoom level, fit the graph, frame the selection. Scroll pans, pinch or Ctrl-scroll zooms around the pointer, and the camera stays where you left it when the graph changes or you visit another pane. The camera buttons wait for the current view's layout.

Execution trace. The latest calls, newest first: how long ago, node and port, the watched value or the failure, duration and outcome. Click a row to reveal its node. It can be resized or hidden.

Inspector. Opens on the selected node. Details starts with Runs on or Backed by: the Cloudflare worker behind the node and the databases it touches, each a link into Cloud or Data (see Connected panes). Then it lists what needs attention, live traffic per port, what it needs and from whom, what it provides and to whom, with a Watch toggle and the last value on each port of a phone that supports it, the routes it opens and is opened from, and its identity (kind, type, route, graph, lifecycle, id). Source shows the class's file.

Findings tab. One card per finding with its severity, what is wrong, why it matters and the fix. The node chips reveal the node on the map.

Ports tab. Every port with node, key, direction, type, wiring, peer, calls, median time and failures. Filter by text or show problems only.

Search language. Plain words match a node's name, type, route, role and graph. Keys narrow it:

KeyMatches
kind: role:kind:screen, role:data
type: name: route:runtime type, label, route pattern
scope: graph:the graph the node sits in, or any graph above it
provides: needs: port:a port key or type, by direction
is:unwired, injected, active, isolated, problem

-word excludes and ~pattern is a regular expression, as in the DevTools pane.

The whole system

Pick Whole system in the source menu to see the app together with the Cloudflare workers and the Spring server it calls. Hangar asks each host the app's client service nodes point at for its own description at /_reaktor/shape. It signs the request with the worker service credentials already on this machine (tmp/worker-service-auth.env), through the same operations plane the Cloud pane uses.

  • Each host is a frame named after its island and host, such as social · social.bestbuds.ai or reaktor-server · api.reaktor.build. Its service nodes are labelled by contract, such as bestbuds.circles.
  • A long-dashed orange wire runs from a host's service node to the client node that calls it, one wire per contract. Calls the app makes over HTTPS, calls one worker makes to another over a service binding, and calls into a Durable Object are all drawn this way.
  • A Durable Object class gets its own frame next to its worker, such as notification-dispatcher · notification.bestbuds.ai/notification-dispatcher, because it runs apart from the worker and keeps its own storage.
  • A client contract that no island serves stays unwired. Today that is notifications: its worker serves hand-written routes and does not implement its contract.

Calls across islands. Under the map, a panel lists the calls every island recorded in the last hour, newest first, with the island, the operation, how long it took and whether it failed. Each call also counts on its wire, so a wire shows how many calls crossed it and how long they took.

  • Click a call to see only its trace: the call, the calls it made, and the calls made on its behalf, indented under it. Their nodes light up on the map. The filter button brings back every trace.
  • The refresh button reads the latest calls. Hangar reads them from ClickHouse through the operations plane, with one fixed read-only query over service_spans.
  • Workers measure time in whole milliseconds, and their clocks only move during I/O, so a call that does no I/O shows 0 ms.

3. Findings

FindingSeverityWhat it catches
UnwiredHighA node needs a port nothing in its graph or the graphs above provides, and dependency injection cannot supply it. The first call through it fails.
DuplicateMediumTwo nodes in one graph provide the same port keys, so which one a consumer gets depends on wiring order. Two services that share only a few HTTP endpoints are not flagged.
CycleMediumNodes that consume each other's ports, found as strongly connected components.
LayeringLowA screen that consumes a service directly, skipping the repositories and interactors that give caching, retries and offline state.
ReachLowScreens no route leads to from the start screen, following routes into containers. Deep-link-only entries show up here on purpose.
IsolatedNoteNodes that neither provide nor consume a port another node uses.
Contract driftHigh / MediumWhole system only. A client operation that no island serves, a request or response schema whose fingerprint differs between the two ends, or a contract version that differs.
InjectedNoteNodes whose inputs come from dependency injection, so the map has no wire for them.
UnusedNoteProvided ports nothing consumes, not counting HTTP endpoints, which exist for outside callers.

Duplicate, Cycle and Layering also mark their wires: dashed, in the rule colour, labelled once with the rule's name.

4. For coding agents

Reaktor Desktop's MCP server answers from whatever the Graph pane is showing, desktop or phone. All four tools are read-only.

ToolReturns
app_graph_nodesNodes matching the pane's search language, with role, graph, route and port counts
app_graph_nodeOne node in full: needs and from whom, provides and to whom, routes, findings, live calls and the last watched value per port
app_graph_findingsFindings at or above a severity
app_graph_pathThe chain of wires from one node to another

5. Checked on the real app

  • Desktop. The BestBuds graph shows 6 graphs (root, the signed-in session and four tabs), 104 nodes, 143 wires and 43 routes. It lays out in 10 to 70 ms once warm; the first layout in a fresh process takes 0.2 to 0.5 s. The findings are one Medium duplicate (the sticker service is provided by two nodes), one Low layering finding (the developer screen calls four services directly), isolated nodes and unused ports.
  • The duplicate is real. Across two runs, the sticker repository's six calls were wired to one of the two providers in one run and to the other in the next, because wiring order follows node ids.
  • Phone. A DevTools debug build on an Android emulator streamed its graph to the desktop on connect: 30 nodes, 24 wires and 9 routes at the sign-in screen, with the sign-in screen lit and the calls it had made counted on its interactor and listed in the trace. The phone advertised port.watch and accepted watches on 20 ports. The MCP tools answered from the phone's graph.
  • Tests. Render tests draw the map at every detail level and the whole pane; the phone test renders every tab from a live emulator; agent tests check that watched values are masked and summarised and that builds without value capture refuse to watch; the desktop suites drive search, reveal, zoom, pinch, fit and camera retention with real pointer input on both the Graph and Cloud panes.

6. Known limits

  • A phone appears only when it runs a DevTools build (bestbuds.devTools=true). Release builds never carry the agent.
  • On a phone, calls are counted for ports in the root graph and the signed-in session graph. The small tab graphs are not tapped yet. A signed-out app makes few calls, so most live traffic appears only after signing in.
  • A watch reports each call's result, not its arguments, and only on phones whose build captures values. This desktop has no watch yet.
  • On the desktop, trace spans are matched to nodes by label, so two nodes with the same label share their counts.
  • The graph is laid out again whenever its structure changes. That is fast at this size, but cards can move, and graphs of thousands of nodes are untested.
  • Two ports with the same key on one node (GET and POST on one path) share one traffic row.
  • Source finds the file from the class name. For a phone it matches by simple type name, so it can miss a class the desktop build does not contain.
  • Route links come from declared routes. Navigation by pattern or deep link does not appear.
  • Sheets and dialogs are not on the map. They live inside screens as local state, not as graph nodes. In BestBuds that is about 25 of them: message actions, reactions, the sticker picker, forward, report, block, media preview and viewers, the poll composer, voice drop, new group and member actions. The two onboarding sub-steps and the tab contents are missing for the same reason; each tab appears as its TabScreen.
  • Features come from the first part of each route. A one-screen feature joins the feature it navigates with, and navigation from the home tabs is ignored because it reaches everything.
  • The pane reads and watches. It does not edit the graph, pause a node or go back in time.

7. How it compares

Researched on 30 September 2026.

  • Unreal Blueprints are the reference for the look and the live behaviour: coloured node headers, pins coloured by type and filled when connected, wires in their pin's colour, comment boxes, pulsing wires while a game runs, a debug object to pick the running instance, and "Watch this value" on pins. Reaktor Graph now has all of these, with the source switch as the debug object. Blueprints also have breakpoints, stepping and editing; Reaktor Graph does not yet.
  • Nx graph (focus, trace between two projects, groups that expand in place) and IntelliJ's dependency diagrams (neighbourhood, paths, cycles). Reaktor Graph has focus, neighbours, upstream, downstream and path, and feature frames. It does not yet save a view.
  • dependency-cruiser and SonarQube / Sonargraph check architecture rules and colour the edges that break them. Reaktor Graph now does the same for its duplicate, cycle and layering rules; rules a workspace declares itself are still to come.
  • Kiali animates traffic on a service graph and can replay a past window. Reaktor Graph pulses each call; it has no replay yet.
  • NiFi and Flink paint load onto the dataflow graph. Reaktor Graph shows call counts, failures, glowing nodes and a calls sparkline; it does not tint nodes by how busy they are.
  • Stately Inspector lights the active state of a live state machine. Reaktor Graph lights the screen on display and the graphs that are active.
  • ELK was chosen over Graphviz, dagre, d3-dag and yFiles because it is the only open-source engine that places ports and nested boxes in one pass and runs inside the JVM.

8. Roadmap

Ordered by value.

  1. Breakpoints on ports. Pause a suspending call as it crosses a port, show the paused node the way Blueprints do, then continue or step to the next port, with an automatic resume so a forgotten pause cannot hang the app. The watch list is the plumbing it builds on.
  2. Time slider. DevTools already records the phone's screens and port events. Scrubbing that recording should show the graph as it was: the screen on display, the back stack and the calls up to that moment.
  3. Changed since the last build. Compare two descriptions and mark new, removed and changed nodes, with a filter for what a change reaches.
  4. Editing. Blueprints are edited, not only read. Drawing a wire or adding a node here would have to write Kotlin, which changes how Reaktor apps are written, so it needs a design decision first.
  5. Saved views and a richer query. Named focus queries, and +Node+2 style depth.
  6. Tab graphs on the phone, watch on the desktop. Tap the tab graphs' ports as well, and give this desktop's own graph the same watch.
  7. Back stack trail and a minimap. Number the back stack along the route links, and add a corner minimap for large graphs.