# Build runnable workflows

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`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-nodespec#nodespec) for each step and [`EdgeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-edgespec#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`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-qwik#grafloriaflow) in Qwik, [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-react#grafloriaflow) in React, and [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-vue#grafloriaflow) in Vue. Plain JavaScript mounts the same graph with [`render`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core#render), and Angular uses [`DiagramCanvasComponent`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-angular-components-diagramcanvascomponent#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`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-diagraminstance#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
```

:::code-group
```js title="JavaScript"
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);
```
```ts title="Angular"
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const NODES: NodeSpec[] = [
  { 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: EdgeSpec[] = [
  { 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' },
];

@Component({
  selector: 'app-workflow',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <button type="button" (click)="run()">Run workflow</button>
    <p>{{ readout }}</p>
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display: block; height: 440px" />
  `,
})
export class WorkflowComponent {
  nodes: NodeSpec[] = NODES;
  edges: EdgeSpec[] = EDGES;
  readout = 'Ready';

  private paint(current: string | null, completed: string[], skipped: string[], activeLink: string | null, completedLinks: string[], skippedLink: string | null): void {
    this.nodes = 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 ?? id} · ${state}`, style: { fill, stroke, strokeWidth: 2 } };
    });
    this.edges = 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 } };
    });
  }

  async run(): Promise<void> {
    const approved = true;
    const path = approved ? ['trigger', 'check', 'deploy', 'finish'] : ['trigger', 'check', 'hold'];
    const pathLinks = approved ? ['start', 'approved', 'finish-link'] : ['start', 'rejected'];
    const completed: string[] = [];
    const completedLinks: string[] = [];
    for (let index = 0; index < path.length; index += 1) {
      const nodeId = path[index];
      if (nodeId === undefined) continue;
      const activeLink = pathLinks[index - 1] ?? null;
      this.paint(nodeId, completed, [], activeLink, completedLinks, null);
      this.readout = `Running: ${nodeId}`;
      await new Promise<void>((resolve) => window.setTimeout(resolve, 500));
      completed.push(nodeId);
      if (activeLink) completedLinks.push(activeLink);
    }
    const skipped = NODES.map((node) => node.id ?? '').filter((id) => !path.includes(id));
    const skippedLink = EDGES.map((edge) => edge.id ?? '').find((id) => !pathLinks.includes(id) && id !== 'finish-link') ?? null;
    this.paint(null, completed, skipped, null, completedLinks, skippedLink);
    this.readout = `Completed the ${approved ? 'approved' : 'rejected'} branch`;
  }
}
```
```tsx title="Qwik"
import { component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const INITIAL_NODES: NodeSpec[] = [
  { 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 INITIAL_EDGES: EdgeSpec[] = [
  { 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' },
];

export default component$(() => {
  const nodes = useSignal<NodeSpec[]>(INITIAL_NODES);
  const edges = useSignal<EdgeSpec[]>(INITIAL_EDGES);
  const readout = useSignal('Ready');

  return (
    <div>
      <button type="button" onClick$={async () => {
        const approved = true;
        const path = approved ? ['trigger', 'check', 'deploy', 'finish'] : ['trigger', 'check', 'hold'];
        const pathLinks = approved ? ['start', 'approved', 'finish-link'] : ['start', 'rejected'];
        const completed: string[] = [];
        const completedLinks: string[] = [];
        const paint = (current: string | null, skipped: string[], activeLink: string | null, skippedLink: string | null): void => {
          nodes.value = INITIAL_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 ?? id} · ${state}`, style: { fill, stroke, strokeWidth: 2 } };
          });
          edges.value = INITIAL_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 } };
          });
        };

        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, [], activeLink, null);
          readout.value = `Running: ${nodeId}`;
          await new Promise<void>((resolve) => setTimeout(resolve, 500));
          completed.push(nodeId);
          if (activeLink) completedLinks.push(activeLink);
          paint(null, [], null, null);
        }

        const skipped = INITIAL_NODES.map((node) => node.id ?? '').filter((id) => !path.includes(id));
        const skippedLink = INITIAL_EDGES.map((edge) => edge.id ?? '').find((id) => !pathLinks.includes(id) && id !== 'finish-link') ?? null;
        paint(null, skipped, null, skippedLink);
        readout.value = `Completed the ${approved ? 'approved' : 'rejected'} branch`;
      }}>Run workflow</button>
      <p>{readout.value}</p>
      <div style={{ height: '440px' }}>
        <GrafloriaFlow nodes={nodes.value} edges={edges.value} fitView readonly />
      </div>
    </div>
  );
});
```
```tsx title="React"
import { useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/renderer';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const NODES: NodeSpec[] = [
  { 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: EdgeSpec[] = [
  { 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' },
];

export function Workflow() {
  const instance = useRef<DiagramInstance | null>(null);
  const [readout, setReadout] = useState('Ready');

  async function run(): Promise<void> {
    const api = instance.current;
    if (!api) return;
    const approved = true;
    const path = approved ? ['trigger', 'check', 'deploy', 'finish'] : ['trigger', 'check', 'hold'];
    const pathLinks = approved ? ['start', 'approved', 'finish-link'] : ['start', 'rejected'];
    const completed: string[] = [];
    const completedLinks: string[] = [];
    const paint = (current: string | null, skipped: string[], activeLink: string | null, skippedLink: string | null): void => {
      api.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 ?? id} · ${state}`, style: { fill, stroke, strokeWidth: 2 } };
      }));
      api.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 } };
      }));
    };

    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, [], activeLink, null);
      setReadout(`Running: ${nodeId}`);
      await new Promise<void>((resolve) => window.setTimeout(resolve, 500));
      completed.push(nodeId);
      if (activeLink) completedLinks.push(activeLink);
      paint(null, [], null, null);
    }

    const skipped = NODES.map((node) => node.id ?? '').filter((id) => !path.includes(id));
    const skippedLink = EDGES.map((edge) => edge.id ?? '').find((id) => !pathLinks.includes(id) && id !== 'finish-link') ?? null;
    paint(null, skipped, null, skippedLink);
    setReadout(`Completed the ${approved ? 'approved' : 'rejected'} branch`);
  }

  return (
    <div>
      <button type="button" onClick={() => { void run(); }}>Run workflow</button>
      <p>{readout}</p>
      <div style={{ height: 440 }}>
        <GrafloriaFlow defaultNodes={NODES} defaultEdges={EDGES}
          onInit={(api) => { api.fitView(30); instance.current = api; }} />
      </div>
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { shallowRef, ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance, EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { 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: EdgeSpec[] = [
  { 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 instance = shallowRef<DiagramInstance | null>(null);
const readout = ref('Ready');

function paint(current: string | null, completed: string[], skipped: string[], activeLink: string | null, completedLinks: string[], skippedLink: string | null): void {
  instance.value?.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 ?? id} · ${state}`, style: { fill, stroke, strokeWidth: 2 } };
  }));
  instance.value?.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 } };
  }));
}

async function run(): Promise<void> {
  const approved = true;
  const path = approved ? ['trigger', 'check', 'deploy', 'finish'] : ['trigger', 'check', 'hold'];
  const pathLinks = approved ? ['start', 'approved', 'finish-link'] : ['start', 'rejected'];
  const completed: string[] = [];
  const completedLinks: string[] = [];
  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.value = `Running: ${nodeId}`;
    await new Promise<void>((resolve) => window.setTimeout(resolve, 500));
    completed.push(nodeId);
    if (activeLink) completedLinks.push(activeLink);
    paint(null, completed, [], null, completedLinks, null);
  }
  const skipped = nodes.map((node) => node.id ?? '').filter((id) => !path.includes(id));
  const skippedLink = edges.map((edge) => edge.id ?? '').find((id) => !pathLinks.includes(id) && id !== 'finish-link') ?? null;
  paint(null, completed, skipped, null, completedLinks, skippedLink);
  readout.value = `Completed the ${approved ? 'approved' : 'rejected'} branch`;
}

function onInit(api: DiagramInstance): void {
  api.fitView(30);
  instance.value = api;
  paint(null, [], [], null, [], null);
}
</script>

<template>
  <button type="button" @click="run">Run workflow</button>
  <p>{{ readout }}</p>
  <div style="height: 440px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" />
  </div>
</template>
```
:::

## 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`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-diagraminstance#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

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `defaultNodes` | `NodeSpec[]` | Not specified | Seeds the mounted flow with its step nodes. |
| `defaultEdges` | `EdgeSpec[]` | Not specified | Seeds the mounted flow with its branch links. |
| `onInit` | `(instance: DiagramInstance) => void` | Not specified | Gives React and Vue the mounted instance. |
| `nodes` (Qwik) | `NodeSpec[]` | Not specified | Supplies controlled node data; the sample updates labels and styles as its runner advances. |
| `edges` (Qwik) | `EdgeSpec[]` | Not specified | Supplies controlled link data; the sample updates link labels and styles during the run. |
| `fitView` (Qwik) | `boolean` | Not specified | Fits the initial workflow in the canvas. |
| `readonly` (Qwik) | `boolean` | Not specified | Disables canvas editing while the runner updates its controlled data. |
| `nodes` (Angular) | `readonly (`NodeSpec` \| [`NodeModel`](doc:grafloria-engine-models-nodemodel#nodemodel))[] \| `undefined` | `undefined` | Controlled nodes; `[(nodes)]` writes canvas changes back to the host. |
| `edges` (Angular) | `readonly (`EdgeSpec` \| [`LinkModel`](doc:grafloria-engine-models-linkmodel#linkmodel))[] \| `undefined` | `undefined` | Controlled 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](https://grafloria.com/demos/interaction/n8n-workflow.html) runs a graph from its trigger, shows running and completed node states, and routes a condition down one branch. Its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/n8n-workflow.html) shows the repository's fuller execution UI.

The [workflow automation builder](https://grafloria.com/demos/interaction/workflow-builder.html) tests a Condition and health-check Switch against sample inputs; its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/workflow-builder.html) includes the step editor and run card.

## Related

- [Execute and compute flows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/execute-and-compute-flows) — separate workflow execution from the graph that displays it.
- [Add nodes from palettes](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/add-nodes-from-palettes) — let users build the graph interactively.
- [The instance and data flow](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/instance-and-data-flow) — understand how specs reconcile into the live model.
