Scenario format v1
Source of truth: LILA_MODELER_ESTRUCTURA.md, section 6 ("Scenario format v1") and ADR-015. This document is the normative specification for *.scenario.json; when it differs from the structure document, the structure document prevails and this file is corrected.
Ticket: LILA-004 (E0 · M0). It is written before the engine. Implemented by packages/engine/src/scenario.ts (zod + JSON Schema) and resolveScenario().
1. What is a scenario
A scenario is a JSON file separate from the .bpmn that holds a model's simulation parameters: when and how long it runs, which calendars and resources exist, and which parameters each diagram element carries, keyed by the BPMN id (ADR-012: the id is the only key; never the name).
Why separate, and why a custom JSON format (ADR-015): N scenarios per diagram; git diffs without DI-coordinate noise; patchable by an agent as elements["Task_1"].processingTime.mean = 400; migrates 1:1 to a jsonb column. The vocabulary (field and distribution names) follows BPSim 2.0 so that edge adapters stay trivial, but BPSim and qbp are not the canonical format.
What it is not: it carries no geometry, element names, or documentation — that lives in the .bpmn (ADR-013). It carries no results — that is RESULTS_FORMAT.md.
Naming convention: <name>.scenario.json, alongside the project's .bpmn (examples/pedido/as-is.scenario.json).
2. Structure
2.1 Root
| Field | Type | Required | Description |
|---|---|---|---|
$schema |
string (URL) | no | https://lila-modeler.org/schema/scenario/1.json. For editors only; the engine does not fetch it. |
version |
integer | yes | Format version. In v1 it is exactly 1. Any other value ⇒ error. |
name |
string | yes | Scenario name. Appears in results and in lila compare. |
description |
string | no | Free text. |
model |
string (path) | yes¹ | Path to the .bpmn, relative to the scenario file. |
extends |
string (path) | no | Parent scenario it inherits from (§ 6). |
run |
object | yes¹ | Run parameters (§ 2.2). |
calendars |
object | no | Calendars keyed by name (§ 2.3). No calendar ⇒ 24×7. |
resources |
object | no | Resource pools keyed by name (§ 2.4). No resources ⇒ infinite capacity. |
elements |
object | no | Parameters keyed by BPMN id (§ 2.5). |
¹ Required in the resolved scenario (after applying extends), not in every file: a child that declares extends inherits model and run from the parent.
Unknown keys are not accepted at the root (strict): a misspelled field is an error, not a silent pass.
2.2 run
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
start |
ISO 8601 string with offset | yes | — | Zero instant of the virtual clock. The only field not expressed in seconds. Its offset fixes the timezone in which calendars are read. |
duration |
number (seconds) | no² | — | Simulated duration. Cuts the run short. |
warmup |
number (seconds) | no | 0 |
Cases started before warmup are excluded from the statistics (they still consume resources). |
replications |
integer ≥ 1 | no | 1 |
Independent runs. With > 1 the result carries mean, sd, and ic95 per KPI. Bizagi Modeler recommends 30. |
seed |
integer | no | 1 |
PRNG seed. Byte-for-byte determinism per (seed, replication, elementId) (ADR-017). The default is applied by the engine, not the schema: without declaring it, the W-SIN-SEED warning is emitted (R-DEG-4). |
baseTimeUnit |
"s" | "min" | "h" | "day" |
no | "s" |
Presentation only: the unit times are printed in. It does not change a single internal number. |
currency |
ISO 4217 string | no | — | Cost currency. If absent, amounts are reported without a symbol. |
serviceLevel |
number > 0 (seconds) | no | — | Target cycle time. Reporting only: it does not change the simulation. With it, the result adds process.withinServiceLevel and process.byEndEvent[*].withinServiceLevel, the fraction of completed cases whose cycle time is at most this value (docs/RESULTS_FORMAT.md § 5). |
² At least one of run.duration or a triggerCount on a start in elements (rule R6). If both are present, whichever occurs first wins.
Runtime work protection (#368)
run.duration limits simulated time, not the amount of work at one instant. R-TOK-7
in SEMANTICS.md independently limits effective events per case and instant, including
zero-duration timers, and aborts with E-LIMITE-SIN-AVANCE when exceeded. The budget is
max(100_000, 1_024 × (IR nodes + IR flows)), resets on time advancement, and is not a
scenario field. Exceptionally expensive finite cases can exceed it. Timer defaults and
R-DEG-3 are unchanged; event-gateway branches without a time still never fire.
2.3 calendars
Map key → { intervals: [...] }. The key is the identifier cited by resources[*].calendar and elements[*].calendar. The key default is the calendar taken by every pool that does not declare its own (R-CAL-10); elements do not inherit it.
| Field | Type | Required | Description |
|---|---|---|---|
intervals |
array (≥ 1) | yes | Open windows of the pattern. |
intervals[].days |
array of "MON"|"TUE"|"WED"|"THU"|"FRI"|"SAT"|"SUN" |
one selector | Weekly: days the window applies to. |
intervals[].monthDays |
array (≥ 1) of integers 1…31 or -1…-31 |
one selector | Monthly (#82, R-CAL-12): day of every month; negative counts from the end (-1 = last day). A day a month does not have is skipped. |
intervals[].monthWeekdays |
array (≥ 1) of { nth, day }, nth in 1…5 or -1…-5, day as in days |
one selector | Monthly (#82, R-CAL-12): the nth weekday of the month; { "nth": -1, "day": "FRI" } is the last Friday. |
intervals[].dates |
array (≥ 1) of "MM-DD" |
one selector | Annual (#82, R-CAL-13): that date every year. "02-29" only opens in leap years; "02-30" is an error. |
intervals[].from |
string "HH:MM" (24 h) |
yes | Opening time, local to run.start's offset. |
intervals[].to |
string "HH:MM" (24 h) or "24:00" |
yes | Closing time, exclusive. Must be > from; an overnight window is split into two intervals. "24:00" is midnight of the next day and is only allowed here, never in from: without it the format has no way to say "until the end of the day", and a hand-written 24×7 calendar would lose 60 s every night. |
Each interval carries exactly one day selector: days, monthDays, monthWeekdays or dates (R18). A calendar can mix them with several intervals.
| Field | Type | Required | Description |
|---|---|---|---|
holidays |
array of "YYYY-MM-DD" or "MM-DD" |
no | Closed days (#82, R-CAL-14): the whole day, whatever intervals say. "YYYY-MM-DD" once; "MM-DD" every year. Each must be a real date ("2026-02-29" is an error, "02-29" is not). |
"calendars": {
"oficina": {
"intervals": [
{ "days": ["MON", "TUE", "WED", "THU", "FRI"], "from": "09:00", "to": "18:00" },
{ "monthWeekdays": [{ "nth": -1, "day": "SAT" }], "from": "09:00", "to": "13:00" }
],
"holidays": ["01-01", "12-25", "2026-11-02"]
}
}
Semantics (ADR-016): a weekly pattern relative to run.start; a task only starts within its effective calendar — the intersection of its pools' calendars with its own, R-CAL-4 — and its processingTime consumes only calendar time (it pauses when the shift closes, resumes when it opens); closed time is reported as offHoursWait, separate from resourceWait; utilization is computed over the time available according to the calendar. Overlapping intervals within the same calendar are merged (union, not sum). Monthly and annual recurrence and holidays are read in run.start's offset too (R-CAL-12 … R-CAL-14). No DST and no own timezone in v1 (§ 4, R-CAL-15).
2.4 resources
Map key → pool. The key is the one cited by elements[*].resources[].ref.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | no | the key | Name used in reports. |
type |
"role" | "equipment" |
no | "role" |
Classification only; does not change semantics. |
capacity |
integer ≥ 1, or array of { calendar, capacity } (≥ 1 element) |
yes | — | Simultaneous units in the pool. In the per-interval form, capacity_i units while calendar calendar_i is open (R-CAL-11). Mutually exclusive with calendar (R16). |
costPerHour |
number ≥ 0 | no | 0 |
Cost per busy hour (not per available hour). |
fixedCost |
number ≥ 0 | no | 0 |
Fixed cost per token served. |
calendar |
string (key in calendars) |
no | default if it exists |
Without calendar the pool uses the calendar named default; if that does not exist either, it is available 24×7 (R-CAL-10). |
Queue: FIFO per pool, ordered by enablement instant, ties broken by seq.
Shift-based capacity (LILA-164). The same role can have a different headcount per shift — Bizagi
Modeler's level-4 example: 3 day nurses and 1 night nurse. That is one pool with per-interval
capacity, not one pool per shift:
"resources": {
"enfermera": {
"name": "Nurse", "type": "role", "fixedCost": 5,
"capacity": [
{ "calendar": "dia", "capacity": 3 },
{ "calendar": "noche", "capacity": 1 }
]
}
}
Semantics (R-CAL-11 in SEMANTICS.md, §12):
- the pool is open whenever any of its calendars is (union); if the shifts cover 24 h, the
pool is a 24×7 and no task has
offHoursWait; - capacity at
tis the sum of thecapacity_iwhosecalendar_iare open att. Two overlapping calendars add up (unlike intervals within the same calendar, which are merged):[{ "dia": 2 }, { "24x7": 1 }]gives 3 units during the day and 1 at night, because they are two distinct groups of units of the same role; - when a slot closes, tasks in progress are not interrupted: the pool can temporarily run above its capacity until they finish (same rule as R-REC-7);
- a task's
quantityis validated against the weekly maximum, not the declared sum: 3 + 1 units in disjoint shifts are never 4 simultaneous units; - utilization is integrated slot by slot:
busyTime / Σᵢ (capacityᵢ × openTimeᵢ)over the measurement window (R-CAL-9).
The numeric form is the single-slot case: { "capacity": 3, "calendar": "dia" } and
{ "capacity": [{ "calendar": "dia", "capacity": 3 }] } produce exactly the same result.
2.5 elements
Map BPMN id → parameters. The keys are diagram ids: nodes (Task_…, StartEvent_…, Timer_…) and sequence flows (Flow_…).
| Field | Type | Applies to | Default | Description |
|---|---|---|---|---|
processingTime |
distribution (§ 3) | tasks, timers (intermediate and boundary), branch catch events of an event-based gateway | no time (0 s) | Duration of the work, in seconds. On an intermediate timer it is the delay, with no resource. On a boundary timer it is the deadline: it cuts its task short when the boundary interrupts (R-BND-1) and spawns a parallel token when it does not (R-BND-10). Without it the boundary never fires (SEMANTICS.md R-BND-8). On a branch catch event of an event-based gateway — timer or message, the message being modelled as a wait of this time — it is what the branch races with, and without it that branch never fires (R-EVG-2, R-EVG-5). |
resources |
array of { ref, quantity } |
tasks | — | ref = key in resources; quantity integer ≥ 1, default 1. Without resources ⇒ infinite capacity. |
selection |
"and" | "or" |
tasks with resources |
"and" |
and: starts when all pools have capacity simultaneously (checked on every release; no partial resources are held ⇒ no deadlock). or: queues on all of them, starts with whichever becomes available first, and withdraws from the others; if several are free at once, the one listed first in resources wins (R-REC-6). |
fixedCost |
number ≥ 0 | any node | 0 |
Fixed cost per token completed at the element. |
interTriggerTimer |
distribution (§ 3) | starts (including the timer start) | — | Time between arrivals, in seconds. |
triggerCount |
integer ≥ 1 | starts (including the timer start) | — | Maximum number of cases generated by that element (Bizagi Modeler's "Max arrival count"). |
calendar |
string (key in calendars) |
starts, timers, and tasks | — | Arrival calendar: an arrival that falls in closed hours is shifted to the next open instant. It is also allowed on a task, where it is intersected with its pools' calendars (R-CAL-4); a timer runs 24×7 unless it declares one (R-EVT-3), boundary timers included (R-BND-3). |
probability |
number in [0, 1] |
sequence flows | even split | Probability of taking the flow. On XOR it is distributed by cumulative probability; on OR each outgoing path is independent. The range is checked by the linter (E-PROB-RANGO), not the schema. |
conditions |
array of { flowTaken, probability } |
sequence flow leaving a diverging XOR | — | Probability of taking the flow given what the case already did (ADR-028, R-COND-1…5). The first entry whose flowTaken the case has already traversed replaces this flow's declared probability; if none matches, the plain probability applies. flowTaken is the id of a sequence flow of the model (E-REF-DESCONOCIDA otherwise, W-COND-INALCANZABLE if it cannot precede the gateway). On any other element the field is still reserved (§ 4). |
Assigning a whole lane: the web panel can fill resources for every task of a lane in one action (resources section, "Assign lane"). It is a bulk edit of this same per-task field — the lane is a label of the diagram (docs/SEMANTICS.md § 2) and is never stored in the scenario — and tasks that already have resources are listed and require confirmation before being replaced.
Note on absent elements: a diagram element that does not appear in elements is valid and takes its defaults (a task with no time or resources, a flow with an even split). elements is a map of exceptions, not a mandatory mirror of the model.
3. Distributions
Object { "type": "...", <named parameters> }. Named parameters, never positional (ADR-015: this is what rules out Prosimos's scipy-style API). All values in seconds when the distribution describes a time. These are the 13 from BPSim 2.0 plus the constant; user is the empirical one.
type |
Parameters | Constraints | Notes |
|---|---|---|---|
constant |
value |
value ≥ 0 |
Deterministic. |
uniform |
min, max |
min ≤ max, min ≥ 0 |
Continuous. |
triangular |
min, mode, max |
min ≤ mode ≤ max, min ≥ 0 |
|
exponential |
mean |
mean > 0 |
mean is the mean, not the rate λ. |
normal |
mean, sd |
sd ≥ 0 |
Truncated to ≥ 0; the linter warns if P(x < 0) > 1%. |
truncatedNormal |
mean, sd, min, max |
sd ≥ 0, min ≤ max |
Explicit truncation by rejection. |
lognormal |
mean, sd |
mean > 0, sd ≥ 0 |
mean and sd are of the variable, not of its logarithm (as in Bizagi Modeler and qbp; they are converted internally to the log's μ and σ). |
gamma |
shape, scale |
shape > 0, scale > 0 |
Mean = shape × scale. |
erlang |
k, mean |
k integer ≥ 1, mean > 0 |
mean is the total mean, not that of each phase. |
weibull |
shape, scale |
shape > 0, scale > 0 |
|
beta |
alpha, beta, min, max |
alpha > 0, beta > 0, min ≤ max |
Standard beta rescaled to [min, max]. |
poisson |
mean |
mean > 0 |
Discrete. |
binomial |
n, p |
n integer ≥ 1, 0 ≤ p ≤ 1 |
Discrete. |
user |
points: [{ value, probability }] |
probability ≥ 0; ≥ 1 point |
Discrete empirical. If the probabilities do not add up to 1 they are normalized with a warning. |
Any type outside this list is a validation error with "not supported" text (ADR-021). Any extra parameter inside a distribution is also an error (strict object): {"type":"normal","mean":60,"stddev":10} fails citing stddev.
4. Reserved fields
These fields are in the schema (they are syntactically accepted, documented, and do not break a file written today), but the v1 engine rejects them with an explicit error when resolving the scenario. They are never ignored silently (ADR-021).
| Field | Where | What it will do once it exists |
|---|---|---|
priority |
elements[task] |
Priority in the resource queue, instead of plain FIFO. |
preempt |
elements[task] |
Whether a higher-priority task can preempt one in progress. |
batch |
elements[task] |
Grouping tokens to process together. |
conditions |
elements[task] and any element that is not a flow leaving a diverging XOR |
Branching by an expression over case data. On a flow leaving a diverging XOR it is implemented since ADR-028 (§ 2.5): it routes by the flows the case already took, not by its data. |
timezone |
calendars[*], run |
Its own timezone with DST, instead of the fixed offset of run.start (ADR-016). Until then every calendar keeps run.start's offset for the whole run, across DST changes (R-CAL-15). |
holidays was on this list until #82 implemented it (§ 2.3, R-CAL-14).
Error text: the same pattern as the rest of the "not supported" errors, citing the field and the element's id.
5. Validation rules
The first six are taken verbatim from the structure document; the rest are derived from section 6 and the cited ADRs.
| # | Rule |
|---|---|
| R1 | All times are in seconds, except run.start. |
| R2 | baseTimeUnit only affects presentation. |
| R3 | The keys of elements must exist in the IR: if one is missing, error citing the id; an absent entry produces a warning only for tasks, timers, starts and diverging XOR/OR gateways without outgoing probabilities or nonempty conditions (SEMANTICS §17, #360). |
| R4 | probability only on sequence flows (on a node it is E-PROB-EN-NODO). |
| R5 | interTriggerTimer / triggerCount only on starts, including the bpmn:startEvent with timerEventDefinition (which SEMANTICS.md § 2 maps to start). A bpmn:intermediateCatchEvent with a timer is a delay, never a generator: both fields there are E-CAMPO-NO-APLICA. A triggerCount without interTriggerTimer means triggerCount arrivals at t = 0 (R-ARR-1), not a mute start. |
| R6 | At least one of run.duration or a triggerCount on a start (one on a non-generating element does not count as a stop condition). With triggerCount alone, the run ends when the heap empties. |
| R7 | version must be 1; the root and every object are strict (unknown key ⇒ error). |
| R8 | model and run must exist in the resolved scenario; run.start must be ISO 8601 with offset and designate an instant that exists: a real calendar date (2026-02-31, 2026-13-01, and 2026-02-29 are errors, 2024-02-29 is not), time 00:00:00–23:59:59, and offset ±00:00–±23:59. 24:00 is not allowed here (it is in intervals[].to, R13): as a starting instant it is written as 00:00 of the next day. |
| R9 | Every ref in elements[*].resources[] must exist in resources; every calendar key must exist in calendars, including that of each slot of resources[*].capacity when it is a list (E-REF-DESCONOCIDA citing resources.<pool>.capacity[i].calendar). Error citing the id and the key. |
| R10 | The probabilities of the outgoing flows of an XOR gateway: if missing, an even split; if they do not add up to 1, they are normalized with a warning; the isDefault flow gets the remainder. On OR each outgoing path is independent and is not normalized. |
| R11 | Each distribution's parameters must satisfy its constraints (§ 3). normal with P(x < 0) > 1% produces a warning, not an error. |
| R12 | Reserved fields (§ 4) produce an explicit error. |
| R17 | conditions only on a sequence flow leaving a diverging XOR (elsewhere E-RESERVADO on a node, E-CAMPO-NO-APLICA on a flow). Each flowTaken must be a sequence flow of the model (E-REF-DESCONOCIDA) that can precede the gateway (W-COND-INALCANZABLE if not), and each probability must be in [0, 1] (E-PROB-RANGO). See SEMANTICS.md § 6.1 and ADR-028. |
| R13 | intervals[].to > intervals[].from; a window that crosses midnight is declared as two intervals. to also allows "24:00" (midnight of the next day); from does not. |
| R14 | selection only makes sense with resources; declaring it without resources is an error. |
| R15 | extends: the path must resolve to an existing file, and the chain must not contain cycles (§ 6). |
| R16 | resources[*].capacity by interval and resources[*].calendar are mutually exclusive: the calendar is already given per slot, and declaring both leaves it undefined which one wins. Error E-CAPACIDAD-Y-CALENDARIO citing the pool. Each capacity[i].capacity is an integer ≥ 1 and the list cannot be empty (E-REC-CAPACIDAD). The JSON Schema cannot express this rule: docs/scenario.schema.json is generated from zod, and capacity's anyOf cannot see its sibling calendar, so a pool declaring both passes the schema and is rejected only by validateScenario (or the guard in core/sim.ts). Anyone validating with the schema alone — an editor, an external CI — must also run the linter. Pinned in a test since LILA-164 (packages/engine/test/scenario.capacity-slices.qa.test.ts, § 8). |
| R18 | Each intervals[] entry declares exactly one of days, monthDays, monthWeekdays or dates. monthDays are 1…31 or -1…-31, nth is 1…5 or -1…-5, dates are "MM-DD" that exist in some year, and holidays are real "YYYY-MM-DD" dates or "MM-DD". Schema errors. A calendar whose every opening falls on its own holidays is E-CAL-VACIO (R-CAL-14). The JSON Schema only carries part of this rule: it has the pattern of dates and holidays and the 1…31/1…5 bounds, but not «exactly one selector», ≠ 0, or civil validity (02-30); those live in zod's refinements, so a tool that only uses the schema must also run the validator. |
Errors vs. warnings: an error prevents simulation; a warning travels in the RunResult's warnings[] and is printed in the CLI. A field applied to an element type that does not accept it (R4, R5, R14) is an error, not a warning: it is almost always a wrong id.
Schema defects (the ones zod catches before R3–R17: wrong type, out of range, unknown key, nonexistent variant) come out in Spanish and cite the path — run.warmup: debe ser ≥ 0 —, with the same text in the CLI, in the MCP, and in the scenario panel. The catalog is erroresEnEspanol in packages/engine/src/scenario.ts, and parseScenario is the only gate that applies it; whatever that map does not translate falls through to zod's es locale. One exception: the unknown-key error appears in the CLI with the § 17 text (E-CLAVE-DESCONOCIDA: clave no reconocida por el esquema: …, produced by schemaIssueLines), because the § 17 catalog overrides the map. This document's own messages (R8, R11, R13, E-CAL-VACIO…) are written by the schema and override the map. Semantic errors and warnings are a different matter: they are defined by the § 17 catalog in docs/SEMANTICS.md.
6. extends semantics
{ "extends": "as-is.scenario.json", "resources": { "cajero": { "capacity": 3 } } }
- Deep merge: the child is applied onto the parent object by object. Anything not mentioned is inherited intact.
nulldeletes the key:{"resources": {"horno": null}}removes thehornopool from the resolved scenario.- Arrays are replaced whole, not merged element by element: a task's
resources: [...], a calendar'sintervals: [...], and auserdistribution'spoints: [...]are substituted in full. It is the only predictable semantics for a list without keys. - Chains allowed: A
extendsBextendsC. It is resolved from the root down (C, then B, then A). - Cycles rejected: any cycle in the chain is an error, citing the files involved.
- Paths relative to the child's file, not to the working directory.
modelis also resolved relative to the file where it is written. __proto__,constructor, andprototypeare ignored: in JavaScript these are not ordinary keys (they write onto the object's prototype), so the merge skips them in any object at any depth, without warning. No field in § 2 is named this way; inside an array they are caught by the schema asE-CLAVE-DESCONOCIDA(LILA-204).- Validation (§ 5) applies to the resolved scenario, not to each file separately: that is why a delta may omit
modelandrun.
7. Examples
7.1 AS-IS (examples/pedido/as-is.scenario.json)
Copied verbatim from the structure document, section 6.
{
"$schema": "https://lila-modeler.org/schema/scenario/1.json",
"version": 1,
"name": "AS-IS",
"description": "Operación actual, 2 cajeros y 3 cocineros",
"model": "model.bpmn",
"run": {
"start": "2026-09-07T08:00:00-06:00",
"duration": 2592000,
"warmup": 3600,
"replications": 30,
"seed": 42,
"baseTimeUnit": "min",
"currency": "MXN"
},
"calendars": {
"oficina": { "intervals": [ { "days": ["MON","TUE","WED","THU","FRI"], "from": "09:00", "to": "18:00" } ] }
},
"resources": {
"cajero": { "name": "Cajero", "type": "role", "capacity": 2, "costPerHour": 220, "fixedCost": 0, "calendar": "oficina" },
"cocinero": { "name": "Cocinero", "type": "role", "capacity": 3, "costPerHour": 180, "calendar": "oficina" },
"horno": { "name": "Horno", "type": "equipment", "capacity": 1 }
},
"elements": {
"StartEvent_Pedido": { "interTriggerTimer": { "type": "exponential", "mean": 240 }, "triggerCount": 10000, "calendar": "oficina" },
"Task_TomarPedido": { "processingTime": { "type": "triangular", "min": 60, "mode": 120, "max": 300 },
"resources": [ { "ref": "cajero", "quantity": 1 } ], "fixedCost": 2.5 },
"Task_Preparar": { "processingTime": { "type": "normal", "mean": 480, "sd": 90 },
"resources": [ { "ref": "cocinero" }, { "ref": "horno" } ], "selection": "and" },
"Task_Revisar": { "processingTime": { "type": "constant", "value": 90 },
"resources": [ { "ref": "cajero" }, { "ref": "cocinero" } ], "selection": "or" },
"Timer_Reposo": { "processingTime": { "type": "constant", "value": 600 } },
"Flow_Aprobado": { "probability": 0.78 },
"Flow_Rechazado": { "probability": 0.22 }
}
}
7.2 TO-BE as a delta
Copied verbatim from the structure document, section 6:
{ "version": 1, "name": "TO-BE 3 cajeros", "extends": "as-is.scenario.json", "resources": { "cajero": { "capacity": 3 } } }
(deep merge; null deletes; chains allowed; cycles rejected)
Resolved, it is identical to the AS-IS except for resources.cajero.capacity = 3 — which is exactly M0's acceptance test: Scenario.parse(to-be) resolves extends to an object equal to the AS-IS except for capacity = 3.
7.3 Checking the examples against the rules
The AS-IS satisfies every rule in § 5 (examples/pedido/model.bpmn must contain the seven cited ids):
| Rule | How the AS-IS satisfies it |
|---|---|
| R1 | All time numbers are in seconds (duration 2,592,000 = 30 days; warmup 3600 = 1 h; mean 240; min/mode/max 60/120/300; mean/sd 480/90; value 90 and 600). Only run.start is a date. |
| R2 | baseTimeUnit: "min" does not alter any value: times remain in seconds. |
| R3 | The seven keys of elements are ids from the example diagram. |
| R4 | probability appears only on Flow_Aprobado and Flow_Rechazado. |
| R5 | interTriggerTimer and triggerCount appear only on StartEvent_Pedido. |
| R6 | Both run.duration (2,592,000 s) and triggerCount (10,000) are present: for the engine, whichever occurs first ends the run. |
| R7 | version: 1; there are no unknown keys. |
| R8 | model and run are present; start has offset -06:00. |
| R9 | cajero, cocinero, and horno exist in resources; the oficina calendar exists in calendars and is cited by cajero, cocinero, and StartEvent_Pedido. |
| R10 | 0.78 + 0.22 = 1: no normalization or warning. |
| R11 | triangular 60 ≤ 120 ≤ 300 ✓; normal mean 480, sd 90 ⇒ P(x < 0) ≈ 6·10⁻⁸, well below 1% ⇒ no warning; exponential mean 240 > 0 ✓; constant 90 and 600 ≥ 0 ✓. |
| R12 | There are no reserved fields. |
| R13 | 09:00 < 18:00 ✓, no midnight crossing. |
| R14 | selection only on Task_Preparar and Task_Revisar, both with resources. Task_TomarPedido has a single resource and takes the default and. |
| R15 | The AS-IS does not use extends; the TO-BE points to an existing sibling file, with no cycle. |
Defaults exercised by the example: cocinero with no fixedCost ⇒ 0; horno with no calendar ⇒ 24×7; Task_Preparar and Task_Revisar with resources and no quantity ⇒ 1; Timer_Reposo with no resources ⇒ pure delay; Task_TomarPedido with no selection ⇒ and.
8. Field mapping: Lila ↔ BPSim 2.0 ↔ qbp ↔ Bizagi Modeler
What this is for: adapters live at the edges and are only written when a consumer appears (ADR-015) — qbp import (Prosimos/Simod fixtures), .bpsim export/import for Sparx EA, and reading Bizagi Modeler's published examples. This table is the contract for those adapters. — = no equivalent.
| Lila field | BPSim 2.0 | qbp | Bizagi Modeler (UI name) |
|---|---|---|---|
name |
bpsim:Scenario/@name |
— | Scenario name |
description |
bpsim:Scenario/@description † |
— | Description |
version, $schema |
— | — | — |
model |
— (BPSim is embedded in the .bpmn) |
— (qbp is embedded in the .bpmn) |
— |
extends |
bpsim:Scenario/@inherits † |
— (a single scenario per file) | — |
run.start |
bpsim:ScenarioParameters/@start |
qbp:processSimulationInfo/@startDateTime |
Start date |
run.duration |
bpsim:ScenarioParameters/@duration |
— | Duration |
run.warmup |
bpsim:ScenarioParameters/@warmup † |
— | — |
run.replications |
bpsim:ScenarioParameters/@replication |
— | Replications (what-if only) |
run.seed |
bpsim:ScenarioParameters/@seed † |
— | — |
run.baseTimeUnit |
bpsim:ScenarioParameters/@baseTimeUnit |
— | Base time unit |
run.currency |
bpsim:ScenarioParameters/@baseCurrencyUnit |
qbp:processSimulationInfo/@currency |
Currency |
calendars[k] |
bpsim:Calendar (iCalendar value) † |
qbp:timetables/qbp:timetable |
Calendars |
calendars[k].intervals[] |
iCal rules inside bpsim:Calendar † |
qbp:rule/@fromWeekDay,@toWeekDay,@fromTime,@toTime |
Recurrence + start time + duration |
resources[k] |
bpmn:Resource referenced by bpsim:ResourceParameters |
qbp:resources/qbp:resource |
Resource |
resources[k].name |
bpmn:Resource/@name |
qbp:resource/@name |
Name |
resources[k].capacity (integer) |
bpsim:Quantity |
qbp:resource/@totalAmount |
Availability |
resources[k].capacity[] (by interval) |
several bpsim:Quantity, one per bpsim:Calendar † |
— (one timetableId per resource) |
Resources → Calendars → quantity (the «Resource | Morning shift | Day shift | Night shift» table from the calendar analysis) |
resources[k].costPerHour |
bpsim:CostParameters/bpsim:UnitCost |
qbp:resource/@costPerHour |
Cost per hour |
resources[k].fixedCost |
bpsim:CostParameters/bpsim:FixedCost † |
— | Fixed cost |
resources[k].type |
— | — | Type (role / equipment) |
resources[k].calendar |
reference to bpsim:Calendar † |
qbp:resource/@timetableId |
Resource calendar |
elements[id].processingTime |
bpsim:ProcessingTime |
qbp:element/qbp:durationDistribution |
Processing time |
elements[id].resources[].ref |
bpsim:ResourceParameters/bpsim:Selection (or bpsim:Role) |
qbp:element/qbp:resourceIds/qbp:resourceId |
Activity resources |
elements[id].resources[].quantity |
bpsim:Quantity (in ResourceParameters) |
— (one resource per task) | Quantity |
elements[id].selection (and/or) |
— | — | AND / OR |
elements[id].fixedCost |
bpsim:CostParameters/bpsim:FixedCost |
— | Fixed cost (activity) |
elements[start].interTriggerTimer |
bpsim:InterTriggerTimer |
qbp:arrivalRateDistribution |
Interval / time between arrivals |
elements[start].triggerCount |
bpsim:TriggerCount |
qbp:processSimulationInfo/@processInstances |
Max arrival count |
elements[start].calendar |
reference to bpsim:Calendar † |
qbp:arrivalRateDistribution + arrival timetable |
Arrival calendar |
elements[flow].probability |
bpsim:Probability |
qbp:sequenceFlow/@executionProbability |
% per flow |
priority (reserved) |
bpsim:PriorityParameters/bpsim:Priority † |
— | — |
preempt (reserved) |
bpsim:PriorityParameters/bpsim:Interruptible † |
— | — |
elements[flow].conditions |
bpsim:ControlParameters/bpsim:Condition † |
— | — (no Bizagi equivalent; ADR-028) |
calendars[k].holidays |
exception dates of the iCal rules † | — | Holidays (calendar) |
batch, timezone (reserved) |
— | — | — |
In bold, the four mappings already fixed in the structure document.
† To be verified against the BPSim 2.0 XSD when writing the .bpsim adapter. The row is documented as intent, not as a verified fact.
Known limitations of the external formats, which is why none of them serves as canonical (ADR-015):
- BPSim 2.0 has been frozen since 2016, has no open-source tooling, and, embedded in the
.bpmn, mixes layout with parameters. - qbp supports a single scenario per file and one resource per task:
selection,quantity > 1, and multiple scenarios have nowhere to go. Import is an acceptable degradation; export loses information and must warn. - Bizagi Modeler does not export simulation parameters (verified against 5 real files: only colors travel in
bizagi:). The Bizagi Modeler column is there to name things the same way its UI does, not to exchange files. - Prosimos uses positional scipy-style parameters and carries no license: it is only used as a numeric oracle during development, never as a format.
9. Version changes
version is an integer. v1 grows additively: new optional fields and reserved fields that become implemented do not bump the version. It moves to 2 only if the meaning of an existing field changes or one disappears. The engine rejects a version it does not recognize; it never guesses.
Changes already applied within v1:
- Per-interval
resources[*].capacity(LILA-164, § 2.4).versionstays at 1: it is a widening of the type, not a change of meaning. Every earlier v1 scenario remains valid and produces the same result byte for byte, because the integer form is the single-slot case. A scenario using the new form is not understood by an older engine: the strict schema rejects it with a type error, which is the correct degradation (never guess).