Skip to main content
CFDLAcademy
All chapters

Part 2 · The core language · Chapter 4 of 28

Streams and schedules

Part I treated a schedule as "when the cash occurs" and left it at that. This chapter makes timing precise, because timing is money: the same totals placed differently produce different NPVs, different coverage ratios in a given year, and different reconciliations against a bank's statement. The schedule sub-language exists so that every timing decision a real contract makes — bill on the last business day, settle net-45, pay in advance — is written down rather than approximated.

The grid and the schedule are different things

The time grid (one per model) defines the periods that exist. A schedule places a stream's occurrences onto that grid. The two are deliberately independent: a monthly rent can live on a daily grid, and the rent stays monthly.

version 0.1
model "grid-vs-schedule"
time calendar daily from 2026-01-01 for 90

entity asset shop : Asset.Real

// A monthly claim on a daily grid: twelve occurrences a year, not ninety.
stream shop.rent on entity asset.shop inflow currency USD {
  schedule every month on day 1 from 2026-01-01 to 2026-03-31
  amount = 2600
}

// A daily claim on the same grid.
stream shop.card_settlement on entity asset.shop inflow currency USD {
  schedule every day from 2026-01-01 to 2026-03-31
  amount = 310
}

Choose the grid for the finest-grained cash that matters; every coarser schedule states its own cadence. The strides are every day, every week, every month, every quarter, and every year. A stride coarser than the grid is normal. A stride finer than the grid has nowhere to land, and is refused.

Day rules: where in the month

A monthly stride needs to know which day. Two rules cover practice:

// Rent due on the 15th.
schedule every month on day 15 from 2026-01 to 2026-12

// A cash sweep on the last day of each month — February included, because
// eom clamps to each month's real length rather than assuming 30 or 31.
schedule every month on eom from 2026-01 to 2026-12

on day 31 in a month shorter than 31 days lands on that month's last day: on a daily grid, February's occurrence falls on 2026-02-28. Only a day outside 1 to 31 is refused. Write on eom when the contract says month-end, even so. "The 31st" and "month-end" are different contractual claims, and the reader of your model should see which one the contract made.

Within-period placement: start, mid, end

Every occurrence also has a position within its period, and this single keyword carries more valuation weight than any other in the chapter:

// Default: cash at the period's end — payment in arrears, the ordinary annuity.
schedule every year from 2026-01 to 2029-01

// start: cash at the period's open — payment in advance, the annuity due.
// Rent and leases are due; debt service is in arrears.
schedule every year start from 2026-01 to 2029-01

// mid: the mid-period convention — cash treated as arriving at the period's
// midpoint, the standard treatment for operating flows earned continuously.
schedule every year mid from 2026-01 to 2029-01

The finance content here is the ordinary-annuity / annuity-due distinction from any valuation course, plus the mid-year convention. Three identical totals give three different NPVs, ordered start > mid > end, because earlier cash discounts less. When a valuation you are matching uses the mid-year convention and your model does not, the gap is systematic and this keyword is the fix.

start, mid and end are one axis with three positions, so a schedule takes at most one. All three work on a one-shot date as well as on a stride. Write schedule on 2028-01 mid for a lump that represents a year of activity, and schedule on 2028-01 end for a disposal — a reversion is taken at the close of the holding period, not on the date named. What differs between the two forms is only the default: a stride defaults to end, a one-shot to start.

Business days: conventions and calendars

Real payments do not land on weekends or holidays; they roll. A schedule can say so:

// July 4th 2026 is a Saturday. On the US calendar, `following` rolls the
// payment to Monday the 6th.
schedule on 2026-07-04 convention following calendar "us"

The conventions are the standard fixed-income set: following (next business day), preceding (previous), modified_following (next, unless that crosses into a new month — then previous), modified_preceding (the mirror), and none. The calendars: "weekend" (Saturday/Sunday only), "us", "uk", "target" (the euro settlement calendar).

When to reach for this: models that reconcile to actual settlement — debt service, receivables, anything checked against a bank ledger — on daily grids. A monthly deal model usually does not need rolling; a securitization's payment-date logic absolutely does. modified_following is the default convention of most loan documentation, and now you know why it exists: plain following can pay January's interest in February, which changes which month's coverage test it lands in.

Exceptions: except and also

A pattern with a handful of contractual exceptions stays one schedule instead of dissolving into listed dates:

// Daily accrual, skipping a contractual holiday, adding a make-up date.
schedule every day from 2026-07-01 to 2026-07-05 except [2026-07-03] also [2026-07-08]

Use these for genuine exceptions — a rent holiday, a skipped draw, one extra settlement. If you find yourself writing five also dates, the claim is probably not "a pattern with exceptions" but two different streams, and the model reads better saying so.

Payment terms: net N

The most commercially important timing tool in the chapter. Activity and cash are different events — January's services, invoiced at month-end, paid net-45, are March cash — and net states the offset:

version 0.1
model "payment-terms-demo"
time calendar monthly from 2026-01 for 12

entity asset agency : Asset.Intangible

// Billed monthly, collected 45 days later: January's invoice is March cash.
stream agency.collections on entity asset.agency inflow currency USD {
  schedule every month net 45 from 2026-01 to 2026-06
  amount = 40000
}

// Salaries leave in the month they are earned.
stream agency.payroll on entity asset.agency outflow currency USD {
  schedule every month from 2026-01 to 2026-06
  amount = 26000
}

