Skip to main content

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.

What this section is for

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 }
JSON is the canonical form

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.

Read the red boxes as "things that make work wait"

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 keymust match
releases[].product, schedule[].product, conwip[].producta products[].id
products[].steps[].resourcea resources[].id
steps[].consumes[].consumablea consumables[].id
steps[].operator.poolan operators[].id
steps[].setupa from or a to in that resource's changeovers
resources[].min_run[].setupa setup named on a step of that resource
A name that does not resolve is refused, not ignored

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​

pagewhat it covers
Distributionsevery duration in the model, and the seven shapes one can take
Resourcesmachines, bays and crews: capacity, queueing, setups, dedication
Products and stepswhat flows, the route it takes, and every option on a step
Releaseshow work arrives: streams, a fixed population, or a named schedule
Operatorspeople as a constrained pool, with shifts and breaks
Consumablesmaterial a step draws down, and how it is replenished
Downtimebreakdowns and planned maintenance, and how they collide
Batchingwork that runs together, and how a batch forms
Dispatch ruleswhich waiting job goes next, expressed as weights
Cascadingmachines that start the next job before the last one finishes
Reading the resultsevery field the engine prints, and how to define a KPI
Examplesthree complete models, each run end to end

Top-level keys​

keytyperequiredwhat it means
schema_versionintegerno, default 1Document format version. Bump only when the format changes incompatibly; today every model is 1.
namestringyesA 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_secondintegerno, default 1000Clock resolution. ⚠️ An integer tick, per ADR-0003; this says how fine.
horizon_secondsnumberyesHow long to simulate, after warm-up.
warmup_secondsnumbernoStatistics 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.
resourcesarray of resourceyesEvery machine, bay, crew or station work can queue for. At least one is required.
operatorsarray of operatorno⚠️ 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.
consumablesarray of consumablenoStores of material a step can draw from, with their own replenishment. Omit if nothing is consumed.
productsarray of productyesWhat flows through the plant, each with the ordered route it takes.
releasesarray of releaseyesHow work arrives. Required unless the model is closed (conwip).
conwipconwipnoA 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.
chargesarray of chargenoPrices 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.
schedulearray of scheduledlotnoLots 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.
profilesarray of profilenoNamed 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.

Why the clock is not a float

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.