Skip to content

Scenario Script

Write Python that modifies or analyses an assembled scenario.

When to use this

  • Build what-if variants of a scenario — scale demand, drop a location, shift costs
  • Enrich a scenario with computed data
  • Bridge between analyses, for example feeding Center of Gravity results into Network Optimization

For custom logic on raw rows, before a scenario exists, the core's Script node is the one to reach for: it runs on lists of row dicts, this one runs on scenario data.

What you need

  • A scenario wired into the Scenario Data input, from Create Scenario, Load Scenario, or any node that produces scenario data
  • Optionally, a table wired into the Auxiliary Table input — a cadence calendar, exchange rates, an external rate table

Parameters

Setting Options What it is
Python code your main(scenario_data) function Required. The Python this node runs.

Return a dictionary with at least one scenario table. Every scenario table you return must be a list of row dictionaries. Missing scenario tables are carried forward from the input with a warning; an explicitly empty table stays empty. Return the full modified input to preserve scenario metadata and overrides.

For rows or summary metrics, use Scenario to Table. Scenario Script rejects lists, scalar metrics dictionaries and raw values rather than guessing a format.

What this node runs, and where

This node carries code its author wrote, so the whole graph runs in the isolated runner rather than in the API process. That is what makes it safe to offer at all, and it is declared on the node type rather than inferred.

Only the Python standard library is available — math, statistics, collections, itertools, functools, decimal, datetime, json, typing and similar. No pandas, no numpy.

What you can read from scenario_data

19 tables, and every one of them is there even when it has no rows. A load materialises the empty ones too, so scenario_data['supply_state'] is [] for a scenario with no supply state rather than a KeyError. Each row is a dict with at least the keys below, in this order; a column the data carried beyond them follows under its own name. A scenario can also carry tables beyond these — a record type the mappers do not declare is stored under its own name, and a key you add to the scenario dict with a list of rows is stored as a table of that name — so read scenario_data by key rather than assuming the list below is all of it.

Table Keys on each row
bom bom_name, output_product, component_product, quantity_per_output, output_unit, component_unit
cog-results name, latitude, longitude, result_type, weighted_distance, source_location_name, total_weighted_distance, iterations, converged, location_mode
cost-to-serve demand_location, product, cost_type, cost
demand-forecast location, product, date, forecast, forecast_low, forecast_high, model, unit
demand_policy location, product, requested_quantity, quantity_unit, unit_revenue
demand_state location, product, fulfilled_quantity, quantity_unit, realized_revenue
flows source, target, product, quantity, quantity_unit, cost, emissions, emissions_unit, transport_mode, date, unit_cost, load, load_unit, reference
handling_policy location, product, unit_handling_cost
handling_state location, product, realized_cost
inventory_policy location, product, unit_carrying_cost, average_days_of_inventory, unit_carrying_cost_period
inventory_state location, product, average_inventory, inventory_unit, realized_carrying_cost
lanes source, target, product, unit_cost, capacity, lead_time, emissions_per_unit, transport_mode, min_quantity, quantity_unit
location_policy location, mode, fixed_operating_cost
location_state location, is_open, total_fixed_cost
locations id, address, latitude, longitude, type, val
production_policy location, bom_name, unit_production_cost, capacity, quantity_unit, emissions_per_unit
production_state location, bom_name, produced_quantity, quantity_unit, realized_cost, realized_emissions, emissions_unit
supply_policy location, product, capacity, quantity_unit, unit_supply_cost
supply_state location, product, used_quantity, quantity_unit, realized_cost, realized_emissions, emissions_unit

demand_history is not among them. It is transient by declaration — carried between nodes in the run that mapped it, never stored — so a script reading an assembled or loaded scenario will not find it.

Keys beginning with an underscore are another node's state riding along: _origin_scenario_id and _scenario_meta from a load, _scenario_id, _cog_result from a siting run, _optimization_overrides from a set_objective. Read them; do not delete them, and do not treat them as tables.

What the run writes back

Return the scenario and its tables are re-persisted as they stand. Three keys are read differently: they name an enrichment the run produced, and the final output's value for one of them is written into the table beside it.

Key on the run's output Table it lands in
cost_to_serve cost-to-serve
demand_forecast demand-forecast
inventory_state inventory_state

Two of those are spelled one way when read and another when written, and the difference is the point: cost-to-serve is a stored table, cost_to_serve is the key an enrichment arrives under. Absent is not empty — a key your output does not carry leaves the stored table exactly as it was, which is what stops one run erasing what an earlier one computed.

Examples

Scale demand — scenario output

def main(scenario_data):
    """Increase all requested demand by 10%."""
    for d in scenario_data.get("demand_policy", []):
        d["requested_quantity"] = float(d.get("requested_quantity", 0)) * 1.1
    return scenario_data

The auxiliary table input

Wire a second upstream — typically Import Data — into Auxiliary Table. Its rows arrive as a second argument beside the scenario:

def main(scenario_data, table_data):
    """table_data is a list of row dicts from the upstream node, or None
    when the auxiliary port is not wired."""
    cadence = {
        (r["product"], r["destination"]): int(r["weeks_between_deliveries"])
        for r in (table_data or [])
    }
    # ... use cadence to adjust scenario_data ...
    return scenario_data

The one-argument form still works: the Auxiliary Table input is optional.

Workflow wiring

Modify a scenario, then save it:

flowchart LR
    n1["Load Scenario"] --> n2["Scenario Script (into Scenario Data)"]
    n2 --> n3["Create Scenario"]

Modify, then optimize:

flowchart LR
    n1["Load Scenario"] --> n2["Scenario Script (into Scenario Data)"]
    n2 --> n3["Network Optimization"]
    n3 --> n4["Create Scenario"]

Analyse a freshly built scenario:

flowchart LR
    n1["Import Data (locations.csv)"] --> n2["Map Locations"]
    n3["Create Scenario"] --> n4["Scenario Script (into Scenario Data)"]
    n5["Import Data (demand.csv)"] --> n6["Map Demand Policy"]
    n2 --> n3
    n6 --> n3

With an auxiliary table — the scenario goes into Scenario Data, the table into Auxiliary Table:

flowchart LR
    n2["Scenario Script"] --> n3["Create Scenario"]
    n1["Load Scenario"] --> n2
    n4["Import Data (cadence)"] --> n2

Common mistakes

  • Returning rows or a metrics dictionary. Use Scenario to Table and return a list of row dictionaries.
  • Returning only part of the scenario. Modify and return the full input to preserve its metadata and tables.
  • Using this node on raw rows. Use the core Script node before a scenario exists.