lila: BPMN extension and id policy
Source of truth: LILA_MODELER_ESTRUCTURA.md, section 4 (ADR-012 "Element and process identity", ADR-013 "Where each piece of data lives", ADR-014 "A single extension namespace, defined once") and section 5 (the path to lila.moddle.json in the repository). This document is the operative reference for implementing the parser/serializer (packages/engine/src/bpmn/parse.ts) and the moddle descriptor (packages/engine/src/bpmn/lila.moddle.json); it does not repeat the ADRs' reasoning, it applies it.
This document adheres to ADR-012 and ADR-014; any discrepancy between this file and those ADRs is resolved by LILA_MODELER_ESTRUCTURA.md, and this file is corrected accordingly.
1. Namespace
xmlns:lila="https://lila-modeler.org/schema/bpmn/1"
Decided in ADR-014. The IRI does not need to resolve (nothing is published at that URL) — it only needs to be stable and not change before M4. The /1 suffix is the namespace's version, not the process's: it grows additively (new elements are added; an existing one is never removed or repurposed) as long as the namespace stays /1. An incompatible change would require /2 — not anticipated before M4.
The namespace is declared exactly once, in bpmn:definitions, and is used the same way in:
- the editor (
apps/web, viaBpmnModeler({ moddleExtensions: { lila } })), - the CLI and the engine (
packages/engine), - any future server (
packages/server, M6).
All three read the same descriptor: packages/engine/src/bpmn/lila.moddle.json (section 5 of the structure document). There is no second copy of the descriptor anywhere else in the repository.
Round-trip
bpmn-moddle preserves unknown namespaces when doing saveXML after importXML (verified with real BPSim, qbp, and Bizagi Modeler files — see investigacion-2026-09-03/02-bizagi-simulacion.md and 03-editor-bpmn.md). This is what lets lila:* survive a tool that does not know it (a round trip through Camunda Modeler, Signavio, ADORIS, etc.) without that tool needing the descriptor. That behavior is verified for bpmn-moddle and bpmn-js — the core of Camunda Desktop Modeler — in section 8, with and without the descriptor. Should a tool that matters turn out not to preserve foreign namespaces, plan B is an annotations.json sidecar keyed by element id with the same vocabulary (the same field names as the lila:* elements), outside the .bpmn; nothing verified so far needs it.
2. v1 elements
Every extension element is a child XML element, never an attribute of the BPMN element it documents — the one exception is that each lila:* element can have its own attributes (e.g. lila:responsibility has the attributes type and roleRef). The reason for using elements rather than attributes at the insertion point is to allow lists: a bpmn:task can have several lila:systemRef (it uses more than one system), several lila:input/lila:output, and so on. An XML attribute cannot repeat on the same node.
All of them (except versionTag, see below) go inside bpmn:extensionElements of the BPMN element they document (task, event, gateway, lane, or bpmn:process itself):
<bpmn:task id="Task_7f3k2q1" name="Revisar solicitud">
<bpmn:extensionElements>
<lila:responsibility type="R" roleRef="rol-1"/>
<lila:responsibility type="A" roleRef="rol-2"/>
<lila:systemRef ref="sys-crm"/>
<lila:documentRef ref="doc-solicitud"/>
<lila:input ref="doc-solicitud"/>
<lila:output ref="doc-solicitud-revisada"/>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:task>
| Element | Attributes | What it documents | Points to |
|---|---|---|---|
lila:responsibility |
type (R|A|C|I), roleRef |
The element's RACI matrix: one row per responsible-party/role combination. An element can have several lila:responsibility (several roles, or the same role with a different type, which doesn't make sense but is not validated as an error — it's a warning). |
roleRef → id in the role catalog (catalog.json, see ADR-013). |
lila:systemRef |
ref |
System/application involved in the element. | id in the system catalog. |
lila:documentRef |
ref |
Document associated with the element (input, output, or just a general reference). | id in the document catalog. |
lila:riskRef |
ref |
Risk associated with the element. | id in the risk catalog. |
lila:controlRef |
ref |
Control associated with the element (mitigates a risk). | id in the control catalog. |
lila:kpiRef |
ref |
Indicator that measures the element. | id in the KPI catalog. |
lila:input |
ref |
Document/data the element consumes. A semantic subset of documentRef with explicit direction; can coexist with documentRef for the same id. |
id in the document catalog. |
lila:output |
ref |
Document/data the element produces. | id in the document catalog. |
lila:versionTag |
value |
See section 4 (process key). The only element that does not hang off a flow element — it hangs off bpmn:process. |
— (free-form value, no catalog reference). |
All references (roleRef, and the ref of systemRef/documentRef/riskRef/controlRef/kpiRef/input/output) point to ids in the catalog (catalog.json, outside the .bpmn — ADR-013). A dangling reference (an id that does not exist in the catalog) is a lint warning, not a validation error — the catalog can be filled in after the diagram.
versionTag goes as a child of bpmn:process's bpmn:extensionElements (not of bpmn:definitions nor of a task):
<bpmn:process id="credito-solicitud" isExecutable="false">
<bpmn:extensionElements>
<lila:versionTag value="1.3.0"/>
</bpmn:extensionElements>
...
</bpmn:process>
A pattern copied from zeebe:versionTag (Camunda 8), cited as precedent in ADR-012.
Extended attributes (#509)
User-defined properties per element type, as Bizagi Modeler has them ("SLA", "Owner system",
"Risk level"…): the modeller defines an attribute once for an element type and fills it in
on each element of that type. Both halves live in the .bpmn, so they travel with the process and
the project container does not change.
<bpmn:process id="credito-solicitud" isExecutable="false">
<bpmn:extensionElements>
<lila:attributeDefinition id="Attr_k2m8p1q" name="SLA (h)" type="number" appliesTo="task" default="24"/>
<lila:attributeDefinition id="Attr_a91nc0x" name="Risk level" type="list" appliesTo="task">
<lila:option value="Low"/>
<lila:option value="High"/>
</lila:attributeDefinition>
</bpmn:extensionElements>
<bpmn:task id="Task_7f3k2q1" name="Revisar solicitud">
<bpmn:extensionElements>
<lila:attributeValue ref="Attr_a91nc0x" value="High"/>
</bpmn:extensionElements>
</bpmn:task>
</bpmn:process>
| Element | Attributes | Where |
|---|---|---|
lila:attributeDefinition |
id (NCName, Attr_<suffix>), name, type (text|number|list|date), appliesTo (task|gateway|event|subProcess|lane|process), optional default; a list carries its options as lila:option value="…" children |
extensionElements of the bpmn:collaboration when the diagram has one, else of its only bpmn:process (the example above). Readers also accept them on any root element (a pool's process, as early files wrote them) and take each id once, the canonical place first; the editor moves them to the canonical place on the next save of its dialog. |
lila:attributeValue |
ref (the definition's id), value |
extensionElements of the element it describes. |
appliesTogroups BPMN types:taskis every*Task;gatewayevery*Gateway;eventevery*Event, boundary events included;subProcessissubProcess,transaction,adHocSubProcessandcallActivity;laneislaneandparticipant(the pool);processisbpmn:process. Flows, data and text annotations carry none.- Values are text in the XML and are validated, never coerced (
validateAttributeValueinpackages/engine/src/bpmn/attributes.ts): anumberis a plain decimal with a dot (4.5or-2, not4,5,4.,+5,.5nor1e3); adateis a realYYYY-MM-DD(years below 100 included); alistvalue is one of its options. The empty string means "not filled in" and is not written. The editor writes numbers and dates on blur or Enter, and only when they fit, so a half-typed24,5never leaves24behind. - An element without a value shows the definition's
default, which is not copied onto the element. - Values point to the definition by
id, never by name, so renaming a definition keeps them. The editor still asks before renaming, retyping, removing options from or deleting a definition that elements have values for, naming the removed options and offering to clear only the values that no longer fit. A value whose definition is gone (edited by hand, another tool) is kept, shown under itsrefin the panel and in the process document, never dropped; several values of one attribute on one element are all shown. - The definitions stay put when pools come and go: adding the first pool or deleting the last one
carries them to the new root, deleting a pool whose process still held them moves them to the
collaboration first, and pasting a pool never copies them (it would repeat every
id). A task inside a collapsed sub-process sees the same definitions as the rest of the diagram. - The process document (Word/HTML) lists every filled-in attribute (own value, else the default) as a labelled line under its element: the process and its pool under the description, each lane under its heading, each task, gateway, event and sub-process (call activities included, by their BPMN type) in its section.
- A model without extended attributes is written byte for byte as before; one with them opens in a
tool without the
liladescriptor with no warnings, and comes back intact (section 8).
What is NOT v1
Simulation parameters (processingTime, resources, interTriggerTimer, etc.) are not part of this extension — they live in *.scenario.json, separate from the .bpmn (ADR-007, ADR-013; full format in SCENARIO_FORMAT.md). lila: documents the process (RACI, systems, documents, risks, controls, KPIs, version); it does not parameterize it for simulation.
3. Id policy (ADR-012)
Element id
- The id of every BPMN element (
bpmn:task@id,bpmn:sequenceFlow@id, etc.) is the only key for attaching business or simulation data. Never the visible name (name). - Format: a valid NCName (an XML
Namewithout:),<TypePrefix>_<suffix>, e.g.Task_7f3k2q1,Gateway_a91nc0x,Flow_k2m8p1q. The editor's ids carry the modeller's own suffix; when the engine itself has to mint one (the id collision branch of the importer) the suffix is a deterministic counter checked against the ids already taken (Task_2,Task_3, …), so importing the same file twice never yields different ids (#326). - Prefix per type (not exhaustive, grows with the IR):
Start_,End_,Task_,Gateway_(XOR/OR/AND share a prefix; the exact type lives in the IR, not in the id),Timer_,Flow_,SubProcess_. - An existing id is never regenerated on import, export, or rename (renaming changes
name, notid). Regenerating the id would break any external reference:*.scenario.jsonentries keyed by id, event log rows, catalog references. - A new id is generated when copying/pasting an element — a copied element is a distinct entity and must not carry the original's data (
lila:*, scenario) under the same id. - Reversible sanitization for foreign non-NCName ids: some tools (Bizagi Modeler among them) can emit ids that are not valid NCNames. On import, Lila sanitizes those ids to NCName and stores the
sanitizedId → originalIdmap inir.source.originalIds(seeProcessIR's shape in section 6 of the structure document), so a re-export can restore the original id if the destination tool needs it. Sanitization is deterministic (the same foreign id → the same sanitized id), so reimporting the same file does not generate different ids every time.
The editor applies the engine's same sanitizeXmlIds before handing the document to bpmn-js and
keeps the map per modeler instance. On export, it restores in a single pass both the declarations
and the references (sourceRef, targetRef, default, bpmnElement, and textual references),
including BPMNDI's. Opening is prepared on a candidate instance and only replaces the active canvas
once the import fully completes; a malformed XML or one with no renderable diagram does not replace
the previous XML, selection, services, or history. If the reader reported a semantic loss — for
example, a broken topological reference — export requires a visible decision with the warning's
detail, because the serialized tree can no longer reconstruct that content.
Process id
- A process's logical key:
bpmn:process@id(ASCII slug) +lila:versionTag. Example:credito-solicitud+1.3.0. This is the key that identifies "the same process, version X" across reimports and edits — it is not justprocess@id, because two versions of the same business process can (and generally should) coexist as distinct files or commits with the sameprocess@id. bpmn:process@idis generated just like any other element id (NCName; see above), but a readable ASCII slug is recommended (credito-solicitud, notProcess_7f3k2q1) because, besides being a technical key, it also works as a folder/file name in the project layout.lila:versionTagis a free-form string (semver is recommended,1.3.0, but not enforced by the schema — a user can version asv2,2026-Q3, etc.). Absent by default; its absence is not an error.
exporter / exporterVersion
Every .bpmn that Lila writes declares, in bpmn:definitions:
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:lila="https://lila-modeler.org/schema/bpmn/1"
exporter="Lila Modeler"
exporterVersion="1.0.0-beta.1"
...>
exporterVersion is the @lila-modeler/engine package version that generated the file (always the engine's, even when it is the web app that saves it: a single helper writes both attributes, marcarExportador in packages/engine/src/bpmn/ids.ts) — it is used to diagnose behavior differences between engine versions, the same way bpmn-js and Camunda Modeler do with their own exporter/exporterVersion. Bizagi Modeler, by comparison, does not declare these attributes in its export (verified in 5 real files — see investigacion-2026-09-03/02-bizagi-simulacion.md), which makes it impossible to know which version of Bizagi Modeler generated a given file; Lila avoids that problem from day one.
4. Moddle descriptor (example)
Lives in packages/engine/src/bpmn/lila.moddle.json (the single definition — section 5 of the structure document). The shape it will have (v1 example, subject to the minor adjustments bpmn-moddle may require during implementation):
{
"name": "Lila",
"uri": "https://lila-modeler.org/schema/bpmn/1",
"prefix": "lila",
"xml": {
"tagAlias": "lowerCase"
},
"types": [
{
"name": "Responsibility",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "type", "isAttr": true, "type": "String" },
{ "name": "roleRef", "isAttr": true, "type": "String" }
]
},
{
"name": "SystemRef",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "DocumentRef",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "RiskRef",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "ControlRef",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "KpiRef",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "Input",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "Output",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "ref", "isAttr": true, "type": "String" }
]
},
{
"name": "VersionTag",
"superClass": ["Element"],
"meta": { "allowedIn": ["bpmn:ExtensionElements"] },
"properties": [
{ "name": "value", "isAttr": true, "type": "String" }
]
}
]
}
Implementation notes (not normative, resolved in the ticket that creates the real file):
meta.allowedIndocuments the intent (everything exceptVersionTaggoes in a flow element'sextensionElements;VersionTaginbpmn:process's); bpmn-moddle does not validateallowedInat parse time — the real validation of "who can contain what" is done bypackages/engine/src/bpmn/validate.ts.associationsare not used because none of these types need to replace or extend an existing BPMN type; they are all new elements that hang offbpmn:extensionElements, which already accepts anyvalues[]from a declared namespace.- Every property is
isAttr: true(attributes of thelila:*element, not its own children) — this keeps each element on a single line of XML and is enough for v1 because none of them needs free text or nesting.
5. Complete example fragment
bpmn:extensionElements of a task with RACI responsibility, catalog references, and process version:
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:lila="https://lila-modeler.org/schema/bpmn/1"
id="Definitions_1"
exporter="Lila Modeler"
exporterVersion="1.0.0-beta.1"
targetNamespace="http://lila-modeler.org/schema/bpmn">
<bpmn:process id="credito-solicitud" name="Solicitud de crédito" isExecutable="false">
<bpmn:extensionElements>
<lila:versionTag value="1.3.0"/>
</bpmn:extensionElements>
<bpmn:startEvent id="Start_9k2m1qa" name="Solicitud recibida">
<bpmn:outgoing>Flow_a1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_7f3k2q1" name="Revisar solicitud">
<bpmn:extensionElements>
<lila:responsibility type="R" roleRef="rol-1"/>
<lila:responsibility type="A" roleRef="rol-2"/>
<lila:systemRef ref="sys-crm"/>
<lila:documentRef ref="doc-solicitud"/>
<lila:input ref="doc-solicitud"/>
<lila:output ref="doc-solicitud-revisada"/>
<lila:kpiRef ref="kpi-tiempo-revision"/>
</bpmn:extensionElements>
<bpmn:incoming>Flow_a1</bpmn:incoming>
<bpmn:outgoing>Flow_a2</bpmn:outgoing>
</bpmn:task>
<bpmn:endEvent id="End_k9x3m2z" name="Fin">
<bpmn:incoming>Flow_a2</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_a1" sourceRef="Start_9k2m1qa" targetRef="Task_7f3k2q1"/>
<bpmn:sequenceFlow id="Flow_a2" sourceRef="Task_7f3k2q1" targetRef="End_k9x3m2z"/>
</bpmn:process>
</bpmn:definitions>
Correspondence with the referenced catalog (catalog.json, outside this document — see SCENARIO_FORMAT.md/LILA_MODELER_ESTRUCTURA.md section 5 for its location): rol-1, rol-2, sys-crm, doc-solicitud, doc-solicitud-revisada, and kpi-tiempo-revision are ids that must exist there; if they don't, validate(ir) emits a dangling-reference warning for each one, not an error.
6. Consistency with the ADRs
- ADR-012 (element and process identity): implemented in section 3 of this document — NCName with a type prefix, never regenerated, new on copy, reversible sanitization, process key =
process@id+versionTag,exporter/exporterVersionindefinitions. - ADR-014 (a single namespace, defined once): implemented in sections 1, 2, and 4 — a single IRI, a single descriptor shared by editor/CLI/server, elements (not attributes at the insertion point) to allow lists, additive growth, round-trip verified headless in section 8 (the
annotations.jsonfallback was not needed).
If in the future some decision in this document conflicts with a new or revised ADR, the ADR wins and this document is updated to reflect it (never the other way around).
7. Round-trip in the web app
The web app opens a .bpmn with bpmn-js and writes it back out with saveXML, i.e. with
bpmn-moddle serializing the tree bpmn-moddle was able to read. That fixes exactly what
survives an open-and-export round trip (LILA-192, apps/web/src/modelerXml.ts):
Preserved
- The file's original ids, including the ones that are not NCName. On import they are reversibly sanitized (section 3), and on export they are restored one by one, in attributes, in text, and in BPMNDI's references, with the original's quoting and entities.
- This document's
lila:elements and foreign extensions (bizagi:and the like): theliladescriptor goes inmoddleExtensions, and the rest travels as generic content. - The diagram (
bpmndi), the documentation, the names, and the topology. exporter/exporterVersionindefinitions, whichmarcarExportadoralways rewrites.
Not preserved
- Id references that point to an element the file never declares —
messageRef,dataStoreRef,categoryValueRef,dataObjectRef—: bpmn-moddle does not resolve them, they never reach the tree, and the exported file no longer carries them. The fixtures inexamples/bizagi-exportsare a real case. - Whatever import discards with a warning about unparseable content or an unresolved topological reference: if it never entered the model, it cannot come back out in the XML.
How it is reported (never silently, which was LILA-192's complaint)
- The status bar shows the complete list as an error — not a warning —, with the ids: «N elementos o referencias se perderán al exportar: …» (N elements or references will be lost on export: …) (LILA-193).
- Everything that writes a
.bpmnwith loss first passes through the same dialog, with that same list and two ways out: «Exportar .bpmn» (Export .bpmn) offers «Exportar igualmente»/«Cancelar» (Export anyway / Cancel), and saving the project offers «Guardar igualmente»/«Cancelar» (Save anyway / Cancel). Cancel neither downloads nor writes anything to disk. autorizarExportacioncuts off with an error any lossy export that does not carryaceptarPerdida, i.e. that dialog's explicit yes: the XML that feeds the simulation — which the user never sees — never carries it, and the save snapshot only carries it after the yes.- On Electron, canceling the dialog while saving returns «no se guardó» (not saved) to the window-close handler, so the close is cancelled and nothing is lost.
None of this rewrites the serializer: byte-for-byte fidelity with the source file is not a promise the app makes, and preserving broken attributes would require a serializer of its own.
8. Round-trip in other tools (LILA-068)
The whole bet of ADR-014 — a namespace of our own inside the .bpmn, instead of a sidecar —
only pays off if another tool can open a Lila file, save it and give the lila: back untouched.
Verified headless against the pair Camunda Desktop Modeler is built on (it is Electron + bpmn-js;
see investigacion-2026-09-03/03-editor-bpmn.md): bpmn-moddle 10.2.0 and bpmn-js 18.28.0,
the versions this repository installs.
What travels is examples/pedido/model.bpmn with one element of every v1 type (section 2) written
into it by the engine's own writer — eighteen lila: elements (the extended attributes included) spread over several owner elements,
with repetitions — and the round trip runs twice: with lila.moddle.json, which is what Lila's
editor and CLI do, and without it, which is what a foreign tool has (an undeclared namespace
that bpmn-moddle keeps as generic content). In both cases, and through both libraries, the save
gives back:
- every
lila:element, hanging off the same owner element and with the same attribute values; - the namespace, declared once in
bpmn:definitions; - the annotated elements'
bpmn:documentation, their ids and the diagram (bpmndi).
It also holds when the other tool writes a prefix of its own (only the IRI is normative, section 1), and the second save is identical to the first: from there on the file is a fixed point.
Tests: packages/engine/test/bpmn-roundtrip.test.ts (bpmn-moddle) and
apps/web/src/bpmnRoundtrip.test.ts (bpmn-js), with the annotated file and the comparison in
packages/engine/test/bpmn-roundtrip.fixture.ts. What is compared is an inventory of the lila:
elements read off the XML text, normalized for prefix and attribute order, not the whole file byte
for byte: bpmn-moddle reformats whatever it reserializes, and section 7 already says byte fidelity
is not a promise.
| Tool | What reads and writes its XML | lila: after open/save |
How it was checked |
|---|---|---|---|
| Camunda Desktop Modeler (and any editor built on bpmn.io's libraries) | bpmn-js 18.28.0 over bpmn-moddle 10.2.0 | preserved | headless round trip over that same core, with and without the descriptor (apps/web/src/bpmnRoundtrip.test.ts) |
| Any tool on bpmn-moddle alone (converters, servers, the Lila CLI) | bpmn-moddle 10.2.0 | preserved | packages/engine/test/bpmn-roundtrip.test.ts |
| Lila Modeler itself (web and desktop) | bpmn-js with moddleExtensions: { lila } |
preserved | section 7, plus apps/web/src/exportar.test.ts |
| Signavio | proprietary, SaaS | not verified | no account available |
| ADONIS | proprietary | not verified | no licence available |
| Bizagi Modeler | proprietary | not verified | no licence available; the opposite direction — Lila keeping a foreign bizagi: across open/export — is covered by the examples/bizagi-exports fixtures in apps/web/src/exportar.test.ts |
What this does not cover. Camunda Desktop Modeler's user interface was not driven: the headless
test goes through the very moddle instance bpmn-js reads and writes XML with (importXML is
moddle.fromXML, saveXML is moddle.toXML, and nothing in between touches the tree that gets
serialized), but not through its canvas, which jsdom cannot draw. Signavio and ADONIS were not
tried at all, for lack of access; if either turns out to drop foreign namespaces, it drops them for
every lila: element at once, which is what the inventory above would measure.
Plan B stays unwritten. The annotations.json sidecar that section 1 keeps as a fallback is not
needed: no tool verified here discards the namespace. Its ticket gets opened the day a tool that
matters is shown to drop it, and the file format does not change before that.