@sinomoe/workflow-kit
v0.1.1
Published
A production-oriented React workflow editor kit with typed nodes, validation, history, and host-owned UI.
Downloads
35
Maintainers
Readme
Workflow Kit
A React workflow composition kit built on React Flow 12.
Workflow Kit provides the editor runtime, graph model, node contracts, validation helpers, history, persistence hooks, and React Flow integration needed to build a host-owned workflow builder. Product teams keep control of chrome, node UI, copy, publishing flows, and business validation.
Status
This package is in early extraction. The public API is usable for local experiments, but the project is still hardening toward a stable open-source release.
Install
npm install @sinomoe/workflow-kit react react-dom @xyflow/react zustandImport the library styles once and render the composer inside a sized container:
import '@sinomoe/workflow-kit/style.css'
export function Page() {
return (
<div style={{ height: '100vh' }}>
<Builder />
</div>
)
}Quick Start
import {
Composer,
defineNode,
type Graph,
} from '@sinomoe/workflow-kit'
import '@sinomoe/workflow-kit/style.css'
const startNode = defineNode({
type: 'start',
defaultData: { triggerMode: 'chat' },
createTitle: () => 'Start',
getPorts: () => ({
outputs: [{ id: 'out', label: 'Start' }],
}),
})
const answerNode = defineNode({
type: 'answer',
defaultData: { text: '' },
createTitle: count => `Answer ${count}`,
getPorts: () => ({
inputs: [{ id: 'in', label: 'Input' }],
}),
renderInspector: ({ node, readOnly, actions }) => (
<textarea
value={String(node.data.text ?? '')}
disabled={readOnly}
onChange={event => actions.updateNodeData({ text: event.target.value })}
/>
),
})
const initialGraph: Graph = {
nodes: [],
edges: [],
}
export function Builder() {
return (
<Composer
registry={[startNode, answerNode]}
defaultValue={initialGraph}
onChange={(graph, meta) => {
console.log(graph)
console.log(meta.action)
}}
autoSave={{ delayMs: 750 }}
onSaveDraft={async (graph) => {
await saveDraft(graph)
}}
onPublish={async (graph) => {
await publishWorkflow(graph)
}}
/>
)
}This is enough for an uncontrolled editor. For a controlled editor, pass
value and update it in onChange(graph, meta).
Draft saving is host-owned. onSaveDraft defines the save operation, while
autoSave opts the composer into calling it after committed graph changes. If
autoSave is omitted, use custom chrome to call
context.persistence.saveDraft() or run your own policy from
onChange(graph, meta).
Documentation
- Docs Index: map of the integration and design docs.
- Architecture: public/internal boundaries and runtime ownership.
- API Stability: stable, experimental, and internal-intent contracts.
- Composer API Review: composer render props and chrome contexts.
- Node API Review: typed node definitions and node actions.
- Mutation Contract:
onChangemetadata, history, undo/redo, and transient updates.
Repository Layout
src/: reusable library source.docs/: architecture notes and public API boundary decisions.examples/workflow-lab/: standalone Vite app with demo chrome, custom nodes, inspector UI, note editing, and theme switching.
The library must not import from examples/. The example may depend on the
library and any host-side dependencies it needs. The example is not the minimal
API shape; it is a fuller product-style reference.
Boundary Principles
- Core owns graph data, editor interaction state, React Flow integration, history, persistence hooks, validation plumbing, and extension contracts.
- Host code owns chrome, palette UI, inspector UI, node card presentation, localized copy, publish UX, node-specific forms, icons, and business validation.
- Core exposes graph and edge-level query helpers. It does not infer variable scope, execution reachability, or upstream/downstream semantics.
- History entries store stable action codes, not display labels. Host chrome maps those codes to localized text.
- React Flow is an intentional dependency. The kit is not trying to be a portable graph engine.
- React Flow remains an internal renderer boundary. Public graph contracts use
Workflow Kit types such as
PositionandViewportinstead of exposing React Flow node, edge, viewport, or instance types.
Core Concepts
Workflow Kit keeps the persisted workflow model small and host-owned:
type Graph = {
nodes: GraphNode[]
edges: GraphEdge[]
viewport?: Viewport
}GraphNode.data is the only node-specific payload. The kit never interprets
that object beyond passing it back to your node definition and render callbacks.
This keeps serialization, persistence, validation, and migrations under the
host application's control.
NodeDefinition describes how a node type behaves inside the editor:
typeis the stable node kind stored on each graph node.defaultDataseeds newly created nodes.createTitleandcreateDescriptionprovide defaults for new nodes.getPortsreturns connectable input and output handles.getLayoutSizehelps auto layout and fit-to-view use realistic node sizes.renderCanvasNoderenders fully custom canvas node content.renderInspectorcan render node-specific settings inside the inspector.execution: 'none'marks visual/helper nodes that should be ignored by execution-structure validation.
renderCanvasNode is the foundation for canvas customization. It remains a
fully custom render path: the kit supplies handles and interaction context, and
your renderer owns the node content. When you want the kit's visual shell
without adopting any built-in copy or layout, wrap your content in NodeShell:
import { NodeShell, defineNode } from '@sinomoe/workflow-kit'
const textNode = defineNode<{ prompt: string }>({
type: 'text',
defaultData: { prompt: '' },
renderCanvasNode: context => (
<NodeShell context={context}>
<div>{context.node.data.prompt}</div>
</NodeShell>
),
})NodeShell only renders a styled container and state classes for selected,
bundled, dragging, and read-only states. It does not render titles,
descriptions, buttons, placeholders, or other product copy. For custom port
placement, use NodeHandle from the kit instead of importing React Flow
components directly.
Use defineNode<TData>() to keep each node definition's data type connected
to its callbacks, renderers, and node actions:
const textNode = defineNode<{ prompt: string }>({
type: 'text',
defaultData: { prompt: '' },
createTitle: count => `Text ${count}`,
getPorts: () => ({
inputs: [{ id: 'in', label: 'Input' }],
outputs: [{ id: 'out', label: 'Output' }],
}),
renderCanvasNode: ({ node }) => (
<span>{node.data.prompt || 'Empty prompt'}</span>
),
renderInspector: ({ node, actions, readOnly }) => (
<textarea
value={node.data.prompt}
disabled={readOnly}
onChange={event => actions.updateNodeData({ prompt: event.target.value })}
/>
),
})See Node API Review for the current stability notes on
NodeDefinition, CanvasNodeRenderContext, NodeRenderContext, and node
actions.
Host Integration
Composer owns canvas interaction, selection, connections, history, viewport,
keyboard shortcuts, and React Flow integration. Host code provides product UI
through render props:
renderChrome(context)renders top bars, bottom controls, palettes, validation panels, and publish controls. Use actions fromChromeContextinstead of reaching into the canvas implementation.renderInspectorPanel(context)renders the inspector shell. The providedInspectorContextincludes the selected node, definition, validation issues, panel width, and title/description update actions.InspectorShellis an optional styled container for custom inspector panels. It applies panel sizing, floating-layer behavior, theme tokens, and basic form-control inheritance without rendering any labels or product copy.renderEdgeAction(context)renders optional controls on selected edges, such as an insert-node button.renderDefaultNodeContent(context)renders the default card body for node definitions that do not providerenderCanvasNode, while still using the kit's default card and handles.
For app-level validation, pass validationIssues and optionally
validateConnection. validateGraphStructure covers structural checks such as
duplicate IDs, unknown node types, dangling edges, invalid ports, and
disconnected execution nodes. Domain rules, permissions, runtime variables, and
publish blockers should stay in the host application.
Public API
Consumers should import from the package root:
import {
Composer,
defineNode,
validateGraphStructure,
type Graph,
} from '@sinomoe/workflow-kit'The intended public surface includes:
Composer- composer contracts such as
ComposerProps,ChromeContext,InspectorContext,PaletteState, andHistoryActionMap - graph contracts such as
Graph,GraphNode,GraphEdge,GraphChangeMeta,NodeDefinition,AnyNodeDefinition, andPort - helpers such as
defineNode,createRegistry,createGraphQuery,getNodePorts,pruneNodePorts,createNode,updateNodeMeta,applyAutoLayout, andvalidateGraphStructure
See Mutation Contract for graph-change history and
onChange metadata semantics.
Files below components/, hooks/, react-flow/, utils/, and most of
composer/ are implementation details unless re-exported from src/index.ts.
Composer Options
Composer supports both uncontrolled and controlled graph state through
defaultValue, value, and onChange. onChange receives metadata for every
committed composer change:
<Composer
registry={registry}
value={graph}
onChange={(nextGraph, meta) => {
setGraph(nextGraph)
console.log(meta.action, meta.source)
}}
/>Panel width persistence is enabled by default with the key
workflow-kit-panel-width. For apps that render multiple composers, pass a
stable panelStorageKey per instance, or set it to false to disable local
storage persistence:
<Composer
registry={registry}
defaultValue={graph}
panelStorageKey="acme-workflow-builder-panel-width"
defaultPanelWidth={620}
minPanelWidth={560}
/>Keyboard shortcuts are enabled by default and listen at document scope. In pages
with multiple workflow editors, use keyboardScope="composer" or disable the
built-in shortcuts and provide your own chrome actions:
<Composer
registry={registry}
defaultValue={graph}
keyboardScope="composer"
/>Example App
npm install
npm run devThe dev script starts examples/workflow-lab.
Development
npm install
npm run typecheck
npm run lint
npm test
npm run test:smoke
npm run build
npm run check:api
npm run check:consumer
npm run build:exampleUse npm run pack:check before publishing to verify the npm package contents.
It builds the library, checks the public API boundary, verifies a real tarball
consumer project, and prints the exact npm package file list.
License
MIT
