Run Status File

Contents

Overview | Location | Format | Configuration | Assumptions | Examples | See Also

Overview

File: run_status.json

Macro writes a small JSON file recording how a run ended: whether it solved, whether the solution is provably optimal, or how it failed. It exists for processes that launch Macro and cannot catch a Julia exception — a shell script, a Slurm job array, a parameter sweep driver, a workflow scheduler.

Without it, such a caller sees only the process exit code, which is 1 for every uncaught exception. An infeasible model, a malformed input file, and a typo in a setting are indistinguishable. The status file makes them distinguishable by reading a single field.

Exceptions are still thrown as normal. The file is a record, not a replacement for error handling.

Location

The run status file is written in two places:

  1. During and after the run: at <case_path>/run_status.json (in the case directory itself). This is the copy to watch from outside — its path is known before the run starts, and it is written even when the run fails before any results directory exists.
  2. After a successful run: copied into the outer results directory (e.g. results_001/run_status.json), alongside the log file and settings.json, so each result set carries its own record.
my_case/
├── run_status.json           ← written here at the start, then updated as the run ends
└── results_001/
    ├── run_status.json       ← written here after the run completes successfully
    ├── my_case.log
    ├── settings.json
    └── results/
        ├── capacity.csv
        └── ...

A failed run writes only the case-level file. For a Perfect Foresight run that is because no results directory is created: the solve fails before any output is written. A Myopic run is different — it creates its results directory up front and writes each period's outputs as it goes, so a failure in period 3 leaves a partial results directory behind, containing periods 1 and 2 but no settings.json and no status file. Treat a results directory without a run_status.json as incomplete.

Format

A single JSON object. Example of a Myopic run whose third period hit its time limit:

{
  "status": "SUBOPTIMAL",
  "case_path": "/path/to/my_case",
  "timestamp": "2026-03-15T09:16:45.331",
  "elapsed_seconds": 281.44,
  "output_path": "/path/to/my_case/results_001",
  "termination_status": "1: OPTIMAL, 2: OPTIMAL, 3: TIME_LIMIT"
}

And of an infeasible one:

{
  "status": "INFEASIBLE",
  "case_path": "/path/to/my_case",
  "timestamp": "2026-03-15T09:14:02.118",
  "elapsed_seconds": 118.02,
  "exception": "InfeasibleModel",
  "message": "Model (period 3) is infeasible: termination status INFEASIBLE.",
  "termination_status": "INFEASIBLE",
  "label": "period 3"
}

Fields

FieldAlways presentDescription
statusyesHow the run ended. See the table below.
case_pathyesPath of the case that was run.
timestampyesWhen the file was written.
elapsed_secondsyesWall-clock seconds since the run started. 0.0 in the RUNNING marker.
output_pathon successPath of the results directory, so a caller that did not choose it can find it.
termination_statuson solve outcomesThe solver's own verdict, e.g. OPTIMAL, TIME_LIMIT, INFEASIBLE. For a Myopic run, one comma-separated <period>: <status> entry per period that was solved — periods skipped by Restart or StopAfterPeriod are absent, which is why entries carry their period number.
exceptionon failureName of the exception type, e.g. InfeasibleModel, ArgumentError.
messageon failureThe exception message, truncated at 1000 characters. The log file keeps the full text.
labelon solver failuresWhich model failed, e.g. period 3. Empty for single-period runs.
primal_statuson SOLVE_FAILEDThe solver's primal status, e.g. NO_SOLUTION.

Status values

statusMeaning
RUNNINGWritten before the work starts. Still present means the process never finished — see the assumptions below.
OKSolved to optimality. Results were written.
SUBOPTIMALA usable solution that is not provably optimal: a limit reached with a feasible incumbent, a solver holding a feasible point it could not certify as optimal, or a Benders solve that stopped short of converging. Results were written.
INFEASIBLEThe solver proved the model has no feasible solution.
INFEASIBLE_OR_UNBOUNDEDThe solver could not tell infeasible from unbounded, usually because presolve detected the problem.
UNBOUNDEDThe objective is unbounded.
SOLVE_FAILEDThe solver terminated without a usable solution for another reason, such as a numerical failure, or a Benders solve whose bounds crossed (NEGATIVE GAP).
ERRORAnything that is not a solve outcome: a failed input load, a bad setting, an unexpected error.

OK and SUBOPTIMAL are the two statuses whose results are complete and can be consumed. Output files may nonetheless exist for other statuses — a Myopic run writes each period as it goes, and a Benders solve with crossed bounds still writes its results — so the status, not the presence of a results directory, is what says whether the numbers can be used.

Configuration

Both settings are keyword arguments to run_case. There are no run status settings in macro_settings.json or case_settings.json.

ArgumentTypeDefaultDescription
write_statusBooltrueWrite the run status file.
status_file_pathAbstractString<case_path>/run_status.jsonPath of the run status file to write. The copy in the results directory keeps the same file name.

Assumptions

  • RUNNING is written before the work starts. A run killed by the operating system — a scheduler walltime, an out-of-memory kill — never reaches either the success or the failure path. The marker means such a run leaves RUNNING behind rather than the previous run's OK, which a watcher would otherwise read as success. A RUNNING status on a job that is no longer alive means it was killed.
  • Overwritten on each run. The case-level file always reflects the most recent run. The copy in the results directory is what preserves the record of earlier runs, since each gets its own results_00N/ directory when OverwriteResults = false.
  • Failures never reach the results directory. The copy is only written on success, so a results directory with no run_status.json is one whose run did not finish. A Myopic run can leave such a directory behind, holding the periods that completed before the failure.
  • Never half-written. The file is written to a temporary file in the same directory and renamed over the target, so a poller reads either the previous contents or the new ones, never a partial document.
  • The directory must already exist. A status_file_path pointing into a directory that has not been created produces a warning, not a new directory: creating one would silently produce a case directory for a mistyped path.
  • Write failures are warnings, not errors. If the file cannot be written — a read-only case directory, for instance — Macro warns and continues. The status file must never replace the exception it is reporting.

Examples

Read the status in Julia

using JSON3

status = JSON3.read(read(joinpath("my_case", "run_status.json"), String))
status.status == "INFEASIBLE" && @warn "Infeasible in $(status.label)"

Catch the exception directly instead

If the caller is itself Julia, catch the exception rather than reading the file:

using MacroEnergy

try
    (case, solution) = run_case(@__DIR__)
catch e
    e isa InfeasibleModel && @warn "Infeasible: $(e.status) in $(e.label)"
    rethrow()
end

Disable the status file

using MacroEnergy

run_case(@__DIR__; write_status=false)

See Also

  • run_case — full list of keyword arguments
  • Log File — the human-readable record of the same run, which keeps the untruncated error message
  • Settings Output — the settings.json file archived alongside it