Coding agents
Agents write machine ids, orphan nodes and engine-invalid constructs into models that still parse. The fix is the same one a human gets: run the linter and read the findings. A coding agent can do that itself, it only has to be told when.
Once the plugin is set up, npx bpmnlint is all an agent needs. Pick the option your harness supports. They combine well: instructions for the habit, a hook for the guarantee, CI as the backstop.
Tell the agent in its instructions
Works with every harness that reads a project instruction file: AGENTS.md, CLAUDE.md, .cursor/rules, .github/copilot-instructions.md.
## BPMN
After creating or changing a `.bpmn` file, run `npx bpmnlint <file>`.
Fix every reported error before you finish. Treat warnings as review
comments: fix them unless there is a reason not to, and say why.
Never silence a rule in `.bpmnlintrc` to get a clean run.This relies on the agent following the instruction. It usually does, and nothing enforces it.
Enforce it with a hook
A hook runs the linter for the agent, every time, and hands the findings straight back. In Claude Code that is a PostToolUse hook on file edits:
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/lint-bpmn.sh"
}
]
}
]
}
}#!/usr/bin/env bash
# .claude/hooks/lint-bpmn.sh
file=$(jq -r '.tool_input.file_path // empty')
[[ "$file" == *.bpmn ]] || exit 0
npx bpmnlint "$file" >&2 || exit 2Exit code 2 tells Claude Code to show the linter output to the agent, which then fixes the model and triggers the hook again. Other harnesses with post-edit hooks work the same way: lint the changed file, return the findings on failure.
Keep CI as the backstop
Whatever happens in the session, the merge gate lints every model again. An agent that skipped the instruction still cannot ship a broken model.
Which findings block
npx bpmnlint exits non-zero only on error findings. With the recommended presets the Miragon naming and layout findings are warn: the agent sees them, the hook does not block on them. To make them blocking for agents, extend plugin:@miragon/rules/all in the config the agent lints with. See Presets.
Building your own agent
No coding harness, but your own pipeline that generates BPMN from a model call? Use the programmatic API and feed the errors back as the next prompt. generateModel stands for your model call.
import BpmnModdle from 'bpmn-moddle';
import Linter from 'bpmnlint/lib/linter';
import {
createBundledResolver,
getDefaultLintConfig,
} from '@miragon/bpmnlint-plugin-rules';
const config = getDefaultLintConfig({ engine: 'c8' });
const moddle = new BpmnModdle(config.moddleExtensions);
const linter = new Linter({
config,
resolver: createBundledResolver(),
});
async function errorsIn(xml: string) {
const { rootElement } = await moddle.fromXML(xml);
const results = await linter.lint(rootElement);
return Object.entries(results).flatMap(([rule, findings]) =>
findings
.filter((finding) => finding.category === 'error')
.map(
(finding) => `${rule} on <${finding.id}>: ${finding.message}`,
),
);
}
let xml = await generateModel(task);
for (let attempt = 0; attempt < 3; attempt++) {
const errors = await errorsIn(xml);
if (errors.length === 0) break;
xml = await generateModel(
`${task}\n\nFix these lint errors:\n${errors.join('\n')}`,
);
}