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, and an explicitly empty table stays empty. Return every table and key you were given, so that its tables, metadata and overrides go on with it: a scenario that leaves one out stops the run with an error naming it. To empty a table, set it to [].

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.

The code may import only a fixed list of standard-library modules, shown under Allowed imports in the node's panel. Any other import is refused before the node runs, and the refusal lists the modules that are allowed. No pandas, no numpy. The code may also use only the names it defines, imports or takes from Python's builtins; any other name is refused the same way.

What you can read from scenario_data

35 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
consolidation-lanes source, target, baseline_transports, consolidated_transports, transports_saved
consolidation-shipments source_row, shipment, transport, transport_row, disposition, reason, source, target, date, product, transport_mode, load, load_unit, quantity, quantity_unit, transport_date, transport_load, transport_load_unit
consolidation-summary baseline_transports, consolidated_transports, transports_saved, transports_saved_share, excluded_rows
consolidation-weekly week_start, baseline_transports, consolidated_transports, transports_saved
consolidation_state source_row, shipment, transport, transport_row, disposition, reason, source, target, date, product, transport_mode, load, load_unit, quantity, quantity_unit
cost-to-serve demand_location, product, cost_type, cost, allocation_type, system_location, system_reason, source_product, source_location, source_target, source_transport_mode, source_bom_name
demand-forecast location, product, date, forecast, forecast_low, forecast_high, model, unit
demand_history location, product, date, quantity, 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, quantity_unit
handling_state location, product, realized_cost
inventory_policy location, product, unit_carrying_cost, average_days_of_inventory, unit_carrying_cost_period, quantity_unit
inventory_state location, product, average_inventory, inventory_unit, realized_carrying_cost
item-segments product, location, class, reason, detail, periods, nonzero_periods, mean, standard_deviation, cv, adi, total_quantity, unit, first_period, last_period
lanes source, target, product, unit_cost, capacity, lead_time, emissions_per_unit, emissions_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, tags, val
objective-terms term, value, unit, weight, contribution
optimization-details objective_type, objective_sense, profit_weight, emissions_weight, objective_value, status, proven_optimal, best_bound, mip_gap, solver, problem_class, solve_seconds, presolve_seconds, time_budget_seconds, precision_target_pct, simplex_iterations, branch_and_bound_nodes, variables, integer_variables, constraints, nonzeros, presolved_variables, presolved_constraints, presolved_nonzeros, matrix_coefficient_min, matrix_coefficient_max, cost_coefficient_min, cost_coefficient_max, bound_min, bound_max, rhs_min, rhs_max, max_constraint_violation, debug_mode, debug_binding_step
penalties origin, step, kind, location, product, source, target, mode, constraint_params, bound, reached, missed_by
production_policy location, bom_name, unit_production_cost, capacity, quantity_unit, emissions_per_unit, emissions_unit
production_state location, bom_name, produced_quantity, quantity_unit, realized_cost, realized_emissions, emissions_unit
products product, product_group, description, tags
rules rule, kind, scope, member, source, target, location, product, mode, sense, bound, reached, slack, slack_share, missed_by, status, unit, when_used, constraint_params
scenario-unit-factors table, key, unit, factor
scenario-units quantity_unit
segmentation-method measure, measure_definition, group_by, period_grain, window_start, window_end, periods_in_window, gap_treatment, x_max_cv, y_max_cv, class_rule, min_periods, min_nonzero_periods, reported_but_not_classified_on, seasonality_removed, outliers_treated, rows_read, rows_in_window, rows_outside_window, items_seen
supply_policy location, product, capacity, quantity_unit, unit_supply_cost
supply_state location, product, used_quantity, quantity_unit, realized_cost, realized_emissions, emissions_unit
unit_conversions product, from_quantity, from_unit, to_quantity, to_unit

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 an add_constraint or add_costs. Read them; do not delete them, and do not treat them as tables.

Return every key you were given. Change scenario_data and return it, or build a new dictionary as {**scenario_data, "demand_policy": new_rows}. A returned scenario that leaves out any table or underscore key it was given stops the run with an error naming each one; to empty a table, set it to [].

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
    n7["Import Data (products.csv)"] --> n8["Map Products"]
    n8 --> 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. The run stops and names what is missing, such as Center of Gravity's sites or XYZ Segmentation's classes. Modify and return the full input, or start the new dictionary from {**scenario_data}.
  • Using this node on raw rows. Use the core Script node before a scenario exists.
  • Editing a table that is still empty. Results tables such as flows hold no rows until an optimization has run upstream, so scaling them changes nothing. When the scenario this node returns is identical to the one it received, the run reports a Script changed nothing warning on the node.

Messages this node can show

A run gives this node 10 minutes. A node that takes longer stops with Time limit reached.

Like every node, it can also stop on the checks before a run and the run's limits.