Skip to content

[ESSREDUCE] Minimal implementation-independent workflow specifications - #690

Draft
SimonHeybrock wants to merge 6 commits into
mainfrom
653-minimal-workflow-spec
Draft

SimonHeybrock wants to merge 6 commits into
mainfrom
653-minimal-workflow-spec

Conversation

@SimonHeybrock

@SimonHeybrock SimonHeybrock commented Aug 5, 2026

Copy link
Copy Markdown
Member

Fresh counter-proposal for #653, as announced in the review on #689 — I propose closing that PR in favor of this one.

The core difference to #689: the spec is pure interface. No workflow factory, no sciline keys, no use_workflow_default — nothing that presumes a particular implementation or execution location. The same spec must make sense whether the workflow runs as a local sciline pipeline, behind a web service, or as a cluster job; compute binding is a parallel mechanism, deliberately out of scope here.

The second commit extends the first with what the essapps architecture sketch needs (D13, see the comment below): outputs as a typed model and data-reference fields, so that any output of one workflow run can be checked against the parameter it feeds in the next.

Main decisions (full rationale in ADR 0001, included in this PR):

  • One pydantic params model per workflow instead of per-sciline-key Parameter entries. Enables cross-parameter validation and JSON Schema for free. The graph-derived "select outputs → see relevant params" feature is dropped, not ported: it treats output selection as workflow slicing, which only the sciline implementation can express.
  • Outputs are a pydantic model too, in the same vocabulary as params: title and description as field metadata, optional fields allowed, primary output first. Chaining is then a type check between an output field and a parameter field, and where a framework stores an output is decided by the field's type, not by the spec.
  • Data fields: a parameter or output holding a file or array rather than a literal is a field of type Ref, a reference to an output of an earlier run or to a dataset the framework did not compute, annotated with the format of the bytes (raw NeXus file, scipp object, opaque file) and, for scipp data, an ArraySpec (dims, unit, coord units, binned). There is no separate input section: an input is a parameter of data-field type. Collections of data fields are allowed on both sides, and a reference may name one element of a collection output. The spec says nothing about how a workflow gets at the bytes, a path or an in-memory object; that is decided where the workflow is called, by the executor binding, so a framework can add an in-memory path for chained runs without touching a spec. The spec layer still imports no scipp; the structural check of a scipp object against its ArraySpec lives in spec.conversions.
  • title and description mandatory; identity is name + version — no instrument, no WorkflowId class. Scoping and global uniqueness are the enumerating context's responsibility. An optional code_revision is provenance, not identity.
  • Two forms with a one-way projection: WorkflowSpec (in-process, holds the model classes, full validation) and SerializedWorkflowSpec (plain data, params and outputs as JSON Schema, JSON-round-trippable). Data fields appear in the schema under a dataField key with their format and structure. No inverse — validators don't survive JSON Schema, so authoritative validation stays with the process owning the model classes; remote consumers validate optimistically. This matches the announcement-as-contract design from Rethink workflow registration: move schema ownership to domain packages esslivedata#889.
  • Scipp-free shared parameter vocabulary (unit enums, range/edges models with cross-field validation, Quantity for small values with a unit); scipp conversions quarantined in spec.conversions. Value defaults are set by workflow authors at the use site, not by the vocabulary.
  • No registry: enumeration mechanisms are out of scope, any works against the same spec type. One requirement on workflow packages: the module defining a spec must be importable without importing the workflow code.
  • Explicit goal: ess.livedata WorkflowSpec eventually inherits from this. esslivedata already declares outputs as a model with field metadata, so the remaining difference is field types: its sc.DataArray template fields become data fields constrained by ArraySpec.

Deferred until a consumer exists, listed in the ADR so they are not lost: declared failure reasons, an intermediate flag for retention, declared collection keys, and spec discovery via entry points.

Adds pydantic>=2.5 as an essreduce dependency (already present in the locked CI environments). The existing ess.reduce.parameter/workflow/widgets machinery is superseded but untouched; removal is a later hard break — no one depends on it currently.

🤖 Generated with Claude Code

WorkflowSpec describes a workflow's user-facing interface (identity,
title/description, one pydantic params model, structural output
descriptions) without factories, sciline keys, or registries, so generic
UIs can be generated from it regardless of where compute happens.
serialize() projects one-way onto SerializedWorkflowSpec (params as JSON
Schema) for cross-process consumers; authoritative validation stays with
the process owning the model class.

