Skip to content

process dump: JSON Data and workflow outputs - #7589

Draft
elinscott wants to merge 15 commits into
aiidateam:mainfrom
elinscott:dump-data-json
Draft

process dump: JSON Data and workflow outputs#7589
elinscott wants to merge 15 commits into
aiidateam:mainfrom
elinscott:dump-data-json

Conversation

@elinscott

Copy link
Copy Markdown
Contributor

TL;DR

A MultiplyAddWorkChain, dumped as it is today:

$ verdi process dump 5 --include-outputs --path dump-plain
Report: Using specified output path: `<my-pwd>/dump-plain`
Success: Raw files for process `5` dumped into folder `dump-plain`.

$ tree -a dump-plain
dump-plain
├── 01-multiply-6
│   ├── .aiida_dump_safeguard
│   ├── aiida_node_metadata.yaml
│   └── inputs
│       └── source_file
├── 02-ArithmeticAddCalculation-8
│   ├── .aiida_dump_safeguard
│   ├── aiida_node_metadata.yaml
│   ├── inputs
│   │   ├── .aiida
│   │   │   ├── calcinfo.json
│   │   │   └── job_tmpl.json
│   │   ├── aiida.in
│   │   └── _aiidasubmit.sh
│   └── outputs
│       ├── aiida.out
│       ├── _scheduler-stderr.txt
│       └── _scheduler-stdout.txt
├── aiida_dump_log.json
├── .aiida_dump_safeguard
├── aiida_node_metadata.yaml
└── README.md

Crucially, the number the workflow computed is nowhere in that tree.

With this PR, it is dumped when using the new flags:

$ verdi process dump 5 --include-outputs --include-workflow-outputs --include-data-json --path dump-with-flags
Report: Using specified output path: `<my-pwd>/dump-with-flags`
Success: Raw files for process `5` dumped into folder `dump-with-flags`.

$ tree -a dump-with-flags
dump-with-flags
├── 01-multiply-6
│   ├── .aiida_dump_safeguard
│   ├── aiida_node_metadata.yaml
│   ├── inputs
│   │   └── source_file
│   ├── node_inputs
│   │   ├── x.json
│   │   └── y.json
│   └── node_outputs
│       └── result.json
├── 02-ArithmeticAddCalculation-8
│   ├── .aiida_dump_safeguard
│   ├── aiida_node_metadata.yaml
│   ├── inputs
│   │   ├── .aiida
│   │   │   ├── calcinfo.json
│   │   │   └── job_tmpl.json
│   │   ├── aiida.in
│   │   └── _aiidasubmit.sh
│   ├── node_inputs
│   │   ├── code.json
│   │   ├── x.json
│   │   └── y.json
│   ├── node_outputs
│   │   ├── remote_folder.json
│   │   └── sum.json
│   └── outputs
│       ├── aiida.out
│       ├── _scheduler-stderr.txt
│       └── _scheduler-stdout.txt
├── aiida_dump_log.json
├── .aiida_dump_safeguard
├── aiida_node_metadata.yaml
├── node_outputs
│   └── result.json
└── README.md

$ cat dump-with-flags/node_outputs/result.json
5

Problem

Discussed extensively in #7588: a dump does not contain Data nodes nor the returned outputs of WorkflowNodes.

Changes

Two new flags

  • off by default
  • exposed on verdi process dump, verdi group dump and verdi profile dump
  • added as keywords on ProcessNode.dump(), Group.dump() and Profile.dump().

--include-data-json writes every linked Data node that has no repository content as <link-label>.json, wherever the dump already places that node's repository-backed siblings. A Dict writes its dictionary, a List its list, an Int/Float/Str/Bool its bare value, and anything else its attributes (see e.g. remote_folder.json above, which holds the calculation's remote_path)

--include-workflow-outputs gives each WorkflowNode a node_outputs directory of the nodes it returned, with repository-backed returns copied out exactly as they are for a calculation. These files are not dumped with --include-outputs: that flag governs a calculation's CREATE outputs, and a workflow's RETURN outputs are something separate. (We could possibly reconsider the flag names though...)

