Skip to content

Store workflow output - #14062

Open
erlenlh wants to merge 13 commits into
log-workflow-outputfrom
store-workflow-output
Open

Store workflow output#14062
erlenlh wants to merge 13 commits into
log-workflow-outputfrom
store-workflow-output

Conversation

@erlenlh

@erlenlh erlenlh commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Issue
Resolves #13320

Approach
Second of two PRs splitting the original "Store workflow output" work. The base
PR (#14301) captures workflow job
output and writes it to the ERT log; this one makes that output available to the
GUI and ties it to the experiment it belongs to.

  • Adds WorkflowEvent, carrying the output of a single job invocation so it can
    travel over the status queue like the other run model events. It is a plain
    BaseModel rather than a RunModelEvent because PRE_EXPERIMENT hooks run
    before an ensemble exists, so the mandatory iteration and run_id of
    RunModelEvent are not always available.
  • Persists the events alongside the experiment, one JSON object per line in
    workflow_events.jsonl. Appending keeps every hook of a run in one file, in
    the order they ran.
  • Emits an event per job invocation from run_workflows. Workflows skipped
    because the user cancelled are reported as cancelled rather than dropped, so
    the run is not misrepresented as having completed.

Events are held back until an experiment exists, since PRE_EXPERIMENT hooks run
before storage is created, and are flushed in a finally block so output
survives a workflow that stops the experiment. Failing to persist is logged but
does not abort the experiment.

Events are consumed by the GUI in the follow-up PRs in this stack.

  • PR title captures the intent of the changes, and is fitting for release notes.
  • Added appropriate release note label
  • Commit history is consistent and clean, in line with the contribution guidelines.
  • Make sure unit tests pass locally after every commit (git rebase -i main --exec 'just rapid-tests')

When applicable

  • When screenshots are changed: Review screenshot-PR in ert-testdata,
    merge screenshot-PR in ert-testdata before merging this PR.
  • When there are user facing changes: Updated documentation
  • New behavior or changes to existing untested code: Ensured that unit tests are added (See Ground Rules).
  • Large PR: Prepare changes in small commits for more convenient review
  • Bug fix: Add regression test for the bug
  • Bug fix: Add backport label to latest release (format: 'backport release-branch-name')

@codecov-commenter

codecov-commenter commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.79%. Comparing base (5682c06) to head (dedec36).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                   Coverage Diff                   @@
##           log-workflow-output   #14062      +/-   ##
=======================================================
+ Coverage                91.77%   91.79%   +0.01%     
=======================================================
  Files                      486      486              
  Lines                    34849    34902      +53     
=======================================================
+ Hits                     31983    32038      +55     
+ Misses                    2866     2864       -2     
Flag Coverage Δ
cli-tests 35.98% <84.61%> (+0.08%) ⬆️
fuzz 43.88% <56.92%> (+0.03%) ⬆️
gui-tests 58.30% <84.61%> (+0.04%) ⬆️
performance-and-unit-tests 81.07% <100.00%> (+0.04%) ⬆️
test 45.49% <81.53%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/ert/run_models/event.py 99.00% <100.00%> (+0.17%) ⬆️
src/ert/run_models/run_model.py 93.39% <100.00%> (+0.89%) ⬆️
src/ert/storage/local_experiment.py 96.42% <100.00%> (+0.07%) ⬆️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds structured capture and persistence of workflow-job output (stdout/stderr) so that workflows hooked into experiment runs can be logged to <UPDATE_LOG_PATH>/<run_id>/workflows.log and appended to the experiment’s storage directory workflows.log. It introduces a dedicated status event for workflow output, and wires both CLI and GUI monitors to write these logs.

Changes:

  • Capture stdout/stderr for internal and external workflow jobs (including failure cases) and surface per-invocation results.
  • Introduce RunModelWorkflowLogEvent with a stable log-entry format, write it to update-log output paths, and append it to experiment storage.
  • Add unit/UI tests and docs describing the new workflow logging behavior.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ert/config/ert_script.py Adds thread-aware stdout/stderr capture proxy, plus ExternalScriptError handling to avoid ERT-internal stack traces for external job exit failures.
src/ert/config/external_ert_script.py Writes subprocess stdout/stderr to current streams (so ErtScript capture can record it) and raises ExternalScriptError with an exit-code message.
src/ert/workflow_runner.py Adds WorkflowJobResult and WorkflowRunner.jobResults() to preserve per-invocation output (including repeated job runs).
src/ert/run_models/event.py Introduces RunModelWorkflowLogEvent with as_log_entry() and write_as_log() for update-log persistence.
src/ert/run_models/run_model.py Emits workflow log events per job, buffers log entries until an experiment exists, and appends them to experiment storage.
src/ert/storage/local_experiment.py Adds workflow_log_path and append_workflow_log() to persist workflow log entries alongside the experiment.
src/ert/cli/monitor.py Writes workflow log events to <UPDATE_LOG_PATH>/<run_id>/workflows.log during CLI runs.
src/ert/gui/experiments/run_dialog.py Writes workflow log events to the GUI output path.
tests/ert/unit_tests/workflow_runner/test_workflow_runner.py Adds coverage for external-job stdout capture, exit-code error reporting, and per-invocation job results.
tests/ert/unit_tests/workflow_runner/test_ert_script.py Adds coverage for stdout/stderr capture, failure behavior, and concurrent/threaded capture isolation.
tests/ert/unit_tests/run_models/test_workflow_log_event.py New tests for RunModelWorkflowLogEvent log formatting and file append semantics.
tests/ert/unit_tests/run_models/test_status_events_serialization.py Extends status-event serialization coverage to include RunModelWorkflowLogEvent.
tests/ert/unit_tests/run_models/test_base_run_model.py Adds tests asserting workflow log event emission and storage persistence behavior across hooks/iterations.
tests/ert/unit_tests/gui/experiments/test_run_dialog.py Adds GUI test to verify workflow log events are written to the output path.
tests/ert/ui_tests/cli/test_cli.py Adds CLI UI test ensuring workflow output is written into the update log path.
docs/ert/reference/workflows/complete_workflows.rst Documents that hooked workflow output is also written to update-log and experiment storage logs.
docs/ert/reference/configuration/keywords.rst Documents the workflow log location/format under UPDATE_LOG_PATH, including storage-copy behavior and hook scope.
Suppressed comments (1)

tests/ert/unit_tests/workflow_runner/test_ert_script.py:183

  • Thread exceptions can be missed, and the join loop doesn’t assert that worker threads actually finished. Add an is_alive() assertion after each join so this test fails deterministically instead of leaking threads on failure paths.
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join(timeout=10)

Comment thread tests/ert/unit_tests/workflow_runner/test_workflow_runner.py Outdated
Comment thread tests/ert/unit_tests/workflow_runner/test_ert_script.py Outdated
Comment thread docs/ert/reference/workflows/complete_workflows.rst Outdated
@erlenlh
erlenlh force-pushed the store-workflow-output branch from 69e54f9 to 53b84d8 Compare August 3, 2026 14:00
Comment thread docs/ert/reference/workflows/complete_workflows.rst Outdated
@erlenlh
erlenlh force-pushed the store-workflow-output branch from 11a454c to 74d8bfe Compare August 5, 2026 07:42
@codspeed-hq

codspeed-hq Bot commented Aug 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 36 untouched benchmarks


Comparing store-workflow-output (a5b9131) with main (21e9203)1

Open in CodSpeed

Footnotes

  1. No successful run was found on log-workflow-output (002c15e) during the generation of this report, so main (21e9203) was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@erlenlh
erlenlh force-pushed the store-workflow-output branch from 03e0e55 to 598ec95 Compare August 10, 2026 07:49
@erlenlh
erlenlh requested a balanced review from Copilot August 20, 2026 09:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/ert/config/ert_script.py:63

  • The linked issue requires workflow stdout/stderr to be rerouted away from the user's terminal, but this forwarding path deliberately writes every captured byte back to the original stream. As a result, internal and external workflow output still appears in the terminal in addition to the log, so #13320's requested behavior is not implemented. Suppress forwarding for a thread with an active capture while continuing to forward writes from unrelated threads, and update the passthrough test accordingly.
        if self.wrapped is None:
            return len(s)
        return self.wrapped.write(s)

src/ert/run_models/run_model.py:947

  • This clears all pending events even when append_workflow_events() raises. A transient storage error therefore permanently drops buffered PRE_EXPERIMENT and current-hook output, and later hooks cannot retry it. Clear only events known to have been committed; because the append can partially succeed, this needs an atomic append or a commit result rather than simply retaining or clearing the whole batch.
        except Exception:
            logger.exception("Failed to persist workflow events to storage")
        self._pending_workflow_events = []

docs/ert/reference/workflows/complete_workflows.rst:116

  • This documents a GUI tab that this PR does not add: there is no workflow-event reader or “Workflow events” tab under src/ert/gui, and the PR description explicitly defers frontend consumption to a later PR. Users following these instructions cannot view the stored output as described; document the file as reserved for the future GUI instead.
in the GUI, select the experiment in the *Experiments* tool, then select the
*Workflow events* tab.

The GUI will show a list of all workflow jobs that have
been run for that experiment, and clicking on a job will show its output.

Comment thread docs/ert/reference/workflows/complete_workflows.rst Outdated
@erlenlh
erlenlh force-pushed the store-workflow-output branch 2 times, most recently from 597ce7a to a5b9131 Compare August 27, 2026 07:07
@erlenlh erlenlh mentioned this pull request Aug 27, 2026
10 tasks
@erlenlh
erlenlh changed the base branch from main to log-workflow-output August 27, 2026 07:21
Comment thread docs/ert/reference/workflows/complete_workflows.rst
Comment thread docs/ert/reference/workflows/complete_workflows.rst Outdated
@erlenlh
erlenlh force-pushed the store-workflow-output branch 4 times, most recently from e72d575 to 5aeeefe Compare August 31, 2026 12:47
@erlenlh
erlenlh force-pushed the store-workflow-output branch from 5aeeefe to c95c925 Compare September 1, 2026 12:06
erlenlh and others added 5 commits September 3, 2026 09:37
Internal python workflow jobs wrote straight to sys.stdout/sys.stderr,
so their output was visible in the terminal but not available to ERT
itself. External jobs captured their subprocess output but did not pass
stdout on to the terminal.

Replace sys.stdout/sys.stderr with a proxy while a job runs, recording
what the calling thread writes while still forwarding it to the real
stream. Capture is per-thread, so concurrently running jobs do not pick
up each other's output.

Also stop reporting a failing external job with an ERT stack trace: the
new ExternalScriptError carries the exit code, since the traceback would
only show ERT internals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WorkflowJobRunner.run() set the running flag before validating its
arguments but only cleared it on the success path, so a job rejected for
too few or too many arguments kept reporting that it was still running.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
workflowReport() is keyed by job name, so a workflow running the same job
more than once kept only the output of the last invocation.

Add WorkflowJobResult, recording name, index, arguments, output, status
and timestamp per invocation, and expose the ordered list through
workflowJobResults(). workflowReport() is left as it is, since the run
workflow tool and the CLI still use it.

Jobs skipped because the workflow was cancelled are recorded too, so a
cancelled workflow can be told apart from one that never ran. A job that
stops the workflow is now recorded before the error is raised, so its
output is not lost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Workflow carried no name of its own, so anything reporting on a workflow
had to fall back to the source file path. LOAD_WORKFLOW lets a workflow
be given a name, and that is the name users recognise.

Store the name on Workflow, defaulting to the file name when none is
given, so it can be reported alongside the jobs that were run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Workflow job output only reached the terminal ERT was started from, so
it was lost once that terminal was gone, and a job run outside a terminal
left no record at all.

Write one entry per job invocation to the ERT log, naming the hook, the
workflow and the job, and quoting the arguments, stdout and stderr. This
covers hooked workflows, 'ert workflow' and the GUI Run workflow tool,
since all three go through WorkflowRunner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
erlenlh and others added 8 commits September 3, 2026 09:37
Replace the failed/cancelled boolean pair on WorkflowJobResult with a
single WorkflowJobStatus enum. The two booleans could express states
that do not exist, and a job interrupted mid-run was reported as failed
because only the pre-start cancellation path set cancelled=True. Such a
job is now reported as cancelled.

Also rename workflowJobResults to workflow_job_results and fix the
heading underline length in the workflow documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
_CaptureProxy overrode six members that io.IOBase already provides
identically. encoding, errors and newlines are kept because TextIOBase
defines them as descriptors returning None, so __getattr__ never sees
them, and writable is kept because IOBase defaults it to False.

The starting and result log lines both began with "Workflow job" but
meant different things. They now share one description and always state
the hook, using hook=None for runs outside a hook.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stream proxy and its context manager are self-contained and have
nothing to do with ErtScript beyond the single call that uses them, so
they crowded out the module they lived in.

The new module opens by stating why contextlib.redirect_stdout is not
used, since that is the first thing a reader is likely to wonder.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The log line format is a concern of the runner, not of the result it
records: WorkflowJobResult is also persisted, and the starting line has
to be written before any result exists. Move the formatting onto
WorkflowRunner so both lines are built in one place and the dataclass
stays pure data.

Also drop articles from the test names added here and describe
LOAD_WORKFLOW's second argument as a name, matching the documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Workflow job output is written to the ERT log, but the GUI has no way to
show it, since it never sees the log.

Add WorkflowEvent carrying the output of a single job invocation, so it
can travel over the status queue like the other run model events. It is
a plain BaseModel rather than a RunModelEvent because PRE_EXPERIMENT
hooks run before an ensemble exists, so the mandatory iteration and
run_id of RunModelEvent are not always available.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Workflow output lives only in the ERT log, which the GUI cannot read and
which is not tied to the experiment the workflow ran for.

Record the events alongside the experiment, one JSON object per line in
workflow_events.jsonl, so the output of a hooked workflow can be found
again from the experiment it belongs to. Appending keeps every hook of a
run in one file, in the order they ran.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Hooked workflows produce output that is only in the ERT log, so the GUI
cannot show what a workflow did, neither while it runs nor afterwards.

Emit a WorkflowEvent per job invocation from run_workflows and persist
them to the experiment. Workflows skipped because the user cancelled are
reported as cancelled rather than dropped, so the run is not
misrepresented as having completed.

Events are held back until an experiment exists, since PRE_EXPERIMENT
hooks run before storage is created, and are flushed in a finally block
so output survives a workflow that stops the experiment. Failing to
persist is logged but does not abort the experiment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@erlenlh
erlenlh force-pushed the store-workflow-output branch from c95c925 to dedec36 Compare September 3, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Store output from workflows

6 participants