Includes a scipp-free shared parameter vocabulary (unit enums,
range/edges models with cross-field validation) with scipp conversions
quarantined in spec.conversions, and ADR 0001 recording the design and
its rationale (see #653, scipp/esslivedata#889).

Adds pydantic as an essreduce dependency. The existing
ess.reduce.parameter/workflow machinery is superseded but untouched;
removal is a later hard break.

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

Copy link
Copy Markdown
Member Author

Reminder of the extensions the essapps architecture sketch (scipp/essapps, branch architecture-sketch, docs/developer/architecture.md, decision D13) needs from this spec, so they are not lost while the PR is open. A prototype of all of them lives in ess.apps.spec on that branch.

  • Outputs as a typed model. outputs becomes a pydantic model class in the same vocabulary as params, with title and description as field metadata and optional fields allowed. Chaining is then a type check between an output field and a parameter field. As proposed, non-array outputs have no type, which breaks "outputs can be inputs" for exactly the values that most often feed the next workflow (beam centre, direct beam).
  • A data-reference field type. A field marked with a kind (raw NeXus file, opaque file, scipp array) holds a reference to another record's output at submission and the materialized value (path or scipp object) inside the workflow. Arrays are constrained by the same ArraySpec outputs use. A field may be a union of a literal and a reference. The prototype validates this without importing scipp in the spec layer.
  • Collections on both sides. list[...] and dict[str, ...] of one declared type as parameters and as outputs; a reference may name one element of a collection output by key.
  • ArraySpec.binned, so consumers know event data that must not be plotted directly.
  • Two small spec fields: cheap: frozenset[str], the parameters a warm workflow can change without recomputing the expensive part (what lets a UI offer a slider), and an optional code_revision.
  • Entry points split by role: specs in one group, workflow factories in another under the same name, so a service can load and validate every spec without importing workflow code. This matches the ADR's "the backend never imports a factory" and the esslivedata split.

None of this changes the ADR's core: pure interface, one params model, one-way serialization, identity by name and version.

SimonHeybrock added a commit to scipp/essapps that referenced this pull request Sep 8, 2026
The spike on D3 and D6 the architecture sketch calls for, built as a real
package under packages/essapps (import ess.apps) so it can move into the
scipp/ess monorepo unchanged.

What is in: the spec vocabulary with data-reference fields and collections
(a local copy of the shape in scipp/ess#690 plus the D13 extensions, to be
replaced once that merges); run requests and records; a SQLite record store
with a single-writer lock and the registry of disk copies; a data store with
a private memory cache and a scipp HDF5 disk tier; the binding registry;
one runner for both execution shapes; a session launcher (outputs stay in
memory) and a subprocess launcher (outputs to disk with a completion marker,
reconciled by the backend); the backend with three-layer validation, atomic
group submit with pending outputs, failure and cancellation propagation,
explicit recompute, stand-in resolution for local files, and slots as a
label plus a latest query; views as plain arrays; the Python client
interface; and example workflows for load, rebin, and map-combine.

Repo tooling copied from the monorepo: ruff config, pre-commit, the test
file name check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SimonHeybrock added a commit to scipp/essapps that referenced this pull request Sep 8, 2026
ArrayValue, and Array(spec) with a structure constraint, accept a reference
or an in-process data object and reject plain data, without the spec module
naming scipp. Data-reference fields are now found through optionals, unions,
and collections, so an optional array input is materialized like any other.
as_ref is the one place that decides whether a plain value is a reference;
walk_refs, group rewriting, literal inlining, and materialization all use
it, and ref_fields lists the fields of a model that may hold one.

This is the vocabulary feedback for scipp/ess#690 in code form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SimonHeybrock and others added 2 commits September 14, 2026 10:02
Outputs become a pydantic model class in the same vocabulary as params,
with title and description as field metadata, replacing the dict of
OutputSpec. Without this, non-array outputs had no type, so a beam
centre or direct beam could not be checked against the parameter it
feeds in the next workflow.

New module ess.reduce.spec.data defines data fields: parameters and
outputs holding a file or array rather than a literal. A field is a
union of a reference (OutputRef to a run's output, or DatasetRef to
data the framework did not compute) and the materialized value (path
or scipp object), annotated with DataField(kind, array). Arrays are
constrained by ArraySpec on both sides, which gains a binned flag.
Collections of data fields are allowed and walked. The spec layer
still imports no scipp: the in-process form of an array is validated
as "not plain data", and the structural check against ArraySpec lives
in spec.conversions. In JSON Schema a data field shows only its
reference form, under a dataField key carrying kind and structure.

Adds Quantity (scalar or short vector with unit) to the vocabulary and
an optional code_revision to the spec; SerializedWorkflowSpec carries
outputs_schema next to params_schema.

Driven by the essapps architecture sketch (D13), see the discussion on
#690. ADR 0001 amended accordingly, including the requirement
that a spec module be importable without workflow code and the list of
foreseen extensions (cheap parameters, failure reasons, contribution
output, discovery) deferred until a consumer exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@SimonHeybrock

Copy link
Copy Markdown
Member Author

I do not like how this got complex in the latest commit -- need to think more about how to reference input (and output) data. Should inputs always be filenames and outputs DataArray (or similar)? The fundamental problem appears to be that we "hard-code" the file loading as part of our Sciline workflows, making it tricky to hand an DataArray into a workflow (at least on the spec level), as it feeds into a different workflow node (or nodes). We are avoiding similar problems on the output side by saying "workflows should not have side-effects, file writing happens outside workflows", but doing the same for inputs is much more challenging.

@SimonHeybrock
SimonHeybrock marked this pull request as draft September 17, 2026 08:55
A data field's type was a union of the reference a request names and
the value the workflow receives, a path or a scipp object. That made the
params model wrong in both phases, needed a validator that accepted
anything not plain data, hid that member from the JSON Schema, and put a
materialization instruction, Kind, into a spec meant to be pure
interface.

A data field is now a Ref, annotated with the Format of the bytes and,
for scipp data, an ArraySpec. How a workflow gets at the bytes is decided
where it is called, by the executor binding, so a framework can add an
in-memory path for chained runs without changing a spec or a workflow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SimonHeybrock added a commit to scipp/essapps that referenced this pull request Sep 17, 2026
…object

A data-reference field was typed as a union of the reference a request
names and the value the callable receives, with a Kind on the spec
telling the runner how to materialize it. That made the params model
wrong in both phases, needed a validator accepting anything not plain
data, and put an execution decision into the spec.

A data field now holds a reference only, annotated with the format of
the bytes, which the backend checks between producer and consumer and
the picker filters by. The callable takes the validated params and an
Inputs and asks for a path or a scipp object per parameter; the sciline
wrappers name that form next to the key each parameter maps to. Where
the bytes come from stays with the runner, so an in-memory chain in a
session changes no spec and no binding. Callables return outputs by
field name; the runner validates literals through the outputs model and
stores data outputs. The built-in file spec paragraph is gone with the
"any kind" rule it existed for. Mirrors scipp/ess#690 at 19f1b15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SimonHeybrock added a commit to scipp/essapps that referenced this pull request Sep 17, 2026
The spec module was a copy of scipp/ess#690 with the sketch's additions
mixed in, so every change to the proposal cost two edits and the names
had drifted. essreduce is now a dependency on the PR branch, and
ess.apps.spec keeps what the sketch adds: the spec identity, the
contribution output and the parameters finalize reads, the derived
models the runner and backend validate against, and the dataset
identity.

Names follow the branch: OutputRef and Ref for the two forms of
reference and their union, DataField and data_fields, dataField in the
schema. A dataset reference is the branch's single identity string;
dataset_ref encodes the framework's three identities into it, a PID, an
instrument and run number, or a path, and dataset_path reads the last
back. A dictionary is a dataset reference only when its one key is
dataset, which removes the case where a params model with a single pid
or path field could be mistaken for one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SimonHeybrock and others added 2 commits September 18, 2026 10:36
ADR 0001 listed a contribution output, with the parameters its finalize
stage reads, as a foreseen extension for the additive combine of the
essapps sketch. The sketch no longer needs it: an aggregation over runs
is now two plain specs, a contribute spec and a combine spec, so the
contribution is an ordinary output and the finalize parameters are the
ordinary parameters of the combine spec.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ADR 0001 listed the parameters a warm workflow can change cheaply as a
foreseen spec field. Which parameters are cheap depends on where the
implementation cuts its graph, which is an implementation choice and not
part of the interface. A workflow author who wants a cheap step to be
visible publishes it as a workflow with its own spec, which the ADR
already covers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

essreduce Issues for essreduce.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant