# Build interactive workflows

Use this pattern when you need a mounted editor in which users add workflow steps, connect them, undo edits, and run the graph. The component owns the canvas; the [`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance) is the shared imperative facade behind every binding.

## What you build

The sample mounts three steps in a 720-pixel-high canvas. **Add step** inserts a node through the engine, users connect nodes by dragging from one port to another, **Undo** calls the engine history, and **Run** marks the nodes in graph order as running and then complete. The browser renders the result on the mounted canvas, not on a detached model.

Give the host a resolved height. A canvas whose host has no height renders blank; see [Edit Mermaid diagrams](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/edit-mermaid-diagrams) for the sizing rule.

## 1. Define the workflow data

Use node and edge specs as data. The same document shape works in each binding.

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

export const nodes: NodeSpec[] = [
  { id: 'trigger', position: { x: 80, y: 150 }, size: { width: 140, height: 54 }, label: 'Trigger' },
  { id: 'fetch', position: { x: 300, y: 150 }, size: { width: 140, height: 54 }, label: 'Fetch data' },
  { id: 'save', position: { x: 520, y: 150 }, size: { width: 140, height: 54 }, label: 'Save result' },
];

export const edges: EdgeSpec[] = [
  { id: 'trigger-fetch', source: 'trigger', target: 'fetch' },
  { id: 'fetch-save', source: 'fetch', target: 'save' },
];
```

## 2. Mount the editor

Each binding emits the mounted instance through its initialization callback. Keep that live object in the framework's stable state, then call `getEngine()` for history and `getModel()` for workflow data.

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';
import { nodes, edges } from './workflow-data.js';

const host = document.getElementById('workflow');
if (!host) throw new Error('Missing #workflow');
host.style.height = '720px';
const instance = render(JSON.stringify({ nodes, edges }), host);
const engine = instance.getEngine();
const model = instance.getModel();

const addButton = document.getElementById('add-step');
if (!addButton) throw new Error('Missing #add-step');
addButton.addEventListener('click', async () => {
  await engine.addNode({ type: 'basic', position: { x: 740, y: 150 }, size: { width: 140, height: 54 }, data: { label: 'Notify' } });
  instance.renderNow();
});
const undoButton = document.getElementById('undo');
if (!undoButton) throw new Error('Missing #undo');
undoButton.addEventListener('click', () => { void engine.undo(); });
const runButton = document.getElementById('run');
if (!runButton) throw new Error('Missing #run');
runButton.addEventListener('click', () => {
  for (const node of model.getNodes()) {
    node.setState({ status: 'running' });
    instance.renderNow();
    node.setState({ status: 'completed' });
  }
  instance.renderNow();
});
```
```tsx title="React"
import { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import { nodes, edges } from './workflow-data';

export default function WorkflowEditor() {
  const instance = useRef<DiagramInstance | null>(null);
  const onInit = (value: DiagramInstance) => { instance.current = value; };
  const addStep = async () => {
    const value = instance.current;
    if (!value) return;
    await value.getEngine().addNode({ type: 'basic', position: { x: 740, y: 150 }, size: { width: 140, height: 54 }, data: { label: 'Notify' } });
    value.renderNow();
  };
  const run = () => {
    const value = instance.current;
    if (!value) return;
    for (const node of value.getModel().getNodes()) { node.setState({ status: 'running' }); value.renderNow(); node.setState({ status: 'completed' }); }
    value.renderNow();
  };
  return <div style={{ height: '720px' }}><button onClick={addStep}>Add step</button><button onClick={() => { void instance.current?.getEngine().undo(); }}>Undo</button><button onClick={run}>Run</button><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit={onInit} style={{ display: 'block', height: '100%' }} /></div>;
}
```
```vue title="Vue"
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';
import { nodes, edges } from './workflow-data';

const instance = ref<DiagramInstance | null>(null);
const editableNodes = ref(nodes);
const editableEdges = ref(edges);
function onInit(value: DiagramInstance) { instance.value = value; }
async function addStep() {
  if (!instance.value) return;
  await instance.value.getEngine().addNode({ type: 'basic', position: { x: 740, y: 150 }, size: { width: 140, height: 54 }, data: { label: 'Notify' } });
  instance.value.renderNow();
}
function run() { if (!instance.value) return; for (const node of instance.value.getModel().getNodes()) { node.setState({ status: 'running' }); instance.value.renderNow(); node.setState({ status: 'completed' }); } instance.value.renderNow(); }
</script>
<template><div style="height: 720px"><button @click="addStep">Add step</button><button @click="instance?.getEngine().undo()">Undo</button><button @click="run">Run</button><GrafloriaFlow v-model:nodes="editableNodes" v-model:edges="editableEdges" @init="onInit" style="display:block;height:100%" /></div></template>
```
```ts title="Angular"
import { Component } from '@angular/core';
import { GrafloriaDiagramComponent } from '@grafloria/angular';
import type { DiagramInstance } from '@grafloria/renderer';
import { nodes, edges } from './workflow-data';

@Component({ standalone: true, imports: [GrafloriaDiagramComponent], template: `
  <div style="height:720px"><button (click)="addStep()">Add step</button><button (click)="undo()">Undo</button><button (click)="run()">Run</button>
    <grafloria-diagram [spec]="spec" [options]="options" (ready)="onReady($event)" style="display:block;height:100%"></grafloria-diagram>
  </div>` })
export class WorkflowEditorComponent {
  readonly spec = JSON.stringify({ nodes, edges }); readonly options = {}; private instance: DiagramInstance | null = null;
  onReady(value: DiagramInstance): void { this.instance = value; }
  async addStep(): Promise<void> { if (!this.instance) return; await this.instance.getEngine().addNode({ type: 'basic', position: { x: 740, y: 150 }, size: { width: 140, height: 54 }, data: { label: 'Notify' } }); this.instance.renderNow(); }
  undo(): void { void this.instance?.getEngine().undo(); }
  run(): void { if (!this.instance) return; for (const node of this.instance.getModel().getNodes()) { node.setState({ status: 'running' }); this.instance.renderNow(); node.setState({ status: 'completed' }); } this.instance.renderNow(); }
}
```
```tsx title="Qwik"
import { component$, $, noSerialize, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'trigger', position: { x: 80, y: 150 }, size: { width: 140, height: 54 }, label: 'Trigger' },
  { id: 'fetch', position: { x: 300, y: 150 }, size: { width: 140, height: 54 }, label: 'Fetch data' },
  { id: 'save', position: { x: 520, y: 150 }, size: { width: 140, height: 54 }, label: 'Save result' },
];
const edges: EdgeSpec[] = [
  { id: 'trigger-fetch', source: 'trigger', target: 'fetch' },
  { id: 'fetch-save', source: 'fetch', target: 'save' },
];

export default component$(() => {
  const instance = useSignal<DiagramInstance>();
  const onInit = $((value: DiagramInstance) => { instance.value = noSerialize(value); });
  const addStep = $(async () => {
    const value = instance.value;
    if (!value) return;
    await value.getEngine().addNode({ type: 'basic', position: { x: 740, y: 150 }, size: { width: 140, height: 54 }, data: { label: 'Notify' } });
    value.renderNow();
  });
  const run = $(() => {
    const value = instance.value;
    if (!value) return;
    for (const node of value.getModel().getNodes()) { node.setState({ status: 'running' }); value.renderNow(); node.setState({ status: 'completed' }); }
    value.renderNow();
  });
  return <div style={{ height: '720px' }}><button onClick$={addStep}>Add step</button><button onClick$={$(() => { void instance.value?.getEngine().undo(); })}>Undo</button><button onClick$={run}>Run</button><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit$={onInit} style={{ display: 'block', height: '100%' }} /></div>;
});
```
:::

The canvas shows the three connected steps. **Add step** adds a fourth node, **Undo** removes that engine command, and **Run** transitions each current node through `running` to `completed`. Connect the new step by dragging from an output port to an input port; the renderer and engine create and validate the link.

## 3. Add workflow rules

Ports express whether a step can start or receive a connection. Use [`registerConnectionValidator`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-ext-functions) when a workflow rule applies across all connection gestures:

```ts
import { registerConnectionValidator } from '@grafloria/renderer';

const disposeValidator = registerConnectionValidator(() => true);
// Call disposeValidator() when this feature is unloaded.
```

All registered validators must pass. The registration is process-global, so retain and dispose the returned function; see [Ports and connection rules](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/ports-and-connection-rules).

## Options that matter

| Option | Type | Default | What it does |
|---|---|---|---|
| `height` on the host | CSS size | none | Gives the mounted canvas space to paint. |
| `defaultNodes` | `NodeInput[]` | empty | Supplies initial nodes to framework bindings. |
| `defaultEdges` | `EdgeInput[]` | empty | Supplies initial links to framework bindings. |
| `theme` | `Theme` | light | Selects the visual theme; use [`LIGHT_THEME`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-themes-constants) or [`DARK_THEME`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-themes-constants). |

## Pitfalls

- `undo()` belongs to `instance.getEngine()`, not the renderer instance. See [How Grafloria works](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/how-grafloria-works).
- Keep the instance in a ref, signal, or component field. Do not recreate it during a render.
- Call `renderNow()` when you need the repaint before measuring the canvas; ordinary updates are scheduled.
- The sample runs a visual status update. A production runner must apply its own asynchronous work and status policy while preserving the graph in the model.

## Live demo

Try the [workflow automation builder](https://grafloria.com/demos/interaction/workflow-builder.html), which adds steps with `+`, edits them in a panel, keeps page edits in the engine's undo stack, and runs branches from a trigger. Its source is [workflow-builder.html](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/workflow-builder.html).

Related: [Ports and connection rules](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/ports-and-connection-rules), [Edit Mermaid diagrams](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/edit-mermaid-diagrams), and [How Grafloria works](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/how-grafloria-works).
