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

Saving and exporting

Save the graph as plain data you can store and load again, and export what is on screen as an image.

toObject() on the ref returns the graph as plain data: its entities, its edges and the viewport it's seen from. Store that object wherever you keep documents, and pass it back to KookieFlow to open the board again. toImage() returns a picture of what's on screen.

Save the graph as data

toObject() returns a FlowObject, which holds three things.

FieldTypeMeaning
entitiesEntity[]Every entity in the graph.
edgesEdge[]Every edge in the graph.
viewportViewportWhere the board was scrolled to and how far it was zoomed.

JSON.stringify writes the whole object, as long as everything your app put on its entities can be written. A socket whose widget is { component } holds a React component, which JSON can't write, so give that socket its component again when you load the save.

save-button.tsx
'use client';import type { RefObject } from 'react';import type { KookieFlowInstance } from '@kushagradhawan/kookie-flow';export function SaveButton({ flowRef, boardId }: { flowRef: RefObject<KookieFlowInstance | null>; boardId: string }) {  const save = async () => {    const flow = flowRef.current;    if (!flow) return;    await fetch(`/api/boards/${boardId}`, {      method: 'PUT',      headers: { 'Content-Type': 'application/json' },      body: JSON.stringify(flow.toObject()),    });  };  return <button type="button" onClick={save}>Save</button>;}

Load a saved graph

Give the saved entities and edges to useGraph as its starting graph, and the saved viewport to defaultViewport.

board.tsx
'use client';import { KookieFlow, useGraph, type FlowObject } from '@kushagradhawan/kookie-flow';export function Board({ saved }: { saved: FlowObject }) {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({    initialEntities: saved.entities,    initialEdges: saved.edges,  });  return (    <KookieFlow      entities={entities}      edges={edges}      defaultViewport={saved.viewport}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={onConnect}    />  );}

useGraph reads its starting graph, and KookieFlow reads defaultViewport, only when they first mount. To open a different save in the same place, give the board a key that changes with the save, such as <Board key={boardId} saved={saved} />. React then mounts a new board, so the selection and any computed values start fresh as well.

To swap the graph without remounting, pass the save to setEntities and setEdges from useGraph, and call setViewport(saved.viewport) on the ref. If useGraph keeps undo history, use a key instead: the history isn't cleared, so an undo would bring back a version of the graph you replaced.

A save holds no computed values. If your graph evaluates, load it with a key, and call evaluateAll() in an effect when the board mounts, or put stored results back with setSocketValue. If some types are manual because they cost money, call restoreAll() instead, so that opening the board fetches their results rather than running them. The Evaluation chapter covers all three.

What a save leaves out

A save keeps what describes the graph and drops what describes the moment. These aren't in it:

  • Selection and drags. selected is removed from entities and edges, and dragging from entities.
  • Computed values and evaluation status. Output values and the status the canvas tracks aren't saved. A data.status your app set is part of data, so it is saved.
  • Muted entities. Muting is kept by the store, not on the entity.
  • Stacking. Which node was brought in front of another by pressing it isn't saved.

What a save adds

A save can also hold things your app never wrote into its own entities:

  • Widget edits in flight. A widget value a person set that your onWidgetChange hasn't written back yet is added to that entity's data.values, so the save matches what's on screen.
  • Values filled in from entityTypes. An entity's inputs, outputs, width, height, preview and data.label are saved when its type supplied them. A reloaded entity then states those values itself, so a later change to that type's definition no longer reaches it. If the type table should stay in charge of a type's sockets, delete inputs and outputs from those entities before you store the save.
  • Edge validity. An edge that names both of its sockets carries invalid, which records whether the two socket types are compatible.

Read the current graph

getEntities() and getEdges() return the arrays the canvas is drawing right now. Use them in code that holds the ref but not your graph state.

MethodReturnsMeaning
getEntities()Entity[]Every entity, as the canvas holds it.
getEdges()Edge[]Every edge, as the canvas holds it.

These are the canvas's own arrays, not copies, so don't change them. They differ from what toObject() returns in two ways: selected and dragging aren't removed, and widget edits in flight aren't added. To save the graph, call toObject().

Export an image

toImage(options) returns a data URL of what's on screen, or null when the canvas isn't mounted. Its options have the type CaptureOptions.

OptionTypeDefaultMeaning
pixelRationumber2Image pixels for each pixel of the canvas on screen. Kept between 0.1 and 8.
backgroundstringtransparentA CSS colour to fill behind the graph.
typestring'image/png'The image format, as a MIME type.

The image covers the current view at the canvas's size multiplied by pixelRatio. A browser can refuse a very large drawing buffer, and then the image comes out smaller than you asked for. JPEG has no transparency, so pass a background with type: 'image/jpeg', or transparent areas turn black.

To capture the whole graph, fit the view first. The camera moves to the new view on the next frame, so capture inside requestAnimationFrame.

export-png.ts
import type { KookieFlowInstance } from '@kushagradhawan/kookie-flow';export function exportPng(flow: KookieFlowInstance) {  const view = { ...flow.getViewport() };  flow.fitView({ padding: 40 });  requestAnimationFrame(() => {    const url = flow.toImage({ pixelRatio: 2, background: 'white' });    flow.setViewport(view); // put the person's view back    if (!url) return;    const link = document.createElement('a');    link.href = url;    link.download = 'graph.png';    link.click();  });}

The image includes WebGL nodes, notes, text, widgets and media. The minimap, toolbars, native edit fields and anything you pass as children live outside the canvas, so they're left out.

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
  • Save the graph as data
  • Load a saved graph
  • What a save leaves out
  • What a save adds
  • Read the current graph
  • Export an image