Statements
A statement is the artifact a practitioner recognizes: an operating pro forma, a remittance report, a free cash flow build-up. CFDL produces one per period from the model you already wrote — you declare how it is organized, or take one from a pack or the default, and never maintain its numbers alongside the model.
Every run's results carry a statements section — in the playground it is
the Statement tab. A statement comes from one of three places: the model
declares one, the active pack ships one, or the engine renders the entity
hierarchy as a default, marked default. The default is assembled when
results are rendered, never enters the compiled document, and yields to any
declared statement. A statement the model declares replaces the pack's
statement of the same name.
Declaring one
A declared statement is generated or authored, never both and never
neither (E1369).
A generated statement names an existing hierarchy — structure entity
(the part of tree) or structure category (the dotted path) — and cuts it
at a depth. It enumerates no rows: a node whose children are shown is a
subtotal, a node whose children are cut off is a line carrying all of its
descendants' cash, so the lines partition the cash at every depth. It may
carry a label, a grain, a slice filter (orthogonal to the structure),
and metrics naming declared metrics to publish beside it.
statement portfolio {
label "Portfolio by property"
structure entity
depth 2
}An authored statement states its own rows — line, subtotal, ratio,
spacer — each drawing from a category, stream, slice or entity, from
a contract type and its line by role, or
presenting a published series (series "domain.cre.noi"). A
subtotal folds rows stated elsewhere and claims nothing; a series row is a
fold OF the ledger rather than cash in it, so it claims no streams, stays out
of the bottom line, and refuses a claim clause beside it (E1370); a ratio
divides two operands, each a declared slice or a pack subtotal, and publishes
null where the denominator is zero. This is the form for a pro forma: curated labels, expenses shown
positive under "Less:", a coverage ratio that is a node of no hierarchy.
What makes a generated statement work
Three things, and only the first is something you write.
A category says what a stream is, economically. It is a dotted path whose
first segment is operating, investing or financing — the sections of a
statement of cash flows. A pack's contracts classify the streams they emit, so
in most models you never write one. A hand-written stream can declare its own:
stream tower.parking on entity asset.tower inflow currency USD {
schedule every month from 2026-01 to 2035-12
category operating.revenue.other
amount = 4500
}
A subtotal aggregates categories, every period — net operating income is
everything classified operating.*. Packs declare these; you read them as
domain.<pack>.<name> in the results.
A statement puts them in order with labels and indentation.
Reading one
Every line is signed as it is stored. An expense is negative. A row that a reader expects to see positive — "Less: vacancy", debt service — carries a display sign that flips it for rendering only. The published value stays signed, so anything consuming the JSON still adds up correctly while a rendered statement reads the way a pro forma should.
A ratio is recomputed at the grain it is shown at. An annual coverage ratio
is annual NOI over annual debt service — never the average of twelve monthly
ratios, which is a different and wrong number. Where the denominator is zero
the value is null rather than zero: a period with no debt service has no
coverage ratio, and averaging a zero in would understate the deal.
The bottom line is checked, not assumed. Every statement publishes a
reconciliation — its own total, the total of the universe it reports, and
the residual between them. That universe is the model, with one exception: a
statement filtered by a slice reconciles against the slice's total, because
reporting the filter itself as a shortfall would make a warning fire on a
correct model. If a stream carries no category it appears in a visible
Unclassified row rather than quietly vanishing, because a statement that is
short by an unnoticed line looks entirely plausible.
A slice itself publishes no reconciliation, by design: it is partial on purpose, and must be seen to be — a partial number never dresses as a complete one.
Grain
Grain belongs to the output, not the run. One model can publish a monthly pro forma and an annual summary of the same cash, and both are in the same results document. Each statement carries its own column labels, because an annual view of a monthly model has ten columns where the model has 120.
Several views of one domain
A pack can ship more than one layout over the same categories, because one asset class has more than one reporting convention. The same loan pool reads as a remittance report — principal split scheduled and unscheduled, because that is what a prepayment speed acts on — or as a statement of operations reporting total and net investment income.
Each view is checked for completeness when the pack loads: every category the pack declares appears in exactly one line row. A view cannot quietly omit or double-count while looking plausible.
What each pack ships
| Pack | Statement | Reported at |
|---|---|---|
energy | Project operating statement (default) | model grid |
cre | Operating statement (default) | model grid |
cre | Sources and uses of capital | model grid |
credit | Collections statement (default) | model grid |
credit | Remittance report | model grid |
credit | Statement of operations | model grid |
opco | Free cash flow (default) | model grid |
opco | Sponsor cash flow | model grid |
opco | Statement of cash flows | model grid |
Related
- Domain packs — the categories and contracts each pack provides.
- Reading results and IR — the shape of the results document.
- Pack interface — declaring subtotals and statements, for pack authors.