Note that...

  • Labels of one namespace share a file: alphas__filled and alphas__empty become a single alphas.json rather than a file per port.
  • A label that would have to be written inside or on top of another label's value is dropped with a warning instead
  • A node that carries repository content keeps its directory — so a namespace holding both kinds is split between alphas/ and alphas.json.

Pre-existing collisions

copy_tree has pre-existing issues with file collisions that appear under --flat. This PR mitigates the risk (JSON files are written after the repository copies, so on a name collision the JSON is skipped with a warning); the collision issue should be dealt with in a separate PR.

Notes

Addresses #7588. The issue's secondary question — whether a single-file repository should flatten to <label>.<ext> — is left for a follow-up.

elinscott and others added 15 commits August 27, 2026 12:09
Dumping copies the repository of each linked `Data` node, so a node that
keeps its content in the database, such as the `Dict` of results of a
calculation, reached no file, and the nodes a workflow returned reached
none either.

Add `include_data_json`, off by default and exposed as
`--include-data-json/--exclude-data-json` on `verdi process dump`,
`verdi group dump` and `verdi profile dump`:

- Every linked `Data` node without repository content is written as
  `<link-label>.json` beside the repository-backed ones. A `Dict` writes
  its dictionary, an `Int`/`Float`/`Str`/`Bool` its bare value, anything
  else its attributes.
- A namespaced link label such as `pseudos__Si` is written into one
  `pseudos.json` for the whole namespace.
- An existing file is never overwritten; the JSON is skipped with a
  warning instead.
- Each `WorkflowNode` gains a `node_outputs` directory holding the nodes
  it returned, with repository-backed ones copied out as they are for a
  calculation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the flag to the "Process Dumping" how-to, with the tree a
`MultiplyAddWorkChain` dump produces under it, and a `CHANGELOG` entry
under Unreleased.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An output whose repository is empty, such as a bare `FolderData`, has no
repository content and so is written as JSON like any other such node;
the file holds the empty attributes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The keyword also gives a `WorkflowNode` a `node_outputs` directory; the
parameter docs described only the JSON files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CLI help, the three `.dump()` docstrings and the CHANGELOG
paragraph said `include_data_json` alone gives every `WorkflowNode` a
`node_outputs` directory. `test_workflow_returns_need_both_options`
shows neither option alone produces it.

- Reworded the `INCLUDE_DATA_JSON` help text in
  `cmdline/params/options/main.py`.
- Reworded the `:param include_data_json:` docstring in
  `orm/nodes/process/process.py`, `orm/groups.py` and
  `manage/configuration/profile.py`.
- Reworded the CHANGELOG paragraph to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`.. versionadded:: 2.10` sat mid-paragraph, several sentences below
the "Process dumping" heading, unlike every other versionadded
directive in this file.

- Moved it to sit right after `.. versionadded:: 2.6`, under the
  section heading.
- Noted that an incremental re-dump into an existing directory does
  not add the JSON files to a node already dumped, since the dump log
  marks that node done; use `--overwrite` or a new path to pick them
  up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The nodes a workflow returns and the `Data` nodes that keep their
content in the database are two separate gaps, reached by one flag named
after only the second of them. A returned `SinglefileData` is a file to
copy, and asking for it through `--include-data-json` misdescribes it.

Split the flag in two, both off by default and both exposed on
`verdi process dump`, `verdi group dump` and `verdi profile dump` and as
keywords on `ProcessNode.dump()`, `Group.dump()` and `Profile.dump()`:

- `--include-data-json` writes every linked `Data` node without
  repository content as `<link-label>.json`, wherever the dump already
  places that node's repository-backed siblings.
- `--include-workflow-outputs` gives a `WorkflowNode` its own
  `node_outputs` directory of the nodes it returned, copying out the
  repository-backed ones. A returned `Dict` still needs both flags.
