Cash, state, and the machine
Every model so far has one-way causality: state gates cash, and cash never answers back. A unit's rent can depend on its status. Its status cannot depend on whether the rent arrived. Real deals close the loop constantly: a unit goes delinquent because rent stopped; a trigger traps cash until collections cure; a reserve builds between distribution dates. This chapter adds the constructs that state those claims.
Logic reads settled cash
The engine settles each period in a fixed order: state first, then the period's streams, then the scheduled distributions. When a guard evaluates, every earlier period's cash is therefore a settled fact. The language lets logic read exactly that far:
event delinquency.flag when series_sum("ops.rent", time.t - 1, time.t - 1) < 50 {
set entity asset.suite.status = "vacant"
}The window ends at time.t - 1, and the bound is a rule, not a habit. Logic reads settled history: cash that had already arrived when the period opened. A window that touches time.t or later is refused at compile time (E1134), because the current period's cash does not exist while the current period's logic runs. One consequence is worth learning early: at period 0 there is no settled history, so a window that lies entirely before the first period makes the guard false, with no warning. A condition such as < 50 does not fire in the first period on cash that never existed.
The one-period lag matches the world the model describes. A real delinquency flag also trails the missed payment: the servicer reports it after month-end. The model's lag and the world's lag agree.
The machine: topology instead of flags
Chapter 10 said an event happens as often as it happens, and that once-ness comes from the world rather than the construct. This section shows the other half: a regime that returns, and the construct that constrains it.
version 0.1
model "breach-and-cure"
time calendar monthly from 2026-01 for 12
lifecycle unit {
initial leased
state leased, delinquent
leased -> delinquent when series_sum("core.rent", time.t - 1, time.t - 1) < 50
delinquent -> leased when series_sum("core.rent", time.t - 1, time.t - 1) >= 50
}
entity asset suite {
lifecycle unit
paying init 1.0 next if(time.t == 3 or time.t == 8, 0.0, 1.0)
}
stream core.rent on entity asset.suite inflow currency USD {
schedule every month from 2026-01 to 2026-12
amount = 100 * asset.suite.paying
}A lifecycle block declares a finite state machine. The states are enumerated up front. The machine names one initial state. Each edge is an arrow with an optional when condition. An entity binds the machine by name (lifecycle unit in its block) and opens in the initial state, or in its own state override. From then on, the machine drives the entity's status.
The vocabulary matters, because three different things meet in one line of source. A state, such as leased, is a node on the machine's graph. An edge is the arrow together with its condition: leased -> delinquent when …. The edge is the event — the occurrence that forces the move. An arrow may also leave a state and return to the same state, as in leased -> leased. That arrow is called a self-edge. The state is not the edge: leased stays a node, and the self-edge is the arrow through it. Taking a self-edge is a real transition — a renewal re-enters leased and restarts every window anchored to that entry.
Run the model and read the transitions in the results:
- Rent stops in month 3. The machine sees the shortfall in month 4's settled history and moves to
delinquentin month 4. - Rent resumes. Month 5 cures the breach.
- Rent stops again in month 8. The machine moves to
delinquentin month 9 and cures in month 10.
The topology was walked twice. An edge's condition is evaluated each period the entity is in the edge's from state, and only then. Taking the edge moves the machine, which disarms the edge. Re-entering the state re-arms it. Edge availability is the memory, which is what a finite state machine has always meant.
Four rules make the machine reviewable:
- States are enumerated; edges are declared only as used. A misspelled state in an edge is a compile error that names the declared set (
E1316). An edge you did not declare does not exist: absence is the prohibition, and nobody writes the full matrix. - Declaration order resolves ties. When two conditions hold in one period, the first declared edge is taken. At most one transition per entity per period is taken. Waterfall steps follow the same rule.
- An edge without a condition is a permission.
downtime -> leasedwith nowhennever fires on its own. The edge exists so an event's write may take it. An event'sset … statusis validated against the declared edges; where no edge permits the move, the write is refused and the refusal names the edge. A machine that declares no edges leaves events unconstrained, which is what every model before this chapter effectively had. - Every evaluation is recorded. A transition is journaled with the edge taken and the values its condition read, as
reads. A period in which the machine stays put is recorded too: the results'traceholds, for each entity and period, the state, every edge tested and found false, and what each guard read —heldinleasedwithseries_sum("core.rent", …) = 100is as much the record as the move todelinquent. A regime is never again a bare field whose changes the record cannot see, and a trigger that did not trip is a decision with its inputs, not an absence.
active in state closes the loop back to cash. A stream declared active in state leased turns off during delinquency and turns back on after the cure, with no further mechanism. State gates cash; cash moves state; both directions are declared.
Arrival actions: what happens when you get there
A state can carry behavior. Arriving somewhere is an occurrence, and the bookkeeping that belongs to it — reset a counter, record a shortfall, strike a new rate — belongs on the arrival rather than in a separate event that has to restate the condition.
Here is a loan that misses payments, defaults, resumes paying, and is restored
to performing only after a probation period. Two things are needed: a
clock that restarts at each arrival, and bookkeeping that happens on arrival.
The clock needs no field. state_enter(asset.loan, defaulted) is the period
the loan most recently entered defaulted, so time.t - state_enter(asset.loan, defaulted) is the months since (docs/01 §11.5). The bookkeeping is the
arrival action: here, the rate the loan bears while in default.
version 0.1
model "a-loan-that-defaults-and-cures"
time calendar monthly from 2026-01 for 14
entity asset loan {
lifecycle servicing
paying init 1.0 next if(time.t >= 3 and time.t <= 6, 0.0, 1.0)
rate init 0.06
}
lifecycle servicing {
initial current
state current, delinquent, defaulted
// Arriving in default strikes the default rate; arriving back in current
// restores the contract rate, however the loan got there.
on enter defaulted { set rate = 0.09 }
on enter current { set rate = 0.06 }
current -> delinquent when asset.loan.paying < 0.5
delinquent -> defaulted when asset.loan.paying < 0.5
and time.t - state_enter(asset.loan, delinquent) >= 2
delinquent -> current when asset.loan.paying >= 0.5
// The cure is not a payment. It is a payment PLUS a probation period.
defaulted -> current when asset.loan.paying >= 0.5
and time.t - state_enter(asset.loan, defaulted) >= 3
}
stream credit.payment on entity asset.loan inflow currency USD {
schedule every month from 2026-01 to 2027-02
amount = 1000 * asset.loan.paying
}Run it and read the transitions:
t=3 current -> delinquent
t=5 delinquent -> defaulted
t=8 defaulted -> current
Payments stop at month 3 and the loan goes delinquent immediately. Two months
later it defaults. Payments resume at month 7 — and the loan is still in
default, because paying once is not curing. The cure lands at month 8, three
months after entering defaulted, which is exactly what the probation says.
That gap between "started paying again" and "is performing again" is the whole
point, and it is why supervisors write probation periods into the definition of
default in the first place. The elapsed-since-entry read says it in one
subtraction, and it restarts at every entry by construction. The field rate
reads 0.09 from month 5 and 0.06 again from month 8: the arrival actions did
the bookkeeping, and no event restated the condition.
Two grains, and both are real:
on enter <state>carries what is true of the STATE however it was reached. Resetting the clock holds for every edge that arrives, including one you add later.- an action on an edge carries what is true of the PATH taken. A renewal
and a re-let both land in
leased, but the rent is struck differently because of how you arrived — and an entry action cannot say that, since it does not know which edge fired.
Entry actions run first, then the taken edge's: the state's own setup, then the
path's refinement. If both write the same field the later one stands and the
earlier is journalled overridden, naming who wrote it — so a pack's default
and your override are distinguishable in the record rather than silently
merged.
Two rules worth knowing before you reach for them. An action writes fields,
never status: a status write would fire a second transition inside the same
period, and a transition that should cause another transition is an edge out of
the target state, taken next period. And an action reads the same world its
guard does — state as the period opened, settled cash strictly backward — so it
cannot see this period's cash, which is what keeps the whole thing acyclic.
The account: cash that waits
Chapter 11's available is the current period's netted cash, and a monthly waterfall that distributes it stays exactly as it was. What was missing is cash that accumulates: a reserve building toward a target, proceeds waiting for a quarterly date, trapped cash held across a breach. The account is that construct:
version 0.1
model "reserve-cycle"
time calendar monthly from 2026-01 for 12
entity asset suite
entity party sponsor { name = "Sponsor" }
// examples-allow: account.reserve — the balance cycles by design: funded,
// drained by the June release, refilled the month after
account reserve { }
stream ops.rent on entity asset.suite inflow currency USD {
schedule every month from 2026-01 to 2026-12
amount = 1000
}
waterfall dist on entity asset.suite {
schedule every month from 2026-01 to 2026-12
from available
pay top_up to account reserve = max(0.0, 300.0 - prev.reserve)
pay residual to party.sponsor = remaining
}
waterfall release on entity asset.suite {
schedule on 2026-06
from reserve
pay released to party.sponsor = remaining
}The twenty lines above show all three uses:
- A step pays into an account.
pay top_up to account reservefunds the reserve to a 300 target. The step tops up whenever the settled balance (prev.reserve) reads short. - A waterfall draws from an account.
from reservehands the release waterfall the accumulated balance, on the release's own schedule, in place of a hand-written cumulative window. The June release drains the reserve; July's top-up refills it. - Logic reads a balance.
prev.<account>is the balance at the previous period's close, with every allocation included. In the first period it is the account's opening balance, itsinit, zero by default: never absent. So the first top-up is the whole 300 with no special case (docs/01§10.6).
The balance obeys one law: the carried balance, plus the period's declared inflow, plus what steps allocated in, minus what a drawing waterfall took out. The balance publishes as a non-cash series, account.reserve, beside the flows. The step's series is the flow; the account's series is the position; nothing is counted twice.
Two more facts complete the picture. A balance has no floor: an account fed a deal's whole net cash is the deal's cumulative position, and a cumulative position runs negative through the J-curve. What a step may take is floored at zero, because cash that is not there cannot be allocated. A party may also own an account (owner party.sponsor), and allocations paid to that party then accumulate there. A party-owned balance is allocated cash, not an obligation: what the rules still owe is simply not yet allocated. A party may own several accounts; to party.sponsor then no longer says which, and the step is refused (E1324) until it names the account, to account <name>. A stream can move an account directly too: a stream whose header states moves <account> raises or lowers that balance as it pays, which is how a loan's draws and repayments carry its balance.
Windows that hang off a state
The last construct connects the machine to chapter 4's schedules. Every anchor so far was a date or a phase, fixed at compile time. The third anchor is a state entry:
version 0.1
model "delayed-build"
time calendar monthly from 2026-01 for 24
lifecycle project {
initial waiting
state waiting, building, paused
waiting -> building when time.t == 4
building -> paused when time.t == 8
paused -> building when time.t == 10
}
entity asset site {
lifecycle project
}
stream capex.build on entity asset.site outflow currency USD {
schedule every month from state_enter(asset.site, building) for 6 periods
amount = 100
active in state building
}from state_enter(asset.site, building) for 6 periods opens a six-period window at each entry into building. The schedule states "six months of construction from whenever construction starts."
Run the model and watch the parts compose:
- Nothing accrues while the site waits.
- Entry at month 4 opens the window.
- The pause masks months 8–9 through
active in state. The window is presence; the state is activity; the two compose. - Re-entry at month 10 opens a fresh six-period window. A re-entered state re-anchors, which is what a second delinquency's cure period means, and what a renewal's restarted term means.
Inside a window, intervals, placement, and payment terms mean exactly what chapter 4 taught.
More of the machine
A few more constructs complete the picture; each is specified in docs/01.
- An action on an edge. Beside
on enter, an edge may carry its own action block, for what is true of the path rather than the state (docs/01§7.3). - A guard may draw.
sample(inputs.u)in an edge condition is a draw for that entity and period, so a default can arrive at a stated monthly probability:current -> defaulted when sample(inputs.u) < inputs.monthly_default_prob(docs/01§12.2.1). - A scheduled test. An event may run on its own schedule,
event covenant_test schedule every quarter when …, which is how a covenant tested quarterly reads (docs/01§13.1). - A contract's own machine. A contract may bind a lifecycle and gate its lines by state,
in performing run proceeds, interest, principal, and itson startandon endblocks write state at its boundaries (docs/01§8.6).
What can go wrong
A guard that touches the present. series_sum(…, time.t, time.t) in an edge condition or an event condition is refused at compile time (E1134). Settled history ends one period back; the refusal restates the claim.
A state from nowhere. An edge, an opening state, or a set … status that names a state the machine does not declare is a compile error, and the message lists the declared set. The finite set is the whole point: a typo cannot found a phantom regime.
A write with no edge. Once a machine declares edges, an event write that no edge permits is refused at run time. The refusal names the edge, and the journal records the outcome as declined. Where the target state has no incoming edge at all, the refusal comes at compile time, because the write could never be legal.
Waiting for a permission to fire itself. An edge without a condition never self-fires. If the cure should happen on its own, give the cure edge a when. If an event drives the cure, the bare edge is exactly right.
Expecting the trap to leak. A step can take only what the balance holds. The floor is at the allocation, not at the balance. If a release step pays less than expected, read account.<name> first: the position series is the audit trail, and the journal records every movement with the balance before and after.
Exercises
The trapped-cash structure — the machine, the account, and a backward read in one model:
Trap the cash, then let it go
The starter's deal pays its residual straight through — even in the month after collections failed. Add the trap.
- Add a
trappedstep above the residual. Divertremaininginto the trap account whileasset.suite.status == "trapped". - Add a
releasewaterfall drawingfrom trap. Pay the balance to the sponsor when the status is"normal"again.
Predict before you run:
- The machine reads settled rent, so the breach at month 3 traps month 4's cash, not month 3's.
- The trap balance holds exactly one month's rent, for exactly one month.
Then check the series:
account.trapreads0, 0, 0, 0, 100, 0, …— funded at the breach, drained at the cure.release.releasedpays the 100 in month 5, the firstnormalmonth.- No dollar is lost: the sponsor's lifetime total is the same 1,100 the rent produced.
The trap changed when the sponsor was paid, never whether. Timing is the whole construct.
Then, on your own:
- In the breach-and-cure model, make the cure slower than the breach: require two consecutive months of full rent (
series_sum("core.rent", time.t - 2, time.t - 1) >= 100). Predict the new transition months before you run the model. The asymmetry between how fast trouble arrives and how slowly trust returns is one guard's width. - Read the breach-and-cure model's trace for
asset.suite: find the run ofheldperiods before the first breach, the guard it tested, and the rent it read in each period. Then find themovedperiod and follow itsentryto the transition in the journal. Which period's read is the one that decided the move? - Give the reserve model a J-curve. Add a capex outflow larger than early rent, feed the account
fromthe net of both streams, and watch the balance go negative before it climbs. Then write one sentence on why the release still cannot overdraw the balance.