Skip to content
D
Documentation

Build runnable workflows

how-to
3 min readUpdated

Build a branching workflow diagram and run a browser-side path through it, showing the current, completed, and unselected steps and links.

Use this pattern when your application owns the workflow rules and execution. The canvas displays the workflow; your runner decides which branch to take and updates the visible data as each step runs. In this example the runner takes the approved branch.

1. Describe the steps and branches

Use NodeSpec for each step and EdgeSpec for each connection. The graph below branches from Check approval to either Deploy or Hold; the runner takes the approved route through Deploy to Finish.

The bindings pass these specs to a live GrafloriaFlow in Qwik, GrafloriaFlow in React, and GrafloriaFlow in Vue. Plain JavaScript mounts the same graph with render, and Angular uses DiagramCanvasComponent.

2. Mount the graph and run its selected path

Each example starts with the same five nodes and four links. Press Run workflow: the runner marks a node as running, waits, then marks it complete and advances along the selected links. The initial links leaving Check approval are labelled approved and rejected; the final view marks Hold and its link as not selected. Status is represented in the node labels and node/link styles, so the workflow rules and the status display remain application data. The JavaScript, React, and Vue samples reconcile these changes with setNodes() and setEdges() on the mounted DiagramInstance; Angular and Qwik bind updated data arrays.

The canvas fills its parent. Give the host a resolved height; without one, the canvas is blank.

Choose the install command for your framework; these are alternatives, not a single command sequence:

bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer
npm install @grafloria/react @grafloria/engine @grafloria/renderer react react-dom @grafloria/element
npm install @grafloria/vue @grafloria/engine @grafloria/renderer vue @grafloria/element
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @builder.io/qwik @grafloria/element
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element
js
import { render } from '@grafloria/element';

const nodes = [
  { id: 'trigger', position: { x: 40, y: 180 }, size: { width: 140, height: 60 }, label: 'Trigger' },
  { id: 'check', position: { x: 230, y: 180 }, size: { width: 160, height: 60 }, label: 'Check approval' },
  { id: 'deploy', position: { x: 460, y: 90 }, size: { width: 140, height: 60 }, label: 'Deploy' },
  { id: 'hold', position: { x: 460, y: 270 }, size: { width: 140, height: 60 }, label: 'Hold' },
  { id: 'finish', position: { x: 650, y: 90 }, size: { width: 140, height: 60 }, label: 'Finish' },
];
const edges = [
  { id: 'start', source: 'trigger', target: 'check' },
  { id: 'approved', source: 'check', target: 'deploy', label: 'approved' },
  { id: 'rejected', source: 'check', target: 'hold', label: 'rejected' },
  { id: 'finish-link', source: 'deploy', target: 'finish' },
];

const host = document.createElement('div');
host.style.height = '440px';
document.body.append(host);
const button = document.createElement('button');
button.textContent = 'Run workflow';
document.body.insertBefore(button, host);
const readout = document.createElement('p');
readout.textContent = 'Ready';
document.body.insertBefore(readout, host);
const instance = render({ nodes, edges }, host);
instance.fitView(30);

/**
 * @param {string | null} current
 * @param {string[]} completed
 * @param {string[]} skipped
 * @param {string | null} activeLink
 * @param {string[]} completedLinks
 * @param {string | null} skippedLink
 */
function paint(current, completed, skipped, activeLink, completedLinks, skippedLink) {
  instance.setNodes(nodes.map((node) => {
    const id = node.id ?? '';
    const state = id === current ? 'running'
      : completed.includes(id) ? 'completed'
      : skipped.includes(id) ? 'not selected' : 'ready';
    const fill = state === 'running' ? '#dbeafe'
      : state === 'completed' ? '#dcfce7'
      : state === 'not selected' ? '#f1f5f9' : '#ffffff';
    const stroke = state === 'running' ? '#2563eb'
      : state === 'completed' ? '#16a34a'
      : state === 'not selected' ? '#94a3b8' : '#64748b';
    return { ...node, label: `${node.label} · ${state}`, style: { fill, stroke, strokeWidth: 2 } };
  }));
  instance.setEdges(edges.map((edge) => {
    const id = edge.id ?? '';
    const state = id === activeLink ? 'running'
      : completedLinks.includes(id) ? 'completed'
      : id === skippedLink ? 'not selected' : '';
    const stroke = state === 'running' ? '#2563eb'
      : state === 'completed' ? '#16a34a'
      : state === 'not selected' ? '#94a3b8' : '#64748b';
    return { ...edge, label: state || edge.label, style: { stroke, strokeWidth: state === 'running' ? 4 : 2 } };
  }));
}

