Skip to content
D
Documentation

Build interactive workflows

how-to
2 min readUpdated

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 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 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
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.

js
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();
});

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 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.

Options that matter

OptionTypeDefaultWhat it does
height on the hostCSS sizenoneGives the mounted canvas space to paint.
defaultNodesNodeInput[]emptySupplies initial nodes to framework bindings.
defaultEdgesEdgeInput[]emptySupplies initial links to framework bindings.
themeThemelightSelects the visual theme; use LIGHT_THEME or DARK_THEME.

Pitfalls

  • undo() belongs to instance.getEngine(), not the renderer instance. See 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, 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.

Related: Ports and connection rules, Edit Mermaid diagrams, and How Grafloria works.

Was this page helpful?

Build interactive workflows — Grafloria · GPT-5.6 Luna