- The workflow half no longer follows `--include-outputs`, which governs
  a calculation's `CREATE` outputs, not a workflow's `RETURN` outputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A workflow dumped with `--include-workflow-outputs` alone copies out
  its returned `SinglefileData` and passes over its returned `Dict`,
  with `--include-outputs` off: one case pinning that the workflow half
  is neither gated on `--include-outputs` nor on the JSON flag.
- A returned `Dict` is parametrized over all four flag combinations, and
  is written for exactly one of them.
- The CLI-to-API mapping tests for all three commands carry both flags,
  and a further case pins that both default to `False`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The worked example passes `--include-workflow-outputs` alongside
  `--include-data-json`, since the workchain's result is a `Dict`.
- The prose names the two gaps separately and says which flag closes
  which, rather than presenting one as a side effect of the other.
- The CHANGELOG entry and the `versionadded` note name both flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`_nest_by_link_label` warned and dropped a clashing label only when the
value already in its place was a scalar or a list. When that value was a
dictionary, as a `Dict` node's is, the clashing label was merged into
it: the `Dict`'s own key took an unrelated sibling link's value, and
nothing was logged.

- Track the path of each label already placed, and drop any label that
  would be written inside or on top of one, with a warning.
- Siblings of one namespace are unaffected and still share a document.
- Drive the helper over six label sets, the `Dict`-valued parent among
  them.
- Illustrate the namespace rule with `alphas__filled` rather than
  `pseudos__Si`, which carries repository content and so never reaches
  the JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Renumber the added example to the PKs the section already uses, rather
  than starting a second numbering halfway down.
- Illustrate a shared namespace with `alphas__filled`/`alphas__empty`:
  pseudopotentials carry repository content, so they land in a directory
  and never in the JSON.
- Say that a namespace holding both kinds is split between the directory
  and the JSON file.
- Name `List` in the list of what each node type writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recent entries under Unreleased are generated at release time from
the merged pull requests, not written per PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under `--flat`, a Dict input's `<label>.json` written before a
same-named output file was copied silently lost to that copy,
with no warning from the existing "already exists" guard.

- Copy node_inputs' and node_outputs' repository-backed content
  before writing either side's JSON, so the guard sees every file
  that will occupy the shared directory.
- Split `_dump_io_files` into `_copy_io_repository_content` (copy,
  collect JSON) and the existing `_dump_io_json` (write), reused by
  both the calculation and the workflow node_outputs paths.
- Add a regression test: an input Dict and a same-named output
  FolderData file collide, and the FolderData's file survives with
  a warning instead of being silently overwritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Placed under the section heading it read as if the whole process-dump
