Skip to main content
CFDL

Reading results and IR

cfdl compile produces the IR, the canonical compiled model. cfdl run evaluates the IR and produces the results. Both are JSON documents with published schemas.

A small model and its results

version 0.1
model "reading-results"
time calendar monthly from 2026-01 for 12

assume monthly_fee : decimal = 4000
  source { publisher "Signed engagement letter"  as_of 2025-12-15 }

entity asset studio : Asset.Intangible {
  seats = 6
}

stream studio.fees on entity asset.studio inflow currency USD {
  schedule every month from 2026-01 to 2026-12
  amount = inputs.monthly_fee
}

stream studio.rent on entity asset.studio outflow currency USD {
  schedule every month from 2026-01 to 2026-12
  amount = 1500
}

Run at an 8% discount rate ({ "deterministic": { "annual_discount_rate": 0.08 } }), it produces this document, trimmed where … stands:

{
  "results_version": "0.18",
  "model_hash": "3f5e5867a9fe…",
  "ledger_hash": "150154d149ec…",
  "engine": { "name": "cfdl-engine", "version": "0.14.0" },
  "warnings": [],
  "run": { "deterministic": { "annual_discount_rate": 0.08 } },
  "inputs": {
    "assumptions": [
      { "name": "monthly_fee", "shape": "value", "type": "Decimal",
        "value": 4000.0, "origin": "model",
        "source": { "as_of": "2025-12-15", "publisher": "Signed engagement letter" } }
    ]
  },
  "deterministic": {
    "status": "ok",
    "metrics": {
      "model.npv": { "amount": 28782.144631, "currency": "USD" },
      "model.total": { "amount": 30000.0, "currency": "USD" },
      "model.wal_years": 0.541667,
      "run.annual_discount_rate": 0.08,
      "stream.studio.fees.total": { "amount": 48000.0, "currency": "USD" },
      …
    },
    "metric_units": { "model.npv": "money", "model.wal_years": "years", … },
    "series": {
      "stream.studio.fees": {
        "index": { "calendar": "monthly", "start": "2026-01-01", "periods": 12 },
        "offset": 1.0,
        "entity": "asset.studio",
        "values": [ { "amount": 4000.0, "currency": "USD" }, … ]
      },
      …
    },
    "annual_rollup": { … }
  },
  "scenarios": { "status": "not_run", "summaries": [] },
  "monte_carlo": { "status": "not_run", … },
  "statements": { "statements": [ { "id": "by_entity", "label": "Cash by entity", "default": true, … } ] },
  "charts": { "charts": [ { "id": "cash", "label": "Cash flow", "kind": "columns", "default": true, "origin": "default", … } ] },
  "graph": {
    "entities": [
      { "symbol": "asset.studio", "family": "asset", "type": "Asset.Intangible",
        "fields": { "seats": { "value": 6 } } }
    ]
  }
}

The net cash flow never turns negative, so the run publishes no model.irr and no payback.

