oxo-flow-viralrecon — Viral assembly and intrahost variant calling for Illumina and Oxford Nanopore amplicon data
★ Verified · ⇄ Official port of nf-core/viralrecon @ 3.0.0 — same tools, same versions, same commands. Part of the oxo-flow-community catalog.
This workflow turns paired-end Illumina amplicon reads (or Oxford Nanopore
barcode reads — see the nanopore section below) into a complete viral
genomics report: read QC and trimming (FastQC, fastp), host-sequence removal
(Kraken2), alignment to a user-provided reference genome (Bowtie2), primer
trimming (iVar), intrahost variant calling and annotation (iVar → snpEff /
SnpSift), consensus building with low-coverage masking (bcftools), lineage
assignment and deconvolution (Pangolin, Nextclade, Freyja), de novo assembly
with QC (Cutadapt → SPAdes / Unicycler / minia → Bandage / BLAST / QUAST /
ABACAS / plasmidID — any comma-separated combination), and a single
MultiQC report tying everything together. The repository ships a tiny
end-to-end fixture (2 samples, 1000 read pairs each, a 6 kb SARS-CoV-2-like
reference, and stub Kraken2/Pangolin/Freyja databases) so the workflow can be
exercised without downloading anything; point the [config] keys at your own
data to use it for real.
Installation
1. Install oxo-flow
Requires oxo-flow >= 0.17.0 (the fastp empty-reads gates use the when
runtime functions shipped in 0.17.0).
Release binary (recommended):
curl -fL -o oxo-flow.tar.gz https://github.com/Traitome/oxo-flow/releases/latest/download/oxo-flow-latest-x86_64-unknown-linux-gnu.tar.gz
tar xzf oxo-flow.tar.gz && sudo mv oxo-flow /usr/local/bin/
Alternative via conda: conda install -c bioconda oxo-flow-cli (note: the
conda package may lag behind releases; other platform binaries are on the
releases page).
2. Get this workflow
git clone https://github.com/oxo-flow-community/oxo-flow-viralrecon.git
cd oxo-flow-viralrecon
3. Requirements
(a) Reference data — the workflow takes every reference as a local file;
the files shipped in reference/ are test fixtures, so replace them with real
data before running:
- reference genome FASTA (
config.fasta, default reference/genome.fa) and
annotation GFF (config.gff, default reference/genome.gff) — uncompressed,
or set fasta_ends_gz / gff_ends_gz to run the gunzip steps first
- primer scheme BED (
config.primer_bed, default reference/primers.bed) for
the amplicon protocol
- Kraken2 host-removal database as a tar.gz (
config.kraken2_db, default
reference/kraken2_db.tar.gz)
- Pangolin data directory (
config.pango_database) and Freyja barcodes /
lineages CSVs (config.freyja_barcodes / config.freyja_lineages)
- Nextclade dataset: downloaded automatically (
--name sars-cov-2 --tag 2024-10-17--16-48-48Z), or set config.nextclade_dataset to a local dataset
directory to skip the download
- paired-end Illumina FASTQs at
<raw_dir>/<sample>_R1.fastq.gz and
<raw_dir>/<sample>_R2.fastq.gz (config.raw_dir), or — with
platform = "nanopore" — ONT reads as <fastq_dir>/<barcode>XX/reads.fastq.gz
(config.fastq_dir) with sample→barcode mappings in config.samples_list
(see the nanopore section)
(b) Compute — resource labels map 1:1 to the upstream withLabel profiles
(process_single 1c/6 GB, process_low 2c/12 GB, process_medium 6c/36 GB,
process_high 12c/72 GB); the heaviest rules need up to 12 CPUs / 72 GB per
rule. The oxo-flow resource pool queues work rather than oversubscribing.
(c) Tool delivery — conda environments with pinned versions
(envs/*.yaml, conda-forge + bioconda channels, exact pins from the upstream
module environment files). No containers. You need conda or mamba installed;
rules create and reuse the pinned environments from envs/.
Usage
# validate, lint and dry-run the default configuration
./test/run.sh
# or by hand:
oxo-flow validate main.oxoflow
oxo-flow dry-run main.oxoflow --samples first:1
oxo-flow run main.oxoflow --samples first:1
Inputs
Upstream reads samples from a CSV samplesheet
(sample,fastq_1,fastq_2,platform,protocol); oxo-flow discovers samples from
the filesystem instead. Place paired-end FASTQ files at
<raw_dir>/<sample>_R1.fastq.gz and <raw_dir>/<sample>_R2.fastq.gz and set
raw_dir (default test/fixtures/raw, containing the fixture samples S1,
S2). Samples are listed in [[sample_groups]].
Upstream can download its reference, primer scheme, Kraken2 database,
Pangolin data and Freyja barcodes over the network (profiles/genomes.config
URLs, KRAKEN2_BUILD, PANGOLIN_UPDATEDATA, FREYJA_UPDATE). The port has no
network steps: all such inputs are files in the repository, with fixtures
provided. Reference files must be uncompressed; set fasta_ends_gz,
gff_ends_gz or primer_bed_ends_gz to true to run the equivalent of the
upstream GUNZIP_* steps first.
Configuration
Upstream params.* are [config] keys with identical defaults:
| key | default | upstream param |
|---|
fasta, gff, primer_bed | reference/genome.fa etc. | --fasta, --gff, --primer_bed |
kraken2_db | reference/kraken2_db.tar.gz | --kraken2_db |
kraken2_variants_host_filter | false | --kraken2_variants_host_filter |
kraken2_assembly_host_filter | true | --kraken2_assembly_host_filter |
nextclade_dataset_name / _tag | sars-cov-2 / 2024-10-17--16-48-48Z | --nextclade_dataset_name/_tag (MN908947.3 genome config) |
pango_database | test/fixtures/refs/pangolin_db | --pango_database |
freyja_barcodes / freyja_lineages | fixture CSV / JSON files | --freyja_barcodes / --freyja_lineages (freyja boot parses the lineages file as the curated_lineages JSON, so it must stay JSON — the fixture CSV sibling is barcodes-side only) |
freyja_repeats / freyja_depthcutoff | 100 / 0 | --freyja_repeats / --freyja_depthcutoff |
raw_dir / out_dir | test/fixtures/raw / results | samplesheet / --outdir |
platform / protocol | illumina / amplicon | --platform / --protocol |
variant_caller / consensus_caller | ivar / bcftools | --variant_caller / --consensus_caller |
min_mapped_reads |
Outputs
Everything lands under out_dir/results (or the path given by out_dir),
mirroring the upstream publishDir layout:
results/
├── fastqc/{raw,trim}/ FastQC reports (raw + fastp-trimmed)
├── fastp/ fastp JSON/HTML/log per sample
├── kraken2/ Kraken2 reports + host-filtered reads
├── variants/bowtie2/ BAMs, logs, picard_metrics, mosdepth/{genome,amplicon}
├── variants/freyja/{variants,demix,bootstrap}/
├── variants/ivar/ variant TSV/VCF, snpeff/, bcftools_stats/
├── variants/ivar/consensus/bcftools/ filtered VCF, consensus FASTA, quast.consensus,
│ pangolin/, nextclade/, base_qc/
├── variants/ivar/variants_long_table.csv
├── assembly/cutadapt/ primer-trimmed reads, adapters.sub.fa, fastqc/
├── assembly/spades/rnaviral/ scaffolds/contigs/gfa (gzipped), bandage/,
│ blastn/, quast.spades/, abacas/, plasmidid/
├── assembly/unicycler/ scaffolds/gfa (gzipped), bandage/, blastn/,
│ quast.unicycler/, abacas/, plasmidid/
├── assembly/minia/ contigs/unitigs/h5, blastn/, quast.minia/,
│ abacas/, plasmidid/
└── multiqc/ multiqc_report.html, multiqc_data/,
variants_metrics_mqc.csv, assembly_metrics_mqc.csv
Source
- Upstream:
nf-core/viralrecon @
tag 3.0.0 (commit
395079f1d24dce731ac22e03d7a5e71f110103fc)
- Upstream license: MIT (preserved verbatim at
LICENSE.upstream)
- Created 2026-08-15; this workflow may lag behind upstream releases.
- Attribution details: NOTICE.md
Fidelity
Every upstream process and subworkflow on the default path, and what happened
to it in this port:
| Upstream process (module) | Port rule | Notes |
|---|
| CAT_FASTQ | cat_fastq | cat to fastp/{sample}_{1,2}.fastq.gz |
| FASTQC_RAW | fastqc_raw | same args; upstream input-rename step kept (reads symlinked to {sample}_{1,2}.fastq.gz before FastQC so output names match), then renamed into results/fastqc/raw/ |
| FASTP | fastp | ext.args baked in verbatim (cut_front/cut_tail/trim_poly_x/cut_mean_quality 30/...) + --detect_adapter_for_pe, `2> |
| FASTQC_TRIM | fastqc_trim | same args; upstream input-rename step kept (trimmed reads symlinked to {sample}_{1,2}.fastq.gz before FastQC), then renamed into results/fastqc/trim/ |
| KRAKEN2_KRAKEN2 | kraken2 | --db (local), --report-zero-counts, pigz of classified/unclassified pairs; gated on skip_kraken2 + the fastp empty-reads drop (reads_count(...) > 0 when-gate) |
| (channel wiring) | assembly_fastq | passthrough of fastp reads to kraken2/{sample}.unclassified_*.fastq.gz when host filtering is off — replaces upstream ch_assembly_fastq = ch_variants_fastq; see deviations |
| BOWTIE2_ALIGN | align_bowtie2 | index found by find -L on *.rev.1.bt2[l], --local --very-sensitive-local --seed 1, unmapped-filtered samtools view -F4, log tee'd; carries the fastp empty-reads drop when-gate |
| IVAR_TRIM | ivar_trim | -m 30 -q 20 -e (noprimer-gated), optional -x offset, log captured; gated amplicon |
| BAM_SORT_STATS_SAMTOOLS | bam_sort_index_trimmed | merged: samtools cat (single input, dropped) → sort → index → stats/flagstat/idxstats |
| (align branch) | bam_sort_index | same merged trio for the untrimmed BAM |
| PICARD_MARKDUPLICATES | markduplicates / |
Ported branches (all gated off by default, mirroring the upstream
params defaults; the default run is byte-for-byte the amplicon ivar path):
Nanopore platform (platform = "nanopore")
The upstream ARTIC chain is ported as gated rules activated with
--arg platform=nanopore. Reads are discovered per-barcode from
config.fastq_dir (barcode*/reads.fastq.gz); the sample→barcode mapping
comes from config.samples_list (comma-separated, in samplesheet order —
fixture: S1,S2 with barcode1/barcode2), replacing the upstream CSV
samplesheet barcode column. test/fixtures/samplesheet_nanopore.tsv holds
the fixture mapping and test/fixtures/raw_nanopore/barcode{1,2}/reads.fastq.gz
the fixture reads (200 ONT-style SE reads per barcode, clearing
min_barcode_reads = 100). test/run.sh flips the platform and asserts the
nanopore rule set (TDD branch-flip).
| Upstream process (module) | Port rule | Notes |
|---|
| BARCODE_QC (with channel code) | barcode_qc_manifest | reads {config.fastq_dir}/barcode*/ like upstream's barcode channel discovery; fail TSVs (no sample / no barcode / under min_barcode_reads / under min_guppyplex_reads) written only on failure; envs/coreutils.yaml |
| PYCOQC | pycoqc | gated on sequencing_summary (upstream --summary input); envs/pycoqc.yaml (pycoqc 2.5.2) |
| ARTIC_GUPPYPLEX | artic_guppyplex | --min-length 400 --max-length 700 --max-depth --prefix (primer_set_version != 1200 window); envs/artic.yaml (artic 1.6.2 + htslib 1.17, upstream pin) |
| KRAKEN2_NANOPORE | kraken2_nanopore | same KRAKEN2 shell, single-end flagset, unclassified-pairs passthrough to the nanopore bam branch |
| NANOPLOT | nanoplot | --N4 --info --dotplot --heatmap; envs/nanoplot.yaml (1.46.1) |
| ARTIC_MINION | artic_minion | --threads --scheme-version, --skip-ringcheck (amplicon), -x offset when set; envs/artic.yaml |
| FILTER_BAM_SAMTOOLS (nanopore) | filter_bam_samtools_nanopore | merged: samtools view -q 20 → sort → index → flagstat |
| MOSDEPTH_GENOME (nanopore) | mosdepth_genome_nanopore | --fast-mode --by 200; emits the real {prefix}.mosdepth.global.dist.txt names |
| MOSDEPTH_AMPLICON (nanopore) | mosdepth_amplicon_nanopore | --fast-mode --use-median --thresholds ... --by collapsed.bed + all-samples heatmap TSV |
| FREYJA_VARIANTS (nanopore) | freyja_variants_nanopore | same shell on the artic VCF |
| VCFLIB_VCFUNIQ | vcfuniq_nanopore | vcfuniq + gzip (upstream vcflib module); envs/vcflib.yaml (1.0.14) |
| VCF_BGZIP_TABIX_STATS | bcftools_stats_nanopore | merged bgzip + tabix + on the artic VCF |
variant_caller='bcftools' — VARIANTS_BCFTOOLS (call_variants_bcftools,
norm_vcf_bcftools), BCFTOOLS_FILTER (consensus_filter_bcftools) and the
bcftools long table (variants_long_table_bcftools); activates with
--arg variant_caller=bcftools, deactivating the ivar caller chain
consensus_caller='ivar' — CONSENSUS_IVAR (consensus_ivar) with
--arg consensus_caller=ivar
- non-amplicon (shotgun / "wgs") protocol —
--arg protocol=metagenomic;
the untrimmed-BAM counterparts mosdepth_genome_wgs, picard_metrics_wgs,
freyja_variants_wgs, consensus_call_wgs, markduplicates_wgs,
call_variants_bcftools_wgs, consensus_ivar_wgs; upstream derives
variant_caller='bcftools' for non-amplicon runs (nextflow.config), and the
port mirrors that derivation in the wgs rules' when conditions — a
metagenomic run needs no extra --arg, the bcftools caller chain
(call_variants_bcftools_wgs, norm_vcf_bcftools,
consensus_filter_bcftools, variants_long_table_bcftools) runs
automatically and the iVar caller chain stays amplicon-gated
- alternative assemblers —
--arg assemblers=unicycler /
--arg assemblers=minia or any comma-separated combination
(--arg assemblers=spades,unicycler runs both in one run, like upstream)
with their full QC chains (Bandage for spades and unicycler, matching
upstream; plasmidID per assembler like upstream ASSEMBLY_QC)
- PICARD_MARKDUPLICATES —
--arg skip_markduplicates=false
- PLASMIDID —
--arg skip_plasmidid=false (no database download; verified
upstream uses only the reference fasta)
- network downloads —
kraken2_build (leave kraken2_db empty),
freyja_update (leave freyja_barcodes/freyja_lineages empty),
pangolin_updatedata (leave pango_database empty)
- ADDITIONAL_ANNOTATION —
--arg additional_annotation=path/to.gff
Still excluded (see metadata.json): the guppy_basecaller/guppy_barcoder
commercial ONT GPU tools (reads must be basecalled before the workflow;
structural) and the remaining runtime-filter DROPS — the min_mapped_reads
flagstat gate and the zero-variant-sample filters (their reporting half is
ported inside the multiqc rule — see deviations).
Documented deviations
Everything below has no oxo-flow equivalent and is the closest faithful
approximation; none silently change results:
config.assemblers is a comma-separated list in canonical lowercase
form (commas, no spaces). The upstream params.assemblers accepts any
comma-separated combination — e.g. spades,unicycler runs SPAdes AND
Unicycler in the same run — trimming and lowercasing each entry. oxo-flow
when conditions have no in/contains operator, so the port enumerates
every combination of the three assemblers with explicit equality tests:
each assembler family (assemble_* plus its Bandage/BLAST/QUAST/ABACAS/
plasmidID QC chain) is gated on a disjunction of the four combinations
that contain it. Spelling the list with spaces (spades, unicycler) is
not accepted — upstream's trim is not reproducible without an in
operator. (Negative equality gates were rejected as incorrect:
!= 'spades' would wrongly enable minia for
assemblers = 'spades,unicycler'.)
- Runtime-filter DROPS are partially ported (engine 0.17.0
when
runtime functions). The upstream fastp filter (drop samples with 0
reads after trimming, wrapped in if (!params.skip_fastp)) IS ported:
the consumers of the trimmed reads (kraken2, align_bowtie2,
assembly_fastq) carry a
!config.skip_fastp && reads_count('fastp/{sample}_1.fastp.fastq.gz') > 0
gate, matching upstream's per-sample channel drop with the same
short-circuit. The min_mapped_reads flagstat gate (a strict > on the
mapped count parsed out of samtools flagstat text) and the
zero-variant-sample filters still run in Nextflow channel code with no
engine equivalent — a sample cannot be dropped mid-DAG from a flagstat
regex or a bcftools stats record count. The reporting half IS ported:
the multiqc rule regenerates upstream's custom-content TSVs
(fail_mapped_reads_mqc.tsv from the fastp JSONs, fail_mapped_samples_mqc.tsv
from the Bowtie2 flagstats, same headers/rows as multiqcTsvFromList),
written only when samples fail. min_mapped_reads config feeds that
flagstat comparison. Placeholder artifacts (: > {sample}.scaffolds.fa
etc.) stand in where upstream would drop an empty assembly from the
channel; zero-variant samples keep flowing downstream with a
placeholder-header VCF (see ivar_to_vcf).
- MarkDuplicates does not replace
ch_bam. Upstream
BAM_MARKDUPLICATES_PICARD swaps ch_bam so mosdepth, picard metrics and
variant calling all consume the marked BAM. The port publishes the marked
BAM (bam/bai/stats/flagstat/idxstats/metrics) as standalone rules
(, ) while the pipeline keeps the
pre-dedup BAM — the upstream module itself forbids same-name in/out, and a
canonical-path swap is impossible in oxo-flow (the rule would read and
write the same path).
Resources
Resource labels map 1:1 to upstream withLabel profiles: process_single
(1c/6 GB/4 h), process_low (2c/12 GB/4 h), process_medium
(6c/36 GB/8 h), process_high (12c/72 GB/16 h). Fastp/SPAdes memory and
-Xmx JVM sizes are derived from the same values as upstream.
Test
bash test/run.sh
Runs oxo-flow validate + lint + dry-run against the default
configuration (the repo's fixture samples; nothing is downloaded or
executed), then flips platform = "nanopore" and asserts the nanopore rule
set appears in the plan (and the illumina-only chains do not).
License
This workflow is Apache-2.0 (see LICENSE and
NOTICE). It is a port of nf-core/viralrecon, which is MIT
licensed — the upstream license is preserved verbatim at
LICENSE.upstream.