@awesome-flow/core
v0.15.0
Published
The core flow package. Provides building blocks for a flow-based canvas.
Maintainers
Readme
Awesome Flow
Visual interface components
Assembled editor and commands
FlowCanvas from @awesome-flow/core/canvas includes the context, renderer, Add node palette,
fit/zoom controls and on-demand Arrange action. It needs a sized parent and the application's
MantineProvider; it needs no portal targets or DnD provider.
import { FlowCanvas } from '@awesome-flow/core/canvas';
<div style={{ height: 600 }}>
<FlowCanvas
nodeTypes={nodeTypes}
nodeTemplates={nodeTemplates}
flowStorage={flowStorage}
defaultPipelineExecution={settings.pipelineExecution}
hybrid={{ overviewZoom: settings.skeletonZoom }}
templates={{ presentation: 'responsive', extraTemplates }}
controls={{ autoLayout: { rankdir: 'LR' }, fit: true, zoom: true }}
layoutEngine={layoutEngine}
/>
</div>Templates and controls are enabled by default on the assembled component. templates={false}
hides Add node; controls={false} disables the entire built-in toolbar and its palette.
Low-level FlowCanvasRender / FlowCanvasRenderIntersections accept the same options, opt-in.
Their existing showControls delegates to the shared controls. Supply one toolbar per editor.
The palette follows the editor container width: a 320 px overlay on desktop, a bottom drawer
below 640 px. presentation: 'panel' | 'drawer' overrides that choice. It searches registered,
extra and saved templates; optional renderPreview(template) supplies presentation-only content.
Button insertion works on touch and keyboard, with no preview node/handle/runtime registration.
The desktop panel stays open for repeated additions; the drawer closes after adding.
Opening/searching/closing is local UI state and does not modify the document.
Existing custom shells can compose FlowTemplatePalette and FlowCanvasControls under
FlowCanvasContext, or call useFlowCommands() (exported from canvas and hooks):
insertTemplate(template, { position?, insetRight?, reveal?, generation? })returns the new node, or undefined when vetoed/read-only/stale. Without an explicit graph position, it uses the current unobscured viewport center, dimensions, origin and snap grid, with bounded offsets for repeated adds. New nodes are root-level and selected; overview insertion reveals detail, subject to maxZoom.insetRightis an overlay width in screen pixels.generationguards delayed UI actions.fitGraph(options?)fits canonical graph bounds, including unmounted hybrid nodes.autoLayout({ options?, fitView?, fitViewOptions?, signal? })returns Promise: true after applying and optional fit, false for cancellation/read-only/empty graph. Failures reject and publish{ layout: 'error', error }throughuseFlowCommandState(). Concurrent requests share one operation.cancelLayout()aborts the operation. Unmount, document replacement and intervening durable edits cancel it too. Late geometry measurements coalesce into a fresh worker attempt. Selection is preserved.
FlowCanvasContext and FlowCanvas accept beforeInsert(template) (false vetoes) and readOnly
to disable mutating commands. The assembled component also disables RF dragging/connections/deletion.
Hosts still control editing inside custom node inputs. A palette's onInsert overrides insertion;
returning false keeps a drawer open. FlowTemplatesList.onInsert now governs its single Add button.
Legacy FlowTemplatesList / FlowCanvasDropWrapper retain HTML5 DnD and still require FlowDndProvider.
Layout workers
The default layout engine is synchronous Dagre. For large graphs, supply the worker adapter below. This preference is independent of pipeline execution. A Promise around main-thread Dagre does not offload its calculation.
// layout.worker.ts — an entry owned and bundled by the consuming application
import { installFlowLayoutWorker } from '@awesome-flow/core/layout-engine';
installFlowLayoutWorker(self);// Keep the engine stable outside render.
import { createWorkerLayoutEngine } from '@awesome-flow/core/layout-engine';
export const layoutEngine = createWorkerLayoutEngine(
() => new Worker(new URL('./layout.worker.ts', import.meta.url), { type: 'module' })
);Alternatively supply a FlowLayoutEngine for another layout service/algorithm. Its input contains
only topology and geometry, including canonical parents and group state, never application data or
runtime computed values. Results contain positions and group dimensions; current data/selection are
merged on the main thread and committed once. Worker requests terminate on completion, error or abort.
Graph preparation, result application and fit still happen on the main thread.
Noise, Planet Noise and Group Test use this worker adapter and the assembled editor. ECS uses the
same native controls/palette, accepts layoutEngine, and no longer requires #portal-navbar.
Canvas rendering
FlowCanvasRender, FlowCanvasRenderIntersections and FlowEcsCanvas use hybrid rendering by default.
The complete graph stays in FlowStore and GraphRuntime. React Flow handles the camera and gestures;
visible/active nodes get rich DOM controls, while Canvas placeholders cover overview zoom and progressive
mounting. Hidden and unmounted nodes continue to execute their pipelines.
flashNodeOnLogicChanges animates runtime version changes observed after the node view mounts.
Returning from a skeleton does not replay earlier updates; existing error messages remain visible.
Versions also cover runtime cleanup/invalidation, so the flash is a state-update indicator, not an execution counter.
Set hybrid={false} to use full React Flow rendering, or pass hybrid={{ catalog, fallbackSize, farOverview, overviewZoom }}
to customize the hybrid policy. An explicit visibilityWindow={{ ... }} chooses that alternative unless
hybrid options are supplied. Host flow.nodes/flow.edges overrides, global nonzero nodeOrigin and global
nodeExtent retain the ordinary renderer. Hybrid defaults to minZoom: 0.0001; flow.minZoom overrides it.
overviewZoom is the zoom ratio below which passive nodes become skeletons (default 0.6, or 60%).
Rich controls return above overviewZoom * 1.2 to prevent flicker; 0.3 therefore switches below 30%
and restores detail above 36%. Values must be greater than zero and at most one. Lowering it also delays
far-mode eviction when necessary. Selected/focused controls remain available. Updating the threshold
does not recreate the graph or its spatial indexes.
HybridOptionsProvider from @awesome-flow/core/canvas supplies defaults to descendant production
canvases: <HybridOptionsProvider value={{ overviewZoom: 0.3 }}>...</HybridOptionsProvider>.
An explicit canvas option overrides its provider default. Keep catalogs/options stable between renders.
showMinimap uses a cached overview of the complete canonical graph in hybrid mode. Fit controls,
deletion and group drop targets also use canonical state. React Flow's getNodes()/getEdges() return
only the presentation subset: use FlowStore for document queries, layout, serialization and programmatic
edits. Unmeasured nodes use temporary dimension estimates (320 × 160 by default); optional template
silhouettes improve placeholder fidelity. Placeholders do not display live computed text.
Per-instance measurements, explicit dimensions and numeric/pixel CSS dimensions take precedence over
template estimates. Geometry-only proxies preserve edge endpoints when rich controls unmount.
Auto-layout uses those canonical dimension estimates for nodes that have not mounted, so it can lay out
and fit the complete graph without waiting for DOM measurements. Estimates are not written to measured;
rerunning layout after nodes have been measured uses their actual dimensions.
Edge labels
FlowEdgeLabel owns the React Flow label portal, edge-relative positioning, pointer-event guards,
and the canvas Z layer. Consumers provide only the label content:
import { FlowEdgeLabel } from '@awesome-flow/core/canvas';
<FlowEdgeLabel labelX={labelX} labelY={labelY}>
<StatusBadge>Ready</StatusBadge>
</FlowEdgeLabel>;Labels are non-interactive by default. Set interactive when the child contains controls; the
component then enables pointer events and applies React Flow's nodrag and nopan guards. The
label layer remains above edge paths and below nodes.
Per-flow execution and rendering settings
Hosts can supply their own settings directly; no Sandbox settings or browser storage is required:
<FlowCanvasContext
nodeTemplates={nodeTemplates}
defaultPipelineExecution={settings.pipelineExecution} // 'worker' (default) or 'main'
hybrid={{ overviewZoom: settings.skeletonZoomPercent / 100 }}
>
<FlowCanvasRender nodeTypes={nodeTypes} />
</FlowCanvasContext>overviewZoom defaults to 0.6 and accepts 0 < zoom <= 1. Rich nodes return above
1.2 × overviewZoom to avoid flicker near the threshold. Selected/active controls can remain rich.
Settings update an existing flow without replacing its document or runtime. Provider defaults
(PipelineExecutionProvider and HybridOptionsProvider) still apply when a prop is omitted;
FlowCanvasRender hybrid={{ overviewZoom: ... }} overrides the flow's presentation defaults.
Execution preference applies to createNodePipeline pipelines that supply both a main kernel and a
worker factory. Explicit pipeline execution choices remain authoritative, ordinary pipelines run
on the main thread, and a supplied runtime retains its host-owned execution policy. The library
does not automatically move arbitrary JavaScript closures into workers.
FlowEcsCanvas also accepts defaultPipelineExecution and hybrid={{ overviewZoom }}.
Auto layout
<FlowAutoLayout refreshLayout={requested} /> lays out the full logical graph, including nodes
represented only by skeletons. Set requested back to false before requesting another layout.
An initial true requests layout too. Optional options are Dagre graph options (default
{ rankdir: 'LR' }); fitView={false} suppresses the subsequent fit, and fitViewOptions
configures the fit animation (default duration 500 ms).
Options are sampled when a request starts; recreating an options object during a host rerender
does not trigger another layout.
Nested groups are sized from the inside out. Collapsed groups keep their compact dimensions and receive updated expanded bounds for a later expansion. Unmounted node estimates stay out of durable node measurements. Waiting layout/fit requests are cancelled on unmount or document replacement. Dagre calculation is currently synchronous on the main thread; pipeline worker preferences do not move auto layout into a worker.
Persisting multiple flow documents
@awesome-flow/core/documents defines a persistence-neutral document boundary for applications that manage more than one graph:
import {
createFlowSnapshot,
parseFlowSnapshot,
type FlowDocument,
type FlowDocumentRepository
} from '@awesome-flow/core/documents';createFlowSnapshot clones nodes, edges, and groups into a versioned JSON-safe graph while removing transient canvas fields. parseFlowSnapshot validates persisted input before it is installed. remapFlowSnapshotIds creates collision-safe document duplicates while preserving group membership and edge topology. Applications own the concrete repository, metadata, routing, and authorization; Awesome Flow does not depend on a database.
Canvas consumers can capture or install the current graph through getFlowSnapshot() and replaceFlowSnapshot() on the flow store. Replacement is one logical action: transient interaction state is cleared and GraphRuntime rebuilds all pipeline contexts, including when two documents reuse node IDs.
The snapshot intentionally excludes viewport, selection, measurements, computed outputs, and execution history. Store viewport as optional editor metadata on FlowDocument; store immutable versions and run records separately when the host application supports activation and execution.
Reusing templates and host-authored graphs
NodeFactory accepts an optional second argument (kind: 'node' | 'handle' | 'template') => string.
Hosts can inject stable identities for deterministic recipe generation and tests. The default generator
is unchanged; callers injecting identities must keep them unique. Both paths use the existing template
and handle-delta machinery, including added, removed and configured handles.
FlowTemplatesList accepts an optional onInsert(template) callback. When present, each draggable
preview also has a labelled Insert button for keyboard/click authoring. The host owns placement and
snapshot insertion. Omitting the callback keeps the original draggable-only palette. Existing toolbar
actions have accessible names; this does not replace Save as template with a second workflow.
Persist custom NodeTemplate[] through the existing store templates / setTemplates boundary if
the application needs reuse after reload. Applications own storage, validation, migration, save status
and conflict policy. Built-in factory defaults should not be copied into that custom catalog.
Connection contexts
Handle dataType values remain simple string or numeric compatibility keys. A target handle can use dataId with groupBy: 'dataId' when several same-type connections have distinct roles. Pipelines receive complete upstream NodeContext references in context.incoming; the runtime does not extract or wrap a separate port value.
Set optional: true on an override or auxiliary handle to de-emphasize it while unused. The handle remains mounted and connectable, returning to full emphasis when its node is selected or hovered, or when it has a connection. This is presentation metadata only and does not alter compatibility or pipeline grouping.
The selected-node toolbar can duplicate any non-group node and save any configured node as a reusable template. FlowCanvasRenderIntersections composes consumer-provided drag callbacks with its group-intersection callbacks. FlowCanvasRender owns deletion and composes the change/connect/deletion callbacks, so higher-level editors can add interaction such as connection-driven quick insertion without disabling grouping behavior.
React Flow's ancestor-aware stacking keeps children above selected groups. Drag/select groups through
their headers and resize through their resize handles. Empty group interiors allow edge labels and pane
gestures through; consumers do not need custom zIndex overrides.
Groups with maxConnections: 1 resolve to one context, while multi-connection groups resolve to an array. Use getConnectionContexts(context.incoming?.[key]) to normalize either representation to a list while retaining access to the upstream node ID, persisted data, computed output, and its own connection contexts.
Application-owned evaluation
GraphRuntime in @awesome-flow/core/runtime remains usable without a canvas. For persisted graphs,
call validateGraph(entries, edges, getFactory) first. It checks IDs, factories, persisted handles
(including custom-template deltas), directions, data types, cardinality and cycles. The validator
is explicit so existing logical graphs without port declarations remain supported. Runtime traversal
also diagnoses cycles, and getNodeStatus(id) distinguishes ready, running, failed, blocked and invalid.
Minimum-count diagnostics have severity: 'incomplete'; a preview editor can continue evaluating
other branches while showing the missing inputs. Maximum-count/topology defects remain errors.
Missing domain inputs are still owned by the consuming pipeline/application, not inferred from a null output.
Removing nodes via syncNodeList also detaches their incident edges, cleans removed pipelines, and
refreshes affected surviving contexts. Callers do not need a separate edge-removal event to clear
dependent results. Existing contexts retain their identity. A node-list sync with an unchanged ID
set does not redispatch.
Connection arrays follow the declared edge-list order, including after syncEdges inserts, reorders
or rewires an edge. Preserve that order in host semantic cache keys: ordered operations such as
Divide/Power may change even when edge IDs and endpoints are otherwise unchanged. Context objects
and dataId/groupBy lookup contracts remain intact.
NodeEntry.input and runtime.updateInputs(new Map([[nodeId, snapshot]])) supply transient external
inputs as context.input; they do not modify context.data or persisted snapshots. Application nodes
validate their own input shape. Connection contexts, dataId, and groupBy are unchanged.
runtime.subscribe(ids => ...) observes propagation without replacing the legacy version-bump callback.
Wait for runtime.idle and inspect statuses before publishing a generation. dispose() suppresses
further publication/queued dispatch and cleans in-flight work after it settles; it does not abort
arbitrary external promises or undo side effects. For superseding asynchronous previews, use isolated
generations and discard the abandoned session in the host.
Pass runtime={applicationRuntime} to FlowCanvasContext to observe that session. In this mode the
provider does not install a graph or dispatch edits independently. useUpdateNodeData still updates
editor configuration; the host must observe the editor snapshot, resolve inputs, and publish its next
runtime. The default canvas-owned mode consumes canonical store commits and batches adjacent runtime changes in a microtask. Do not let both host and editor own evaluation.
getConnectionTargets(template, dataType) in @awesome-flow/core/utils returns every compatible input
role with its template index and display label. Use that index on the instantiated node's handles to
connect the user's chosen role. getHandleLabel honors explicit labels before generating a fallback.
Unified store and persistence
The canvas owns one FlowStore. Nodes, edges, groups and templates remain in that store, together with transient interaction state and indexed connection queries. Computed pipeline outputs and node-version subscriptions belong to GraphRuntime, outside Zustand.
Use useFlowStore(selector) for editor state and useNodeContext() / useNodeVersion() for runtime output. Prefer narrow selectors. selectNodeConnections and selectHandleConnections return stable edge-ID arrays; handle identity includes node ID, direction and handle ID.
StoreContext exposes flowStore and flowPersistence. flowStore.subscribeCommits(listener) reports applied changes; subscribeDocument(listener) reports durable revisions and document loads. Both return an unsubscribe function. Position changes are durable; selection, dragging, measured dimensions and resizing flags are transient. Explicit width/height changes remain durable.
Enable built-in persistence with flowStorage={{ persist: true, storeName: documentId, persistence: { mode: 'debounce', delay: 450 } }}. Supply persistStorage for a custom Zustand PersistStorage adapter. The controller projects and validates only when writing; its allowlist is nodes, edges, groups, templates, plus registered durableKeys from extension slices. Computed outputs, selection, handles and indexes are never persisted.
useFlowPersistence() exposes reactive phase, dirty, failure, and awaitable ready, flush(), retry(), revalidate(). Projection failures (including non-finite numbers) latch with an actionable data path; correct the document and explicitly call revalidate(). Storage failures can be retried. Neither failure removes the last good stored document. The context owns activation and teardown; headless users own FlowPersistenceController.activate() and await dispose().
See the migration guide for removed APIs, extension registration, runtime ordering and application-owned saves.
Undo, redo, and selection actions
Every FlowStore has bounded document history (100 steps by default). Canvas controls show Undo/Redo;
Ctrl/⌘ Z, Ctrl/⌘ Shift Z, and Ctrl Y operate on the focused editor. Text inputs retain native text undo.
Use one stable, unique storage key per document to restore the graph and both history stacks from IndexedDB:
<FlowCanvas
key={documentId}
nodeTemplates={nodeTemplates}
nodeTypes={nodeTypes}
flowStorage={{
persist: true,
storeName: `flow:${documentId}`,
history: { limit: 100, mergeDelay: 500 },
persistence: { mode: 'debounce', delay: 300 }
}}
/>Without persistence, history lasts for the store's lifetime. flowStorage.history: false disables recording;
controls.history: false hides only the buttons. useFlowHistory() exposes undo(), redo(), clear(),
stopMerging(), batch(fn), and paired begin()/end() for application gestures. Headless callers use
flowStore.history. useFlowHistoryState() exposes canUndo, canRedo, undoCount, and redoCount
through a separate subscription, without extra canvas-store publications.
History records durable node/edge fields, groups, templates and registered durable extension fields.
Selection, camera state, measurements, dragging/resizing flags and runtime outputs are excluded.
Undo preserves current transient state on surviving elements; restored elements are initially unselected.
Pointer gestures and synchronous setter sequences form one step; repeated data edits to the same nodes
merge within mergeDelay. Use batch(fn) for explicit multi-setter commands and stopMerging() to
separate unrelated synchronous commands. Layout is one step. Snapshot/host loads clear history;
hydration restores only the history belonging to the installed checkpoint. Commands must run outside
store observers (schedule a microtask if needed).
The persistence envelope adds history beside state and version; custom storage adapters must retain
that envelope. Graph snapshots themselves remain unchanged. History with custom partialize/merge
projections or a migrated storage version is not restored because it may refer to different graph data.
Projection failures keep the last good checkpoint and use the existing persistence failure channel.
If invalid values remain only in old undo entries, call history.clear() before revalidate().
Call await persistence.flush() before navigation when a completed write is required; browser termination
cannot guarantee completion of asynchronous IndexedDB work. Context teardown owns timer/listener disposal.
The canvas renders one Selection actions toolbar, with a bounding outline for multiple nodes. It uses
canonical geometry, including selected nodes outside the hybrid render window. Duplicate copies selected
groups with their descendants and internal connections; Edit components applies to all selected nodes;
Delete honors onBeforeDelete and deletion callbacks. Single selection also offers the template action.
The controls keep touch-sized targets and stay within the canvas. selectionToolbar={false} disables
the shared toolbar for hosts providing their own selection UI; readOnly hides mutating selection actions.
