WORKFLOW_EXTRACTION_OUTPUT_LABELING_PLAN

Workflow Extraction — Identify & Label Outputs — Implementation Plan

Date: 2026-05-23 Branch: off workflow_state_backfill (after HISTORY_GRAPH_UI_INTEGRATION_PLAN lands) Tracking issue: #22709 (umbrella), #17506 (extraction UI modernization); resolves the longstanding gap that workflow extraction has no first-class way to select and label outputs during extraction. Related:

  • HISTORY_GRAPH_UI_INTEGRATION_PLAN — same-branch backend prep (NotebookEditor independent).
  • MAP_OVER_EMPTY_EXTRACTION_TOOL_REQUEST_PLAN — ships the tool_request_ids primitive the payload extends.
  • QUEUED_EXECUTION_EXTRACTION_TOOL_REQUEST_PLAN — second use case of same primitive.
  • CAPTURE_WORKFLOW_EXECUTION_STATE_PLANtool_execution_state capture path.
  • NOTEBOOK_EXTRACTION_MVP_PLANconsumer of this plan. Notebook MVP sits on top of the primitives shipped here.

At a glance

ProblemExtracted workflows cannot explicitly expose outputs during extraction. extract.py never emits WorkflowOutput rows, and the list-extraction UI has no surface for choosing or labeling concrete outputs. Every workflow output must be marked later in the editor.
Key insight”Include this tool step” and “expose this produced artifact as a workflow output” are related but distinct user decisions. Extraction needs a first-class, per-output primitive so callers can explicitly select exactly which HDA/HDCA artifacts become workflow outputs.
This plan delivers(a) WorkflowExtractionByIdsPayload carries explicit output_labels; (b) the extractor emits a WorkflowOutput row per selected output artifact; (c) the extraction summary annotates every concrete output with its workflow port and suggested label via a reusable 4-tier naming chain; (d) the existing list UI gains a per-output star + rename surface, with stars defaulting off.
Independent valueThe established extraction flow gains precise output-selection infrastructure without changing default behavior: if no outputs are starred/submitted, extraction remains a no-output no-op exactly as today.
Sets upNOTEBOOK_EXTRACTION_MVP_PLAN: report/notebook extraction can pre-star referenced outputs and prefill labels using the same payload instead of duplicating output-marking logic. Future graph-mode extraction UI consumes the same primitive byte-identical.
RiskModerate. The extractor change touches a load-bearing path. The naming chain has four producer-kind branches (Job / ICJ / ToolRequest / TES) with edge cases. All abstractions exist already; this plan composes them.

Why this exists

Today’s extraction story has a quiet structural gap: the user selects which steps go in, the workflow gets created, and then none of the outputs are marked as workflow outputs. The user has to open the editor, find the right step, click the output port, mark it as a workflow output, and label it — for every output they care about. That step is invisible in the extraction-flow research notes and tutorials because everyone has internalized it as “what you do after extraction.”

The fix should not overload the existing tool-step checkbox. A checked tool row means “include this tool step in the workflow.” A starred concrete output means “expose this produced HDA/HDCA as a workflow output.” The list-extraction UI should default all output stars off to preserve today’s behavior, while richer consumers — especially report/notebook extraction — can pre-star the outputs they know are referenced.

The missing pieces are: (1) a payload field for “expose these specific outputs with these labels,” (2) extraction-summary metadata that tells callers which concrete artifacts map to which workflow output ports and what labels to suggest, (3) the extractor honoring the payload, and (4) a UI surface where users can star and label outputs at extraction time instead of after.

This plan is also a precondition for NOTEBOOK_EXTRACTION_MVP_PLAN — a notebook-driven seed has nothing to populate if the underlying extraction can’t accept explicit output selections. Splitting these makes both reviewable in isolation and lets the upstream value land independent of notebook adoption.

Settled decisions

Architecture / seam

