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:
bashnpm 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
jsimport { 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);
tsimport { 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`;
}
}
tsximport { 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>
);
});
tsximport { 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<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 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](/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-models-nodemodel#nodemodel))[] | undefined` | undefined | Controlled 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` | 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
approvedvalue 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.
Related
- Execute and compute flows — separate workflow execution from the graph that displays it.
- Add nodes from palettes — let users build the graph interactively.
- The instance and data flow — understand how specs reconcile into the live model.
Was this page helpful?