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.
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Stable identifier for this axis. Lowercase alphanumeric, with hyphens/underscores. Must be unique within the model. |
label | string | No | Name shown above the axis’s values in the pd4castr app. |
description | string | No | Help text shown in the axis’s tooltip in the pd4castr app. |
type | string | Yes | The value type: number, string, or boolean. A source-binding axis must use string. |
valuesFrom | object | Yes | Where 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. |
default | matches type | Yes, except a source-binding axis with one source | The 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
defaultis 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
uploadFileFormatandtargetFileFormatasparquet. - A partitioned dataset must declare
fileFormatasparquet. - 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
axesarray. - An input that declares
sourcescan’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:
- The source-binding checks.
- The other axis checks and the caps.
storage-location-kind.fetcher-config-invalid.- The checks against axes already published:
axis-type-changedwhen the model group already exists, andaxis-kind-changedandaxis-default-movedwhen 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.
| Check | What it means | What to change |
|---|---|---|
sources-without-participation | An 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-axes | The model declares more than one source-binding axis. | Keep a single source-binding axis; a trigger can resolve only one. |
partition-on-source-binding-axis | An input partitions on a source-binding axis. | Partition on a static axis instead, or let a dataset partition on this one. |
partition-on-participating-input | An 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-mismatch | Two 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-required | The axis has more than one source and declares no default. | Add a default naming one of the source keys. |
source-binding-axis-type | The axis declares a type other than string. | Declare type: "string". |
invalid-source-key | A 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-unbound | No input participates in the source-binding axis. | Declare the participation on an input, or remove the axis. |
source-binding-on-static-axis | An 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-input | An input that declares sources also declares its own storageLocation. | Move storageLocation into each entry of its sources map instead. |
axis-kind-changed | Updating the current revision in place moves an axis between static and source-binding. | Publish a new revision instead. |
axis-default-moved | Updating 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-cap | An 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-cap | The 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-kind | A 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-invalid | A 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.