Skip to content
D
Documentation

Execute and compute flows

how-to
4 min readUpdated

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.

ts
import 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>

In the JavaScript sample's initial render, look for the three connected nodes and the Run flow control above the canvas.

The JavaScript sample mounts the connected three-node workflow and Run flow control.

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 canvas starts with Trigger, Fetch data, and Transform connected below the Run flow control.

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 fieldTypeDefaultWhat it does
status'idle' | 'pending' | 'running' | 'completed' | 'error' | 'warning'Not specifiedSupplies the node's execution state for rendering.
animateStatusbooleanNot specifiedSet to false to keep the status appearance without its motion.
animation.type'marching-ants' | 'flow' | 'pulse' | 'dash-flow' | 'none'Not specifiedChooses 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.

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>

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 readout shows the input value propagated through multiplication, addition, and 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.

The input readout shows the calculated values through the multiply, add, and sink nodes.
FieldTypeDefaultWhat it does
source / targetstringRequired by EdgeSpecIdentifies the source and target nodes of a graph edge.
sourceHandle / targetHandlestringOptionalPins an edge to the declared output and input port ids.
Port dataTypestringNot specifiedLabels 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?