Browse docs
On this page
file-format reference

One file. Plain JSON.

A Caladia project is a single .cala file: UTF-8 JSON. The schema is defined in Zod in packages/file-format, validated on every load, and the source of truth for the TypeScript types throughout the codebase.

1. Top-level shape

Every .cala file is a single JSON object with these top-level keys:

.cala — top-level keys { "kind": "version": "currency": "fxSnapshotVersion": "budget": "project": "calendars": "resources": "nodes": "edges": "loops": "subsystems": "scenarios": } "caladia-project" 3 "USD" "2026.1" number, optional { … } [ … ] [ … ] [ … ] [ … ] [ ] [ ] [ ]
Coral marks required content. Grey-bracketed arrays default to [ ] when empty.

Required scalars:

  • kind: "caladia-project" — literal.
  • version: 3 — integer schema version. Older majors are rejected, not auto-upgraded.
  • currency — 3-letter ISO 4217 code, uppercase.
  • fxSnapshotVersion — identifier of the bundled FX rate snapshot the file was authored against.

project is an object: name, startDate (YYYY-MM-DD), defaultCalendarId, displayUnit (hours / days / weeks). budget and fxRateOverrides are optional top-level fields.

2. The arrays

Seven of the top-level fields are arrays. Each has its own schema; full field-level detail lives in schema.ts.

  • calendars — at least one. Working days (7-tuple Sun–Sat), hours-per-day, holiday preset (US_FEDERAL / CANADA_FEDERAL / EU_COMMON / NONE), preset version pin, exceptions. See Calendars & resources.
  • resources — pools with integer capacity, an associated calendar, and optional cost fields (costRate, costPerUse, hourlyRateDistribution, currencyOverride).
  • nodes — the graph vertices. nodeType is one of start, activity, end, decision, subsystem. Each has duration, position, calendar (or null for project default), resource assignments, optional duration distribution. Decision nodes also carry passProbability and failureDelay; activity / decision nodes can have fixedCost and crash options.
  • edges — dependencies. Each has from, to, type (FS / SS / FF / SF), and a signed lag.
  • loops — wrappers around a body-node selection with a kickout (maxIterations / timeBudget / convergenceCriterion / externalTrigger). See Loops & decision gates.
  • subsystems — reusable sub-DAGs collapsed into a single canvas node. Definition can be inlined or referenced from a sibling .calasub file.
  • scenarios — named what-if overrides. Each is a list of per-node duration tweaks the user can flip between. The base plan is unaffected.

3. Validation

Every load runs through Zod. The validator catches type errors (string where number was expected), enum errors (an edgeType of "XX"), and cross-field invariants (a selectedCrashIndex that doesn’t point at a valid crashOptions entry). Errors include the offending JSON path — nodes[12].duration.unit.

Three behaviours worth knowing:

  • No silent coercions. A string "5" where a number is expected is an error, not a parsed integer.
  • Cross-field refinements are explicit. Constraints like “fixedCost only on activity / decision nodes” fail at parse time, with the path in the error.
  • Errors don’t throw. The loader returns a Result<T, E> union — the UI gets a structured failure value, not an exception.

4. The version-pin policy

Some pieces of data in a .cala file are pointers into bundled lookup tables — holiday presets, FX snapshots. These are pinned at save time: the file records the version of the preset / snapshot it was authored against, and uses that version on every reload.

Without the pin, the same .cala could behave differently if Caladia’s bundled holidays or FX rates changed between releases — schedules would shift, simulation history would be incomparable. Upgrading a file to a newer preset / snapshot is a deliberate user action.

5. Sub-system files (.calasub)

A .calasub is a reusable sub-DAG: the canvas representation of a subsystem node, factored out into its own file. Same JSON shape, different top-level kind. The parent .cala references it inline or by file path; the content is resolved at load time.

6. Minimal example

The smallest meaningfully populated .cala — one calendar, one resource, a three-node start → activity → end chain, the activity with a PERT-beta distribution. Validates against the v3 schema as-is.

{
  "kind": "caladia-project",
  "version": 3,
  "currency": "USD",
  "fxSnapshotVersion": "2026.1",
  "project": {
    "name": "Minimal example",
    "startDate": "2026-05-18",
    "defaultCalendarId": "cal-1",
    "displayUnit": "days"
  },
  "calendars": [
    {
      "id": "cal-1",
      "name": "Standard week",
      "workingDays": [false, true, true, true, true, true, false],
      "hoursPerDay": 8,
      "daysPerWeek": 5,
      "holidayPreset": "NONE",
      "holidayPresetVersion": "2026.1",
      "exceptions": []
    }
  ],
  "resources": [
    {
      "id": "r-1",
      "name": "Engineer",
      "capacity": 1,
      "calendarId": "cal-1",
      "costRate": 80
    }
  ],
  "nodes": [
    {
      "id": "n-start",
      "nodeType": "start",
      "name": "Begin",
      "duration": { "value": 0, "unit": "hours" },
      "position": { "x": 0, "y": 0 },
      "calendarId": null,
      "consumesResources": false,
      "resourceAssignments": []
    },
    {
      "id": "n-build",
      "nodeType": "activity",
      "name": "Build",
      "duration": { "value": 5, "unit": "days" },
      "distribution": { "type": "pert-beta", "min": 3, "mode": 5, "max": 12 },
      "position": { "x": 200, "y": 0 },
      "calendarId": null,
      "consumesResources": true,
      "resourceAssignments": [
        { "resourceId": "r-1", "count": 1, "calendarPolicy": "resourceWins" }
      ]
    },
    {
      "id": "n-end",
      "nodeType": "end",
      "name": "Done",
      "duration": { "value": 0, "unit": "hours" },
      "position": { "x": 400, "y": 0 },
      "calendarId": null,
      "consumesResources": false,
      "resourceAssignments": []
    }
  ],
  "edges": [
    { "id": "e-1", "from": "n-start", "to": "n-build", "type": "FS", "lag": { "value": 0, "unit": "days" } },
    { "id": "e-2", "from": "n-build", "to": "n-end", "type": "FS", "lag": { "value": 0, "unit": "days" } }
  ],
  "loops": [],
  "subsystems": [],
  "scenarios": []
}