Skip to content

Output

Reference

Learn more about the output module in the technical reference.

Write the output data after a run is completed. Use the write function to write output data to the correct location. It recognizes the presence of the app.yaml manifest, so it knows where it should write elements of the output like solutions, metrics, and assets.

You can also use the Output class as a holding place for the decisions made by the decision model. An output is built through options, a solution, statistics, and other assets. An output is written to a destination, through the OutputWriter class. You may use the write function to write an output to a destination, or call the .write method on the OutputWriter class.

The most common destination, and the one used by Nextmv Cloud, is either standard out (stdout) or the local filesystem. The LocalOutputWriter class is provided for this reason and it is the default output writer used by the write function. When writing locally, the output is written, by default, to this locations:

json outputs

Work with json content format outputs. This is the default output format for Nextmv. Assume there is an app.yaml manifest that is defined like this:

app.yaml
# This manifest holds the information the app needs to run on Nextmv.

# Type and runtime specify the language and environment of the app.
type: python
runtime: ghcr.io/nextmv-io/runtime/python:3.11

# Python-specific configurations.
python:
  # All listed packages will get bundled with the app.
  pip-requirements: pyproject.toml # Can be a requirements.txt

# List all files/directories that should be included in the app. Globbing
# (e.g.: configs/*.json) is supported.
files:
  - main.py

# Application configurations.
configuration:
  # Define the content format of the app, one of: json, multi-file.
  content:
    format: json # Read JSON from stdin and write JSON to stdout.

The following script would write the output to stdout as a JSON object:

main.py
import nextmv

solution = {"foo": "bar"}
metrics = {
    "duration": 1.0,
    "value": 2.0,
    "custom_metric": "custom_value",
}

# Write to stdout.
nextmv.write(solution=solution, metrics=metrics)

By default, JSON is serialized using pretty printing. If you want to change the serialization behavior, you can pass the json_configurations parameter. The provided values are passed to the underlying json.dumps method. For example, to get compressed output, you can set:

nextmv.write(
    # ...
    json_configurations={
        "indent": None,  # No indentation for compact output
        "separators": (",", ":")  # Use compact separators
    },
    # ...
)

multi-file outputs

When you need to work with a diverse set of files, use the multi-file content format. Multi-file supports the following file formats:

  • .json
  • Text (utf-8 encoded text)
  • .csv (which must be utf-8 encoded)
  • .xlsx (Excel files)

To work with multi-file outputs, you need to define one or more SolutionFile classes, each of which is associated with a file. You can use the following convenience functions to create these classes:

The output is written to a directory defined by the app.yaml manifest. The default value is the outputs directory, and the filenames are derived from the .name parameter of the SolutionFile classes. If you want to change the output directory, you can customize the manifest or pass the path parameter to the write function.

In the outputs directory, you can configure where different elements of the output are written through the app.yaml manifest. Consider this example manifest:

app.yaml
# This manifest holds the information the app needs to run on Nextmv.

# Type and runtime specify the language and environment of the app.
type: python
runtime: ghcr.io/nextmv-io/runtime/python:3.11

# Python-specific configurations.
python:
  # All listed packages will get bundled with the app.
  pip-requirements: pyproject.toml # Can be a requirements.txt

# List all files/directories that should be included in the app. Globbing
# (e.g.: configs/*.json) is supported.
files:
  - main.py

# Application configurations.
configuration:
  # Define the content format of the app, one of: json, multi-file.
  content:
    format: multi-file # Read and write files from disk.
    # Configuration of the multi-file format.
    multi-file:
      input:
        path: inputs # Directory where the app will read input files from.
      output:
        solutions: outputs/solutions # Directory where the app will write solution files to.
        metrics: outputs/metrics.json # Path where the app will write metrics to.
        assets: outputs/assets.json # Path where the app will write assets to.

If you run the following script:

main.py
import nextmv

# Define a solution file for a JSON file.
json_file = nextmv.json_solution_file("output.json", {"foo": "bar"})

# Define a solution file for a CSV file.
csv_file = nextmv.csv_solution_file(
    "output.csv", [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 40}]
)

# Define a solution file for a text file.
text_file = nextmv.text_solution_file("output.txt", "Hello, World!")