section were new in 2.10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`Dict`, `List` and the `BaseType` scalars all answer `value`; three
branches said so one class at a time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.71%. Comparing base (4019b32) to head (715972d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7589      +/-   ##
==========================================
+ Coverage   80.69%   80.71%   +0.03%     
==========================================
  Files         581      581              
  Lines       47139    47147       +8     
==========================================
+ Hits        38035    38051      +16     
+ Misses       9104     9096       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

elinsc-bot added a commit to elinscott/koopmans that referenced this pull request Aug 27, 2026
CI and RTD built against upstream aiida-core 92c21cc2dd, which lacks the
process-dump JSON and workflow-output flags; they now clone
elinscott/aiida-core at 715972d65e, upstream main with the
aiidateam/aiida-core#7589 commits on top.

- three test jobs and the RTD build fetch the fork commit
- CLAUDE.md records the pin and that it returns to upstream once
  #7589 merges

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
elinsc-bot added a commit to elinscott/koopmans that referenced this pull request Aug 27, 2026
A dumped run showed a step's parsed results and a workflow's own
outputs nowhere; every step now lists them, with the run's answers
indexed at the top.

- consumes aiida-core's include_data_json / include_workflow_outputs
  (fork pin, aiidateam/aiida-core#7589)
- folders for CalcJobs and graphs only; flat inputs/ and outputs/ per
  step; lone files take the link label
- files link only when they hold the same node's content, canonical
  at the producing step
- a graph's outputs/ indexes its answers as links under its own names
- scratch folders, codes, retrieved echoes, engine bookkeeping and
  empty listings are not written; a failed run keeps its stderr
- Si and O2 tutorial dumps inspected live

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@GeigerJ2
GeigerJ2 self-requested a review August 31, 2026 08:44
@GeigerJ2

GeigerJ2 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Thanks for opening the PR, @elinscott. Some top-level design questions. I wrote most of the dumping code quite a while ago and would do plenty differently now, but, I'd try to keep it backwards compatible here, and we can discuss bigger reworks within the framing of v3.

  • --include-outputs currently is a no-op on a WorkflowNode, so it could absorb --include-workflow-outputs, to keep the CLI surface smaller?

  • --include-data-json names a format where its neighbours name content, so maybe --include-data-values? Process nodes already get their attributes and extras into aiida_node_metadata.yaml while Data nodes (currently) get nothing, and the flag is really closing that asymmetry.

  • alphas__empty merging into alphas.json while repo-backed alphas__filled becomes alphas/filled/ is new here: before, a link label always mapped straight onto a path. Always nesting (alphas/empty.json) keeps that rule and drops the clash handling in _nest_by_link_label.

  • The elif writes a node's database content only when it has no repository files. Should that be an if? Additive, and the flag would mean the same thing for every node.

    if node.base.repository.list_object_names():
    linked_node_path.parent.mkdir(parents=True, exist_ok=True)
    node.base.repository.copy_tree(linked_node_path)
    elif self.config.include_data_json:
    json_values[link_label] = _serialize_data_node(node)

  • _serialize_data_node reads like the pydantic Entity.serialize()/from_serialized() pair but means something quite different, a bare value with no identity and no way back, so maybe _json_content to match the local json_values/_dump_io_json vocabulary. Worth saying in a docstring why a fourth view exists at all, given _prepare_<format>, serialize() and the REST translators: none of them produce the bare 2 that makes cat node_outputs/sum.json worth having. Its isinstance chain is also closed, though complete for aiida-core today, and duck-typing on .value isn't the fix since that would hand json.dumps an enum member. singledispatch is behaviour-identical and reads better, but the win I'd actually care about, a plugin type opting in, needs a public dispatcher, and this one is a private name in the private aiida.tools._dumping, nothing re-exported. Turning it into a supported extension point is a v3 follow-up, flagging it now so we design towards it rather than discover it later; landing the shape here is cheap and optional.

    def _serialize_data_node(node: orm.Node) -> Any:
    """Return the JSON-native content of a ``Data`` node.
    ``Dict``, ``List`` and the ``BaseType`` scalars answer their ``value``, any other node its attributes
    (``EnumData`` deliberately falls in the second group: its ``value`` is the enum member, not JSON). Attributes
    pass through ``clean_value`` on storage, which refuses anything that is not JSON-serializable, so the result can
    always be handed to ``json.dumps``.
    :param node: The node to serialize
    :return: A JSON-native object
    """
    if isinstance(node, (orm.Dict, orm.List, orm.BaseType)):
    return node.value
    return node.base.attributes.all

    what that would look like
    # aiida/tools/_dumping/executors/process.py
    from functools import singledispatch
    
    
    @singledispatch
    def _json_content(node: orm.Data) -> Any:
        """Return the JSON-native content of a ``Data`` node: its attributes, unless its type says otherwise."""
        return node.base.attributes.all
    
    
    @_json_content.register
    def _(node: orm.Dict | orm.List | orm.BaseType) -> Any:
        """``Dict``, ``List``, and the ``BaseType`` scalars (``Int``, ``Float``, ``Bool``, ``Str``) answer ``value``."""
        return node.value

    A plugin could then opt in without an aiida-core change, once the dispatcher is public:

    @_json_content.register
    def _(node: MyPluginData) -> Any:
        return node.value

    Output-identical on every aiida.data type in core, EnumData included, and a plugin only registers if it wants something different: dispatch is on the MRO, so a subclass of Dict or Int inherits value and anything else gets the attributes default, with no plugin code at all.

  • One file per node, or one node_outputs.json with an entry per link label? Per-file can do everything the questions above ask for: nest always, write arraydata.json beside arraydata/, carry uuid and pk. It just has to answer each of them separately, and carrying identity turns sum.json from 2 into an object, which costs it the thing it is best at. One file settles all three at once, since an entry is an object with identity, content, and a pointer when there are files, at the price of cat node_outputs/sum.json. Per-file is what's in the PR and fine to ship, I'd just rather we chose it than defaulted into it. What do you think?

    where it would live, and one entry per node
    02-ArithmeticAddCalculation-8/
    ├── aiida_node_metadata.yaml     # unchanged, the process node itself
    ├── inputs/, outputs/            # unchanged
    ├── node_inputs.json             # every INPUT_CALC link
    ├── node_outputs.json            # every CREATE/RETURN link, nested by namespace
    └── node_outputs/                # only when a linked node has repository content
        └── alphas/
            └── filled/
                └── data.txt
    
    {
      "sum": {"uuid": "...", "pk": 12, "node_type": "data.core.int.Int.", "value": 2},
      "alphas": {
        "empty":  {"uuid": "...", "node_type": "data.core.int.Int.", "value": 2},
        "filled": {"uuid": "...", "node_type": "data.core.folder.FolderData.",
                   "repository": "node_outputs/alphas/filled/"}
      }
    }

Zooming out, and this probably wants its own issue: the reason all of the above are open questions is that the dump was designed around processes, and Data nodes never got the same treatment. They enter only as "copy the linked node's repository into node_inputs/<label>/", so as bytes with no identity. A Dict has no bytes and therefore reaches no file, which is #7588; a repository-backed node's database side vanishes, which is the elif; code.json carries no label because identity was never modelled; and namespaces had to be solved twice, once as directories and once inside a JSON document. v3 could close all four at once by giving Data nodes what process nodes already have, identity plus content.

v3 sketch

The internal vocabulary is already right, only the directory names it maps onto are misleading:

# _generate_calculation_io_mapping
aiida_entities = ['repository', 'retrieved', 'inputs', 'outputs']
default_dirs   = ['inputs',     'outputs',   'node_inputs', 'node_outputs']

So internal repository lands in a directory called inputs/, and internal retrieved lands in one called outputs/. Using the internal names on disk gives one meaning per word:

concept today proposed
the process node's own repository inputs/ repository/
what came back from the scheduler outputs/ retrieved/
repository content of input-linked Data node_inputs/ inputs/
repository content of output-linked Data node_outputs/ outputs/
database side of every linked Data nothing aiida_node_links.json

repository works for a CalcFunctionNode too, whose repository holds source_file rather than a submission script.

MultiplyAddWorkChain-5/
├── aiida_node_metadata.yaml        # the workflow node itself
├── aiida_node_links.json           # RETURN: result
├── 01-multiply-6/
│   ├── aiida_node_metadata.yaml
│   ├── aiida_node_links.json       # INPUT_CALC: x, y | CREATE: result
│   └── repository/
│       └── source_file
└── 02-ArithmeticAddCalculation-8/
    ├── aiida_node_metadata.yaml
    ├── aiida_node_links.json       # INPUT_CALC: x, y, code | CREATE: sum, remote_folder
    ├── repository/
    │   ├── .aiida/{calcinfo,job_tmpl}.json
    │   ├── _aiidasubmit.sh
    │   └── aiida.in
    └── retrieved/
        ├── _scheduler-std{err,out}.txt
        └── aiida.out

One entry per link, always identity and the database side, plus a repository pointer when the node has files. That is the elif gone, and alphas__filled nests identically on disk and in the document, so _nest_by_link_label goes too.

A repo/ + db/ top-level split is the other obvious shape, and I'd argue against it: it organises the dump by AiiDA's storage implementation rather than by what someone is looking for, so a single calculation's inputs would split across db/inputs/x.json and repo/inputs/structure/, and you would need to know where AiiDA stores each type before knowing where to look.

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.

2 participants