Getting started
InstallationQuick startControlled and uncontrolled state
Entities
The entity modelSockets and widgetsEntity typesFrames and groupsText and commentsImages, video and models
Edges and connections
EdgesConnections
Data
EvaluationSaving and exportingUndo and redoLayout and alignmentThe flow storeGraph queriesClipboard and cloning
Interaction
Canvas and cameraSelectionKeyboardSelection toolbarContext menusDrawingDropping and pasting media
Styling
ThemingEntity stylingFonts and text renderingMinimap
API reference
KookieFlowThe ref instanceHooksUtilitiesTypes
StartEntitiesDataAPI
MarkdownOpenAIClaude

Graph queries

Ask what feeds an entity, what it feeds, what order things run in and whether the graph is valid, and work out rewiring edits such as inserting a node on an edge.

The graph engine answers questions about how a graph is wired: what feeds an entity, what it feeds, what order things run in, where the loops are, and which inputs are missing. It also works out the wiring for common edits, such as putting a new node in the middle of an edge.

You can ask these questions two ways. The live store has an action for each one, and it keeps its own index of the wires up to date. For a graph you hold yourself, build an index from its edges and call the same functions directly.

Ask the live graph

From a component rendered inside KookieFlow, get the store with useFlowStoreApi() and call the query actions on getState(). They take no index, because the store already has one. The flow store chapter covers reaching the store.

This button adds everything that feeds the selected nodes to the selection:

select-upstream-button.tsx
'use client';import { useFlowStoreApi } from '@kushagradhawan/kookie-flow';export function SelectUpstreamButton() {  const store = useFlowStoreApi();  const selectUpstream = () => {    const { selectedEntityIds, walkUpstream, selectEntities } = store.getState();    const ids = new Set(selectedEntityIds);    for (const id of selectedEntityIds) {      for (const upstream of walkUpstream(id)) ids.add(upstream);    }    selectEntities([...ids]);  };  return <button onClick={selectUpstream}>Select upstream</button>;}

Selection lives only in the store, so this is safe in a controlled graph. The store's answers use its own entities, which have their sockets filled in from entityTypes.

Store actionSame as
getIncomers(id), getOutgoers(id)getIncomers(index, id), getOutgoers(index, id)
getEntityEdges(id), getInputEdges(id), getOutputEdges(id)the functions of the same name
getEdgesBetween(a, b)getEdgesBetween(index, a, b)
walkUpstream(id), walkDownstream(id)the functions of the same name
getAnalysis()computeAnalysis(ids, index, muted), computed again only after topologyVersion changes
wouldCreateCycle(source, target)wouldCreateGraphCycle(index, source, target)
getAffectedEntities(changed)getAffectedEntities(changed, index, order, muted)
getConnectedComponents()getConnectedComponents(ids, index)
areConnected(a, b)areConnected(index, a, b)
getExecutionOrder(id)getExecutionOrder(index, id)
getReadyEntities(ids, completed)getReadyEntities(ids, index, completed)
validate(socketTypes?)validateGraph(entities, edges, index, socketTypes)
isGraphComplete()isGraphComplete(entities, index)
getCompatiblePorts(entityId, socketId, isInput, socketTypes, allowCycles?)getCompatiblePorts(entityId, socketId, isInput, entities, socketTypes, index, allowCycles)

Two functions have different names when you import them. The cycle check is wouldCreateGraphCycle, because the package's wouldCreateCycle is a different function: it checks whether putting an entity inside a frame would make a frame contain itself. The validator is validateGraph.

Build an index for a graph you hold

buildAdjacencyIndex(edges) reads your edges once and returns an AdjacencyIndex. Every pure query takes that index as its first argument. Build it again whenever your edges change.

upstream-count.tsx
'use client';import { useMemo } from 'react';import { buildAdjacencyIndex, getIncomers, walkUpstream, type Edge } from '@kushagradhawan/kookie-flow';export function UpstreamCount({ edges, entityId }: { edges: Edge[]; entityId: string }) {  const index = useMemo(() => buildAdjacencyIndex(edges), [edges]);  const direct = getIncomers(index, entityId).length;  const total = [...walkUpstream(index, entityId)].length;  return <span>{direct} direct inputs, {total} entities upstream</span>;}

