Language guide
CFDL is a language for saying what a deal is. You declare the terms — who the parties are, what money moves, when, and on what basis — and the engine derives the schedule, the cash flows and the metrics.
There is no calculation to write and no order of operations to get right. The same model always produces the same numbers.
The shape of a model
Every model declares four things:
version 0.1
model "first-look"
time calendar monthly from 2026-01 for 12
entity asset tower
stream tower.rent on entity asset.tower inflow currency USD {
schedule every month from 2026-01 to 2026-12
amount = 10000
}A version, a name, a timeline, and at least one entity and one stream. That model runs, and reports 120,000 of cash across twelve months.
Try it in the playground — nothing to install.
Time
The timeline is the grid every amount is evaluated on.
time calendar monthly from 2026-01 for 120calendar may be annual, quarterly, monthly or daily; from is the
first period and for the count.
Choose the grain at which something varies. If a mortgage's interest changes every month, the model is monthly. Reporting it annually is a separate question, answered later by a statement rather than by the timeline. A schedule finer than the calendar is rejected rather than silently collapsed, because occurrences inside one period cannot be told apart.
To value cash beyond the modelled horizon — a terminal value struck off a forward year — extend the grid without extending the cash:
time calendar monthly from 2026-01 for 120 project 12Entities
An entity is something that holds cash: a property, a borrower, a project, a fund.
entity asset tower : CRE.Asset.RealProperty
entity party acme : CRE.Party.TenantThe first word is the entity's family — asset for something that produces
or consumes cash, party for someone who contracts, owns or lends. The second
is its name, and what follows the colon is its type, checked against the
active pack's vocabulary. A model with no pack still has the base types:
Asset.Real, Asset.Financial, Asset.Intangible and Party.
Every stream belongs to an entity, so cash totals per entity as well as per
model. Where a model declares hierarchy with part of, a parent's total
includes its children.
An asset's type may carry a lifecycle — a closed set of states it moves through, with events that move it between them, and contracts and streams that switch on where it is. See the object model.
Streams
A stream is one economic line item over time — the atom everything else is built from. It says who, which way, in what currency, on what schedule, and how much.
stream tower.opex on entity asset.tower outflow currency USD {
schedule every month from 2026-01 to 2035-12
amount = 4200
}inflow and outflow set the sign. Values are stored signed — an outflow is
negative — so a period's cash is the sum of its streams and nothing else.
Schedules
schedule on 2026-03
schedule every month from 2026-01 to 2030-12
schedule every quarter from 2026-01 to 2030-12
schedule every month on day 15 from 2026-01 to 2030-12on day 15 places the cash within its period. Where cash sits inside a period
changes what it is worth, so CFDL asks rather than assuming.
Amounts that vary
amount is an expression evaluated once per
period. time.t is the period index, counting from zero:
amount = 25000 * pow(1.03, time.t / 12.0)That is rent escalating 3% a year on a monthly grid.
Turning a stream on and off
stream tower.percentage_rent on entity asset.tower inflow currency USD {
schedule every month from 2026-01 to 2035-12
active when time.t >= 24
amount = 1500
}Assumptions
assume names a value once, so the model reads as terms rather than numbers:
assume vacancy_rate = 0.05
stream tower.vacancy on entity asset.tower outflow currency USD {
schedule every month from 2026-01 to 2035-12
amount = 10000 * inputs.vacancy_rate
}An assumption can be a distribution rather than a number, which is what turns one model into a range of outcomes:
assume exit_cap ~ Normal(mean=0.065, stdev=0.005, clip=[0.05, 0.08])A deterministic run uses the mean; a Monte Carlo run samples it, seeded, so the same run reproduces exactly. See Stochastic modeling.
Curves
A curve is a dated series — a forward price, an index — declared once and read by date:
curve power_price linear {
2026-01: 42.10
2027-01: 44.75
2028-01: 46.20
}amount = 1200 * curve_value("power_price", time.date)The mode says how a value between two stated points is found: linear
interpolates, step holds the last point forward.
obs.* is a different thing — observations supplied at run time rather than
declared in the model.
See Curves.
State
Some quantities depend on the period before: a loan balance, a survival factor.
A state says so directly.
state balance {
init 1000000
next prev * (1 - 0.01)
}init is period zero; next computes each later period from prev. Both take
an expression directly, with no =, the way schedule and active when do. A
state is
not cash — it never reaches the model's total. It exists so a stream can
read it:
amount = state.balance * 0.005Writing the recurrence directly is often the only way to match a published figure exactly, because a source that escalates an already-rounded number each year is not computing a power of its base.
Waterfalls
Some cash is not earned, it is allocated. A securitisation pays its tranches
in strict order; a fund returns capital before it pays carry. A waterfall
declares that order over a pot:
waterfall deal.distribution on entity asset.trust {
schedule every month from 2026-01 to 2030-12
from asset.trust.available_funds
pay servicing to party.servicer = 12500.0
pay senior to asset.class_a = 6250.0
pay residual to party.certificate = remaining
}Every step is pay <name> to <payee> = <expr>, and a step takes what it asks
for or what is left, whichever is smaller. remaining is what survives the
steps above; paid.<step> and owed.<step> read what an earlier step did.
A waterfall runs after the period's streams and states, so it shares out money that already exists. Its steps publish as series, so a waterfall declared later can draw on an earlier one's payment as its own pot — a fund's carry becoming a management company's. See Waterfalls.
Contracts and packs
Everything so far builds streams by hand. A pack lets you declare business terms instead, and expands them into the streams those terms imply.
use pack "cre" version "0.1.0"
contract cre.lease {
term 2026-07..2031-12
terms {
base_rent = 25000
}
}That is a lease, not a stream — and the pack turns it into the streams a lease produces, each already classified so it lands on the right line of a statement.
Declare a contract more than once by giving it a suffix, so the pieces stay separable in the results:
contract cre.lease.suite_200 { ... }
Four packs ship today: CRE, credit, energy and operating companies. See pack contracts for every contract and the terms it reads.
Use contracts for what the pack understands and streams for everything else.
The two mix freely in one model, and a hand-written stream can declare its own
category so it is counted in the same subtotals.
Splitting a model up
Past a certain size, one file stops helping. Any model can be split across a directory, which the compiler reads whole:
deal/
time.cfdl
structure.cfdl
contracts.cfdl
Declaration order does not matter. See Multi-file models.
What you get back
A run produces a results document containing:
deterministic.series— every stream per period, plusmodel.net_cash_flowdeterministic.metrics— NPV, IRR, MOIC, payback, weighted average lifestatements— the pro forma, when the pack declares onemonte_carlo— percentiles and trial summaries, when trials were asked for
Read it in the playground, through the Python SDK, or as JSON. See Reading results.
Where to go next
| Examples | Eight short lessons, then worked deals checked against published sources |
| How-to guides | Schedules, packs, scenarios, curves, metrics |
| Reference | Expressions, contracts, metrics, statements, diagnostics |
| Specification | The normative definition, for implementers |