React SDK
Render the Linkage workflow editor (React Flow) in your app
The Linkage React SDK (@linkage-open/lib/react) lets you ship a collaborative workflow editor with a single component. Linkage handles the canvas UI, presence/cursors, node chrome, and opinionated editor behavior so you can focus on your workflow product and backend.
Installation
Install the package in your application:
npm install @linkage-open/libPeer dependencies
The SDK expects several peer dependencies to already exist in your project. Install any that you do not already use:
npm info @linkage-open/lib peerDependencies --jsonNote: Install any packages returned by this command to keep your project aligned with the SDK requirements.
Styles
Import Linkage styles once in your app (for example in your root layout). This includes the React Flow base styles:
import "@linkage-open/lib/styles.css";Quick start
Configure the provider and editor
Wrap your workflow UI with ClientProvider and render the StandaloneEditor. It fetches node types from your uploaded registry and then renders the editor.
import { ClientProvider, StandaloneEditor } from "@linkage-open/lib/react";
export function WorkflowEditor() {
return (
<ClientProvider
config={{
clientAppId: process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_ID!,
clientAppSecret: process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_SECRET!,
userId: process.env.NEXT_PUBLIC_LINKAGE_USER_ID!,
workflowId: "workflow_123",
}}
>
<StandaloneEditor
buildConfig={{
schemaVersion: "1.0.0",
controlBlocks: true,
stemNode: true,
}}
/>
</ClientProvider>
);
}workflowId is both the workflow identifier and the collaboration room id. userId scopes presence and permissions. schemaVersion should match the version you uploaded in your registry.
If you want editor changes to persist via /api/v1/state, provide the workflow client secret via workflowClientSecret. If your deployment uses the same secret for project and workflow access, you can reuse clientAppSecret.
Advanced: manual node type loading
If you want full control over node type loading (custom caching, auth, error handling, SSR shims), build nodeTypes yourself and pass them into Editor.
import { ClientProvider, Editor, useBuildNodeTypes } from "@linkage-open/lib/react";
export function WorkflowEditor() {
const { result, loading, error } = useBuildNodeTypes({
config: {
clientAppId: process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_ID!,
clientAppSecret: process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_SECRET!,
schemaVersion: "1.0.0",
controlBlocks: true,
stemNode: true,
},
});
if (loading) return null;
if (error) return <pre>Failed to load node types</pre>;
return (
<ClientProvider
config={{
clientAppId: process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_ID!,
clientAppSecret: process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_SECRET!,
userId: process.env.NEXT_PUBLIC_LINKAGE_USER_ID!,
workflowId: "workflow_123",
}}
>
<Editor nodeTypes={result?.nodeTypes} />
</ClientProvider>
);
}Hooks
StandaloneEditor uses useBuildNodeTypes internally. Use the hooks below when you want to build custom tooling around the editor (sidebars, run buttons, inspectors, etc.).
Run visualization (HTTP adapter)
Use the execution adapter contract to render live run status, logs, and node outputs. The HTTP factory hides transport details by preferring SSE and falling back to polling.
import {
createHttpExecutionAdapter,
RunPanel,
} from "@linkage-open/lib/react";
const adapter = createHttpExecutionAdapter({
transport: "auto",
headers: {
"x-client-id": process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_ID!,
"x-client-secret": process.env.NEXT_PUBLIC_LINKAGE_CLIENT_APP_SECRET!,
},
endpoints: {
startRun: () => "/api/v1/runs",
fetchRun: ({ runId }) => `/api/v1/runs/${runId}`,
eventsPoll: ({ runId }) => `/api/v1/runs/${runId}/events`,
eventsSse: ({ runId }) => `/api/v1/runs/${runId}/events`,
},
});
<RunPanel adapter={adapter} workflowId="workflow_123" />;If you want node cards in Editor to display per-node run status, pass runState from useRunAdapter:
import { Editor, useRunAdapter } from "@linkage-open/lib/react";
const run = useRunAdapter({ adapter, scope: { workflowId: "workflow_123" } });
<Editor nodeTypes={result.nodeTypes} runState={run.state} />;Keyboard shortcuts
Default editor shortcuts:
- Delete selected nodes/edges:
BackspaceorDelete - Multi-select:
Shift,Ctrl, orCmd+ click - Box-select: drag on the canvas
useWorkflow
Access the collaborative workflow state and React Flow change handlers.
import { useWorkflow } from "@linkage-open/lib/react";
const {
loading,
nodes,
edges,
localNodes,
localEdges,
handleNodesChange,
handleEdgesChange,
handleConnection,
} = useWorkflow();nodes/edgesare the canonical sets synced through the collaboration layerlocalNodes/localEdgesare the mutable collections React Flow renders (they preserve local selection, dragging, etc.)handleNodesChange,handleEdgesChange, andhandleConnectionfeed directly into<ReactFlow />callbacksloadingistrueuntil the shared store is ready
Use these values to extend the editor with custom sidebars, toolbars, or analytics.
useNode
Work with a single node. It returns the Jotai tuple [node, setNode] scoped to the provided id.
import { useNode } from "@linkage-open/lib/react";
function NodeDetails({ nodeId }: { nodeId: string }) {
const [node, setNode] = useNode(nodeId);
const handleNameChange = (name: string) => {
setNode((current) => ({ ...current, name }));
};
return (
<input
value={node.name}
onChange={(event) => handleNameChange(event.target.value)}
/>
);
}useServerState
Kick off a sync with the Linkage API and surface loading/error state.
import { useEffect } from "react";
import { useServerState } from "@linkage-open/lib/react";
function BootstrapState() {
const { loading, error } = useServerState();
useEffect(() => {
if (error) console.error(error);
}, [error]);
if (loading) return <p>Loading workflow...</p>;
return null;
}Extend the hook or pair it with collaboration listeners when you need to hydrate additional data from the workflow API.
Next
- Define/upload nodes in Node Registry
- Use the CLI to scaffold and upload registries