Linkage LogoLinkage Docs

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/lib

Peer 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 --json

Note: 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: Backspace or Delete
  • Multi-select: Shift, Ctrl, or Cmd + 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 / edges are the canonical sets synced through the collaboration layer
  • localNodes / localEdges are the mutable collections React Flow renders (they preserve local selection, dragging, etc.)
  • handleNodesChange, handleEdgesChange, and handleConnection feed directly into <ReactFlow /> callbacks
  • loading is true until 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