/** @param {number} ms */
const delay = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
async function run() {
  const approved = true;
  const path = approved ? ['trigger', 'check', 'deploy', 'finish'] : ['trigger', 'check', 'hold'];
  const pathLinks = approved ? ['start', 'approved', 'finish-link'] : ['start', 'rejected'];
  const allNodeIds = nodes.map((node) => node.id ?? '');
  const allLinkIds = edges.map((edge) => edge.id ?? '');
  const completed = [];
  const completedLinks = [];

  for (let index = 0; index < path.length; index += 1) {
    const nodeId = path[index];
    if (nodeId === undefined) continue;
    const activeLink = pathLinks[index - 1] ?? null;
    paint(nodeId, completed, [], activeLink, completedLinks, null);
    readout.textContent = `Running: ${nodeId}`;
    await delay(500);
    completed.push(nodeId);
    if (activeLink) completedLinks.push(activeLink);
  }

  const skipped = allNodeIds.filter((id) => !path.includes(id));
  const skippedLink = allLinkIds.find((id) => !pathLinks.includes(id) && id !== 'finish-link') ?? null;
  paint(null, completed, skipped, null, completedLinks, skippedLink);
  readout.textContent = `Completed the ${approved ? 'approved' : 'rejected'} branch`;
}

button.addEventListener('click', () => { void run(); });
paint(null, [], [], null, [], null);

What the run displays

While a step is active, its label ends in running and its fill and border turn blue; the incoming selected link is also blue and labelled running. Completed steps and traversed links turn green. At the end, Hold and the rejected branch are grey and labelled not selected. The status text above the canvas shows the current step and the selected branch result.

The instance is the facade to the mounted canvas and its data model. In these examples, the instance's DiagramInstance methods setNodes() and setEdges() reconcile new specs into that live diagram. The runner—not the graph renderer—owns the example's approval rule and the sequence of steps.

Props used

OptionTypeDefaultWhat it does
defaultNodesNodeSpec[]Not specifiedSeeds the mounted flow with its step nodes.
defaultEdgesEdgeSpec[]Not specifiedSeeds the mounted flow with its branch links.
onInit(instance: DiagramInstance) => voidNot specifiedGives React and Vue the mounted instance.
nodes (Qwik)NodeSpec[]Not specifiedSupplies controlled node data; the sample updates labels and styles as its runner advances.
edges (Qwik)EdgeSpec[]Not specifiedSupplies controlled link data; the sample updates link labels and styles during the run.
fitView (Qwik)booleanNot specifiedFits the initial workflow in the canvas.
readonly (Qwik)booleanNot specifiedDisables canvas editing while the runner updates its controlled data.
nodes (Angular)readonly (NodeSpec | [NodeModel](/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-models-nodemodel#nodemodel))[] | undefined`undefinedControlled nodes; [(nodes)] writes canvas changes back to the host.
edges (Angular)readonly (EdgeSpec | [LinkModel](/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-models-linkmodel#linkmodel))[] | undefined`undefinedControlled links; [(edges)] writes canvas changes back to the host.

Plain JavaScript gets the instance directly from render(). Angular binds its node and edge arrays with the canvas's two-way nodes and edges inputs.

Pitfalls

  • Give the canvas parent a real height. The canvas fills its parent; an unresolved parent height leaves a blank canvas.
  • Keep the workflow rule in your runner. The graph describes steps and connections; replace the example's fixed approved value with your application's branch condition.

See it running

The n8n-style workflow demo runs a graph from its trigger, shows running and completed node states, and routes a condition down one branch. Its source shows the repository's fuller execution UI.

The workflow automation builder tests a Condition and health-check Switch against sample inputs; its source includes the step editor and run card.

Was this page helpful?