A bpmnlint plugin that keeps BPMN diagrams clean: a few Miragon naming and layout conventions, plus the standard structural rules and the Camunda 7 / 8 deployability rules bundled in — so you don't wire up bpmnlint's plugins yourself.
A BPMN diagram can be well-formed and still be wrong — a missing start event, a construct the engine
rejects at deploy, or forty tasks named Activity_0049ryx that no reviewer can read. Linting catches
that automatically, wherever BPMN is written:
- In the modeler — live feedback while you draw, so a mistake is flagged the moment you make it.
- In CI — a merge/deploy gate, so a broken model never ships.
- For AI — agents emit valid-but-off models (machine IDs, orphan nodes, engine-invalid constructs); lint their output and block on error, instead of hoping a human catches it.
Most teams already have these conventions — in a wiki, or in one reviewer's head. This plugin makes them executable.
Install the plugin together with bpmnlint (its peer dependency):
npm install --save-dev @miragon/bpmnlint-plugin-rules bpmnlintAdd the plugin to your .bpmnlintrc and extend one of its configs, like any other plugin:
It ships three configs — pick one by who's modeling and why:
plugin:@miragon/rules/recommended-for-modeling— for purely business/technical models, and for modeler applications that surface linting to their users. Layout hints only: bpmnlint'sstandard-sizeand the layout rules atwarn, the naming/id rules off — a modeler is never blocked on execution-only conventions.plugin:@miragon/rules/recommended-for-automation— for developers automating processes, locally and in CI, on models wired up to a Camunda engine. Every Miragon rule on but as a non-blockingwarn(standard-sizetoo), so ids and layout get flagged without failing the build.plugin:@miragon/rules/all— every Miragon rule aterror, engine-agnostic. The strict gate to opt into when you want findings to fail the build.
Then lint a diagram:
npx bpmnlint diagram.bpmnThe Miragon conventions this plugin adds — each with a docs page and a good/bad example:
| Rule | What it catches |
|---|---|
@miragon/rules/no-generated-ids |
IDs generated rather than chosen (Activity_0049ryx, StartEvent_1) |
@miragon/rules/element-id-naming |
IDs that don't follow the type-prefix + case convention |
@miragon/rules/flow-through-element |
A sequence flow routed through an unrelated shape's body |
@miragon/rules/flow-connection-side |
A sequence flow docked onto the wrong side of a shape |
@miragon/rules/flow-target-alignment |
A flow's target drawn off the row, sloping the main path |
@miragon/rules/flow-crossing |
Two sequence flows whose drawn paths cross each other |
@miragon/rules/flow-orthogonal |
A sequence flow that runs diagonally instead of horizontal and vertical |
Turn any of them on individually, the usual way:
{
"extends": ["bpmnlint:recommended", "plugin:@miragon/rules/recommended-for-modeling"],
"rules": {
"@miragon/rules/no-generated-ids": "error",
"@miragon/rules/element-id-naming": "warn",
},
}Structural checks — start/end events, connectivity, element sizing — come from bpmnlint's own
bpmnlint:recommended. The
Miragon set grows over time; see CONTRIBUTING.md to propose a rule.
Lint every model as a merge gate — bpmnlint exits non-zero on a finding, failing the build:
npx bpmnlint 'models/**/*.bpmn'Deploying to a Camunda engine? Add its deployability layer (bundled — no separate install) to your
extends. These rules read engine-specific properties, so bpmnlint must also parse them with the
matching moddle extension — pick the config for your engine below.
The moddleExtensions line is not optional. Without it the parser doesn't recognize the engine
namespace, so those properties are invisible to the rules — they then report false findings (for
example a service task flagged as missing its task definition even though it has one). A modeler loads
the moddle already; it's the bare npx bpmnlint CLI that needs this line. Prefer to skip the wiring?
The programmatic getDefaultLintConfig({ engine }) below sets up both the layer and its moddle
extension for you.
// .bpmnlintrc
{
"extends": [
"bpmnlint:recommended",
"plugin:@miragon/rules/recommended-for-automation",
"plugin:camunda-compat/camunda-platform-7-24",
],
"moddleExtensions": { "camunda": "camunda-bpmn-moddle/resources/camunda.json" },
}// .bpmnlintrc
{
"extends": [
"bpmnlint:recommended",
"plugin:@miragon/rules/recommended-for-automation",
"plugin:camunda-compat/camunda-cloud-8-10",
],
"moddleExtensions": { "zeebe": "zeebe-bpmn-moddle/resources/zeebe.json" },
}Building a linter in code — a modeler, a CI script, an agent loop? Skip .bpmnlintrc and use the
bundled resolver. It carries every layer (structural + Camunda + Miragon), so there's nothing else
to wire up, and it works offline:
import BpmnModdle from 'bpmn-moddle';
import Linter from 'bpmnlint/lib/linter';
import { createBundledResolver, getDefaultLintConfig } from '@miragon/bpmnlint-plugin-rules';
const { rootElement } = await new BpmnModdle().fromXML(xml);
const linter = new Linter({
config: getDefaultLintConfig({ engine: 'c8' }), // 'c7' | 'c8' — omit for structural-only
resolver: createBundledResolver(),
});
const results = await linter.lint(rootElement);engine picks the Camunda deployability layer + typed moddle; preset picks the Miragon opinion
layer independently — 'modeling' (layout hints only, safe on hand-drawn diagrams) or
'automation' (every Miragon rule at error). When preset is omitted it defaults to
'automation' for an engine-bound config and 'modeling' otherwise. Pass both to decouple them —
e.g. a modeler that wants Camunda 8's typed properties and deployability checks but the relaxed
modeling opinion layer:
getDefaultLintConfig({ engine: 'c8', preset: 'modeling' });Results are keyed by rule; each finding has a category (error | warn). Block on any error — a
CI gate, or the reject signal that sends an AI agent back to fix its output:
const errors = Object.values(results)
.flat()
.filter((finding) => finding.category === 'error');
if (errors.length) process.exit(1);Every rule factory, rule-set and helper is exported from the package root as well.
bpmnlint lints one model at a time, so it is the right fit for anything stylistic or about the correctness of a single diagram. It checks naming conventions, layout, structural soundness, and whether a construct is deployable to a given engine.
However, it cannot answer engine- and automation-specific questions that span a running process, because those depend on more than the diagram in front of it. Whether a call activity passes every variable its child process needs, or whether the technical wiring is correct, is something a model-in-isolation linter simply cannot see. For those cases, and when you want to keep such tests alongside your model in the same build (for example a Gradle task), Miragon/bpmn-to-code and its process testing are the better tool.
- Miragon BPMN Modeler — in-editor linting via the programmatic API, flagging issues live while you model.
See CONTRIBUTING.md.
MIT © Miragon GmbH.