Validation
A bpmnlint-style rule engine runs inside @oge-ui/bpmn-engine: missing start or end events, disconnected and unreachable nodes, gateways that neither fork nor join, exclusive-gateway flows without a condition, implicit splits and joins, missing labels, duplicate ids, sub-processes without a start event, message flows inside one pool and detached boundary events. Turn on live linting for badges and a problems panel, add your own rules, or call validate() — the same pure functions run on a server or in CI.
Live validation
[lint]="true" re-validates after every change. Offending shapes, pools and flows get a severity badge — the badge is aria-hidden and the problem text is appended to the element's accessible name instead — and the header grows a Problems toggle with the count. The problems panel lists every issue, errors first; click or Enter on a row selects the element and pans it into view. Fix the gateway conditions in the properties panel and watch the count drop.
import { ChangeDetectionStrategy, Component, afterNextRender, signal, viewChild } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
import type { OgeBpmnLintChangedEvent } from '@oge-ui/bpmn';
declare const PROCESS_XML: string; // e.g. fetched from your API
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- [lint] validates on every change: badges on offending shapes (the
problem text joins their accessible name), a Problems toggle in the
header and a problems panel — Enter on a row selects the element. -->
<oge-bpmn-editor
#editor
style="height: 460px"
[lint]="true"
(lintChanged)="onLint($event)"
/>
<p>{{ summary() }}</p>
`,
})
export class Demo {
private readonly editor = viewChild.required(OgeBpmnEditor);
protected readonly summary = signal('');
constructor() {
afterNextRender(() => void this.editor().importXml(PROCESS_XML));
}
protected onLint(event: OgeBpmnLintChangedEvent): void {
this.summary.set(`${event.errors} errors, ${event.warnings} warnings`);
}
}Custom rules & overrides
[lintRules] merges with the built-ins: a full rule ({ id, severity, check(model, context) }) is added — or replaces the built-in with the same id — and { id, severity } re-grades a built-in ('off' disables it). Here a custom info rule asks for documentation on user and service tasks, and the label rule is downgraded to info.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBpmnEditor } from '@oge-ui/bpmn';
import type { OgeBpmnLintRulesInput } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-bpmn-editor style="height: 460px" [lint]="true" [lintRules]="rules" />
`,
})
export class Demo {
// a full rule is added (or replaces the built-in with the same id);
// { id, severity } re-grades a built-in, severity: 'off' disables it
protected readonly rules: OgeBpmnLintRulesInput = [
{
id: 'task-needs-documentation',
severity: 'info',
check: (model) =>
Object.values(model.nodes)
.filter(
(node) =>
(node.type === 'userTask' || node.type === 'serviceTask') &&
(node.documentation ?? '').trim() === '',
)
.map((node) => ({
elementId: node.id,
message: 'Document what this task does',
})),
},
{ id: 'label-required', severity: 'info' },
];
}validate() & headless checks
validate() runs the effective rules on demand and returns OgeBpmnLintIssue[] — no live linting needed. The engine function behind it, lintBpmnDiagram(model, rules?), takes any model from readBpmnXml(), so the same checks gate a save endpoint or a CI job over stored diagrams.
import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core';
import { OgeBpmnEditor, lintBpmnDiagram, readBpmnXml } from '@oge-ui/bpmn';
import type { OgeBpmnLintIssue } from '@oge-ui/bpmn';
@Component({
selector: 'demo-root',
imports: [OgeBpmnEditor],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- validate() runs the rules on demand — no live linting needed -->
<oge-bpmn-editor #editor style="height: 420px" />
<button type="button" (click)="check()">Validate</button>
@for (issue of issues(); track issue.key) {
<p>{{ issue.severity }}: {{ issue.message }} ({{ issue.elementId }})</p>
}
`,
})
export class Demo {
private readonly editor = viewChild.required(OgeBpmnEditor);
protected readonly issues = signal<readonly OgeBpmnLintIssue[]>([]);
protected check(): void {
this.issues.set(this.editor().validate());
}
/** The same rules without an editor — a CI gate over stored diagrams. */
protected headless(xml: string): number {
const model = readBpmnXml(xml).model;
return model === null
? 0
: lintBpmnDiagram(model).filter((i) => i.severity === 'error').length;
}
}