EXTRACT_TOOL_REQUEST_STATE_PLAN

Workflow Extraction from Tool Request State — Implementation Plan

Date: 2026-05-16 Branch: graph_workflow_extract — but the seam (step_inputs_by_id) lives in #22706; rebase onto / branch from history_notebook_extract first. This plan is blocked-by-dependency on #22706 landing (or being the base). Predecessor: ICJ_NATIVE_PLAN — this is its “What still requires walking a representative job” section graduated into its own plan. Tracking issue: TBD (deferred — create later, then backfill this line) Related research:

  • vault/research/Problem - YAML Tool Post-Hoc State Divergence.md (canonical longer-term-direction note; corrected 2026-05-16 with the facts below)
  • vault/research/Component - Tool State Specification.md
  • vault/research/Component - Workflow Extraction Models.md
  • vault/research/PR 22706 - Workflow Extraction by IDs.md
  • vault/research/PR 21932 - History Graph API.md

Progress update (2026-05-16, graph_workflow_extract)

Implemented and verified an initial structured extraction slice on graph_workflow_extract:

Verified:

Still pending from this plan:

Why this exists

ICJ_NATIVE_PLAN made the ICJ the unit of selection but explicitly punted parameter state: extract_steps_by_ids is ID-native for wiring yet still reconstructs tool parameters via tool.get_param_values(job)params_from_strings → legacy JobParameter rows (basic.py). For YAML / user-defined tools that flat encoding is lossy (collection runtime metadata, comma-separated collection_type, dce source) — the divergence in Problem - YAML Tool Post-Hoc State Divergence. The FIXME at extract.py:620-624 points exactly here.

This plan re-roots the parameter-state source on the structured ToolRequest.request for executions that have a tool request, and quarantines the legacy params_from_strings path behind a config-gated, tested fallback for jobs predating tool requests.

This is the gate. Graph→workflow and notebook→workflow UI (GRAPH_WORKFLOW_EXTRACTION_PLAN) is cart-before-horse until extraction and the History Graph share one structured-state model. After this lands they do — same ToolRequest.request parse, one identity space.

Verified facts (research, 2026-05-16 — read at dev / history_notebook_extract)

  1. ToolRequest model lib/galaxy/model/__init__.py:1411-1428: request: Mapped[dict] JSONType (:1419), plus state, state_message; rels jobs (ordered Job.id), implicit_collections. Job link Job.tool_request_id FK (:1640), Job.tool_request rel (:1644).
  2. No request_state field on ToolRequest. It exists only on ToolLandingRequest/WorkflowLandingRequest. The FIXME / Post-Hoc-doc wording “ToolRequest.request_state reader” is a misnomer — the real reader reads ToolRequest.request.
  3. ToolRequest.request representation = request_internal: written services/jobs.py:272 as tool_request.request = request_internal_state.input_state after decode() (encoded→int ids). Not dereferenced{src: url, ...} survives.
  4. Map-over is encoded as a Batch, not a plain collection ref: {"__class__":"Batch","values":[{src: hdca|dce, id: N}, …],"linked": bool}. Model BatchRequest parameters.py:132-137. Semantics meta.py:348-372 (expand_meta_parameters_async): linked: true → MATCHED (zip / normal map-over); linked: false → MULTIPLIED (cross-product). Persisted shape: test_tool_execute.py:172-173.
  5. ICJ ↔ ToolRequest is 1:1 by construction. One ToolRequest per mapped execution; all constituent jobs share Job.tool_request_id (same object reused execute.py:256-257; ICJ created once :615; assoc :772-776). Rerun makes a separate job not added to the original ICJ. Multiple distinct tool_request_id on one ICJ only via corruption / manual SQL. A legitimately-built ICJ is never mixed-era.
  6. History Graph already treats >1 distinct tool_request_id for an item as ambiguous (debug + skip producer edge, history_graph.py:301-367). Mirror that rule, do not invent a new one.
  7. Convergence point: History Graph _extract_inputs walks {src,id} leaves with boltons.iterutils.remap — it already descends into Batch values, so graph edge identity resolves through Batch today. The extractor reuses that ref-walk for wiring; the only net-new piece is the request_internal → workflow_step_linked parameter conversion.

Settled decisions (this conversation)

The atomic piece

A new lib/galaxy/tool_util/parameters/convert.py sibling (working name to_workflow_step_state / workflowify): request_internalworkflow_step_linked, using the existing visit_input_values visitor.

Current state to build on

Reuse as-is:

FileReuse
lib/galaxy/workflow/extract.py (#22706)extract_steps_by_ids skeleton, _finalize_workflow, _original_hda/_hdca, IdKey/IdAssociations, ICJ branch (outputs via ICJ.output_dataset_collection_instances)
lib/galaxy/managers/history_graph.py_extract_inputs {src,id} remap idiom — lift/share, do not re-implement
lib/galaxy/tool_util/parameters/visitor.pyvisit_input_values — the converter’s traversal
lib/galaxy/tool_util/parameters/state.pyRequestInternalToolState (read ToolRequest.request into it), workflow_step_linked ToolState
lib/galaxy/model/__init__.pyImplicitCollectionJobs.representative_job (#22706), Job.tool_request

Rewrite:

FileScope
lib/galaxy/workflow/extract.pystep_inputs_by_id → dispatch: structured branch (ToolRequest.request → converter + ref-walk) vs legacy branch (current body, untouched, quarantined)
lib/galaxy/tool_util/parameters/convert.pyadd the converter
lib/galaxy/tool_util/parameters/__init__.pyexport it
lib/galaxy/webapps/galaxy/services/workflows.pysurface linked:falseRequestParameterInvalidException; thread the config flag
lib/galaxy/config schema + config/galaxy.yml.sampleworkflow_extraction_fallback_to_legacy_state: true + deprecation comment

Add:

FileScope
model helperImplicitCollectionJobs.tool_request (derive via representative job; encode the 1:1-by-construction + >1-ambiguous trichotomy in one place)
fallback telemetrywarning + counter when legacy path taken (history_id + step count)

Delete: nothing now. Legacy path is quarantined, deleted only at end-of-deprecation.

Files to touch (checklist)

lib/galaxy/tool_util/parameters/convert.py / __init__.py

lib/galaxy/managers/history_graph.py (or a shared util)

lib/galaxy/model/__init__.py

lib/galaxy/workflow/extract.py

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

lib/galaxy/config + config/galaxy.yml.sample

Tests — see red-to-green.

Red-to-green test order

Per project convention (tests first, then make green).

  1. Commit 1 — RED fidelity matrix. Covered for the current slice. lib/galaxy_test/api/test_workflow_extraction.py now has ToolRequest-backed API coverage for repeat data state (cat1), scalar user-tool state (gx_boolean_user), multiple-data user-tool state (gx_data_multiple_user), collection extraction state (collection_paired_test_y, gx_data_collection_sample_sheet_y), multi-input matched Batch (cat1), and list:paired / subcollection map-over (collection_paired_structured_like). Data/scalar/multiple-data cases invoke and assert output. Collection cases assert extracted workflow state only because workflow invocation currently projects YAML/GalaxyTool collection inputs differently than direct ToolRequest execution. Do not assert a re-run ToolRequest: workflow invocation jobs do not create ToolRequest rows today.
  2. Commit 2 — converter unit + spec. Complete for the current slice. Converter implemented with focused unit tests, workflow_step_linked was confirmed already wired in the spec runner, and parameter_specification.yml now covers representative linked states for multiple data, optional multiple data, repeat data, and sample-sheet collection parameters. test_parameter_specification.py and the JSON-schema mirror are green.
  3. Commit 3 — structured dispatch. Core implementation complete. The full lib/galaxy_test/api/test_workflow_extraction.py API file passes with 48 passed / 1 skipped, including existing basic/mapped ID extraction, structured fidelity API tests for repeat data, scalar user-tool, multiple-data user-tool, collection extraction states, multi-input matched Batch, and list:paired / subcollection map-over. step_inputs_by_id dispatch + shared ref-walk + ImplicitCollectionJobs.tool_request. Legacy path unchanged for no-tool-request jobs.
  4. Commit 4 — fallback flag + telemetry. Implementation and focused tests complete. Config key, warning/counter, the false-flips-to-hard-error path, and the structured-conversion-does-not-fallback invariant are covered.
  5. Commit 5 — {src:url} → annotated input step. Implementation, focused unit test, and by-IDs API coverage complete.
  6. Commit 6 — ambiguous >1 tool_request_id. Helper behavior and focused unit test complete; mirrors History Graph by returning no ToolRequest and warning.

Run after each: ./run_tests.sh -api lib/galaxy_test/api/test_workflow_extraction.py and (commits 2) pytest test/unit/tool_util/test_parameter_specification.py. Full workflow extraction API file is green for the current slice. E2E sweep (run YAML tool → Extract → run extracted) stays folded into lib/galaxy_test/selenium/test_custom_tools.py once the separate collection runtime projection gap is addressed.

Out of scope (do not pull in)

Resolved questions

Unresolved questions

References (in-repo)