net 45 counts days; net 2 months steps by the calendar (which is not sixty days). Two consequences to internalize, both visible the first time you run such a model. Cash can land after the activity window ends — collections continue past June here. And two invoices can settle in the same period — under net-30 on a monthly grid, January's and February's invoices both arrive in March, and they sum; nothing is displaced. This is precisely the working-capital timing gap that spreadsheet models fake with a "collections lag" row, except here it is a stated term of the claim, applied by the engine, and visible in the series.

Run this model and look at the shape. Payroll is level from January, collections are zero until mid-March, and the model is cash-negative for its first quarter. That is a business profitable on paper and starving for cash, in nine lines. That shape is why payment terms are a language feature and not a footnote.

Anchors: phases and states

A date is one anchor for a schedule. Two others let the schedule follow the model's own structure instead of the calendar.

A phase is a named range of dates. In schedule position, phase_start("name") and phase_end("name") are the phase's first and last dates, and phase_enter("name") is the instant the phase begins. Move the phase and every schedule anchored to it moves with it.

A state entry is the moment an entity's lifecycle enters a state. from state_enter(<entity>, <state>) for <n> periods opens a window of n grid periods at each entry, resolved while the model runs. The window starts whenever the machine enters the state, and a second entry opens a second window.

version 0.1
model "anchors"
time calendar monthly from 2026-01 for 36

phase build from 2026-01 to 2026-12
phase operate from 2027-01 to 2028-12

lifecycle venue {
  initial closed
  state closed, trading
  closed -> trading when time.t == 14
}

entity asset hall : Asset.Real {
  lifecycle venue
}

// A lump on the day the operating phase begins.
stream hall.opening_costs on entity asset.hall outflow currency USD {
  schedule on phase_enter("operate")
  amount = 50000
}

// Insurance through the whole build phase, wherever its dates move.
stream hall.builders_risk on entity asset.hall outflow currency USD {
  schedule every month from phase_start("build") to phase_end("build")
  amount = 1200
}

// Six months of launch marketing from whenever trading starts.
stream hall.launch on entity asset.hall outflow currency USD {
  schedule every month from state_enter(asset.hall, trading) for 6 periods
  amount = 8000
}

Run it. The insurance pays through 2026, the opening costs land in January 2027, and the launch spend runs for six months from the period the venue starts trading. Lifecycles are taken up later in the course; what matters here is that "for eighteen months from whenever construction starts" is one schedule line.

Payment terms on a contract

A pack contract lowers its own streams, so it states its payment terms once, on the contract: payment net <n> (days, or net <n> months) for a lag, or payment start, mid or end for a placement. Every line the contract lowers settles on the clause. A clause may name one line by its role, payment revenue net 30, and then governs that line alone.

version 0.1
model "contract-payment-terms"
use pack "energy" version "0.1.0"
time calendar monthly from 2026-01 for 24

entity asset plant : Energy.Asset.GenerationFacility

// The offtaker pays each month's invoice 45 days after the month that
// earned it; every stream the contract lowers settles on that one clause.
contract energy.ppa.plant_a on entity asset.plant {
  term 2026-01..2026-12
  payment net 45
  terms {
    quantity = 6000
    price = 3000
    escalation = 0.0
    degradation = 0.005
    availability = 1.0
  }
}

The amount is still computed in the month that earned it; only the cash moves. January's revenue arrives in March, and the last invoice lands in February 2027, after the contract's term has ended.

What is deliberately absent

Two things practitioners sometimes look for are not in the schedule language, on purpose. There are no weekday schedules ("every Tuesday") — a claim that specific belongs on a daily grid with explicit dates, and except/also handle the exceptions. And there are no stub periods as an automatic feature. A short first period is a real economic claim: what does the partial period pay? So the language makes you state it — a one-shot stream for the stub amount, beside the regular pattern — rather than infer a pro-rata amount you never wrote. When you meet a stub in the capstone's construction loan, that is how it will be written, and the reviewer will see the stub's amount as a line, not an inference.

What can go wrong

Out of bounds — a schedule whose own dates fall off the grid is a compile error naming the schedule (E2103). An occurrence pushed off the grid by net terms at the horizon's edge compiles, and the run is refused instead. Either way, the fix is a decision: extend the horizon or change the claim.

on day 31 meets February — clamped to February's last day. If the contract means month-end, say eom, so the claim reads as written.

A phase that does not exist — phase_start("operatons") with a typo is caught at compile, because phase references resolve like every other name.

Exercises

Exercise

Two timing terms

The totals are already right; the timing is wrong. Apply the two stated terms.

  1. Make collections settle net 45.
  2. Add the 15,000 retainer deposit at the start of January, written as a one-shot placed with start.

Predict the shape before you run:

  • Name the first month with any collections cash.
  • Name the months that receive two invoices' worth at once.

Run, then check the series against your prediction. The lifetime total rises by exactly the deposit — 15,000 — because net moves cash without creating or destroying it.

Loading exercise…

Then, on your own:

  1. Take the payment-terms model above and change collections to net 2 months. Predict which periods change before running — remember the calendar steps by months, not by 60 days.
  2. Build a four-payment annual stream three ways — start, mid, end — at a 10% discount rate, and rank the three NPVs before running. You are re-deriving the annuity-due relationship, and the engine will grade you.