The model
Any unfamiliar word is in the glossary.
A model is one document — YAML or JSON, the same keys either way. It describes your operation: what machines or people you have, what work flows through them, how long each step takes, what goes wrong, and how work arrives. Nothing else is needed to run a simulation.
The examples on these pages are YAML, because that is what you write by hand: it is the same document with comments, so the reason for a number can sit beside the number. JSON is what a generator emits and what the engine prints. The CLI reference says which extensions are read as which.
Every key the engine reads is documented here, on the page for the thing it belongs to, with its type, whether it is required, and what happens if you leave it out. If a keyword exists, it is on one of these pages. Start with a complete example if you would rather read a working model than a reference.
The shape of a document
# yaml-language-server: $schema=https://clockwright.dev/schema/model.json
name: "a label for this model"
horizon_seconds: 604800.0
warmup_seconds: 86400.0
resources:
- { id: tool, servers: 2 }
products:
- id: widget
steps:
- resource: tool
process_seconds: { kind: exponential, mean: 300.0 }
releases:
- product: widget
interarrival_seconds: { kind: exponential, mean: 200.0 }
The same document, as a generator emits it and as cwright propose writes a candidate. A
model's
model_hash is taken over this form whichever syntax the file was written in, so the two
blocks above and below are one model with one hash — a YAML file is read, converted, and
hashed exactly as if you had typed the JSON.
{
"name": "a label for this model",
"horizon_seconds": 604800.0,
"warmup_seconds": 86400.0,
"resources": [ { "id": "tool", "servers": 2 } ],
"products": [ { "id": "widget", "steps": [
{ "resource": "tool", "process_seconds": { "kind": "exponential", "mean": 300.0 } } ] } ],
"releases": [ { "product": "widget",
"interarrival_seconds": { "kind": "exponential", "mean": 200.0 } } ]
}
Those six keys are the smallest model that runs. Everything else is optional and adds a mechanism: operators, consumables, downtime, batching, dispatch rules.
How the abstractions interact
One entity is released, walks its product's route, and at every step competes for the things that constrain it. That is the whole model:
Solid arrows are always there; dotted ones are optional mechanisms you switch on by adding a key to a step. A model with none of them is still a model: work arrives, queues for a machine, and leaves.
Every one of them is a constraint, and a simulation is worth running exactly when two or more of them interact. If nothing in your process ever waits for anything else, arithmetic answers you faster than this will.
The pieces are joined by name
The document is flat. Nothing nests inside anything else; the joins are string ids:
| this key | must match |
|---|---|
releases[].product, schedule[].product, conwip[].product | a products[].id |
products[].steps[].resource | a resources[].id |
steps[].consumes[].consumable | a consumables[].id |
steps[].operator.pool | an operators[].id |
steps[].setup | a from or a to in that resource's changeovers |
resources[].min_run[].setup | a setup named on a step of that resource |
cwright validate rejects a step naming a resource that does not exist, and says which
names do. That check exists because a mis-joined dataset produces a model that runs,
looks plausible, and is quietly wrong — a whole product silently dropped, or twelve steps
pointing at a machine nobody declared.
The pages
| page | what it covers |
|---|---|
| Distributions | every duration in the model, and the seven shapes one can take |
| Resources | machines, bays and crews: capacity, queueing, setups, dedication |
| Products and steps | what flows, the route it takes, and every option on a step |
| Releases | how work arrives: streams, a fixed population, or a named schedule |
| Operators | people as a constrained pool, with shifts and breaks |
| Consumables | material a step draws down, and how it is replenished |
| Downtime | breakdowns and planned maintenance, and how they collide |
| Batching | work that runs together, and how a batch forms |
| Dispatch rules | which waiting job goes next, expressed as weights |
| Cascading | machines that start the next job before the last one finishes |
| Reading the results | every field the engine prints, and how to define a KPI |
| Examples | three complete models, each run end to end |
Top-level keys
| key | type | required | what it means |
|---|---|---|---|
schema_version | integer | no, default 1 | Document format version. Bump only when the format changes incompatibly; today every model is 1. |
name | string | yes | A label for the run. It appears in results and in validate output, and it is part of the model hash, so renaming a model gives it a new identity. |
tick_scale_per_second | integer | no, default 1000 | Clock resolution. ⚠️ An integer tick, per ADR-0003; this says how fine. |
horizon_seconds | number | yes | How long to simulate, after warm-up. |
warmup_seconds | number | no | Statistics are reset at this mark. ⚠️ A hard boundary, not a hint. Every resource, consumable and operator figure is measured from here and from nowhere else. cwright run additionally re-truncates the cycle-time series only, via MSER-5; cwright simulate does not truncate at all. So a warmup_seconds of 0 measures the empty-system transient in every utilisation and queue number you read. |
resources | array of resource | yes | Every machine, bay, crew or station work can queue for. At least one is required. |
operators | array of operator | no | ⚠️ Skipped when empty, deliberately. model_hash is taken over the canonical serialisation, so a field that always appears would change the hash of every model ever written the moment it was added — and a hash that moves for a feature a model does not use invalidates every cached result for nothing. |
consumables | array of consumable | no | Stores of material a step can draw from, with their own replenishment. Omit if nothing is consumed. |
products | array of product | yes | What flows through the plant, each with the ordered route it takes. |
releases | array of release | yes | How work arrives. Required unless the model is closed (conwip). |
conwip | conwip | no | A fixed circulating population instead of an arrival process. ⚠️ Mutually exclusive with releases and schedule: a closed network with arrivals is an open network, and none of the closed-form answers apply. |
charges | array of charge | no | Prices on quantities the run already reports. ⭐ Absent costs nothing, and a model with charges is bit-identical to the same model without them — trace_hash included — because a price is read off the finished outcome and takes no part in any decision. |
schedule | array of scheduledlot | no | Lots released at explicitly listed times, in addition to releases. ⚠️ Sorted by at_seconds at compile time, so a document may list them in any order — but two lots at the same instant are released in document order, which is the only thing that makes a run reproducible. |
profiles | array of profile | no | Named calendar profiles: a repeating, piecewise-constant factor over a cycle, pointed at by a release's rate_profile, a step's scale_profile, a resource's or pool's roster_profile, or a consumable's lead_time_profile. ⭐ A profile consumes no randomness, so one of factor 1.0 everywhere is bit-identical to no profile at all. |
Units, everywhere
Every duration in a model is in seconds, and every key that holds one says so in its
name. The engine's clock is an integer tick — tick_scale_per_second says how many ticks
a second is, and the default of 1000 gives millisecond resolution.
A floating-point clock makes event ordering depend on rounding, which makes a run
irreproducible in a way no test can pin down. The tick is an i64. You will not notice it
except that two runs of the same model give the same answer, for ever.
What a model does not contain
- No schedule of what to do next. That is a scheduler's job, and yours is better at it. A model describes the plant; the simulation tells you what a policy would do over months.
- No results. Running the model produces a separate results document.
- No random seed. Randomness is derived from the replication number and the identity of the thing drawing, so replication 3 is replication 3 on any machine, in any order, with any number of workers.