@arbiterflow/editor
v1.0.0
Published
Embeddable React editor for ArbiterFlow.
Downloads
732
Readme
@arbiterflow/editor
Embeddable React workflow tooling for ArbiterFlow — the same canvas and workflow list that power the hosted dashboard, shipped as components you can drop into your own app.
Drag nodes from a palette, wire them together, configure them through a schema-driven inspector, then save, publish, and watch runs execute live over the graph.
Install
npm install @arbiterflow/editorReact 18 or 19 is a peer dependency and is not bundled.
Usage
The fastest complete integration is ArbiterFlowStudio: the workflow list and
the editor together, with navigation between them.
import { ArbiterFlowStudio } from "@arbiterflow/editor";
import "@arbiterflow/editor/styles.css";
export function WorkflowsPage() {
return (
<ArbiterFlowStudio
projectId="7f3c…"
authMode="proxy"
serverUrl="/api/arbiterflow"
height="100vh"
/>
);
}That is the whole workflow-management surface — browse, search, create, open, edit, publish, run, delete — in one component. The stylesheet is a separate import; it is not injected by the JavaScript bundle.
For a full walkthrough — requirements, the proxy route, framework wiring, and troubleshooting — see the integration guide.
Choosing a component
| Component | Renders | Use when |
|---|---|---|
| ArbiterFlowStudio | List + editor, with its own navigation | You want workflow management on one route and do not care about the URL of the open workflow |
| ArbiterFlowWorkflowList | Just the list | You already have routing and want the list and editor on separate pages |
| ArbiterFlowEditor | Just the canvas | You pick the workflow yourself, or you only ever edit one |
All three take the same connection props (projectId, serverUrl, authMode,
apiKey), so moving between them is a rename.
Two routes instead of one
ArbiterFlowStudio owns navigation internally. If you would rather the open
workflow live in your URL, use the list and the editor directly — the list never
routes, it just hands you back the workflow that was clicked.
// app/admin/workflows/page.tsx
<ArbiterFlowWorkflowList
projectId={projectId}
authMode="proxy"
serverUrl="/api/arbiterflow"
onSelectWorkflow={(workflow) => router.push(`/admin/workflows/${workflow.slug}`)}
/>
// app/admin/workflows/[slug]/page.tsx
<ArbiterFlowEditor
projectId={projectId}
authMode="proxy"
serverUrl="/api/arbiterflow"
workflowSlug={slug}
/>ArbiterFlowStudio can also be URL-driven, if you want its top bar but your own
routing — pass openWorkflowSlug and onOpenWorkflowChange to control it.
Authentication
Pick one of three modes.
authMode="proxy" — how you should embed this. Point serverUrl at a
catch-all route in your own app. That route authenticates the visitor however
you already do, then forwards to ArbiterFlow with x-api-key injected
server-side. No credential reaches the browser, and you get to enforce your own
permissions on the way through. Every request the editor makes is
${serverUrl}/v1/projects/${projectId}/…, so a single catch-all covers it.
authMode="apiKey" sends the apiKey prop from the browser. Anyone who
opens devtools can read it, so only use it where every visitor is already
entitled to that key — a local tool, or an internal page behind your own login
where the key is scoped to a single project.
authMode="session" is the ArbiterFlow dashboard's own cookie surface. It
is not available to other origins.
Props
Connection (all components)
| Prop | Type | Default | Description |
|---|---|---|---|
| projectId | string | — | Required. Project to operate on. |
| serverUrl | string | https://arbiterflow-api.solopress.ai | Base URL of the ArbiterFlow API, or your proxy route. |
| apiKey | string | — | Sent as x-api-key when authMode is "apiKey". |
| authMode | "proxy" \| "apiKey" \| "session" | "apiKey" | See Authentication. |
| theme | "dark" \| "light" \| "system" | "dark" | Palette to paint with. See Theming. |
ArbiterFlowStudio
| Prop | Type | Default | Description |
|---|---|---|---|
| openWorkflowSlug | string \| null | — | Controls which workflow is open. Omit to let the studio track it. |
| onOpenWorkflowChange | (slug: string \| null) => void | — | Fires on open and on back, for syncing to your URL. |
| height | string \| number | "100%" | Height of the root. |
| title / description | ReactNode | — | Header copy on the list screen. |
| canCreate / canDelete | boolean | true | Hide list controls to match your RBAC. |
| canPublish / canRun / canDatabaseWrite | boolean | — | Passed through to the editor. |
| customNodes | Record<string, ComponentType<CustomNodeProps>> | — | Passed through to the editor. |
ArbiterFlowWorkflowList
| Prop | Type | Default | Description |
|---|---|---|---|
| onSelectWorkflow | (w: WorkflowListItem) => void | — | Required. A row was clicked; you decide what that means. |
| onWorkflowCreated | (w: WorkflowListItem) => void | onSelectWorkflow | Fires after creation, before the list refreshes. |
| title | ReactNode | "Workflows" | Pass null to drop the header entirely. |
| description | ReactNode | — | Sub-heading copy. |
| headerActions | ReactNode | — | Rendered beside the create button. |
| canCreate / canDelete | boolean | true | Hide controls to match your RBAC. |
| height | string \| number | "100%" | Height of the root. |
| className | string | — | Applied to the root. |
ArbiterFlowEditor
| Prop | Type | Default | Description |
|---|---|---|---|
| workflowSlug | string | "default-routing" | Which workflow to load. |
| mode | "edit" \| "runView" \| "graphView" | "edit" | runView replays a run; graphView renders a static definition. |
| viewRunId | string | — | Run to display when mode is "runView". |
| initialDefinition | WorkflowDefinition | — | Graph to render before (or instead of) fetching. |
| height | string \| number | "100vh" | Height of the editor root. |
| customNodes | Record<string, ComponentType<CustomNodeProps>> | {} | Override or extend node renderers by node type. |
| onControlsChange | (c: EditorControls \| null) => void | — | Hands save/publish/run out so the host can render its own chrome. |
| onRunStatusChange | (status) => void | — | Fires on run status transitions in run view. |
| canPublish / canRun / canDatabaseWrite | boolean | true | Hide controls to match your RBAC. |
Permissions
The components render the controls you allow and nothing more, but they cannot
enforce anything — the browser is not a trust boundary. Gate canCreate,
canDelete, canPublish and canRun to keep the UI honest, and enforce the
same rules in your proxy route, which is the only place a check counts.
Next.js
The bundle carries a "use client" directive, so App Router consumers can
import it directly from a server component — no wrapper module needed. It reads
no browser APIs at module scope, so next/dynamic with ssr: false is not
required either.
Styling
Import the stylesheet anywhere, including a root layout:
import "@arbiterflow/editor/styles.css";Every rule in it — including Tailwind's reset and utility classes — is confined to the editor's root element, so it cannot reach the rest of your page. You do not need to wrap it in a cascade layer, scope the import to a subtree, or worry about it colliding with your own Tailwind build.
The scoping prefix carries no specificity of its own, so your theme is not demoted beneath the editor's and the editor's is not demoted beneath yours.
Theming
Every component takes a theme prop — "dark" (default), "light", or
"system" to follow the OS. To follow your own switch instead, pass the mode
you already track:
const { resolvedTheme } = useTheme(); // next-themes
<ArbiterFlowStudio theme={resolvedTheme === "light" ? "light" : "dark"} … />Switching does not remount the canvas — the palette is CSS custom properties, so unsaved edits and scroll position survive.
Using your own colours
Colours resolve through --color-af-* properties, and the utilities are
indirections onto them (.bg-af-bg { background-color: var(--color-af-bg) }).
Redefine one, scoped to .arbiterflow-editor, and everything using it follows:
.arbiterflow-editor {
--color-af-accent: #0ea5e9;
}Yours wins: the shipped values are emitted at zero specificity.
shadcn/ui
The mapping is already written. Import it after the base stylesheet:
import "@arbiterflow/editor/styles.css";
import "@arbiterflow/editor/shadcn.css";It points the editor's tokens at the variables shadcn already defines
(--background, --card, --primary, --border, --destructive,
--chart-1 … --chart-5), so the editor adopts your palette — and, because it
maps to your variables rather than to fixed values, it keeps up when you switch
themes and follows your light/dark class automatically. Still pass theme, so
the canvas and code editors know which mode is active.
The full token list and the reasoning behind the mapping are in the integration guide.
License
MIT
