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 modeled 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,
container for a grouping that scopes cash: a fund, a portfolio, an SPV, a
transaction. 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, Party, and the four Container.* types.
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 may carry a lifecycle — a declared finite state machine, with guarded edges that move it and contracts and streams that switch on where it is. A model declares the machine itself, or the type brings one from its pack. See below, and lifecycles and state.
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.
Fields that move
Some quantities depend on the period before: a loan balance, a survival factor, a reserve. Those belong to the thing they describe, so they are declared on it.
entity asset loan : Credit.Asset.Tranche {
seniority = 1
balance init 1000000
next prev * (1 - 0.01)
}init is the value at period zero; next computes each later period, with
prev bound to the field's own previous value. Both take an expression
directly, with no =, the way schedule and active when do. A field with no
next simply holds.
Read it by naming the thing:
amount = asset.loan.balance * 0.005prev.asset.loan.balance reads the close before this one — which is how a debt
schedule charges interest on the average of a period's opening and closing
balance without declaring the quantity twice.
A field is not cash: it never reaches the model's total. It exists so a stream, waterfall or event can read it.
Writing 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.
An untyped entity may carry a block too — fields, a machine binding — and a typed entity's block is additionally checked against the pack's vocabulary.
The lifecycle machine
A regime that changes and can change back is a lifecycle: enumerated
states, an initial one, and edges declared only as used, each with an
optional when guard.
lifecycle unit {
initial leased
state leased, delinquent
leased -> delinquent when series_sum("ops.rent", time.t - 1, time.t - 1) < 50
delinquent -> leased when series_sum("ops.rent", time.t - 1, time.t - 1) >= 50
}An entity binds it with lifecycle unit in its block. A guard is evaluated
each period the entity is in the edge's from-state — edge availability is the
memory — and it reads settled series strictly backward,
so realized cash drives state. active in state leased on a stream closes
the loop back to cash. A schedule can hang a window off an entry:
from state_enter(asset.site, building) for 18 periods. See
lifecycles and state.
The account
Cash that accumulates between distribution dates gets its own construct:
account reserve { }A waterfall step pays into it (pay top_up to account reserve = ...), a
waterfall draws it (from reserve in place of a hand-written cumulative
window), and logic reads the settled balance as prev.reserve. available
keeps meaning this period's netted cash, so a monthly-distributing waterfall
is untouched; the account is the accumulated position, published as the
non-cash series account.<name>, every movement journaled. See
Waterfalls.
Quantiles
Some economics depend on dispersion inside one period — a battery earning
the spread between a month's expensive and cheap hours, overage rent above a
breakpoint. A quantile declares that within-period distribution, and three
functions read it: quantile_mean(name, lo, hi) averages a slice,
quantile_at(name, share) reads a point, quantile_of(name, value) inverts
it. See Stochastic modeling.
Waterfalls
Some cash is not earned, it is allocated. A securitization 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 available
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 fields and streams, so it shares out money that already exists — and only at the periods its schedule names, with cash accumulating (in an account, when one is declared) between distribution dates. 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 {
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 defines 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.
Metrics of your own
The engine mints model.* metrics and a pack mints domain.* ones. A
metric declaration mints the model's own — a figure evaluated once, at the
horizon, over the finished projection:
metric gross_revenue = series_sum("ops.revenue", 0, 4)
metric total_cost = series_sum("ops.cost", 0, 4)
metric margin = metric.gross_revenue + metric.total_costA metric folds any series the model publishes, and metrics compose in
declaration order — margin reads the two above it rather than repeating
their expressions. irr(party.lp) and moic(party.lp) read a participant's
realized return from their account, and exist in a metric and nowhere else.
Every declared metric is published as metric.<name> beside the engine's,
in every scenario and every Monte Carlo trial. See
Metrics.
Slices
A slice names a deliberately partial selection of the model's streams —
one artist's royalties, a portfolio minus a discontinued line, one asset over
two years:
slice artist_a_royalties {
entity asset.artist_a
category "operating.revenue.royalty"
}
slice middle_years {
entity asset.co
window from 2027-01 to 2028-01
}Clause kinds intersect, values within a kind union, except subtracts, and
window bounds the periods. Results publish each slice's selection, matched
streams, net series, and total/NPV/IRR — and no reconciliation, because a
partial number must not dress as a complete one. See
Slices.
Statements
A statement declares how results are organized. The generated form names a
hierarchy the results already carry and a depth to cut it at:
statement portfolio {
label "Portfolio by property"
structure entity
depth 2
}Or a statement states its own rows — line, subtotal, ratio, spacer —
for the pro forma whose labels and ordering no tree supplies. One or the
other, never both. A model that declares none gets its entity hierarchy as a
default. See Statements and reporting.
Slices and statements are views: they move no cash and change no identity, so declaring one moves neither results hash. A declared metric is a figure the model claims, so it does.
What you get back
A run produces a results document containing:
deterministic.series— every stream per period, attributed to its owning entity and category, plusmodel.net_cash_flowand each account's balance asaccount.<name>deterministic.metrics— NPV, IRR, MOIC, payback, weighted average life, and every declaredmetric.<name>statements— every declared or pack statement, or the default entity hierarchy when nothing declares oneslices— each declared slice's selection, streams, net series and figuresgraph— the model's entity graph: symbols, families, types,part ofmonte_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 |