AI agent code skill to generate valid BPMN XML files.
SaferSkills independently audited bpmn-xml-generator (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Produces valid BPMN 2.0 XML parseable by bpmn-moddle and executable by BPMN 2.0-compliant process engines.
This skill targets engines that parse BPMN via `bpmn-moddle` (the bpmn-io library), such as Camunda and Flowable-compatible engines. Key facts that govern every file you generate:
| Fact | Detail |
|---|---|
| Root namespace | xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" |
| Root element | <definitions> with mandatory id and targetNamespace (any valid URI; not read by the engine — it is required by the BPMN 2.0 XML schema only) |
| Each process | <process id="..." isExecutable="true"> |
| Conditions on flows | <conditionExpression>{{expression}}</conditionExpression> (engine-specific; verify with your engine — FEEL is the BPMN-standard alternative) |
| Assignees / dynamic values | {{variables.fieldName}} inside string attributes |
| DMN integration | <businessRuleTask camunda:decisionRef="decision-id"> — decisionRef must match the id of a DMN definition already registered in the engine |
| IDs | All elements must have a unique id attribute. Convention: snake_case or PascalCase_N |
isExecutable | Always true for processes intended for execution |
Never generate a BPMN file from an incomplete description. If the user's prompt is missing any of the items below, ask them before proceeding. Ask all missing items in a single response (not one at a time).
order-approval). Must be URL-safe, lowercase, hyphen-separated.implementation type? (webhook or http)PT1H, 2026-12-01T09:00:00Z, R3/PT24H)id of the DMN definition that must already exist in the engine? (That id goes in camunda:decisionRef.)dataInputAssociation)userTask, which variables should the frontend see? (maps to dataInput/dataOutput in ioSpecification)BPMNDiagram section), required by tools like bpmn.io? If yes, bpmn-auto-layout will be run automatically after saving (requires Node.js ≥ 18 and internet access for npx).If the user describes a flow like "approve or reject", confirm:
exclusiveGateway (XOR — one path only)?Read references/elements.md for the full attribute list of each element. Below is the complete list of supported types — do not use any element not on this list.
| XML element | Type | Notes |
|---|---|---|
<startEvent> | startEvent | None, Timer, Message, Signal, Conditional |
<endEvent> | endEvent | None, Terminate, Message, Signal, Error, Escalation |
<intermediateCatchEvent> | intermediateCatchEvent | Timer, Message, Signal, Conditional |
<intermediateThrowEvent> | intermediateThrowEvent | Message, Signal, Escalation |
<boundaryEvent> | boundaryEvent | Timer (interrupting/non), Error, Message, Signal, Escalation |
| XML element | Type | Notes |
|---|---|---|
<task> | generic task | passes through immediately |
<userTask> | userTask | assignee, candidateGroups, formKey, dueDate |
<serviceTask> | serviceTask | implementation="webhook" or "http" |
<scriptTask> | scriptTask | expression evaluated against variables |
<sendTask> | sendTask | dispatches message/webhook |
<receiveTask> | receiveTask | waits for correlated message |
<businessRuleTask> | businessRuleTask | camunda:decisionRef must contain the id of a previously registered DMN definition |
<callActivity> | callActivity | calls another process definition |
<subProcess> | subProcess | embedded sub-flow |
| XML element | Behavior |
|---|---|
<exclusiveGateway> | XOR — exactly one outgoing flow taken |
<parallelGateway> | AND — all flows activated (join requires all tokens) |
<inclusiveGateway> | OR — one or more flows based on conditions |
<eventBasedGateway> | race — first event wins |
| XML element | Notes |
|---|---|
<sequenceFlow> | sourceRef, targetRef, optional <conditionExpression> |
CMMN, Choreography, Conversation, DataStore, Association (rendered only).
<?xml version="1.0" encoding="UTF-8"?>
<definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL
http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd"
id="Definitions_{process-key}"
targetNamespace="http://bpmn.io/schema/bpmn" <!-- any valid URI; this value is not used by the engine -->
exporter="bpmn-xml-generator"
exporterVersion="1.0">
<process id="{process-key}" name="{Process Name}" isExecutable="true">
<!-- ═══ START ═══ -->
<startEvent id="start" name="Start">
<outgoing>flow_start_to_first</outgoing>
</startEvent>
<!-- ═══ ACTIVITIES ═══ -->
<!-- ... -->
<!-- ═══ GATEWAYS ═══ -->
<!-- ... -->
<!-- ═══ END ═══ -->
<endEvent id="end" name="End"/>
<!-- ═══ SEQUENCE FLOWS ═══ -->
<!-- List ALL flows at the bottom for readability -->
<sequenceFlow id="flow_start_to_first" sourceRef="start" targetRef="..."/>
</process>
</definitions><userTask id="review_task" name="Review Request"
camunda:assignee="{{variables.manager}}"
camunda:candidateGroups="approvers">
<extensionElements>
<camunda:formData>
<camunda:formField id="approved" label="Approved?" type="boolean"/>
<camunda:formField id="comment" label="Comment" type="string"/>
</camunda:formData>
</extensionElements>
<ioSpecification>
<dataInput id="in_amount" name="amount"/>
<dataInput id="in_reason" name="reason"/>
<dataOutput id="out_approved" name="approved"/>
<dataOutput id="out_comment" name="comment"/>
</ioSpecification>
<incoming>flow_to_review</incoming>
<outgoing>flow_from_review</outgoing>
</userTask><serviceTask id="notify_erp" name="Notify ERP"
implementation="webService">
<extensionElements>
<camunda:connector>
<camunda:connectorId>webhook</camunda:connectorId>
</camunda:connector>
</extensionElements>
<incoming>flow_to_notify</incoming>
<outgoing>flow_from_notify</outgoing>
</serviceTask><exclusiveGateway id="gw_approval" name="Approved?">
<incoming>flow_to_gw</incoming>
<outgoing>flow_approved</outgoing>
<outgoing>flow_rejected</outgoing>
</exclusiveGateway>
<sequenceFlow id="flow_approved" sourceRef="gw_approval" targetRef="notify_approved">
<conditionExpression xsi:type="tFormalExpression">{{variables.approved == true}}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow_rejected" sourceRef="gw_approval" targetRef="end_rejected">
<conditionExpression xsi:type="tFormalExpression">{{variables.approved == false}}</conditionExpression>
</sequenceFlow><intermediateCatchEvent id="wait_24h" name="Wait 24h">
<incoming>flow_to_timer</incoming>
<outgoing>flow_from_timer</outgoing>
<timerEventDefinition>
<timeDuration xsi:type="tFormalExpression">PT24H</timeDuration>
</timerEventDefinition>
</intermediateCatchEvent><boundaryEvent id="timeout_boundary" name="Timeout"
attachedToRef="review_task"
cancelActivity="true">
<outgoing>flow_timeout</outgoing>
<timerEventDefinition>
<timeDuration xsi:type="tFormalExpression">PT48H</timeDuration>
</timerEventDefinition>
</boundaryEvent><!-- 'credit-score-decision' must be the id of a DMN definition already registered in the engine -->
<businessRuleTask id="credit_check" name="Credit Score Check"
camunda:decisionRef="credit-score-decision">
<extensionElements>
<camunda:in variables="all"/>
<camunda:out variables="all"/>
</extensionElements>
<incoming>flow_to_credit</incoming>
<outgoing>flow_from_credit</outgoing>
</businessRuleTask><!-- Start on external message -->
<startEvent id="start_on_message" name="Payment Received">
<messageEventDefinition messageRef="msg_payment"/>
</startEvent>
<message id="msg_payment" name="payment_received"/>
<!-- Intermediate wait for message -->
<intermediateCatchEvent id="wait_payment" name="Wait for Payment">
<incoming>flow_to_wait</incoming>
<outgoing>flow_after_payment</outgoing>
<messageEventDefinition messageRef="msg_payment"/>
</intermediateCatchEvent>This skill uses {{expression}} as the default condition syntax — not FEEL (#{}) and not UEL (${}). Expression syntax is engine-dependent; verify with your engine.
{{variables.amount > 1000}} → boolean condition
{{variables.status == 'approved'}} → string equality
{{variables.items.length > 0}} → array check
{{variables.manager || '[email protected]'}} → fallback
{{variables.score >= 700 && variables.debt < 5000}} → compoundUse in:
<conditionExpression> on sequence flowscamunda:assignee attribute on userTask{{}} in timer values)Before outputting the final XML, verify every item:
id<sequenceFlow> has sourceRef and targetRef pointing to existing element IDs<outgoing> child<incoming> child<exclusiveGateway> outgoing flows all have <conditionExpression> (except a default flow if default attribute is set)<parallelGateway> used for both split AND join — the join must have multiple <incoming> matching the split's <outgoing><boundaryEvent> has attachedToRef pointing to a valid activityP, dates are full UTC timestampsisExecutable="true" on <process>targetNamespace is set on <definitions><!-- ... rest of process -->)/tmp/bpmn-layout-runner/layout.mjs: node /tmp/bpmn-layout-runner/layout.mjs <output-file> <output-file>If /tmp/bpmn-layout-runner/ does not exist yet, set it up first:
mkdir -p /tmp/bpmn-layout-runner && \
cd /tmp/bpmn-layout-runner && \
npm init -y > /dev/null && \
npm install bpmn-auto-layout && \
node -e "
import { readFileSync, writeFileSync } from 'fs';
import { layoutProcess } from 'bpmn-auto-layout';
const [,,i,o] = process.argv;
writeFileSync(o||i, await layoutProcess(readFileSync(i,'utf8')), 'utf8');
console.log('Layout applied.');
" > layout.mjsActually: write layout.mjs with the Write tool (see below), then run npm install bpmn-auto-layout inside /tmp/bpmn-layout-runner/.
layout.mjs content:
import { readFileSync, writeFileSync } from 'fs';
import { layoutProcess } from 'bpmn-auto-layout';
const [,, input, output] = process.argv;
const xml = readFileSync(input, 'utf8');
const layouted = await layoutProcess(xml);
writeFileSync(output || input, layouted, 'utf8');
console.log('Layout applied successfully.');This overwrites the file in-place, adding a <bpmndi:BPMNDiagram> section with auto-computed shapes and edges. Confirm to the user that layout was applied and the file is ready for bpmn.io.
Read these when you need deeper detail — do not load them all at once:
| File | When to read |
|---|---|
references/elements.md | Full attribute reference for each element type |
references/examples.md | 4 complete end-to-end BPMN examples (approval, onboarding, timer escalation, message correlation) |
references/validation-errors.md | Common bpmn-moddle parse errors and how to fix them |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.