The results, section by section

  • results_version states the document's shape: 0.18.
  • Provenance. model_hash identifies the model: a hash of the compiled IR without its views (slices and statements), so two users who look at identical results differently share a model hash. It also leaves out where anything is written: a comment, a blank line, spacing inside an expression, or a declaration moved to another file leaves it as it was. A declared metric is a figure the model claims, so it does move model_hash. ledger_hash covers what happened: the series, the journal and the transitions. engine names the engine and its version. The run configuration is in neither hash; the results publish it as run, the configuration as the run used it, with each inputs file's content in place of its path. Each scenario and the Monte Carlo section carry their own ledger_hash. Store all of it next to any number you publish, or package it: cfdl package writes a manifest of the files' checksums and every hash, which cfdl verify checks without running the model and cfdl reproduce checks by running it.
  • warnings lists what the run noticed and did not refuse. Read it before the figures.
  • run is the run configuration as the run used it: the discount rate or curve, the as-of date, the parameters, each inputs file's content, each scenario and the Monte Carlo trials.
  • inputs.assumptions is the review page: one row per assumption, a set's fields each on their own row, with its shape, type, unit, value, origin (the model's statement, the run's override, or the inputs file) and the source it cited: the model's, or the inputs file's; a value the run overrode cites none.
  • deterministic.metrics is a flat map of the engine's metrics and the model's declared ones (metric.*); money metrics carry amount and currency. model.npv appears only when the run states a discount rate or a discount series. See Metrics.
  • domain_metrics is a top-level section: the active pack's metrics, each with its lineage. It is present when a pack computed them.
  • deterministic.series holds every stream per period, each attributed to its owning entity and its category, with its placement offset and cash, which is false for a stream the totals leave out. An option's payoff names the entity the option is written on. A series lowered from a contract also names its contract and its line, and a stream that moves an account names it in moves. Beside the streams: model.net_cash_flow, per-entity nets, each account's balance as the non-cash series account.<name>, each waterfall step's shortfall as shortfall.<waterfall>.<step>, entity fields, the pack's domain.<pack>.* subtotals, and a contract's valuation figures as metric.<contract>.<figure>.
  • deterministic.annual_rollup regroups the series by calendar year.
  • deterministic.journal and deterministic.transitions record every causal act, with an act's own consequences nested under it as children, and every lifecycle transition. An event's firing (fire) and a transition carry the values their condition read as reads; an option's exercise carries its payoff amount and the entity it is credited to. A transition of a refined machine names its master state, and one that re-entered a state by history is marked resumed.
  • deterministic.trace says why every period is what it is, as runs of one reason. For each entity with a lifecycle, every period's evaluation: the state, the guards tested and what each read, and whether it moved, held, or had its state set by an action. For each event and option, its test each period. For each stream, ran or the gate that stopped it, such as after_schedule, out_of_state or inactive. For each field, its source. A reason an act caused names the act's position in the journal as entry.
  • statements holds every rendered statement, each ending in a line that reconciles to the model's cash. A model that declares none, with no pack, gets its entity hierarchy, marked "default": true.
  • slices holds each declared slice's selection, matched streams, net series and figures.
  • charts holds each chart the pack or the run configuration declares, or the defaults when neither declares one, resolved against the run: the series keys each draws, with its role, unit and fold. A model with no pack and no chart gets its cash by category, marked "default": true. What each metric measures is deterministic.metric_units: money, rate, ratio, multiple, years, count or number.
  • graph is the model's structure: graph.entities, each with its symbol, family, type, part of parent and the fields it states, and graph.contracts, each with its type, parties, terms and the streams lowered from it.
  • scenarios holds one summary per scenario. Each carries its metrics, its series and annual rollup, its journal, transitions and trace, its slices and its statements. A declared metric the scenario could not evaluate is listed under omitted with the reason, not published as a number.
  • monte_carlo holds the trial count and seed; metrics, each metric's summary across trials (mean, stdev, min/max, percentiles p01 to p99, and trials, the count of trials that published it); aggregates, the NPV's mean, median, standard deviation and p_negative, the share of trials with a negative NPV; the trial summaries, each with its metrics and its own omitted; a journal giving the share of trials in which each act occurred; and a trace giving the same for each actor's reasons, such as a lifecycle moving into default.

The Python SDK exposes the series as results.cashflows() (wide, PeriodIndex) and results.annual().

IR anatomy

The IR is the canonical compiled model, deterministically ordered with stable IDs. Its sections include the model's time and phases, entities, assumptions (series and quantiles among them, as shapes of assumption), contracts, streams (with lowered schedule and expression slots), events, options, lifecycles, subtotals, views (slices and statements), runs, required_refs (what the model needs from outside) and provenance. Useful habits:

  • Commit it: IR diffs show exactly what a model change did.
  • Inspect pack lowering: contracts appear as the streams they expanded into.

Schemas

Machine-readable schemas are published here: IR schema · Results schema.