The index is built from edges alone, so an entity with no edges isn't in it, and the lookups below return an empty array for that entity.

FieldTypeWhat it holds
outgoingMap<string, Map<string, Edge[]>>Edges leaving each entity, grouped by socket id.
incomingMap<string, Map<string, Edge[]>>Edges arriving at each entity, grouped by socket id.
byEntityMap<string, Edge[]>Every edge touching each entity. An edge from an entity to itself appears once.

An edge with no sourceSocket or targetSocket is grouped under '__default__'. The arrays belong to the index, and some queries return them directly, so copy one before you change it.

FunctionReturns
getIncomers(index, id)string[]: the ids of entities wired into id, each once.
getOutgoers(index, id)string[]: the ids of entities id is wired into, each once.
getEntityEdges(index, id)Edge[]: every edge touching id.
getInputEdges(index, id)Edge[]: the edges arriving at id.
getOutputEdges(index, id)Edge[]: the edges leaving id.
getEdgesBetween(index, a, b)Edge[]: the edges between a and b, in either direction.

Walk upstream and downstream

walkUpstream(index, id) yields every entity that feeds id, directly or through others. walkDownstream(index, id) yields every entity that id feeds. Both are generators.

  • The starting entity isn't included, and each entity is yielded once.
  • Loops are safe. A walk never visits an entity twice, so it ends.
  • The order isn't run order. The walk goes depth-first. Use getExecutionOrder() when order matters.
  • You can stop early. Break out of the loop and the walk goes no further.
const { walkUpstream, entityMap } = store.getState();let loader: string | undefined;for (const id of walkUpstream('render')) {  if (entityMap.get(id)?.type === 'loader') {    loader = id;    break;  }}

getAffectedEntities(changed, index, order?, muted?) takes one id or an array, and returns everything downstream of them. Pass a topological order, such as computeAnalysis(...).topologicalOrder, to get the result in run order. Without one, the result isn't sorted. Muted entities are included.

Find the order and the cycles

computeAnalysis(entityIds, index, mutedEntityIds?) sorts the graph in the order it can run. Pass every entity id, because an entity with no edges isn't in the index. It returns a CachedAnalysis without its topologyVersion. The store's getAnalysis() returns the full object, and computes it again only after the store's topologyVersion changes. The flow store chapter lists what changes it.

FieldTypeWhat it holds
topologicalOrderstring[] | nullEvery entity, each after everything that feeds it. null when the graph has a loop.
executionLevelsstring[][] | nullEntities grouped into steps. Everything in one step can run once the steps before it are done. Muted entities are left out. null when the graph has a loop.
hasCyclesbooleanWhether the graph has a loop.
cycleEntityIdsstring[]The entities on a loop, and the entities a loop feeds.
rootsstring[]Entities with nothing wired into them.
leavesstring[]Entities that feed nothing, not counting entities in cycleEntityIds.
topologyVersionnumberThe store's topologyVersion when the analysis was computed.

The rest of the functions answer narrower questions:

FunctionReturns
wouldCreateGraphCycle(index, source, target)boolean: whether a new edge from source to target would close a loop. true when they're the same entity. It walks only what's downstream of target and stops as soon as it reaches source, so you can call it while someone drags a connection.
getExecutionOrder(index, id)string[]: id and everything upstream of it, in run order. If that part of the graph has a loop, the same ids in no particular order.
getReadyEntities(ids, index, completed)string[]: the entities in ids that aren't in completed and whose inputs from ids are all in completed.
getConnectedComponents(ids, index)Map<string, string[]>: groups of entities joined by edges in either direction, keyed by one member of each group. An entity with no edges is a group of its own.
areConnected(index, a, b)boolean: whether any chain of edges joins a and b, in either direction. true when they're the same entity.