POST /api/workflows/extract
  payload:
    job_ids / hdca_ids / tool_request_ids / implicit_collection_jobs_ids / hda_ids   ← existing/branch-dependent
    dataset_names / dataset_collection_names                                         ← existing (input naming)
    output_labels: list[OutputLabelHint]                                             ← NEW explicit output exposure


  _validate_extract_by_ids_payload (services/workflows.py)
     - dedup output_labels by (kind, id)
     - sanitize label (strip; 255 max; reject empty)
     - resolve each label target against outputs produced by selected tool/ICJ/tool-request steps
     - reject orphan labels and duplicate workflow-output labels deterministically


  extract_steps_by_ids (workflow/extract.py)
     - existing wiring
     - NEW post-wire pass: for each output_label, look up producer + port_name via
       the id_to_output_pair / output_hdcas maps already built; call
       step.create_or_update_workflow_output(output_name=port, label=hint.label, uuid=None)


GET /api/histories/{history_id}/extraction_summary  → WorkflowExtractionSummary
  per concrete tool output:
    output_name: Optional[str]                                                       ← NEW workflow/tool port
    suggested_name: Optional[str]                                                    ← NEW
    suggested_name_source: Optional[SuggestedNameSource]                             ← NEW
    exposed: bool                                                                    ← NEW, false for history UI


  managers/workflow_extraction_naming.suggested_output_name(trans, content_id, kind)
     - dispatch by producer kind (Job / ICJ / ToolRequest / TES where available)
     - return SuggestedName via 4-tier chain


WorkflowExtractionForm.vue
  - inputs: existing rename surface, pre-fill from suggested input name (unchanged)
  - tool outputs: NEW star + label field per concrete output
  - history extraction defaults exposed=false; report/notebook extraction may preseed exposed=true
  - submit emits output_labels only for starred/exposed outputs

The naming-chain helper is the central reusable abstraction. Consumers in this plan: extraction summary (history) and the notebook MVP plan. Future graph-mode UI hydrates the same way.

Steps

1. Naming-chain helper

2. Payload extension + validator

3. Extractor emits WorkflowOutput rows

4. Extraction summary suggests names

5. Form output-star + rename surface

6. Verify

Files to touch

FileStepScope
lib/galaxy/managers/workflow_extraction_naming.py1new — 4-tier name resolver per producer kind
lib/galaxy/webapps/galaxy/services/base.py1branch-dependent: promote _parsed_tool_source_for_tes to public only if TES naming path exists
lib/galaxy/schema/workflows.py2, 4OutputLabelHint; output_labels on payload; output-level output_name / suggested_name / suggested_name_source / exposed on summary outputs
lib/galaxy/webapps/galaxy/services/workflows.py2sanitization + dedup + orphan-label validator
lib/galaxy/workflow/extract.py3post-wire pass emitting WorkflowOutput rows
lib/galaxy/webapps/galaxy/services/histories.py4populate output-level port, suggested-name, and exposed metadata in summary
client/src/components/History/WorkflowExtraction/types.ts5output-level exposure and label UI state
client/src/components/History/WorkflowExtraction/WorkflowExtractionCard.vue5per-output star + label affordance on tool rows
client/src/components/History/WorkflowExtractionForm.vue5output exposure/label state + submit threading
client/src/api/histories.ts5extractWorkflowByIds payload type extension
client/src/api/schema/index.ts2, 4regenerated
test/unit/app/managers/test_workflow_extraction_naming.py1new
lib/galaxy_test/api/test_workflow_extraction.py3, 4output_labels + summary cases
client/src/components/History/WorkflowExtractionForm.test.ts5output star + rename flow
lib/galaxy_test/selenium/test_workflow_extraction.py5E2E star + label round-trip

What this sets up for downstream

Out of scope

Confirmed assumptions / remaining checks

Research Notes — Output naming chain

Folded from research conducted 2026-05-23 (originally for NOTEBOOK_EXTRACTION_MVP_PLAN; promoted here as the primitives’ home).

Why the original 3-tier proposal needed adjustment

The first-draft chain was ToolOutput.labelToolOutput.nameHDA.name. Two issues:

  1. ToolOutput.label is a Cheetah template, not a finished string (tool_util/parser/output_objects.py:53,69,196). Example: ${tool.name} on ${on_string}. Rendering needs runtime params / on_string, which aren’t always reachable — particularly for jobless ToolRequest producers.
  2. The current extractor never creates WorkflowOutput rows (extract.py:179–202, 747–755). The dataset_names / dataset_collection_names payload fields only label synthesized data_input / data_collection_input input steps. This plan is what fixes that.

