This page focuses on what happens after the graph is mounted: update execution status as a runner advances, or follow live links to recompute values. For the framework-specific mounting setup, see Build runnable workflows.
Grafloria does not run your business logic for you. Set each live node's status as your runner advances, and calculate values by following the diagram's current links. Each framework example uses typed graph data; the JavaScript example shows the same host-side logic in the browser.
The JavaScript example runs in a browser app whose build resolves npm package imports.
Prepare the graph and its host-side logic
This module declares two small graphs: a workflow whose nodes receive execution statuses, and a typed-port pipeline that calculates input × 3 + 10. compute() follows the live link models, while execute() updates the mounted model's status and active-wire style.
tsimport type { DiagramModel } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
export const executionNodes: NodeSpec[] = [
{ id: 'trigger', position: { x: 60, y: 120 }, size: { width: 130, height: 54 }, label: 'Trigger' },
{ id: 'fetch', position: { x: 260, y: 120 }, size: { width: 130, height: 54 }, label: 'Fetch data' },
{ id: 'transform', position: { x: 460, y: 120 }, size: { width: 130, height: 54 }, label: 'Transform' },
];
export const executionEdges: EdgeSpec[] = [
{ id: 'fetch-link', source: 'trigger', target: 'fetch' },
{ id: 'transform-link', source: 'fetch', target: 'transform' },
];
export const computeNodes: NodeSpec[] = [
{
id: 'in', position: { x: 40, y: 120 }, size: { width: 120, height: 56 }, label: 'input',
ports: [{ id: 'in.out', side: 'right', type: 'output', dataType: 'number' }],
data: { value: 2 },
},
{
id: 'mul', position: { x: 240, y: 120 }, size: { width: 120, height: 56 }, label: '× 3',
ports: [
{ id: 'mul.in', side: 'left', type: 'input', dataType: 'number' },
{ id: 'mul.out', side: 'right', type: 'output', dataType: 'number' },
],
data: { operation: 'multiply', factor: 3, value: 0 },
},
{
id: 'add', position: { x: 440, y: 120 }, size: { width: 120, height: 56 }, label: '+ 10',
ports: [
{ id: 'add.in', side: 'left', type: 'input', dataType: 'number' },
{ id: 'add.out', side: 'right', type: 'output', dataType: 'number' },
],
data: { operation: 'add', amount: 10, value: 0 },
},
{
id: 'out', position: { x: 640, y: 120 }, size: { width: 120, height: 56 }, label: 'sink',
ports: [{ id: 'out.in', side: 'left', type: 'input', dataType: 'number' }],
data: { operation: 'sink', value: 0 },
},
];
export const computeEdges: EdgeSpec[] = [
{ id: 'l1', source: 'in', target: 'mul', sourceHandle: 'in.out', targetHandle: 'mul.in' },
{ id: 'l2', source: 'mul', target: 'add', sourceHandle: 'mul.out', targetHandle: 'add.in' },
{ id: 'l3', source: 'add', target: 'out', sourceHandle: 'add.out', targetHandle: 'out.in' },
];
export async function execute(model: DiagramModel, report: (message: string) => void): Promise<void> {
const order = ['trigger', 'fetch', 'transform'];
for (const id of order) model.getNode(id)?.setState({ status: 'pending' });
for (const id of order) {
const node = model.getNode(id);
if (!node) continue;
const incoming = model.getLinks().filter((link) => link.targetNodeId === id);
for (const link of incoming) link.updateStyle({ animation: { type: 'flow' } });
node.setState({ status: 'running', animateStatus: true });
report(`running: ${id}`);
await new Promise<void>((resolve) => window.setTimeout(resolve, 500));
node.setState({ status: 'completed' });
for (const link of incoming) link.updateStyle({ animation: { type: 'none' } });
}
report('flow completed');
}
export function compute(model: DiagramModel, value: number): string {
const source = model.getNode('in');
if (source) source.data.value = Number.isFinite(value) ? value : 0;
for (const id of ['mul', 'add', 'out']) {
const node = model.getNode(id);
if (!node) continue;
const incoming = model.getLinks().find((link) => link.targetNodeId === id);
const fromId = incoming?.sourceNodeId;
const from = fromId ? model.getNode(fromId) : undefined;
if (!from || typeof from.data.value !== 'number') continue;
const incomingValue = from.data.value;
if (node.data.operation === 'multiply') {
node.data.value = incomingValue * node.data.factor;
} else if (node.data.operation === 'add') {
node.data.value = incomingValue + node.data.amount;
} else {
node.data.value = incomingValue;
}
}
const valueAt = (id: string): number => {
const node = model.getNode(id);
return typeof node?.data.value === 'number' ? node.data.value : 0;
};
return `→ ×3=${valueAt('mul')} → +10=${valueAt('add')} → sink=${valueAt('out')}`;
}
export function watchCompute(model: DiagramModel, report: (message: string) => void): (value: number) => void {
let currentValue = Number(model.getNode('in')?.data.value ?? 0);
const refresh = (): void => report(compute(model, currentValue));
model.on('link:added', refresh);
model.on('link:removed', refresh);
refresh();
return (value: number): void => {
currentValue = value;
refresh();
};
}
The execution example sets pending, then marks one node running and completed at a time; the node status classes and link animation are rendered by Grafloria. The computing example stores each result in the live node data and prints the derived chain above the canvas. Its order is fixed for this pipeline; for a graph that branches or can be reordered, derive a topological order from your graph.
Execute a flow
Use execution statuses when your application already knows which work is ready, active, or complete. The runner remains host code; the mounted diagram displays the status changes and animated incoming link. For framework-specific mounting, see Build runnable workflows.
html<div style="height: 430px">
<button id="run" type="button">Run flow</button>
<output id="status">ready</output>
<div id="app" style="height: 380px"></div>
</div>
<script type="module">
import { render } from '@grafloria/element';
/** @type {import('@grafloria/renderer').NodeSpec[]} */
const nodes = [
{ id: 'trigger', position: { x: 60, y: 120 }, size: { width: 130, height: 54 }, label: 'Trigger' },
{ id: 'fetch', position: { x: 260, y: 120 }, size: { width: 130, height: 54 }, label: 'Fetch data' },
{ id: 'transform', position: { x: 460, y: 120 }, size: { width: 130, height: 54 }, label: 'Transform' },
];
/** @type {import('@grafloria/renderer').EdgeSpec[]} */
const edges = [
{ id: 'fetch-link', source: 'trigger', target: 'fetch' },
{ id: 'transform-link', source: 'fetch', target: 'transform' },
];
const host = document.querySelector('#app');
const button = document.querySelector('#run');
const output = document.querySelector('#status');
if (!(host instanceof HTMLElement) || !(button instanceof HTMLButtonElement) || !(output instanceof HTMLOutputElement)) {
throw new Error('The flow demo elements are missing.');
}
const instance = render({ nodes, edges }, host);
const model = instance.getModel();
button.addEventListener('click', async () => {
const order = ['trigger', 'fetch', 'transform'];
for (const id of order) model.getNode(id)?.setState({ status: 'pending' });
for (const id of order) {
const node = model.getNode(id);
if (!node) continue;
const incoming = model.getLinks().filter((link) => link.targetNodeId === id);
for (const link of incoming) link.updateStyle({ animation: { type: 'flow' } });
node.setState({ status: 'running', animateStatus: true });
output.value = `running: ${id}`;
await new Promise((resolve) => window.setTimeout(resolve, 500));
node.setState({ status: 'completed' });
for (const link of incoming) link.updateStyle({ animation: { type: 'none' } });
}
output.value = 'flow completed';
});
</script>
tsimport { AfterViewInit, Component, signal, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { DiagramModel } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
import { execute, executionEdges, executionNodes } from './flow-logic';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<button type="button" (click)="run()">Run flow</button>
<output>{{ status() }}</output>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;height:380px" />
`,
})
export class ExecuteFlowComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes: NodeSpec[] = executionNodes;
edges: EdgeSpec[] = executionEdges;
readonly status = signal('ready');
private model: DiagramModel | null = null;
ngAfterViewInit(): void {
this.model = this.canvas().activeEngine()?.getDiagram() ?? null;
}
async run(): Promise<void> {
if (this.model) await execute(this.model, (message) => { this.status.set(message); });
}
}
tsximport { component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
export default component$(() => {
const nodes: NodeSpec[] = [
{ id: 'trigger', position: { x: 60, y: 120 }, size: { width: 130, height: 54 }, label: 'Trigger' },
{ id: 'fetch', position: { x: 260, y: 120 }, size: { width: 130, height: 54 }, label: 'Fetch data' },
{ id: 'transform', position: { x: 460, y: 120 }, size: { width: 130, height: 54 }, label: 'Transform' },
];
const runningNodes: NodeSpec[] = nodes.map((node) => ({ ...node, label: `${node.label} · running` }));
const edges: EdgeSpec[] = [
{ id: 'fetch-link', source: 'trigger', target: 'fetch' },
{ id: 'transform-link', source: 'fetch', target: 'transform' },
];
const running = useSignal(false);
return (
<div style={{ height: '430px' }}>
<label><input type="checkbox" bind:checked={running} /> Run flow</label>
<output>{running.value ? 'running: all workflow stages' : 'ready'}</output>
<div style={{ height: '380px' }}>
<GrafloriaFlow nodes={running.value ? runningNodes : nodes} edges={edges} />
</div>
</div>
);
});
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import type { DiagramModel } from '@grafloria/engine';
import { execute, executionEdges, executionNodes } from './flow-logic';
export function ExecuteFlow() {
const model = useRef<DiagramModel | null>(null);
const [status, setStatus] = useState('ready');
return (
<div style={{ height: 430 }}>
<button type="button" onClick={() => {
if (model.current) void execute(model.current, setStatus);
}}>Run flow</button>
<output>{status}</output>
<div style={{ height: 380 }}>
<GrafloriaFlow defaultNodes={executionNodes} defaultEdges={executionEdges}
onInit={(instance: DiagramInstance) => { model.current = instance.getModel(); }} />
</div>
</div>
);
}
vue<script setup lang="ts"> import { shallowRef, ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; import type { DiagramModel } from '@grafloria/engine'; import { execute, executionEdges, executionNodes } from './flow-logic'; const model = shallowRef<DiagramModel | null>(null); const status = ref('ready'); function onInit(instance: DiagramInstance): void { model.value = instance.getModel(); } async function run(): Promise<void> { if (model.value) await execute(model.value, (message) => { status.value = message; }); } </script> <template> <div style="height:430px"> <button type="button" @click="run">Run flow</button> <output>{{ status }}</output> <div style="height:380px"> <GrafloriaFlow :default-nodes="executionNodes" :default-edges="executionEdges" @init="onInit" /> </div> </div> </template>
In the JavaScript sample's initial render, look for the three connected nodes and the Run flow control above the canvas.
Press Run flow in the JavaScript, Angular, React, or Vue sample to move each node through pending, running, and completed. While a node runs, its incoming link uses the flow animation; the status readout follows the same order. In Qwik, check Run flow to reconcile running annotations into the mounted nodes and update the readout; that example uses node labels rather than engine status animation. The other samples set node statuses and link styles on the mounted model.
The status value is a NodeModel state field. Grafloria renders statuses such as running, completed, error, and warning as status classes; animateStatus: false disables motion while preserving the status treatment. Link animation is set through the live link's updateStyle() method.
| Option or field | Type | Default | What it does |
|---|---|---|---|
status | 'idle' | 'pending' | 'running' | 'completed' | 'error' | 'warning' | Not specified | Supplies the node's execution state for rendering. |
animateStatus | boolean | Not specified | Set to false to keep the status appearance without its motion. |
animation.type | 'marching-ants' | 'flow' | 'pulse' | 'dash-flow' | 'none' | Not specified | Chooses the link animation; this example uses flow while the link is active and none afterward. |
The runner above is intentionally small. Add your own cancellation, retries, failure policy, or branching rules around the same model updates. If you expose a controlled React graph, wire its change callback so user edits do not get replaced by stale application state; see the React quick start.
Compute values along the live links
Use live topology when graph edits should change the computation path. The host reads the current links, calculates the downstream values, and updates a readout; typing a new source value recomputes the chain. The ports and sourceHandle / targetHandle fields declare numeric ports and the endpoints each edge names, but the arithmetic remains application code.
html<div style="height: 430px">
<label>Input <input id="source" type="number" step="1" value="2"></label>
<output id="result"></output>
<div id="app" style="height: 380px"></div>
</div>
<script type="module">
import { render } from '@grafloria/element';
/** @type {import('@grafloria/renderer').NodeSpec[]} */
const nodes = [
{ id: 'in', position: { x: 40, y: 120 }, size: { width: 120, height: 56 }, label: 'input',
ports: [{ id: 'in.out', side: 'right', type: 'output', dataType: 'number' }], data: { value: 2 } },
{ id: 'mul', position: { x: 240, y: 120 }, size: { width: 120, height: 56 }, label: '× 3',
ports: [{ id: 'mul.in', side: 'left', type: 'input', dataType: 'number' }, { id: 'mul.out', side: 'right', type: 'output', dataType: 'number' }],
data: { operation: 'multiply', factor: 3, value: 0 } },
{ id: 'add', position: { x: 440, y: 120 }, size: { width: 120, height: 56 }, label: '+ 10',
ports: [{ id: 'add.in', side: 'left', type: 'input', dataType: 'number' }, { id: 'add.out', side: 'right', type: 'output', dataType: 'number' }],
data: { operation: 'add', amount: 10, value: 0 } },
{ id: 'out', position: { x: 640, y: 120 }, size: { width: 120, height: 56 }, label: 'sink',
ports: [{ id: 'out.in', side: 'left', type: 'input', dataType: 'number' }], data: { operation: 'sink', value: 0 } },
];
/** @type {import('@grafloria/renderer').EdgeSpec[]} */
const edges = [
{ id: 'l1', source: 'in', target: 'mul', sourceHandle: 'in.out', targetHandle: 'mul.in' },
{ id: 'l2', source: 'mul', target: 'add', sourceHandle: 'mul.out', targetHandle: 'add.in' },
{ id: 'l3', source: 'add', target: 'out', sourceHandle: 'add.out', targetHandle: 'out.in' },
];
const host = document.querySelector('#app');
const sourceInput = document.querySelector('#source');
const output = document.querySelector('#result');
if (!(host instanceof HTMLElement) || !(sourceInput instanceof HTMLInputElement) || !(output instanceof HTMLOutputElement)) {
throw new Error('The computing-flow demo elements are missing.');
}
const instance = render({ nodes, edges }, host);
const model = instance.getModel();
let sourceValue = 2;
const recompute = () => {
const source = model.getNode('in');
if (source) source.data.value = sourceValue;
for (const id of ['mul', 'add', 'out']) {
const node = model.getNode(id);
if (!node) continue;
const link = model.getLinks().find((item) => item.targetNodeId === id);
const sourceId = link?.sourceNodeId;
const upstream = sourceId ? model.getNode(sourceId) : undefined;
if (!upstream || typeof upstream.data.value !== 'number') continue;
if (node.data.operation === 'multiply') node.data.value = upstream.data.value * node.data.factor;
else if (node.data.operation === 'add') node.data.value = upstream.data.value + node.data.amount;
else node.data.value = upstream.data.value;
}
/** @param {string} id */
const valueAt = (id) => model.getNode(id)?.data.value ?? 0;
output.value = `→ ×3=${valueAt('mul')} → +10=${valueAt('add')} → sink=${valueAt('out')}`;
};
sourceInput.addEventListener('input', () => {
const value = Number(sourceInput.value);
sourceValue = Number.isFinite(value) ? value : 0;
recompute();
});
model.on('link:added', recompute);
model.on('link:removed', recompute);
recompute();
</script>
tsimport { AfterViewInit, Component, signal, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { DiagramModel } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
import { computeEdges, computeNodes, watchCompute } from './flow-logic';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<label>Input <input type="number" step="1" value="2" (input)="update($event)" /></label>
<output>{{ result() }}</output>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;height:380px" />
`,
})
export class ComputingFlowsComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes: NodeSpec[] = computeNodes;
edges: EdgeSpec[] = computeEdges;
readonly result = signal('');
private model: DiagramModel | null = null;
private setInput: ((value: number) => void) | null = null;
ngAfterViewInit(): void {
this.model = this.canvas().activeEngine()?.getDiagram() ?? null;
if (this.model) this.setInput = watchCompute(this.model, (message) => { this.result.set(message); });
}
update(event: Event): void {
const input = event.target;
if (input instanceof HTMLInputElement) this.setInput?.(Number(input.value));
}
}
tsximport { component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
export default component$(() => {
const nodes: NodeSpec[] = [
{
id: 'in', position: { x: 40, y: 120 }, size: { width: 120, height: 56 }, label: 'input',
ports: [{ id: 'in.out', side: 'right', type: 'output', dataType: 'number' }], data: { value: 2 },
},
{
id: 'mul', position: { x: 240, y: 120 }, size: { width: 120, height: 56 }, label: '× 3',
ports: [
{ id: 'mul.in', side: 'left', type: 'input', dataType: 'number' },
{ id: 'mul.out', side: 'right', type: 'output', dataType: 'number' },
], data: { operation: 'multiply', factor: 3, value: 0 },
},
{
id: 'add', position: { x: 440, y: 120 }, size: { width: 120, height: 56 }, label: '+ 10',
ports: [
{ id: 'add.in', side: 'left', type: 'input', dataType: 'number' },
{ id: 'add.out', side: 'right', type: 'output', dataType: 'number' },
], data: { operation: 'add', amount: 10, value: 0 },
},
{
id: 'out', position: { x: 640, y: 120 }, size: { width: 120, height: 56 }, label: 'sink',
ports: [{ id: 'out.in', side: 'left', type: 'input', dataType: 'number' }], data: { operation: 'sink', value: 0 },
},
];
const edges: EdgeSpec[] = [
{ id: 'l1', source: 'in', target: 'mul', sourceHandle: 'in.out', targetHandle: 'mul.in' },
{ id: 'l2', source: 'mul', target: 'add', sourceHandle: 'mul.out', targetHandle: 'add.in' },
{ id: 'l3', source: 'add', target: 'out', sourceHandle: 'add.out', targetHandle: 'out.in' },
];
const input = useSignal('2');
return (
<div style={{ height: '430px' }}>
<label>Input <input type="number" step={1} bind:value={input} /></label>
<output>→ ×3={Number(input.value) * 3} → +10={Number(input.value) * 3 + 10} → sink={Number(input.value) * 3 + 10}</output>
<div style={{ height: '380px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
</div>
</div>
);
});
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import type { DiagramModel } from '@grafloria/engine';
import { computeEdges, computeNodes, watchCompute } from './flow-logic';
export function ComputingFlows() {
const setInput = useRef<((value: number) => void) | null>(null);
const [result, setResult] = useState('');
return (
<div style={{ height: 430 }}>
<label>Input <input type="number" step={1} defaultValue={2}
onChange={(event) => setInput.current?.(Number(event.currentTarget.value))} /></label>
<output>{result}</output>
<div style={{ height: 380 }}>
<GrafloriaFlow defaultNodes={computeNodes} defaultEdges={computeEdges}
onInit={(instance: DiagramInstance) => {
const model: DiagramModel = instance.getModel();
setInput.current = watchCompute(model, setResult);
}} />
</div>
</div>
);
}
vue<script setup lang="ts"> import { ref, shallowRef } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; import type { DiagramModel } from '@grafloria/engine'; import { computeEdges, computeNodes, watchCompute } from './flow-logic'; const result = ref(''); const setInput = shallowRef<((value: number) => void) | null>(null); function onInit(instance: DiagramInstance): void { const model: DiagramModel = instance.getModel(); setInput.value = watchCompute(model, (message) => { result.value = message; }); } function update(event: Event): void { const input = event.target; if (input instanceof HTMLInputElement) setInput.value?.(Number(input.value)); } </script> <template> <div style="height:430px"> <label>Input <input type="number" step="1" value="2" @input="update" /></label> <output>{{ result }}</output> <div style="height:380px"> <GrafloriaFlow :default-nodes="computeNodes" :default-edges="computeEdges" @init="onInit" /> </div> </div> </template>
In the JavaScript sample's initial render, look for the calculated readout above the canvas: the source value flows through ×3 and +10 to the sink.
The JavaScript, Angular, React, and Vue readouts start with the graph's initial value (2), then update after input events and link additions or removals by reading the current connections. If a node no longer has an incoming value, these samples leave its previous value in place; define a reset or missing-input policy that matches your application. The Qwik readout recalculates when its bound input changes for this fixed three-stage pipeline; it does not subscribe to rewiring.
| Field | Type | Default | What it does |
|---|---|---|---|
source / target | string | Required by EdgeSpec | Identifies the source and target nodes of a graph edge. |
sourceHandle / targetHandle | string | Optional | Pins an edge to the declared output and input port ids. |
Port dataType | string | Not specified | Labels each port's value type; the example uses number at both ends of every connection. |
The links and typed ports describe the graph; they do not execute the arithmetic. Keep computation and missing-input rules in your application, and in the JavaScript, Angular, React, and Vue variants recalculate from the live model when the source value or topology changes. For port configuration and connection rules, see Configure ports and Validate connections.
Was this page helpful?