If you want the graph to run, you don't need to schedule it yourself with these. Evaluation runs entities in order and tracks their status.

Validate a graph

validateGraph(entities, edges, index, socketTypes?, isTypeCompatible?) returns a list of GraphValidationIssue. An empty list means it found nothing.

typeFieldsWhat it means
'cycle'entityIdsThe graph has a loop. entityIds holds the entities on it and the ones it feeds.
'unconnected-required'entityId, portId, portNameAn input has no edge and no defaultValue.
'type-mismatch'edgeId, sourceType, targetTypeAn edge joins two socket types that aren't compatible.

This run button checks the graph first. If it finds problems, it lists them instead of running:

run-button.tsx
'use client';import { useState } from 'react';import { useFlowStoreApi, type GraphValidationIssue, type SocketType } from '@kushagradhawan/kookie-flow';function describe(issue: GraphValidationIssue): string {  switch (issue.type) {    case 'cycle':      return `A loop runs through ${issue.entityIds.join(', ')}.`;    case 'unconnected-required':      return `${issue.entityId} needs something wired into ${issue.portName}.`;    case 'type-mismatch':      return `Edge ${issue.edgeId} connects ${issue.sourceType} to ${issue.targetType}.`;  }}export function RunButton({ socketTypes }: { socketTypes: Record<string, SocketType> }) {  const store = useFlowStoreApi();  const [problems, setProblems] = useState<string[]>([]);  const run = () => {    const issues = store.getState().validate(socketTypes);    setProblems(issues.map(describe));    if (issues.length === 0) void store.getState().evaluateAll();  };  return (    <div>      <button onClick={run}>Run</button>      {problems.map((problem) => <p key={problem}>{problem}</p>)}    </div>  );}

Keep these rules in mind when you read the results:

  • Only defaultValue makes an input optional. A value someone set with the input's widget doesn't count, so an input with a widget and no defaultValue is reported until it's wired.
  • Types are checked only if you ask. Pass socketTypes, the same object you give KookieFlow, or your own isTypeCompatible(sourceType, targetType). Without either, no 'type-mismatch' issue is reported.
  • With socketTypes, compatible means the two types are the same, either one is 'any', either type's compatibleWith is '*', or one type's compatibleWith lists the other. Your own isTypeCompatible replaces that rule.
  • The pure function sees only the sockets you wrote. If entityTypes fills in an entity's sockets, your own entities don't have them. Call the store's validate() instead.

isGraphComplete(entities, index) returns true when every input without a defaultValue has an edge. It's the 'unconnected-required' check as a single answer.

getCompatiblePorts(entityId, socketId, isInput, entities, socketTypes, index?, allowCycles?) lists the sockets on other entities that a socket could connect to, as Array<{ entityId, socketId, socketName, socketType }>. Pass isInput as true when the starting socket is an input, and you get outputs back. When you pass an index and allowCycles isn't true, sockets that would close a loop are left out. The store always has an index, so pass true there to keep them.

Rewire around an entity

computeInsertOnEdge() and computeBypass() work out an edit without making it. You apply the result to your own state, so a controlled graph keeps it and undo can take it back.

computeInsertOnEdge(edge, newEntity, sourceEntity, targetEntity) splits an edge from A to B into A to the new entity, and the new entity to B. It returns:

  • removeEdgeId: the id of the edge to remove.
  • newEdges: two edges. The first goes from A to the new entity's first input, and the second from the new entity's first output to B. Their ids are the old id with -a and -b added.
  • entityPosition: halfway between the positions of A and B. A missing entity counts as (0, 0).

This component puts a new Blur node in the middle of any edge you click:

