Convert one nf-core module directory into a Galaxy tool wrapper. Input is a path to modules/nf-core/<name>/ (or any directory of the same shape: main.nf + meta.yml + environment.yml + optional tests/). Output is a self-contained tool dir: tool.xml, macros.xml, _provenance.yml, with <test> blocks pinned to remote nf-core/test-datasets URLs.
The Mold authors a Galaxy tool XML wrapper directly from the nf-core module shape. Recurring conventions (tuple(meta, path) channels, the task.ext.args escape hatch, versions emits, and environment.yml package pins) provide useful evidence, but they do not make the translation mechanical. It does not depend on summarize-nextflow — that Mold summarizes whole pipelines, the wrong granularity for one module.
The Mold is run per module by an outer harness (a script or human loop). Cross-module batches are not its concern.
The Mold expects:
- A path to the module directory (
modules/nf-core/<name>/, or any local clone of that shape).
- Optional module pin: tag, branch, or commit SHA of
nf-core/modules. When absent, the cast skill resolves to git rev-parse HEAD of the dir’s containing repo.
- Optional test-datasets pin: SHA of
nf-core/test-datasets to use for <test> block location URLs. When absent, the cast skill resolves to a recent SHA on the module’s pipeline-of-record branch (best-effort; recorded in _provenance.yml either way).
The Mold does not accept “convert a subworkflow” — meta.yml with a populated components: field is out of scope (composes other modules; route to a separate subworkflow Mold not in this plan).
Outputs
Three files in a sibling output directory the harness specifies:
<output_dir>/
tool.xml # primary wrapper (harness may specify another filename)
macros.xml # tool-local macros (token, requirements, version_command, citations)
_provenance.yml # nfcore source SHA, file hashes, mold revision, generated_at
tool.xml shape (skeleton; idiomatic IUC layout):
<tool id="<name>" name="<name>" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="23.1">
<description><!-- meta.yml description, first sentence --></description>
<macros>
<import>macros.xml</import>
</macros>
<expand macro="requirements"/>
<expand macro="version_command"/>
<command detect_errors="exit_code"><![CDATA[
<!-- script: body translated, $task.ext.args → $extra_args -->
]]></command>
<inputs>
<!-- per nfcore-channel-input-to-galaxy-collection + nfcore-meta-map-to-galaxy-params -->
<param name="extra_args" type="text" optional="true" .../> <!-- per task.ext.args pattern -->
</inputs>
<outputs>
<!-- one Galaxy <data> / <collection> per output: channel, minus the versions channel -->
</outputs>
<tests>
<test>
<param name="reads_1" location="https://raw.githubusercontent.com/nf-core/test-datasets/<sha>/.../test_1.fastq.gz"/>
<output name="trimmed" location="https://raw.githubusercontent.com/nf-core/test-datasets/<sha>/.../trimmed.fastq.gz"
checksum="sha256$..."/>
</test>
</tests>
<help><!-- meta.yml description --></help>
<expand macro="citations"/>
</tool>
_provenance.yml shape (canonical):
nfcore_source:
modules_repo: nf-core/modules
module_path: modules/nf-core/<name>
branch: master
git_sha: <sha at conversion time>
meta_yml_hash: <sha256 of meta.yml at conversion>
main_nf_hash: <sha256 of main.nf at conversion>
environment_yml_hash: <sha256 of environment.yml at conversion>
test_datasets_sha: <sha of nf-core/test-datasets pin>
generated:
by_mold: convert-nfcore-module-to-galaxy-tool
mold_revision: <copy _provenance.json.mold.revision>
cast_target: <copy _provenance.json.cast_target>
cast_artifact_sha: <copy harness cast_bundle_sha256; null if unavailable>
on_date: <conversion date, YYYY-MM-DD>
overrides: []
Procedure
The cast skill is an agent procedure, not a parser or code generator. No implementation exists that can reliably tokenize arbitrary Groovy/Nextflow and emit a Galaxy wrapper. The agent must interpret the module as a whole, reconcile meta.yml with main.nf, and make contextual decisions about command behavior, input and output cardinality, conditional interfaces, test fixtures, and Galaxy conventions.
Use ordinary tools for operations they can settle exactly: calculate file hashes, resolve supplied or repository SHAs, read simple YAML values, copy unambiguous dependency pins or DOIs, and run validation commands. These mechanical operations support the translation; they do not make the translation itself deterministic. Record ambiguity and unsupported syntax in _provenance.yml.overrides instead of presenting an inference as parser output.
1. Read the module
Open meta.yml, main.nf, environment.yml, and tests/main.nf.test (when present). Reject early if meta.yml.components: is populated (subworkflow composing other modules; out of scope — see Non-goals).
Compute sha256 of each file and capture for _provenance.yml.
2. Build <requirements>
Walk environment.yml.dependencies:. Each bioconda::<name>=<version> becomes a Galaxy <requirement type="package" version="<version>"><name></requirement> entry. For mulled / multi-package environments, declare every package — bioconda’s mulled-resolution produces an equivalent image (per component-nextflow-containers-and-envs).
Record any forced divergence from upstream container choice in _provenance.yml.overrides.
Per nfcore-channel-input-to-galaxy-collection, decide the Galaxy input shape from the process’s input channel cardinality. Per nfcore-meta-map-to-galaxy-params, triage meta-map keys into Galaxy params, conditionals, or drops.
Emit a final extra_args text param per nfcore-task-ext-args-to-galaxy-additional-options if (and only if) the script body interpolates ${task.ext.args} / args2 / args3.
4. Translate <outputs>
For each output: channel that isn’t the versions emit, decide cardinality first, then shape (per galaxy-discover-datasets §Convert Mold posture). The Nextflow glob alone is not enough — path('*.bam') (N files, one per element of an upstream collection) and path('*.{bai,csi,crai}') (exactly one file, alternation across mutually-exclusive extensions) look the same but map to different Galaxy idioms.
Choose every format value from nextflow-path-glob-to-galaxy-datatype. When that guide does not settle the value, when an alternation contains differently supported extensions, or when Planemo rejects a candidate, consult galaxy-datatypes-conf and its datatypes_conf.xml.sample companion. Never emit an extension merely because it appears in a filename. If no suitable registered datatype exists, use format="data" for an XML <data> output or collection element; for an input <param type="data">, omitting format is also legal and avoids a false constraint. Record the original extension, the registry lookup, and the generic fallback in _provenance.yml.overrides so the loss of datatype specificity is visible.
- Single output, deterministic name (
path("${prefix}.json")) → <data name="..." format="json" from_work_dir="${prefix}.json"/>. No <discover_datasets>.
- Single output, variable extension (alternation glob like
path("*.{bai,csi,crai}"), or path("${prefix}.${ext}") where ext is computed): the channel emits one file whose extension depends on inputs or args. Map to a <data> with the most-common registered extension as format=, plus a <change_format> block that flips on the input ext or the responsible param. If the variants do not all have registered Galaxy datatypes, the generic data fallback above is acceptable; add <change_format> rules only for registered variants when they materially improve downstream behavior. Preserve the upstream invocation byte-for-byte and capture the result with a tight mv — ln -s '$input' 'input.${input.ext}' to stage with the upstream-expected name, run the tool exactly as the nf-core script: body does, then select the one concrete output path using the same input/parameter conditions and move it to '$output_name'. Do not use brace expansion such as mv 'input.${input.ext}'.{bai,csi,crai} '$output_name': the shell supplies three source operands even when only one file exists, so mv requires the Galaxy output path to be a directory. Do not use <collection> + <discover_datasets> for this shape — there is no list. Direct write to '$output_name' (instead of mv) is the secondary form, used only when the upstream script: body itself passes an output-path arg to the tool. See galaxy-discover-datasets §Convert Mold posture Rule 2 for the conditional-move example, including the explicit-output variant and the from_work_dir callout.
- Multi-output, list cardinality (true glob like
path('*.bam') where the upstream process emits N files keyed by element identifier) → <collection type="list" name="..." format="bam"> with <discover_datasets pattern="__name_and_ext__" visible="true"/>.
- Multi-output, paired cardinality (
tuple val(meta), path("*_R{1,2}.fastp.fastq.gz")) → <collection type="paired" ...> with a custom (?P<name>...)_R(?P<identifier_1>[12])... regex.
versions channel → drop; the <version_command> carries that load (per nfcore-versions-emit-to-galaxy-version-command).
Cardinality heuristic: if the upstream input: channel is tuple(meta, path) (one item per process invocation) and output: emits one path per concept, the output is single — even when the path is glob-shaped. Process cardinality = output cardinality unless the script explicitly fans out.
5. Translate script: to <command>
LLM step. Pass:
- The verbatim
script: body.
- The Galaxy
<inputs> and <outputs> already chosen.
- The
task.ext.args mapping (text param → $extra_args).
Ask only for the Cheetah-flavored Galaxy command. Wrap in <![CDATA[...]]>. Set detect_errors="exit_code" unless a comment in the original script: argues otherwise.
Command whitespace: after rendering Cheetah, Galaxy trims each line and replaces newlines with spaces (implementation). Do not copy Nextflow’s end-of-line shell continuation backslashes into <command>: a backslash followed by a newline becomes a backslash followed by a space, changing argument parsing. A single invocation can span plain argument lines without continuations; keep Cheetah #if, #for, and #set directives on their own lines. Separate distinct shell commands explicitly with && when the next depends on success, or ; when unconditional sequencing is intended. This does not prohibit Cheetah escapes such as \${GALAXY_SLOTS:-1}.
For example, staging and invocation remain distinct after whitespace normalization:
ln -s '$input' staged_input &&
seqkit stats
--tabular
--threads \${GALAXY_SLOTS:-1}
$extra_args
staged_input
> '$stats'
6. Emit <version_command>
Per nfcore-versions-emit-to-galaxy-version-command: extract the primary tool’s version-emit line from the heredoc or topic: versions annotation, strip Nextflow escaping (\$( → $( etc.), and wrap in <![CDATA[...]]>.
7. Emit <citations> and <help>
<citations> are DOIs from meta.yml.tools[].doi (one <citation type="doi">…</citation> per tool). <help> is the meta.yml.description (humanized one-liner; expanded into a paragraph if meta.yml has a longer prose block).
8. Emit <test> blocks (remote URLs)
Read tests/main.nf.test. For each test that asserts a successful run with a non-trivial fixture:
- Resolve every input fixture path to a
raw.githubusercontent.com/nf-core/test-datasets/<test_datasets_sha>/... URL. Pin <test_datasets_sha> upfront — never use a branch ref.
- Emit Galaxy
<param ... location="https://..."/> for inputs.
- For outputs, prefer
<output name="..." location="https://..." checksum="sha256$..."/> when expected output bytes are available; compute SHA-256 from those bytes, not the nf-test snapshot JSON. An upstream snapshot’s MD5 can instead be used as <output name="..." md5="..."/> without an expected-output file (Galaxy test attributes). Reuse a hash only when the Galaxy output should be byte-identical; renamed input files can change embedded filenames. Otherwise use meaningful content assertions and record the verification divergence in _provenance.yml.overrides.
Account for every eligible upstream test by name: emit its Galaxy test, or record its omission and reason in _provenance.yml.overrides and report coverage as incomplete. Do not silently drop distinct input shapes or parameter settings. The at-least-one-test shipment requirement below is a minimum, not a coverage-completion criterion.
When tests/main.nf.test has no usable fixture (stub-only coverage, missing test file), the convert Mold does not ship a placeholder <test>. It surfaces the gap in _provenance.yml.overrides and exits with a non-zero status; the harness escalates to human review. Every shipped wrapper carries at least one <test> block backed by a real fixture (per nfcore-stub-block-to-galaxy-noop-test and the reviewer Mold’s dimension #6).
9. Emit _provenance.yml
Collect: nf-core module source (repo, path, branch, git_sha), file hashes, test-datasets pin, mold metadata, any overrides (forced divergence from upstream container, dropped stub block, hand-edits the cast skill chose to apply).
Read the cast bundle’s sibling _provenance.json when filling generated. Copy mold.revision to mold_revision and its top-level cast_target to cast_target. cast_target identifies the bundle adapter that produced this skill; it is not the provider or model executing the conversion. Put execution provider/model details in the harness run record, not in _provenance.yml.
Copy the harness runtime metadata’s cast_bundle_sha256 into cast_artifact_sha. It identifies the frozen bundle actually loaded, including packaged references and cast provenance; _provenance.json.mold.content_hash hashes only the authored index.md, not the generated bundle, and must not be substituted. The Pi harness checks equality against its run record. Without supplied bundle metadata, use YAML null and record the unavailable bundle identity in overrides rather than guessing.
10. Convergence loop: lint, test, fix
Iterate until clean. Both gates exit on structured signals — never free-text grep.
10.1 Lint
planemo lint <output_dir> (see planemo-lint). Treat by exit code:
- Exit 0 — proceed to 10.2.
- Non-zero — read the diagnostic block. XSD failures are hard: fix the XML and re-emit. Soft findings (missing
<help>, missing <citations>) are fixed in place by the cast skill, then loop.
10.2 Test
planemo test <output_dir> --test_output_json <output_dir>/_planemo_test_report.json (see planemo-test). Always pass --test_output_json; planemo’s stdout is for humans and intentionally not part of the cast skill’s parsing surface.
After the run:
- Validate. AJV-check the JSON against planemo-test-report (
validate-planemo-test-report CLI from @galaxy-foundry/planemo-test-report-schema). A schema-invalid report means the pinned planemo SHA drifted or the run aborted before writing the report — escalate to human triage; do not classify.
- Classify from schema fields, not free-text.
tests[].data.job is dict[str, Any] (extra-allow) — its inner keys come from the Galaxy job state and are not constrained by planemo-test-report, so treat them as best-effort signals:
tests[].data.status == "success" → pass.
tests[].data.status == "failure" + data.problem_log matches an output-discovery pattern (<discover_datasets> mismatch, missing dataset name, format mismatch) → adjust the corresponding <output> / <discover_datasets> block.
tests[].data.status == "failure", and data.job.stderr (when present) carries upstream tool stderr → inspect data.job.command_line when available before revising the <command> Cheetah translation; check for backslash-space arguments and missing shell separators as well as tool options.
tests[].data.status == "error" with HTTP/URL signals → fixture-availability fault; verify the nf-core/test-datasets URL resolves and consider a local test-data/ fallback, recording the divergence in _provenance.yml.overrides.
- Any other failure shape → human triage.
- Stop when lint and test both clear and every eligible upstream test is either covered or explicitly recorded as omitted. Passing emitted tests does not resolve recorded coverage gaps; report them as incomplete coverage.
The convergence loop is bounded (default 3 attempts). On exhaustion, the cast skill writes whatever it has and surfaces the final _planemo_test_report.json (plus the lint diagnostics) for human triage.
Non-goals
- Subworkflow conversion.
meta.yml.components: populated → out of scope. Routed to a separate Mold (not in this plan).
- Pipeline conversion. Whole nf-core pipelines stay with nextflow-to-galaxy; this Mold runs at the module tier.
- Discovery / dedup against IUC. The new repo coexists with IUC by design (different interface contract for the same upstream tool).
- Cross-module refactoring. Per-module unit; harness owns batches.
- Tool Shed publication. The output is a tool dir on disk;
.shed.yml and shed publication live in the new repo’s CI, not in this Mold.
Caveats
meta.yml may lie. Hand-authored, can drift from script: IO. When the LLM-inferred IO disagrees with meta.yml, prefer meta.yml and surface the disagreement in _provenance.yml.overrides.
- Container directive ternaries require pulling both branches; the bioconda pin from
environment.yml is the source of truth for <requirements>. Don’t substitute a hand-picked container source.
task.ext.args with embedded Groovy logic can’t be cleanly mapped to a single text param. Surface as a text param plus a heavy <help> block; document the divergence in _provenance.yml.overrides.
- Stub-only tests. When
tests/main.nf.test has only stub-mode coverage (-stub-run), the convert Mold cannot derive a Galaxy <test> from it (per nfcore-stub-block-to-galaxy-noop-test). Surface the gap; let the harness decide whether to author a <test> by hand.
Reference dispatch (for casting)
research → the 5 nf-core→Galaxy translation notes (nfcore-channel-input-to-galaxy-collection, nfcore-meta-map-to-galaxy-params, nfcore-task-ext-args-to-galaxy-additional-options, nfcore-versions-emit-to-galaxy-version-command, nfcore-stub-block-to-galaxy-noop-test) plus nextflow-path-glob-to-galaxy-datatype, galaxy-datatypes-conf, component-nf-core-tools, component-nextflow-containers-and-envs, and galaxy-discover-datasets. All are copied verbatim into the cast bundle under references/notes/, loaded per each ref’s used_at/load; the raw datatype registry travels as the on-demand companion references/notes/datatypes_conf.xml.sample.
cli-tool → planemo carries the pinned install metadata; flows into the cast bundle’s _required_tools.json via the PR #235 mechanism.
cli-command → planemo-lint and planemo-test cast to JSON sidecars; consulted on-demand inside the §10 loop.
schema → planemo-test-report copied verbatim into the cast bundle; the convergence loop AJV-validates --test_output_json output against it before classifying failures.
examples — pending: 3 hand-picked Wave 1 modules (one trivial, one paired-aware, one with conditional). Used for round-trip smoke testing before this Mold ships.