solution_files = [json_file, csv_file, text_file]
metrics = {
    "duration": 1.0,
    "value": 2.0,
    "custom_metric": "custom_value",
}
nextmv.write(solution_files=solution_files, metrics=metrics)

You should expect the output structure to look like this:

.
├── app.yaml
├── main.py
├── outputs
   ├── metrics.json
   └── solutions
       ├── output.csv
       ├── output.json
       └── output.txt
├── pyproject.toml
└── uv.lock

When working with binary files, such as Excel files, you must define your own SolutionFile class. The most important parameter of this class is the .writer, which is a Callable (function) that you provide. The signature of this function is as follows:

def writer(file_path: str, data: Any) -> None:
    pass

The file_path establishes the location where this data is written to. The .name defined in the class is going to be given to this function, with the correct directory already joined. This .writer can receive additional arguments and keyword arguments, which you can define in the SolutionFile class through the .writer_args and .writer_kwargs parameters.

Using the same app.yaml manifest as before, consider the following Python script:

main.py
from typing import Any

import nextmv


# Define a custom writer for an Excel file.
def excel_writer(file_path: str, data: Any) -> None:
    import pandas as pd

    df = pd.DataFrame(data)
    df.to_excel(file_path, index=False)


# Define a solution file for an Excel file.
excel_file = nextmv.SolutionFile(
    name="output.xlsx",
    data=[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 40}],
    writer=excel_writer,
    writer_args=[],  # Optional, you don't need to define this if no args are needed
    writer_kwargs={},  # Optional, you don't need to define this if no kwargs are needed
)

solution_files = [excel_file]
metrics = {
    "duration": 1.0,
    "value": 2.0,
    "custom_metric": "custom_value",
}
nextmv.write(solution_files=solution_files, metrics=metrics)

After running it, you should see an output structure like this:

.
├── app.yaml
├── main.py
├── outputs
   ├── metrics.json
   └── solutions
       └── output.xlsx
├── pyproject.toml
└── uv.lock

Assets

A run in Nextmv Cloud can include custom assets, such as those used in custom visualization.

You can use the assets argument of the write function to include these assets.

For example, you can create a simple plot, which consists of a Plotly bar chart with radius and distance for a planet. Consider the following Python script.

main.py
import json

import plotly.graph_objects as go

import nextmv


def main():
    """Main function that runs the model."""

    # Read the input.
    loaded_input = nextmv.load()
    name = loaded_input.data["name"]
    options = loaded_input.options

    ##### Insert model here

    # Print logs that render in the run view in Nextmv Console.
    message = f"Hello, {name}"
    nextmv.log(message)

    if options.details:
        detail = f"You are {loaded_input.data['distance']} million km from the sun"
        nextmv.log(detail)

    assets = _create_visuals(name, loaded_input.data["radius"], loaded_input.data["distance"])

    # Write output and metrics.
    nextmv.write(
        options=options,
        solution={"message": message},
        metrics={
            "value": 1.23,
            "message": message,
        },
        assets=assets,
    )


def _create_visuals(name: str, radius: float, distance: float) -> list[nextmv.Asset]:
    """Create a Plotly bar chart with radius and distance for a planet."""

    fig = go.Figure()
    fig.add_trace(
        go.Bar(x=[name], y=[radius], name="Radius (km)", marker_color="red", opacity=0.5),
    )
    fig.add_trace(
        go.Bar(x=[name], y=[distance], name="Distance (Millions km)", marker_color="blue", opacity=0.5),
    )
    fig.update_layout(
        title="Radius and Distance by Planet", xaxis_title="Planet", yaxis_title="Values", barmode="group"
    )
    fig = fig.to_json()

    assets = [
        nextmv.Asset(
            name="Plotly example",
            content_type="json",
            visual=nextmv.Visual(
                visual_schema=nextmv.VisualSchema.PLOTLY,
                visual_type="custom-tab",
                label="Charts",
            ),
            content=[json.loads(fig)],
        )
    ]

    return assets


if __name__ == "__main__":
    main()

The .assets can be a list of Asset objects, or a list of dictionaries that comply with the custom assets and custom visualization schemas, whichever the case may be.