insert-on-click.tsx
'use client';import {  KookieFlow,  computeInsertOnEdge,  useGraph,  type Edge,  type EdgeChange,  type Entity,} from '@kushagradhawan/kookie-flow';const blurNode = (): Entity => ({  id: crypto.randomUUID(),  type: 'default',  position: { x: 0, y: 0 },  data: { label: 'Blur' },  inputs: [{ id: 'in', name: 'In', type: 'image' }],  outputs: [{ id: 'out', name: 'Out', type: 'image' }],});export function InsertOnClick() {  const graph = useGraph({ history: true });  const insert = (edge: Edge) => {    const entity = blurNode();    const { removeEdgeId, newEdges, entityPosition } = computeInsertOnEdge(      edge,      entity,      graph.getEntity(edge.source),      graph.getEntity(edge.target)    );    graph.onEntitiesChange([{ type: 'add', entity: { ...entity, position: entityPosition } }]);    graph.onEdgesChange([      { type: 'remove', id: removeEdgeId },      ...newEdges.map((next): EdgeChange => ({ type: 'add', edge: next })),    ]);  };  return (    <div style={{ height: 480 }}>      <KookieFlow        entities={graph.entities}        edges={graph.edges}        onEntitiesChange={graph.onEntitiesChange}        onEdgesChange={graph.onEdgesChange}        onConnect={graph.onConnect}        onEdgeClick={insert}      />    </div>  );}

computeBypass(entityId, index) removes an entity and joins what fed it to what it fed. It returns removeEntityId, removeEdgeIds with every edge in and out of the entity, and newEdges. The first edge in is joined to the first edge out, the second to the second, and so on. An edge left without a partner is removed and not replaced. The new ids are bypass-<entityId>-0, bypass-<entityId>-1 and so on.

The store has actions that make these edits straight away: insertOnEdge(edgeId, newEntity) and bypassEntity(entityId). Like every store edit, they don't call onEntitiesChange or onEdgesChange, so in a controlled graph use the compute functions and report the changes yourself.

Mute an entity

muteEntity(id) and unmuteEntity(id) are store actions, and isMuted(id) tells you which state an entity is in. A muted entity isn't sent to onEvaluate. Instead, the value on its first input passes straight to its first output, its second input to its second output, and so on.

Muting lives only in the store. It isn't part of your entities, it isn't reported, toObject() doesn't save it, and nothing on the canvas shows it. If people need to see or save it, keep it in your entity data and call muteEntity() to match.

Collapse into a subgraph

computeCollapseToSubgraph(entityIds, frameId, entities, index, layout?) works out how to fold a set of entities into one frame. It returns the frame's bounds, the edges to remove, new edges that connect outside entities to the frame, and frameInputs and frameOutputs that record which original socket each frame port stands for. Pass the socket layout from useSocketLayout() as layout so entities without a height are measured as they're drawn.

computeExpandSubgraph(frameId, childEntities, internalEdges, entities, index, portMapping) is the reverse. It returns removeEntityId, removeEdgeIds, restoreEntities, restoreEdges and reconnectEdges. collapseToSubgraph() and expandSubgraph() on the ref make both edits and report them, and the Frames and groups chapter shows how to use them.

Getting started
  • Installation
  • Quick start
  • Controlled and uncontrolled state
Entities
  • The entity model
  • Sockets and widgets
  • Entity types
  • Frames and groups
  • Text and comments
  • Images, video and models
Edges and connections
  • Edges
  • Connections
Data
  • Evaluation
  • Saving and exporting
  • Undo and redo
  • Layout and alignment
  • The flow store
  • Graph queries
  • Clipboard and cloning
Interaction
  • Canvas and camera
  • Selection
  • Keyboard
  • Selection toolbar
  • Context menus
  • Drawing
  • Dropping and pasting media
Styling
  • Theming
  • Entity styling
  • Fonts and text rendering
  • Minimap
API reference
  • KookieFlow
  • The ref instance
  • Hooks
  • Utilities
  • Types
Reference
  • Live demo
  • llms.txt
  • GitHub
MIT licensed. © 2026 Kushagra Dhawan.
On this page
  • Ask the live graph
  • Build an index for a graph you hold
  • Walk upstream and downstream
  • Find the order and the cycles
  • Validate a graph
  • Rewire around an entity
  • Mute an entity
  • Collapse into a subgraph