flow-ngx
v0.5.2
Published
FlowNGX — Visual process design and execution for Angular. Diagram canvas, typed process models, validation, step-by-step execution engine, undo/redo and JSON import/export.
Maintainers
Readme
FlowNGX
Visual process design and execution for Angular.
🚀 Live demo & docs → flw.136.67.40.6.nip.io — designer, telecom sample workflows (simple → very complex), runs portal with replays, and full documentation.
FlowNGX is an Angular-native library for drawing process diagrams and executing them step by step — a lightweight SVG canvas, strongly typed process models, a framework-independent execution engine, and signal-based state, with no external diagramming dependency.
npm install flow-ngxRequires Angular ≥ 22.
Features
- 🎨 Diagram canvas — SVG rendering, drag & drop, pan, zoom-to-cursor, configurable snap grid
- 🧩 Node types — start, task, decision, end; connections with labels and branch conditions
- 🔀 Edge routing — curved beziers or right-angle ("square") routing with circuit-style hop arcs where connections cross, switchable at runtime
- 🖌 Style profiles — classic diagram-tool palettes and icon sets (
default, telecom-flavouredtelecom), selectable per node; themable via config or CSS custom properties (switching profiles never restyles an existing diagram) - ✅ Validation — structural rules with typed issue codes (missing start/end, dangling connections, unreachable nodes, ambiguous decision branches, …)
- ▶️ Execution engine — pure TypeScript, run-all or step-by-step ("next"), pluggable node handlers and decision resolvers, per-node retry policies (
data.retry), chunked batch progress (reportProgress), execution counts and timings - 🌐 Declarative HTTP tasks — put
data.http({ url, method, params, headers, body }) on a node with{{variable}}templating and the built-in handler executes the call (execution.httpTasksorcreateHttpTaskHandler()) - 🔦 Runtime visuals — current-node highlight, per-state styling (idle / running / completed / failed / skipped), taken-path coloring, alternate flows dimmed or hidden via a toggle, count badges and progress bars
- 📜 History & events — full execution history, typed event callbacks
- ⏪ Runs vs replays — a live run produces a versioned run snapshot (process document + recorded context); a replay (
replayContext) animates that recording without executing anything - 🔢 Content revisions — documents carry a workflow
revisionand per-layout revisions (BPM-deployment style); every run is stamped with the workflow revision it executed against - 🗺 Named multi-layouts — several arrangements of one workflow (
presentations+activeLayout, BPMN-DI style); recordings embed only the active layout - 🎚 Feature switches — limit what users can do (
featuresconfig: editing, execution, zoom/pan, undo, import/export, styling) - ↩️ Undo / redo, 🔒 read-only mode, 📄 JSON import/export with schema-validated, logic/presentation-separated documents
Quick start
import { Component } from '@angular/core';
import { FlowCanvasComponent, ProcessDefinition, provideFlowNgx } from 'flow-ngx';
@Component({
selector: 'app-designer',
imports: [FlowCanvasComponent],
providers: [provideFlowNgx({ styleProfile: 'telecom' })],
template: `<flow-canvas #canvas [definition]="process" (executionEvent)="log($event)" />`,
})
export class DesignerComponent {
process: ProcessDefinition = {
schemaVersion: 2,
id: 'hello',
name: 'Hello process',
nodes: [
{ id: 'start', type: 'start', label: 'Start', position: { x: 40, y: 100 } },
{ id: 'work', type: 'task', label: 'Do work', position: { x: 220, y: 92 } },
{ id: 'end', type: 'end', label: 'Done', position: { x: 440, y: 100 } },
],
connections: [
{ id: 'c1', sourceId: 'start', targetId: 'work' },
{ id: 'c2', sourceId: 'work', targetId: 'end' },
],
};
log(event: unknown) {
console.log(event);
}
}The canvas exposes its per-instance services for full programmatic control:
@ViewChild(FlowCanvasComponent) canvas!: FlowCanvasComponent;
// Editing
this.canvas.diagram.addNode('task', { x: 100, y: 100 });
this.canvas.diagram.undo();
const json = this.canvas.diagram.exportJson();
// Execution
this.canvas.execution.registerTypeHandler('task', async (node, api) => {
api.variables['result'] = await doRealWork(node);
});
await this.canvas.execution.runAll({ creditCheck: 'yes' });
this.canvas.execution.runtimeOf('work').executionCount; // how often it ranArchitecture
| Layer | Contents | Angular? |
| ------------- | ----------------------------------------------------------------------- | ---------- |
| core/ | models, JSON serialization, validator, execution engine, style profiles | none |
| state/ | FlowDiagramService, FlowHistoryService, FlowExecutionService | signals |
| components/ | flow-canvas, flow-node, flow-connection | standalone |
| config/ | provideFlowNgx(), FLOW_NGX_CONFIG, theming | DI |
The core layer has no Angular or RxJS imports — the engine also runs in plain Node/worker contexts.
Configuration
provideFlowNgx({
styleProfile: 'telecom', // or 'default', or a custom NodeStyleProfile
grid: { visible: true, size: 20, snap: true },
execution: { maxSteps: 1000, stepDelayMs: 600 },
connections: { routing: 'orthogonal', crossingHops: true, cornerRadius: 8 },
serialization: { separatePresentation: true, includePresentation: true },
features: {
editing: true,
execution: true,
zoomAndPan: true,
undoRedo: true,
importExport: true,
styling: true,
},
showAlternateFlows: true, // dim (true) or hide (false) branches not taken
readonly: false,
theme: { connectionTakenColor: '#16a34a' /* …see FlowThemeConfig */ },
});
// Or per canvas — everything defaults, pass any partial:
// <flow-canvas [definition]="def" [config]="{ connections: { routing: 'orthogonal' } }" />Every theme value is applied as a CSS custom property (--flow-*) on the
canvas, so plain CSS overrides work too.
Process JSON (schema v2)
Documents separate logic from presentation, BPMN-style, and validate
against
schema/process-definition.schema.json
(shipped in this package). definition is the executable part; presentation
(layout, styling, preferred profile) is optional, replaceable, and
independently versioned — a presentation from a newer version is ignored
(nodes auto-lay-out) rather than failing the document. The legacy v1 inline
format is still imported transparently, and exports can be switched to v1 or
to logic-only via the serialization config.
{
"schemaVersion": 2,
"id": "sim-activation",
"name": "SIM Activation",
"definition": {
"nodes": [
{ "id": "start", "type": "start", "label": "Start" },
{
"id": "credit-check",
"type": "decision",
"label": "Credit OK?",
"data": { "variable": "creditCheck" }
},
{ "id": "end", "type": "end", "label": "Done" }
],
"connections": [
{ "id": "c1", "sourceId": "start", "targetId": "credit-check" },
{
"id": "c2",
"sourceId": "credit-check",
"targetId": "end",
"label": "Approved",
"condition": "yes"
}
]
},
"presentation": {
"version": 1,
"profile": "telecom",
"nodes": {
"start": { "position": { "x": 60, "y": 220 } },
"credit-check": {
"position": { "x": 690, "y": 202 },
"color": "orange",
"icon": "question"
},
"end": { "position": { "x": 1380, "y": 220 } }
}
}
}exportProcessLogic(definition) produces a logic-only document for backends;
parseProcessDocument(json) returns the runtime model plus the preferred
style profile.
Run snapshots
A recorded execution is its own document type
(schema/run-snapshot.schema.json):
{ snapshotVersion, definition, context }. One format serves both verbs — a
run (live, handlers execute) produces a snapshot via
exportRunSnapshot(definition, context); a replay consumes one via
parseRunSnapshot(json) + execution.replayContext(context), animating the
recorded event history without executing anything. Snapshots embed their
process document, so they replay faithfully even after the process changes.
Static replay is also fully declarative — one binding builds the whole view:
<!-- The canvas fetches, parses, draws and restores by itself: -->
<flow-canvas snapshotUrl="/api/runs/42" [readonlyMode]="true" />
<!-- Or bind the snapshot value directly; add [snapshotReplay] to animate: -->
<flow-canvas
[snapshot]="snap()"
[snapshotReplay]="{ stepDelayMs: 400 }"
(snapshotError)="toast($event)"
/>[snapshot] accepts the envelope object, a parsed { definition, context }
pair, or the raw JSON string. Without [snapshotReplay] the final state is
shown instantly (showContext); with it the history animates
(replayContext). snapshotUrl fetches through your app's HttpClient
when one is provided (interceptors, auth, XSRF apply) and falls back to
plain fetch otherwise.
Decision semantics: the engine reads the variable named by the decision node's
data.variable, stringifies its value and takes the outgoing connection whose
condition matches. One condition-less branch per decision acts as the
else-branch. A custom DecisionResolver can replace this behaviour.
License
Apache License 2.0 — see LICENSE.
