CFDL

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 120

calendar 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 12

Entities

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.Tenant

The first word is the entity's familyasset 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-12

on 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.005

prev.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_cost

A 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, plus model.net_cash_flow and each account's balance as account.<name>
  • deterministic.metrics — NPV, IRR, MOIC, payback, weighted average life, and every declared metric.<name>
  • statements — every declared or pack statement, or the default entity hierarchy when nothing declares one
  • slices — each declared slice's selection, streams, net series and figures
  • graph — the model's entity graph: symbols, families, types, part of
  • monte_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

ExamplesEight short lessons, then worked deals checked against published sources
How-to guidesSchedules, packs, scenarios, curves, metrics
ReferenceExpressions, contracts, metrics, statements, diagnostics
SpecificationThe normative definition, for implementers