Skip to content

Simfile Schema

Simfile v0.1 is a strict, genre-neutral YAML or JSON schema. Unknown keys are rejected. IDs name authored concepts; the schema never assigns domain meaning to them.

simfile_version: "0.1"
name: tiny-world
clock:
seed: run-001
tick: 30s

simfile_version, name, and clock are required. All collection sections default to empty maps.

KeyRequiredValue
simfile_versionyesExactly "0.1".
nameyesA Simfile identifier.
spawnfilenoString source reference; not parsed or started by the CLI.
clockyesRun seed, tick duration, optional simulation rate and phases.
variablesnoMap of variable ID to variable record; defaults to {}.
generatorsnoMap of generator ID to generator record; defaults to {}.
rulesnoMap of rule ID to rule record; defaults to {}.
ledgernoLedger store configuration.
telemetrynoSnapshot sampling configuration.
markersnoMap of marker ID to marker record; defaults to {}.
probesnoMap of probe ID to probe record; defaults to {}.

Identifiers match:

[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*

They start with a lowercase letter and use lowercase letters, numbers, dashes, or underscores. This form is used for the world name, map keys, phase references, variable references, and fed_by.

Durations are a non-negative decimal followed by ms, s, m, h, d, or w, for example 250ms, 20s, 1.5m, or 2h. The schema accepts zero, but clock.tick and clock.sim_per_tick must be positive at runtime.

Ranges are two signed numbers separated by .., with the lower value strictly less than the upper value:

-1..1
0..100

Scopes have exactly these forms:

global
agent:<id>
team:<id>
room:<network>:<room>
pair:<a>:<b>
clock:
seed: office-run-014
tick: 20s
sim_per_tick: 10m
phases:
morning: "07:00"
workday: "09:00"
evening: "18:00"
night: "22:00"
FieldRequiredMeaning
seedyesNon-empty deterministic run seed. simfile run --seed can override it.
tickyesPositive duration of one kernel tick.
sim_per_ticknoSimulated duration advanced per tick; defaults to tick.
phasesnoMap of phase ID to 24-hour HH:MM; defaults to {}.

The finite simfile run --ticks loop does not sleep. tick becomes wall cadence only when a live driver chooses to wait between kernel steps. Phase selection repeats over a 24-hour simulated day.

variables:
filing_pressure:
scope: room:office-floor:case-warroom
initial: 0.4
range: 0..1

Each variables.<id> record accepts:

FieldRequiredMeaning
scopeyesOne valid scope.
initialnoNumeric initial value; defaults to 0 and is clamped to range.
rangeyesStrict lower and upper bounds in min..max form.
measurenoDeclares a measured source.
derivenoDeclares a pure expression source.
fed_bynoReserves writes for one instrument/actor ID.

A variable may use at most one of measure, derive, or fed_by.

variables:
hall_heat:
scope: room:office-floor:office-hall
range: 0..40
measure:
kind: messages_in
scope: room:office-floor:office-hall
window: 30m

measure requires kind and window; its scope is optional. The accepted kind values are:

messages_in
token_mentions
marker_violations
distinct_speakers
mentions_of
ticks_since_last_message

These records validate today, but the finite simfile run batch runtime does not yet compute measured inputs. Do not treat a validated measure as a populated counter in that path.

variables:
social_weather:
scope: global
range: 0..1
derive:
eq: 0.015 * hall_heat + 0.6 * evening_pull

derive contains exactly one non-empty eq string. All variable dependencies must exist, and derived-variable cycles fail at runtime compilation.

variables:
adjudicated_signal:
scope: global
range: 0..1
fed_by: referee

fed_by names the only actor authorized to set that variable through a queued world.act. A generator or rule action targeting the same fed variable is a validation error.

derive.eq, deterministic delta_eq, and deterministic set_eq use the same closed numeric expression language.

Operands are numbers, duration literals, declared variable IDs, and the reserved values t, tick, pi, and e. Operators are unary +/- and binary +, -, *, /, and **, with parentheses. Division by zero and non-finite results become 0.

The accepted functions are:

abs ceil clamp cos exp floor lerp log max min mod pow
sin smoothstep sqrt step

Expressions cannot assign, branch, loop, call arbitrary code, or draw randomness.

Generators, rules, and probes share one recursively nestable when shape.

when:
variable: filing_pressure
above: 0.85
below: 0.95
for: 30m

variable must name a declared variable. At least one of above or below is required; comparisons are strict > and <. Optional for requires the condition to remain true for that much simulated time.

when:
phase: workday

The phase must be declared under clock.phases.

when:
event: wake.recommended
target: room:office-floor:case-warroom
actor: deadline_bites
scope: room:office-floor:case-warroom

target and actor are optional strings; scope is an optional valid scope. Event kind is one of:

clock.sync
rule.fired
world.message
world.dm
world.act
wake.recommended
marker.seen
when:
all:
- phase: workday
- any:
- variable: filing_pressure
above: 0.85
- not:
event: marker.seen

The composition records are all with a non-empty condition list, any with a non-empty condition list, or not with one condition. A bare condition list is invalid.

Generators target one declared, non-fed variable and may have an optional shared when condition.

generators:
deadline_ramp:
kind: deterministic
when:
phase: workday
variable: filing_pressure
delta: 0.02

A deterministic generator requires exactly one writer:

  • delta: add a constant each active tick;
  • delta_eq: add the result of an expression; or
  • set_eq: replace the value with the result of an expression.
generators:
day_texture:
kind: stochastic
variable: evening_pull
uniform: [-0.01, 0.03]

A stochastic generator requires uniform, a two-number tuple. It draws once per active tick from the stream derived from the run seed, generator ID, tick, and draw index, then adds the result to the target. Values are clamped to the variable range after generator application.

rules:
deadline_bites:
fire: per_crossing
when:
variable: filing_pressure
above: 0.85
do:
- action: wake:recommend
to: room:office-floor:case-warroom
FieldRequiredMeaning
firenoonce or per_crossing; defaults to per_crossing.
whenyesOne shared condition tree.
doyesNon-empty action list.

once fires on the first matching tick. per_crossing fires on each false-to-true transition.

Actions have exactly these shapes:

- action: moltnet:message
to: room:<network>:<room>
content: "Pressure is {filing_pressure}."
- action: moltnet:dm
to: agent:<id>
content: "A private world message."
- action: wake:recommend
to: room:<network>:<room>
- action: variable:set
variable: <variable-id>
value: 0.5
- action: variable:delta
variable: <variable-id>
value: 0.1

Room messages require a room scope; DMs require an agent scope. Variable actions require a declared, non-fed variable. Braced placeholders in message content must name declared variables.

markers:
referral_client:
text:
- "Rosa Delgado"
- "Ms. Delgado"
mode: propagation
scopes:
- room:office-floor:case-warroom
FieldRequiredMeaning
textnoList of non-empty aliases. If absent or empty, the marker ID is the alias.
modeyescontainment or propagation.
scopesyesNon-empty list of valid scopes.

Matching is case-insensitive and uses Unicode word boundaries over text-like event payload fields. A containment marker passes only with at least one in-scope hit and no out-of-scope hit. Current propagation evaluation passes with at least one hit in any declared scope; it does not require every scope.

probes:
deadline_observed:
when:
event: wake.recommended
target: room:office-floor:case-warroom
expect:
at_least: 1
FieldRequiredMeaning
whenyesCondition being tested.
expectyesExactly one expectation shape.
afternoAnchor condition for a bounded window.
withinnoDuration after the anchor; required with after.

Expectation shapes are:

expect:
at_least: 1
expect:
at_most: 0
expect:
always: true
expect:
at_end: true

at_least and at_most take non-negative integers. after and within must either both be present or both be absent.

ledger:
store:
kind: jsonl
path: .sim/ledger.jsonl

ledger.store is required when ledger is present. store.kind is optional and defaults to jsonl; accepted values are jsonl, sqlite, and postgres. store.path is an optional non-empty string.

The current finite simfile run writer always emits <out>/ledger.jsonl; it does not route that run record through the configured store kind or path.

telemetry:
snapshot_every: 50

snapshot_every is an optional positive integer. When absent, simfile run writes every variable sample. When present, it keeps samples at ticks divisible by that value and also keeps the final sample.

Terminal window
simfile validate ./Simfile --spawnfile-report .spawn/spawnfile-report.json

Without the report, scope strings are checked only for shape. With it, Simfile builds an index from Spawnfile report nodes and their active Moltnet room bindings. Binding checks cover variable scopes, marker scopes, rule and probe event filters, and rule action destinations. The current binding pass does not inspect measure.scope, pair members, or generator event filters. The report is an explicit validation input; the spawnfile: key never causes an implicit compile or deployment.