Model Axes

An axis is a configurable dimension for your model’s runs that can be wired into other parts of your model configuration to enable powerful features like fanning a single model out to N model runs per trigger based on a combination or partition of input data.

For example, declare a ref_year axis with the values 2023, 2024, and 2025, and every trigger queues three runs instead of one: the same model, run once per reference year, each run reading its own year’s slice of the same input data.

A trigger is whatever starts a run — new input data arriving, or someone pressing “Run model” — as described in Run modes and scheduling.

An axis can also multiply triggers instead of runs, when its values are the keys of an input’s sources. See Source-binding axes.

When to Use an Axis

Declare an axis when the same forecast logic runs over a dimension of your data and each version reads a different slice of one file: for example, one run per reference year. Your model code stays as it is, and every result stays under a single model that people switch between in the pd4castr app.

Sensitivities are distinctly different in that they change the input data for a specific run via SQL, whereas an axis is used to trigger additional model runs for the inputs you already have.

Combined, they can be used to trigger fan-out model runs of individualised input data, with sensitivities applied to each run.

How Axes Change Your Runs

A trigger queues one run per combination of axis values, then multiplies that by your sensitivities:

runs per trigger = (product of each axis's value count) × (1 base run + number of sensitivities)

A model with a three-value ref_year axis, a second axis with two values, and three sensitivities queues 3 × 2 × (1 + 3) = 24 runs every time it triggers. All 24 share the same runDatetime.

Each axis’s default value picks the model’s base run: the run people get without choosing anything. The app opens on it, and the platform’s latest-run API endpoints answer with it. When a model declares more than one axis, the base run is the combination of every axis’s default.

The platform caps a model at 200 runs per trigger and 100 values per axis. The CLI checks your configuration against them on your own machine before it uploads anything, so a publish over either limit fails in your terminal. If you need to trigger more runs than these limits allow, contact support to discuss your requirements.

What Your Model Container Sees

Adding an axis changes nothing inside your model code. The container receives the same environment variables it always did: one INPUT_<KEY>_URL per input, plus OUTPUT_URL and MODEL_RUN_ID. No environment variable carries the run’s axis values.

What changes is the file behind INPUT_<KEY>_URL. For a partitioned input, that file holds only the rows for this run’s value, so the 2024 run downloads the 2024 rows and nothing else. The column the file was partitioned on stays in the file, which means your existing queries that filter or group by it keep working — they just match every row now.

Configuration

Declare axes in the axes array in .pd4castrrc.json. Each entry has the following fields.

FieldTypeRequiredDescription
keystringYesStable identifier for this axis. Lowercase alphanumeric, with hyphens/underscores. Must be unique within the model.
labelstringNoName shown above the axis’s values in the pd4castr app.
descriptionstringNoHelp text shown in the axis’s tooltip in the pd4castr app.
typestringYesThe value type: number, string, or boolean. A source-binding axis must use string.
valuesFromobjectYesWhere the axis’s values come from: { "kind": "static", "values": [...] }, with at least one value, or { "kind": "source-binding" }, whose values are the source keys of the inputs that participate in it.
defaultmatches typeYes, except a source-binding axis with one sourceThe value that defines this axis’s base run. Must be one of valuesFrom.values, or one of the source-binding axis’s resolved source keys.

Declared values must be unique within the axis and match the declared type. A number value must be a whole number between zero and 9,007,199,254,740,991. Both limits keep a value’s text identical everywhere the platform writes it, whether that’s a storage path, a label, or a query. A string value can’t be empty. It can hold letters, digits, underscores, dots, and hyphens, and it must start with a letter or a digit.

Here’s a ref_year axis with three fixed values and a default:

{
  "axes": [
    {
      "key": "ref_year",
      "label": "Reference Year",
      "type": "number",
      "valuesFrom": {
        "kind": "static",
        "values": [2023, 2024, 2025]
      },
      "default": 2024
    }
  ]
}

Source-Binding Axes

A static axis multiplies the runs a trigger queues. A source-binding axis instead multiplies the triggers: each source declared by the inputs that participate in it fires its own trigger, and a complete trigger queues static variants × (1 + sensitivities) runs, each carrying that source’s key as its value on the axis. Two sources that each receive new data at once queue two separate sets of runs.

A minimal source-binding axis looks like this:

{
  "key": "demand_source",
  "label": "Demand source",
  "type": "string",
  "valuesFrom": { "kind": "source-binding" },
  "default": "aemo"
}

See Input sources for how an input declares sources and participates in this axis.

A source-binding axis follows these rules:

  • A model declares at most one source-binding axis.
  • The axis’s default is the default source, and the base run is the default source’s run.
  • Every input that participates in the source-binding axis declares the same set of source keys.
  • The axis takes its order from the first input that participates in it, in config order. Source keys that are whole numbers without a leading zero, such as 2024, come first in ascending numeric order. Start a key with a letter to keep its declared position.

The jobs cap compares the full job count, with every source firing together, against the 200-job cap: on a source-binding axis, fan-out-jobs-over-cap’s message states the axis key, that total, and how many jobs each source would queue on its own, for example axis "demand_source" would queue up to 216 model run jobs if its 6 sources fire together (36 per source), over the limit of 200. The number of sources, static axis values, and sensitivities all contribute to the job count. The 100-value cap on every axis limits how many sources a source-binding axis can declare.

Two sources can have the same run datetime or different ones. By default, all runs from a source’s trigger use the time the platform queues their jobs. A custom run datetime query can derive it from the selected source’s file or an input with a single file or fetcher.

POST /v1/model/{id}/trigger?axis=<axisKey>:<sourceKey> fires that source on demand. Omit the parameter to fire the default source. A static axis key, an unknown key, or a source the model doesn’t declare returns 400. On an ON_DEMAND model, the trigger returns 409 while any run or job of the model is pending or running, for any source. Fire the next source once the earlier runs finish. The trigger pd4castr publish sends at the end of a publish fires the default source. When the selected source has no latest file yet, the trigger fails with Input "<key>" has no latest file for source "<sourceKey>". A failed trigger at the end of a publish leaves the publish successful: the CLI prints Model run did not trigger, or, on an AUTOMATIC model, that the run starts once the input files are processed.

GET /v1/model/{id}/run, GET /v1/model/{id}/run/latest, and GET /v1/model/{id}/run/latest/output take the same axis=<axisKey>:<sourceKey> parameter and return that source’s runs. Without it, /run returns every source’s runs, and the latest-run endpoints return the default source’s run.

Each source’s own cadence depends on every input’s trigger. See Trigger types and run modes.

Partitioning Inputs and Datasets by an Axis

You can partition an input or dataset by mapping an axis to a specific column in its file, using the input’s or dataset’s own axes field. Each run then reads only the rows carrying its own axis value. A file that maps no axis is shared, and every run gets all of it.

An example:

"inputs": [
  {
    "key": "forecast_values",
    "axes": { "ref_year": { "kind": "partition", "column": "REF_YR" } }
  },
  { "key": "mt" }
]

forecast_values reads only its own year’s rows on every run. mt maps no axis, so every run gets the whole file, unchanged.

Six rules govern partitioning:

  • Only a static input can be partitioned, and it must declare both uploadFileFormat and targetFileFormat as parquet.
  • A partitioned dataset must declare fileFormat as parquet.
  • An input or dataset can be partitioned on at most one axis.
  • The key naming the partition must match an axis declared in the model’s axes array.
  • An input that declares sources can’t partition on any axis: a source’s slot holds one file, which can’t be split by partition.
  • No input partitions on a source-binding axis, but a dataset can: its column holds the source keys as values, one partition per source.

The partitioned file itself has to meet two more rules: every row needs a value in the partition column, and the column can hold at most 100 distinct values. Values the axis doesn’t declare count towards the 100, because the platform splits them out too. The CLI checks the file on your machine before it uploads anything, so a file that breaks either rule fails the publish in your terminal.

Axis Values in Model Run Views

A model run view parameter can name an axis with defaultFrom: "run.axis.<key>". The platform accepts that reference and checks the key against your axes array, but it doesn’t yet hand the running view its axis value: an optional parameter resolves to null, and a required one is rejected when the view runs. Set that parameter another way for now.

What You See in the App

You can configure how axes are represented in the pd4castr app with the label and description fields. The app opens on the base run, with every axis at its default.

A source-binding axis shows as a checklist, the same as a static axis. The app opens on the default source’s newest run. Checking another source shows its run at the selected datetime when it has one, or its newest run under its own datetime otherwise.

Changing Axes on a Published Model

Publishing an existing model comes in two shapes: updating its current revision in place, or creating a new revision. Updating an existing model covers how you pick between them. Axes constrain that choice.

Updating a model’s current revision in place can’t add an axis, remove an axis, or move an axis’s default to a different value — any of those changes the revision’s base run, so publish fails and you publish a new revision instead. Adding or removing a non-default value in place is allowed, but removing will hide the related model run variants from the model run history, though their data stays available through the API.

An axis key’s type can’t change across the revisions of one model group, whether you’re updating in place or publishing a new revision. To change an axis’s value type, declare a new key instead of reusing the old one.

Updating a model’s current revision in place can’t change an axis between static and source-binding either, for the same reason it can’t change a default: publish a new revision instead. Adding or removing a non-default source in place is allowed. Removing one stops that source from triggering runs; its past runs stay reachable through the API.

Custom Run Datetime Queries

If your model declares axes, keep your custom run datetime query reading only shared inputs or inputs that declare sources, never a partitioned one. Every run from one trigger is supposed to carry the same runDatetime, and a query reading a partitioned input would read a different set of rows in each run and hand each one a different runDatetime, which breaks that. The file of an input that declares sources is the same for every static variant of one trigger, so the query reads it safely.

On a model with a source-binding axis, the platform runs the model’s runDatetimeQuery against the prepared inputs for each run from a source’s trigger. A query that reads an input declaring sources reads the selected source’s file. A query that reads an input with a single file or fetcher reads the same file for every source. Two sources can therefore resolve to the same datetime or to different ones. A source doesn’t declare its own runDatetimeQuery; naming one on a sources entry fails parse as an unrecognised key.

Publish Errors and Warnings

Every rejection below stops pd4castr publish before it pushes the image or uploads an input file. The create- and update-model endpoints check a configuration in stages, in this order:

  1. The source-binding checks.
  2. The other axis checks and the caps.
  3. storage-location-kind.
  4. fetcher-config-invalid.
  5. The checks against axes already published: axis-type-changed when the model group already exists, and axis-kind-changed and axis-default-moved when you update a revision in place.

A stage that finds failures returns all of them in an array under MODEL_VALIDATION_FAILED. The later stages run only once that stage passes, so fixing one stage’s failures can show the next stage’s.

The CLI runs most of the checks on the configuration file before it builds the image, and reports each failure’s message there. axis-kind-changed, axis-default-moved, storage-location-kind, fetcher-config-invalid, and both warnings run only on the platform, so the CLI reports them from the endpoint’s response. This table covers the checks a source or a source-binding axis can fail, plus the axis checks an author declaring sources is most likely to hit. The rest of the platform’s axes checks aren’t documented here.

CheckWhat it meansWhat to change
sources-without-participationAn input declares sources but doesn’t participate in the source-binding axis, or participates without declaring sources.Declare both on the input, or neither.
multiple-source-binding-axesThe model declares more than one source-binding axis.Keep a single source-binding axis; a trigger can resolve only one.
partition-on-source-binding-axisAn input partitions on a source-binding axis.Partition on a static axis instead, or let a dataset partition on this one.
partition-on-participating-inputAn input that declares sources also declares a partition participation on another axis.Remove the partition; a source’s one-file slot can’t be split.
source-keys-mismatchTwo inputs that participate in one axis declare different sets of source keys.Make every input that participates in the axis declare the same source keys.
source-binding-default-requiredThe axis has more than one source and declares no default.Add a default naming one of the source keys.
source-binding-axis-typeThe axis declares a type other than string.Declare type: "string".
invalid-source-keyA source key falls outside the input key grammar.Rename it to lowercase letters, digits, underscores, and hyphens, starting with a letter or digit.
source-binding-axis-unboundNo input participates in the source-binding axis.Declare the participation on an input, or remove the axis.
source-binding-on-static-axisAn input uses { "kind": "source-binding" } to participate in an axis whose valuesFrom.kind is static.Declare the axis valuesFrom: { "kind": "source-binding" }, or drop the participation.
storage-location-on-participating-inputAn input that declares sources also declares its own storageLocation.Move storageLocation into each entry of its sources map instead.
axis-kind-changedUpdating the current revision in place moves an axis between static and source-binding.Publish a new revision instead.
axis-default-movedUpdating the current revision in place moves an axis’s default to a different value, including a different default source.Publish a new revision instead.
axis-values-over-capAn axis declares more values, or a source-binding axis has more sources, than the 100-value cap.Reduce the number of values or sources.
fan-out-jobs-over-capThe model’s runs per trigger exceed the 200-job cap, counting every source of a source-binding axis as firing together.Reduce the axis values, the sensitivities, or the number of sources.
storage-location-kindA source, or an input with a single file or fetcher, declares the ID of a storage location that holds datasets rather than input files.Declare the ID of a storage location for input files.
fetcher-config-invalidA source’s fetcher rejects its checkQuery or fetchQuery.Fix the SQL the named field points at.
fetch-query-changed (warning)A republish changed a source’s check or fetch query, so that source’s fetch state resets.Publish while no source’s data is arriving, so the reset settles within one cycle.
axis-values-removed (warning)Updating the current revision in place removes a declared value or source, so the runs made for it stay but the run history can no longer reach them.Keep the value or source while its runs need to stay reachable.

Next Steps

  • See Model inputs for the full input configuration reference, including static and data-fetcher inputs.
  • See Input sources for how to declare the sources a source-binding axis takes its values from.
  • See Model datasets for publishing reference data alongside your model.
  • See Sensitivities for how sensitivity runs multiply alongside axis runs.
  • See Publishing for the full publish workflow.
  • See the Configuration file reference for the complete schema.