Final chain (decision NAMING_CHAIN)

  1. User-renamed HDA.name / HDCA.name when diverged from the auto-generated runtime name. Strongest signal of user intent.
  2. Rendered ToolOutput.label via get_output_name(tool, output, params) (managers/jobs.py:2091, wrapping DefaultToolAction.get_output_name at tools/actions/__init__.py:1076). Used when params is reachable.
  3. Bare ToolOutput.label template if not renderable. Still human-authored.
  4. ToolOutput.name (port name like out_file1). Guaranteed present.

Resolution recipes by producer kind

All recipes assume the referenced HDA/HDCA id is access-checked.

(a) Regular finished Job — HDA output:

hda = sa_session.get(HistoryDatasetAssociation, hda_id)
original = _original_hda(hda)                                # extract.py:1078 pattern
assoc = next(a for a in original.creating_job_associations
             if not _skip_output_assoc_name(a.name))
job, port_name = assoc.job, assoc.name
tool = trans.app.toolbox.tool_for_job(job, user=trans.user)  # tools/__init__.py:692
tool_output = tool.outputs.get(port_name)
params = tool.get_param_values(job, ignore_errors=True)
return _apply_chain(hda=original, tool_output=tool_output, port_name=port_name, params=params)

(b) ICJ-produced HDCA (map-over):

hdca = _original_hdca(sa_session.get(HistoryDatasetCollectionAssociation, hdca_id))
port_name = hdca.implicit_output_name                        # model:8110 canonical
icj = hdca.implicit_collection_jobs
rep_job = icj.representative_job
tool = trans.app.toolbox.tool_for_job(rep_job, user=trans.user)
tool_output = tool.output_collections.get(port_name) or tool.outputs.get(port_name)
params = tool.get_param_values(rep_job, ignore_errors=True)
return _apply_chain(hdca=hdca, tool_output=tool_output, port_name=port_name, params=params)

(c) Jobless / queued ToolRequest (no jobs at all):

hdca = _original_hdca(sa_session.get(HistoryDatasetCollectionAssociation, hdca_id))
trica = hdca.tool_request_association                        # model:8151, 1475
port_name = trica.output_name                                # model:1483
tool = _tool_from_request(trans, trica.tool_request)         # reuse extract.py:618
tool_output = tool.output_collections.get(port_name) or tool.outputs.get(port_name)
return _apply_chain(hdca=hdca, tool_output=tool_output, port_name=port_name, params=None)

(d) ToolExecutionState (workflow tool-step capture, no ToolRequest): The TES is always reached through Job.tool_execution_state in practice, so recipe (a) applies. If you genuinely have only the TES:

parsed = parsed_tool_source_for_tes(tes, trans.app.toolbox)  # promote services/base.py:208 helper
outputs, output_collections = parsed.parse_outputs(trans.app)
tool_output = output_collections.get(port_name) or outputs.get(port_name)
return _apply_chain(hdca=hdca, tool_output=tool_output, port_name=port_name, params=None)

Edge cases

Edge caseBehaviorNotes
HDA renamed by user post-jobPrefer renamed HDA.name (level 1)Diverge = renamed; absent rename, levels 1 and 3 are the same string at execution time
Implicit-collection leaf HDA referencedCoerce to parent HDCA + emit warningWorkflowOutput attaches to collection-port-keyed output_name, not leaf elements
Multi-output tool, N starred/seededAll N concrete outputs become WorkflowOutput rows on that step; payload accepts N entries per step. List UI surfaces per-output stars from v1Per PORT_GRANULARITY_NOW
HDA name has unsuitable charsSanitize at the API boundary (strip, max 255)Boundary concern, lives in the validator
Two output_labels resolving to the same (step, output_name)Reject 400 at validator after selected-output resolutionAvoid hidden last-writer behavior and editor duplicate-label warnings
Same id appears twice in output_labelsReject 400 at validator (matches existing dedup at services/workflows.py:280)Boundary invariant
Tool no longer installedNaming chain degrades through b→c→d; HDA.name (level a) still availableConfirm in unit test

Existing helpers identified (reuse, don’t duplicate)

There is no existing “best label for any HDA/HDCA” helper. The new naming-chain module is the right place.