# Build a workflow editor

Use a diagram as the workflow document: nodes represent steps, typed ports constrain
connections, the engine supplies history, and node data records execution state. The result
is an editor that renders a connected workflow, rejects invalid links, can run the graph, and
can save and restore the document.

## When to use this

Use this pattern when users create or edit a directed process rather than view a fixed diagram.
The model remains the source of truth: keep the step configuration in `data`, keep connection
rules on ports, and update the live instance after execution changes.

The same engine drives every binding. The JavaScript example uses [`render`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-core#render);

## Define the workflow document

Create a shared module. The two nodes have explicit input and output ports. The output carries
`dataType: 'event'`, while the input accepts only `event`; a connection from the output to the
input is therefore legal, and a connection in the reverse direction is not. The live objects below are
[`NodeModel`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-engine-models-nodemodel#nodemodel) and
[`PortModel`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-engine-models-portmodel#portmodel); the renderer creates them from
these specs. Use [`NodeSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance#nodespec) and
[`EdgeSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-edgespec#edgespec) for the plain data arrays.

```ts title="workflow.ts"
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

export const nodes: NodeSpec[] = [
  {
    id: 'trigger',
    type: 'workflow-step',
    position: { x: 80, y: 120 },
    size: { width: 170, height: 72 },
    data: { label: 'Receive commit', status: 'idle' },
    ports: [
      { id: 'trigger-out', side: 'right', type: 'output', dataType: 'event' },
    ],
  },
  {
    id: 'deploy',
    type: 'workflow-step',
    position: { x: 380, y: 120 },
    size: { width: 170, height: 72 },
    data: { label: 'Deploy staging', status: 'idle' },
    ports: [
      { id: 'deploy-in', side: 'left', type: 'input', dataType: 'event' },
      { id: 'deploy-out', side: 'right', type: 'output', dataType: 'event' },
    ],
  },
];

export const edges: EdgeSpec[] = [
  {
    id: 'trigger-to-deploy',
    source: 'trigger',
    sourceHandle: 'trigger-out',
    target: 'deploy',
    targetHandle: 'deploy-in',
    type: 'orthogonal',
  },
];

export function setExecutionState(
  instance: import('@grafloria/renderer').DiagramInstance,
  nodeId: string,
  status: 'idle' | 'running' | 'success' | 'failed',
): void {
  const node = instance.getModel().getNode(nodeId);
  if (!node) return;
  node.setData('status', status);
  node.setSelected(status === 'running');
  instance.render();
}

```

For persistence, use [`DiagramSerializer`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-engine-serialization#diagramserializer)
in a browser-safe module:

```ts title="workflow-persistence.ts"
import { DiagramSerializer } from '@grafloria/engine';
import type { DiagramInstance } from '@grafloria/renderer';

export function saveDocument(instance: DiagramInstance): string {
  return JSON.stringify(new DiagramSerializer().serialize(instance.getModel()));
}

export function restoreDocument(instance: DiagramInstance, text: string): void {
  const document = JSON.parse(text) as ReturnType<DiagramSerializer['serialize']>;
  const model = new DiagramSerializer().deserialize(document);
  instance.getEngine().setDiagram(model);
  instance.renderNow();
}
```

Use the second module in the framework samples. It serializes the complete model, including ports
and links. Restoring replaces the engine's diagram and repaints the mounted canvas.

## Mount it in JavaScript

Give the host a resolved height. The canvas displays the two connected steps, and the buttons
change the live model, validate it, and round-trip it through JSON. The example uses the shipped
[`LIGHT_THEME`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-themes-constants#light_theme).

```ts title="main.ts"
import { render } from '@grafloria/element';
import { LIGHT_THEME } from '@grafloria/renderer';
import { nodes, edges, setExecutionState } from './workflow';
import { saveDocument, restoreDocument } from './workflow-persistence';

const host = document.getElementById('workflow')!;
host.style.height = '420px';
const instance = render({ nodes, edges }, host, {
  theme: LIGHT_THEME,
  interaction: { portVisibility: 'always' },
});
const engine = instance.getEngine();

document.getElementById('run')!.addEventListener('click', () => {
  setExecutionState(instance, 'trigger', 'success');
  setExecutionState(instance, 'deploy', 'running');
});
document.getElementById('validate')!.addEventListener('click', () => {
  console.log(engine.validateDiagram({ validateTypes: true, validateConnections: true }));
});
document.getElementById('save')!.addEventListener('click', () => {
  localStorage.setItem('workflow', saveDocument(instance));
});
document.getElementById('load')!.addEventListener('click', () => {
  const text = localStorage.getItem('workflow');
  if (text) restoreDocument(instance, text);
});
document.getElementById('undo')!.addEventListener('click', () => {
  void engine.undo();
});
```

```html title="index.html"
<div id="workflow" style="height: 420px"></div>
<button id="run">Run</button>
<button id="validate">Validate</button>
<button id="save">Save</button>
<button id="load">Load</button>
<button id="undo">Undo</button>
<script type="module" src="./main.ts"></script>
```

![The mounted canvas shows two workflow steps joined by a connecting edge.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/cbbda69c68f899713e6ec8921e4beba6.png)

Clicking **Run** changes the trigger to a success state and selects the deploy step as the running
step; the renderer repaints the model change. A custom node can read the stored `status` to draw
its own success or running badge. Dragging or connecting
nodes creates engine history entries, and **Undo** removes the latest user edit.

## Mount it in Angular, Qwik, React, and Vue

Each binding mounts a real component with the same nodes and edges. Store the instance in the
binding's mount callback; do not create it in render code. The host styles give the canvas height.

:::code-group
```ts title="Angular"
import { Component } from '@angular/core';
import { GrafloriaDiagramComponent } from '@grafloria/angular';
import type { RenderOptions } from '@grafloria/element';
import type { DiagramInstance } from '@grafloria/renderer';
import { nodes, edges } from './workflow';

@Component({
  standalone: true,
  imports: [GrafloriaDiagramComponent],
  template: '<grafloria-diagram [spec]="spec" [options]="options" (ready)="ready($event)"></grafloria-diagram>',
  styles: ['grafloria-diagram { display: block; height: 420px; }'],
})
export class WorkflowComponent {
  readonly spec = { nodes, edges };
  readonly options: RenderOptions = { interaction: { portVisibility: 'always' } };
  instance?: DiagramInstance;

  ready(instance: DiagramInstance): void { this.instance = instance; }
}
```

```tsx title="React"
import { useRef, type ReactElement } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/react';
import { nodes, edges } from './workflow';

export function Workflow(): ReactElement {
  const instance = useRef<DiagramInstance | null>(null);
  return <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
    onInit={(value) => { instance.current = value; }}
    style={{ display: 'block', height: '420px' }} />;
}
```

```vue title="Vue"
<script setup lang="ts">
import { shallowRef } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/vue';
import { nodes, edges } from './workflow';

const instance = shallowRef<DiagramInstance | null>(null);
function ready(value: DiagramInstance): void { instance.value = value; }
</script>

<template>
  <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="ready"
    style="display: block; height: 420px" />
</template>
```

```tsx title="Qwik"
import { component$, $, noSerialize, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { nodes, edges } from './workflow';

export default component$(() => {
  const instance = useSignal<DiagramInstance>();
  const ready = $((value: DiagramInstance) => { instance.value = noSerialize(value); });
  return <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit$={ready}
    style={{ display: 'block', height: '420px' }} />;
});
```
:::

All five mounted canvases show the same connected workflow. Call `getEngine()` through the stored
instance for validation and undo, call `getModel()` to inspect or update step data, and call
`renderNow()` when a subsequent measurement must observe the change synchronously.

## Add validation and execution

Port direction, `dataType`, and connection limits provide structural validation while a user draws.
For a complete document check, call `validateDiagram()` on the [`DiagramEngine`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-engine-engine#diagramengine)
and display its returned
validation result. Your executor can walk from a trigger, set each node's `status` with
`setData()`, and call `render()` after each step. The renderer then shows the state your node
template reads from `data`.

Keep execution separate from editing: a run changes status data, while a drag or connection is a
command on the shared history. For a toolbar action that edits the document, use a shipped command
through the engine's command manager so the action participates in the same undo stack. See
[Commands, events, and undo](https://atloria.dev/p/grafloria-h7YM7amryF/developer/commands-events-and-undo) for the command boundary and
[Ports and validation](https://atloria.dev/p/grafloria-h7YM7amryF/developer/ports-and-validation) for custom rules.

## Persistence and next steps

Store the string returned by `saveDocument()` in your database or browser storage. On load, call
`restoreDocument()` on a mounted instance. Re-register any custom renderer before mounting the
document; serialized data contains the model, not JavaScript renderer functions.

See the live [workflow automation builder](https://grafloria.com/demos/interaction/workflow-builder.html)
for insertion, editing, grouping, run state, and save/load in one editor.

The [n8n-style workflow builder](https://grafloria.com/demos/interaction/n8n-workflow.html) shows
item counts, branch execution, pause/step controls, and a per-node details view.

For a smaller execution-state example, open [Execute flow](https://grafloria.com/demos/interaction/execute-flow.html).

## Pitfalls

- A host without a resolved height renders a blank canvas. Give it a fixed height, a viewport height,
  or a flex/grid size; see [Style a diagram](https://atloria.dev/p/grafloria-h7YM7amryF/developer/style-a-diagram).
- `undo()` belongs to the engine, not `DiagramInstance`; call `instance.getEngine().undo()`.
- Changing node data does not rerun a declarative layout. Call the engine layout method explicitly,
  then repaint if you use the imperative layout path; see [Auto-layout a diagram](https://atloria.dev/p/grafloria-h7YM7amryF/developer/auto-layout-a-diagram).
- A custom node needs the custom-node flag and its renderer must be registered before mount; see
  [Create custom nodes](https://atloria.dev/p/grafloria-h7YM7amryF/developer/create-custom-nodes).
