Version Compatibility
Reference examples tested with: MultiQC 1.21+ (Plotly era), FastQC 0.12+, STAR 2.7.11+, Subread 2.0+, salmon 1.10+, samtools 1.19+, Picard 3.1+, fastp 0.23+
Before using code patterns, verify installed versions match. If versions differ:
- CLI:
<tool> --version then <tool> --help to confirm flags
Config keys and defaults move between MultiQC releases (the plotting backend changed from HighCharts to Plotly at 1.20; flat-plot and AI thresholds shifted). When a default matters, confirm it against the installed version: python -c "import multiqc; print(multiqc.version)" and check that version's config_defaults.yaml.
If code throws an error, run multiqc --help and adapt flags to the installed version rather than retrying.
Automated QC Reports with MultiQC
"Aggregate QC results into one report" -> Walk a directory of tool outputs, parse the metrics those tools already wrote, and render one interactive HTML report plus a parseable multiqc_data/ directory.
- CLI:
multiqc <dir> (scans for recognized tool outputs)
The Load-Bearing Idea: MultiQC Aggregates, It Does Not Measure
MultiQC computes nothing. It SCRAPES the log/metrics files that FastQC, STAR, Picard, salmon, bcftools, etc. already wrote, re-tabulates those numbers, and renders them. Every value in a report traces back to an upstream tool's output file. Four consequences that drive every real decision below:
- The report is a triage SNAPSHOT, not a pass/fail gate. There is no "MultiQC said FAIL -> stop the pipeline." MultiQC has no fail-on-threshold and exits 0 on bad QC. Green/amber/red is either the upstream tool's own status (FastQC writes PASS/WARN/FAIL; MultiQC just displays it) or a threshold a human configured. Gating is a SEPARATE step (see From Report to Gate).
- Garbage upstream becomes a clean-looking report. Run a tool with the wrong strandedness, wrong reference, or dedup on amplicon data, and MultiQC faithfully aggregates the wrong numbers into a polished HTML. Polish is not evidence of correctness.
- An empty report means "nothing matched," not "QC passed." Point MultiQC at the wrong directory or over-filter modules and it emits a near-empty report with a log warning and exit 0. Always check the sample count in the header against the roster expected.
- It is only as current as its parsers. Each module is a hand-written parser keyed to a specific log format. An upstream version bump that changes a header can silently drop a file or mis-map a column.
Basic Usage
multiqc results/ -o qc_report/ # scan results/, write qc_report/multiqc_report.html
multiqc results/ -n project_qc -o qc/ # custom report name
multiqc results/ -m fastqc -m star # ONLY these modules (see scoping below)
multiqc results/ -c multiqc_config.yaml # reproducible config-driven report
Supported Tools
MultiQC ships parsers for 100+ tools. Common assay groupings:
| Stage |
Tools with modules |
| Read QC |
FastQC, fastp, Cutadapt, falco |
| Alignment |
STAR, HISAT2, BWA, Bowtie2, samtools, Qualimap, Picard |
| Quantification |
featureCounts, Salmon, kallisto, RSeQC |
| Variant calling |
bcftools, GATK, Picard, SnpEff, VEP |
| Single-cell |
Cell Ranger, STARsolo |
Module Detection Is Regex - Scope It
Detection runs off searchpatterns.yaml: each module declares a filename glob/regex (fn/fnre) and/or a file-content match (contents/contentsre, bounded by numlines). Loose patterns (.txt, .log, *.json) in a messy directory cause FALSE module matches and PHANTOM samples - a file that is not really that tool's output gets parsed as one. A single file can also satisfy two modules.
Scope explicitly rather than trusting auto-detection across thousands of samples:
multiqc results/ --ignore "*_tmp/" --ignore "work/" # drop paths from the search
multiqc results/ -m fastqc -m star -m salmon # run ONLY named modules
multiqc results/ -e snippy -e custom_content # run all EXCEPT named modules
Tighten an over-loose pattern by overriding sp: in the config (sp: {mytool: {fn: 'realname*.txt'}}). Production configs pin sp: and module_order instead of relying on detection.
Sample Names Are Derived, Not Declared
Sample names are NOT read from a manifest. MultiQC derives each name from the matched filename (or a sample column inside the file), then "cleans" it by trimming a ~100-entry default list of extensions (fncleanexts: .gz, .fastq, .bam, fastqc, ...). This is how sampleAR1.fastq.gz, sampleA.sorted.bam, and sampleA.salmon/ all collapse to one sampleA row gathering read, alignment, and quant metrics.
The same mechanism is the #1 large-cohort bug:
- Merge - two genuinely different inputs clean to the same name (two lanes both reduce to
sampleA) and silently overwrite each other's metrics.
- Split - one sample appears as several rows because different tools cleaned its name differently (one kept
_L001, another stripped it).
multiqcdata/multiqcsources.txt maps every parsed file to the sample name it produced - read it first when diagnosing duplicate/missing rows. Controls:
| Need |
Control |
| Add suffixes to strip (keep defaults) |
extrafncleanexts: in config (do NOT override fnclean_exts, which replaces the defaults) |
| Use the log filename as the name |
--fnassname (config usefilenameassample_name) |
| Disambiguate by directory |
--dirs / -d, --dirs-depth N |
| Keep full names, no cleaning |
--fullnames / -s |
| Rename at report time |
--replace-names map.tsv (pattern -> replacement, two columns) |
| Offer toggleable name sets |
--sample-names headered.tsv (relabel buttons, does not merge rows) |
General Statistics and Conditional Formatting Are Configured, Not Authoritative
The General Statistics table is one row per sample with columns each module contributes. Cell colors come from tablecondformattingrules (numeric gt/lt/eq/ge/le, string seq/scontains/sne). A red ">10% duplication" cell is red because someone wrote that rule (or because a module ships a built-in default rule), not because biology says 10% is bad. Treat formatting as a configured convenience; absence of red is not a pass, and presence of red is not a biological verdict. Column visibility/order/naming are config too (tablecolumnsvisible, tablecolumnsplacement, tablecolumnsname).
Large Cohorts: MultiQC Downgrades Automatically
To keep the single HTML openable, MultiQC silently changes rendering as series counts grow. The exact thresholds have moved across versions - verify against the installed config_defaults.yaml - but the behaviors are:
| Behavior |
Config key |
Effect |
| Table -> violin/beeswarm plot |
maxtablerows (~500) |
above the limit the General Stats "table" becomes a distribution plot; per-cell view is lost |
| Interactive plot deferred |
plotsdeferloading_numseries (~100) |
viewer must click to render |
| Interactive -> flat image |
plotsflatnumseries (moved across versions; HighCharts-era 100, current default much higher) |
plots render as static PNG/SVG |
Force a mode for reproducible visuals across cohort sizes: --flat / --interactive (config plotsforceflat / plotsforceinteractive). At tens of thousands of samples, also scope with -m/--ignore or split into per-batch reports - MultiQC holds all parsed data in memory before rendering.
Custom Content (Injecting Custom Metrics)
Two mechanisms; --custom-data-file does NOT exist.
mqc suffix - any file named *mqc.{tsv,csv,txt,yaml,json,png,...} is auto-discovered and rendered with no config. The suffix is what makes it findable.
customdata in the config - define a section with plottype (bargraph, linegraph, table, generalstats, image, ...) and inline data or a search pattern. plot_type: generalstats injects columns straight into General Statistics.
From Report to Gate (the decision MultiQC does not make)
MultiQC is a viewer; QC GATING is separate. The machine-readable truth lives in multiqcdata/: multiqcdata.json (all parsed values), per-module multiqc*.txt tables, and multiqcgeneral_stats.txt. Build a gate ON TOP of that file, not by scraping the HTML:
multiqc results/ -o qc/ --data-format json # write multiqc_data.json
# a downstream script parses qc/multiqc_data/multiqc_data.json,
# applies thresholds, and exits non-zero / quarantines failing samples.
This is the correct division of labor: MultiQC presents; the pipeline (nf-core modules, a purpose-built gater like CheckQC, a Nextflow/Snakemake check, or a parse-and-exit script) decides. Building fail-on-threshold logic inside MultiQC is a category error.
AI Summaries Send Data Off-Network
MultiQC (1.27+) can prepend an LLM-written natural-language summary (--ai / --ai-summary, --ai-summary-full; providers via aiprovider: seqera, openai, anthropic, awsbedrock, custom; keys via OPENAIAPIKEY / ANTHROPICAPIKEY / SEQERAACCESSTOKEN). It is OFF by default. When enabled it transmits the aggregated QC metrics - and, unless aianonymizesamples is set, the SAMPLE NAMES - to an external API over the internet. For clinical, patient, or embargoed data this can be a data-governance violation; use --no-ai to strip AI controls from a shared report, or the in-browser on-demand mode (summary stays in browser local storage, not baked into the distributed HTML). Confirm the exact key spelling against the installed version.
Reproducibility
- Pin both the MultiQC version and the upstream tool versions. Parsers, default thresholds, and column sets shift between releases; a report from 1.30 is not byte-stable against one from 1.14.
- Pin a config, treat the report as a deliverable. The nf-core pattern: ship
multiqcconfig.yml locking title, moduleorder, sample-name cleaning, and sp: patterns; expose --multiqcconfig to layer a user config on top (both apply, user wins). nf-core also emits a methodsdescription_template.yml so the report carries auto-generated methods text and citations for only the tools that ran, plus a consolidated software-versions table.
- Cross-check the roster. If 3 of 100 BAMs failed earlier and produced no metrics, MultiQC reports a clean 97-sample report with no indication 3 are missing. It aggregates what exists and has no notion of an expected sample set.
Common Errors
| Symptom |
Cause |
Fix |
| Near-empty report, exit 0 |
No files matched a search pattern (wrong dir, over-filtered) |
Check header sample count; read multiqc_sources.txt; relax -m/--ignore |
| Two samples merged into one row |
Names collide after fncleanexts cleaning |
extrafncleanexts, --dirs, or --replace-names; verify in multiqcsources.txt |
| One sample split across rows |
Tools cleaned the name differently |
--fnassname or extrafncleanexts to normalize |
| Phantom sample / wrong module |
Loose pattern matched an unrelated file |
--ignore the path or tighten sp:; restrict with -m |
| Metric missing after a tool upgrade |
Upstream log-format drift broke the parser |
Pin tool + MultiQC versions; check the module changelog |
| "table" rendered as a violin plot |
Rows exceeded maxtablerows |
Raise the limit or split the cohort |
| Sensitive sample names left the network |
AI summary enabled |
--no-ai, or aianonymizesamples; default is off |
Related Skills
- read-qc/quality-reports - Generate the FastQC/falco inputs MultiQC aggregates
- read-qc/fastp-workflow - Preprocessing QC that feeds the report
- read-qc/rnaseq-qc - Post-alignment RNA-seq metrics surfaced in MultiQC
- workflows/rnaseq-to-de - Full pipeline that emits a MultiQC report as a deliverable
References
- Ewels P, Magnusson M, Lundin S, Käller M. MultiQC: summarize analysis results for multiple tools and samples in a single report. Bioinformatics. 2016;32(19):3047-3048. doi:10.1093/bioinformatics/btw354
- MultiQC documentation: docs.seqera.io/multiqc (post-2024 features incl. Plotly plots and AI summaries are documented here and in the GitHub CHANGELOG, not a separate paper)