EXTRACT_STEP_LABELS_PR

Workflow Extraction: Allow Step Labels

Status — IMPLEMENTED & VERIFIED (2026-06-08)

Built on branch extract_step_labels exactly as planned below; all open questions resolved (see Decisions). 12 files changed, +511/−21.

Backend

Client (schema regenerated via make update-client-api-schema)

Verification — all green:

Not yet done: commit / PR open. The detail below is the as-built spec (kept for review + the notebook rebase).

Where this sits

PR progression:

  1. HEAD / #22853 — keep input + output labels unique (backend 400s on dup/empty/overlong; form de-dups input names client-side, predicts the output-label 400 inline).
  2. This PR (implemented)allow labeling tool / mapped-tool steps, the way we already label inputs and outputs. Off by default in both UI and API: an unlabeled step stays label = None (today’s behavior). Labels join the existing single uniqueness namespace.
  3. Notebook extraction (rebase of extract_next / EXTRACT_NOTEBOOK_PR.md)seed step labels from the notebook so a report can reference workflow steps by stable label. This PR only adds the capability; the notebook PR fills in the seeding.

This is the missing prerequisite for “extract the report along with the workflow”: a report references a step, and a referenced step needs a stable label to point at — exactly as an exposed output needs a label. Today only inputs (dataset_names / dataset_collection_names) and outputs (output_labels) can be labeled; tool steps cannot be labeled at all.

The downstream consumer is Workflow.step_by_label (model __init__.py), which raises on duplicate labels — so app-enforced label uniqueness (there is no DB-level uniqueness constraint; WorkflowStep.label is just nullable Unicode(255)) is exactly what the report PR will rely on.


High-level review of the notebook plan (EXTRACT_NOTEBOOK_PR.md)

The notebook plan is sound and already assumes this PR exists implicitly but never builds it:

One thing the notebook plan should add when it rebases (note for that PR, not this one): the §7 “schema/workflows” and “WorkflowExtractionForm.vue” bullets gain a suggested_label row, mirroring the existing suggested_name/exposed rows. Nothing in the notebook plan conflicts with this PR; it cleanly slots a label seed onto the step rows this PR makes labelable.

No correction needed to the notebook plan’s architecture. The only gap is the one this PR fills.


Design decisions

Add to WorkflowExtractionByIdsPayload:

class StepLabelHint(Model):
    kind: Literal["job", "implicit_collection_jobs"]   # plain tool job vs. mapped (ICJ) step
    id: DecodedDatabaseIdField       # the job id or icj id (same id you already pass in the bucket)
    label: str

step_labels: list[StepLabelHint] = Field(default_factory=list, ...)

Mirrors output_labels: list[OutputLabelHint] exactly. A step is labeled iff it appears in step_labels; absence = unlabeled. Tool steps already arrive in two buckets (job_ids and implicit_collection_jobs_ids), and kind discriminates which — the hint references the same id the caller already put in the bucket.

Rejected — STEP_LABEL_PARALLEL_ARRAYS. Mirror dataset_names: job_labels parallel to job_ids, implicit_collection_jobs_labels parallel to implicit_collection_jobs_ids. Two parallel arrays, and because most steps are unlabeled you’d need null sentinels to hold position — fragile and noisier than the structured list. Inputs use parallel arrays only because every input always has a name (a default); steps are optionally labeled, which is the output_labels shape, not the dataset_names shape.

extract_steps_by_ids already keeps a single step_labels: set[str] covering input step labels (extract.py:661). Tool-step labels go into the same set: a workflow step label is globally unique among all steps. So a provided step label must be unique against other step labels and against the input names. Generalize HEAD’s _validate_input_names into one validator over the union {dataset_names ∪ dataset_collection_names ∪ step_labels}: non-empty, ≤255, unique. Same rejection-not-truncation rule HEAD established.

POST /api/histories/{id}/extract_workflow (WorkflowExtractionPayloadextract_steps) is the HID-based legacy submit; the client does not use it (the form uses POST /api/workflows/extractextract_by_ids). Do not add step labels there. It keeps its existing input-name validation from HEAD; nothing else changes. Scoping step labels to the by-ids path keeps the surface minimal and matches where output_labels already lives (also by-ids-only).


API piece

lib/galaxy/schema/workflows.py

lib/galaxy/webapps/galaxy/services/workflows.py

lib/galaxy/workflow/extract.py (extract_steps_by_ids + extract_workflow_by_ids)

output_labels application (extract.py:760-772) is unchanged; step labels and output labels are orthogonal (one labels the step, the other labels a concrete output port).

Client API schema regen (required before the UI piece compiles)

Tests — lib/galaxy_test/api/test_workflow_extraction.py (red→green, mirror the output_labels tests at 1154+, reuse _extract_and_download_workflow_by_ids / assert_steps_of_type / _assert_extract_rejected / _seed_two_inputs_and_run_cat1):

Unit — test/unit/.../workflow/test_extract*.py (cheapest layer for the wiring):


UI piece

The tool card has affordances for outputs (star + rename pencil) but none for the step itself. Step labels behave like output labels, not like input names: explicit, optional, and validated by disabling submit on collision — not auto-suffixed (inputs auto-suffix only because they always carry a default name).

client/src/components/History/WorkflowExtraction/types.ts

WorkflowExtractionCard.vue

WorkflowExtractionForm.vue

Tests — WorkflowExtractionForm.test.ts (mirror the existing input/output blocks):

Selenium — TestWorkflowExtraction (one round-trip, since the label field is a genuinely new UI path the form translation tests can’t prove end-to-end): label a tool step, extract, assert the created workflow’s step carries the label. One test only — collection topology / namespace rejection stay at API+vitest per the notebook plan’s “cheapest faithful layer” rule.


Decisions (resolved)

All open items resolved — plan is ready to implement.