A spec is plain data that describes your diagram's intent; a live model is the identity-bearing object that holds that data while the diagram runs.
Follow a spec update through reconciliation to see what changes and which live objects retain their identity; see Introduction for the model, engine and instance entry points.
From intent to live objects
The shared input layer converts specs into models and reconciles subsequent spec lists against those models. You do not need to construct engine objects to describe a flow.
mermaidflowchart LR S["Plain specs"] --> B["Framework binding or renderer instance"] B --> M["Live diagram model"] E["Engine: commands, layout, validation"] --> M M --> R["Renderer: visible geometry"] B --> R
| Intent you supply | Live object | What it holds |
|---|---|---|
NodeSpec | NodeModel | Type, position, size, payload, metadata and ports |
PortSpec | PortModel | Direction, side, glyph, data type and connection constraints |
EdgeSpec | LinkModel | Endpoints, routing, connector, labels and bends |
GroupSpec | GroupModel | Membership and frame geometry |
Give nodes and edges explicit, stable id values when you intend to update them. For a plain spec with an existing id, reconciliation updates the existing object rather than constructing another one. Entries without ids receive node-<index> or edge-<index> ids, so their identity depends on their position in the list.
Payload and metadata have different jobs
Put application payload in data, such as an order status or domain identifier. Put diagram-adjacent settings in metadata. The node's top-level label, sublabel and shape fields are conveniences for metadata.label, metadata.sublabel and metadata.shape; they are not writes to data.
On the node update path, a supplied data object replaces the payload dictionary. Metadata entries are applied by key. An omitted position leaves an existing node where it is, and an omitted selected leaves the user's selection alone.
Describe a flow, then query its live identity
Run this module in the browser. It creates a sized container and mounts the flow with render(). It supplies two boxes, a labeled connection and a fitted zone, then changes the first box's label without replacing its live node.
Install the packages the module imports:
bashnpm install @grafloria/element @grafloria/renderer @grafloria/engine
tsimport { render } from '@grafloria/element';
import type {
EdgeSpec,
GroupSpec,
NodeSpec,
PortSpec,
} from '@grafloria/renderer';
export function configureFlow() {
const output: PortSpec = {
id: 'intake-out', side: 'right', type: 'output',
};
const input: PortSpec = {
id: 'review-in', side: 'left', type: 'input',
};
const nodes: NodeSpec[] = [
{
id: 'intake', label: 'Intake',
position: { x: 80, y: 100 },
size: { width: 150, height: 60 },
data: { orderId: 'order-42', status: 'received' },
metadata: { domain: 'orders' },
ports: [output],
},
{
id: 'review', label: 'Review',
position: { x: 360, y: 100 },
size: { width: 150, height: 60 },
ports: [input],
},
];
const edges: EdgeSpec[] = [{
id: 'handoff', source: 'intake', target: 'review',
sourceHandle: 'intake-out', targetHandle: 'review-in',
type: 'orthogonal', label: 'submit',
}];
const groups: GroupSpec[] = [{
id: 'stage', label: 'Order processing',
children: ['intake', 'review'], padding: 30,
style: { fill: '#f3f4f6', stroke: '#d7dbe0' },
}];
const container = document.createElement('div');
container.style.height = '400px';
document.body.appendChild(container);
const instance = render({ nodes, edges, groups }, container);
const model = instance.getModel();
const intakeBefore = model.getNode('intake');
instance.setNodes(nodes.map(node =>
node.id === 'intake' ? { ...node, label: 'Received' } : node
));
instance.fitView();
return {
sameNode: intakeBefore !== undefined &&
intakeBefore === model.getNode('intake'),
outputPort: model.getPortById('intake-out'),
handoff: model.getLink('handoff'),
intakeIsMember: model.getGroup('stage')?.members.has('intake') ?? false,
};
}
const result = configureFlow();
console.log(result);
The container has a height of 400px. The resulting diagram shows Received connected to Review inside Order processing. The returned sameNode is true; outputPort and handoff are live objects, and intakeIsMember is true. The setters return void; query the model when you need the objects they created.
These setters reconcile whole lists, not individual patches: an omitted node or link is removed. Removing a group through setGroups() keeps its boxes. For user-facing edits that belong on the undo stack, use commands and history rather than treating reconciliation as an editing command.
Geometry is intent, not a drawing instruction
Omit ports to get four deterministic bidirectional ports: top, right, bottom and left. Their ids use <nodeId>__<side>. Supply a port list when you need named endpoints, as the sample does. A handle pins an edge to a port; a bare side such as 'right' also resolves to a port on that side.
In the sample, named handles resolve the edge spec to live ports, and the returned handoff exposes the resulting live link; see How Grafloria works for endpoint and geometry intent. See route and label edges and the live edge demos.
A group is membership, not merely a rectangle behind nodes. Its children become members. bounds pins the frame; without it, reconciliation fits the frame around the children using padding (default 20). Members travel with their group. See group and nest nodes and the live group demos.
Specs are not the persistence format
The instance also accepts live nodes and links: NodeInput is NodeSpec | NodeModel, and EdgeInput is EdgeSpec | LinkModel. The repository's Mermaid viewer passes parsed nodes, links and groups directly to the renderer instead of reducing them to a smaller spec projection.
Save the live model in the shared, versioned document format, not a framework's projection. Read documents and kits for that boundary, and state and event flow for returning edits to controlled application state.
Was this page helpful?