# How to Structure a Snakemake RNA-seq Workflow

By the end of this guide you will have a working, modular Snakemake RNA-seq workflow that takes paired-end FASTQ files through quality control, splice-aware alignment, gene-level counting, and differential expression, and you will know how to run it locally or on a Slurm cluster, add conda-pinned software environments, and debug the errors that trip up most first attempts. The structure shown here is the same shape used by published, peer-reviewed pipelines such as RNA-Seq-Pop, an end-to-end Snakemake workflow for [RNA sequencing](/blog/guides/rna-sequencing) data that covers quality control, differential expression, and variant calling [1], and bollito, a modular single-cell RNA-seq pipeline built on Snakemake that integrates more than 30 tools [2]. That convergence matters because a good structure generalizes across assay types, from bulk RNA-seq to long-read [transcriptomics](/knowledge/bioinformatics/modern-transcriptomics-bulk-single-cell-spatial) [3] and RNA proximity ligation assays [4], [5].

What you need on hand before starting:

- Conda or Miniforge installed (Miniforge recommended, since it ships with mamba, which solves environments faster)
- Snakemake 8 or newer, installed in its own conda environment
- A Unix-like shell (Linux, macOS, or Windows Subsystem for Linux)
- At least 8 CPU cores and roughly 32 GB of RAM for the alignment step with STAR on a mammalian genome
- A small set of paired-end FASTQ files, real or test data, and a reference genome FASTA with a GTF or GFF3 annotation file

## Why structure matters more than tools

A bioinformatics workflow manager solves three problems that hand-typed shell scripts cannot: it tracks which files depend on which, it reruns only what changed, and it records enough provenance that a colleague can reproduce your result months later. Snakemake does this with a Python-based domain-specific language where each processing step is a rule that maps inputs to outputs. The pattern is validated across the field. RNA-Seq-Pop uses it to chain quality control, differential expression, and variant calling into a single reproducible workflow applicable to any organism [1]. The RNAcontacts pipeline uses it to process RNA proximity ligation data with a two-pass alignment strategy, processing multiple transcriptomic datasets uniformly [4], [5]. A haplotype-resolved proteogenomics pipeline built in Snakemake coordinates variant calling, transcript discovery, and mass spectrometry search across paired long-read and protein data [3]. And SVNeoPP, a structural-variant neoantigen workflow, credits its Snakemake implementation for modular extension, checkpoint-based restarts, and end-to-end reproducibility [6].

The common thread in all of these is modularity. bollito's authors make the point directly: a modular design makes it easy to incorporate other packages and expand the pipeline with new functionality [2]. Structure is what buys you that modularity. A monolithic 800-line Snakefile cannot be extended. A directory of small rule files can.

## Snakemake RNA-seq workflow structure: the directory layout

The structure format below is the de facto community template. Every path has a job, and the separation between code (workflow), configuration (config), static data (resources), and generated results (results, logs) is what makes the workflow reusable across projects. Here is the layout you will build:

```
rnaseq-project/
├── Snakefile                  # main entry point, defines the target rule
├── config/
│   ├── config.yaml            # paths, parameters, reference files
│   └── samples.csv            # sample sheet, one row per sample
├── workflow/
│   ├── rules/
│   │   ├── qc.smk             # trimming and read-level QC
│   │   ├── align.smk          # index building and alignment
│   │   ├── count.smk          # feature counting and matrix assembly
│   │   └── de.smk             # differential expression
│   ├── scripts/
│   │   └── deseq2.R           # R analysis script, injected with snakemake object
│   └── envs/
│       ├── fastp.yaml         # pinned conda environment definitions
│       ├── star.yaml
│       ├── subread.yaml
│       ├── qc.yaml
│       └── deseq2.yaml
├── resources/                 # reference genome, annotation (static, not generated)
│   └── genome/
├── results/                   # everything the workflow produces
├── logs/                      # per-rule, per-sample log files
└── .profiles/slurm/           # cluster execution profile (optional)
```

Three principles hold this together. First, anything you would edit to run on a new dataset lives in config, never in rule code. Second, every generated file lives under results or logs with a path that fully encodes its provenance, such as results/qc/fastp/ctrl_rep1.html. Third, no rule ever writes into its own input space, which prevents the circular dependency errors described later.

## Step 1: Install Snakemake and scaffold the project

![Four step flow for setting up a Snakemake RNA-seq project: conda environment, directory tree, version check, then config files.](/article-images/illustrations/snakemake-rna-seq-workflow-structure-project-scaffold.webp)

Create a dedicated environment so Snakemake's dependencies never collide with your analysis software, then create the directory tree:

```bash
conda create -n snakemake -c conda-forge -c bioconda snakemake-minimal
conda activate snakemake

mkdir -p rnaseq-project/{config,workflow/rules,workflow/scripts,workflow/envs,resources/genome,results,logs,.profiles/slurm}
cd rnaseq-project
```

Verify the installation:

```bash
snakemake --version
```

Expected output:

```
8.14.0
```

Any Snakemake 8.x version works for everything in this guide. Snakemake 8 split cluster executors into plugins, which is why the Slurm section later installs one explicitly.

## Step 2: Write the sample sheet and config file

The sample sheet is the single source of truth for which samples exist and where their raw reads are. A CSV works well and loads directly into pandas:

```csv
sample_id,condition,fastq_1,fastq_2
ctrl_rep1,control,data/ctrl_rep1_R1.fastq.gz,data/ctrl_rep1_R2.fastq.gz
ctrl_rep2,control,data/ctrl_rep2_R1.fastq.gz,data/ctrl_rep2_R2.fastq.gz
ctrl_rep3,control,data/ctrl_rep3_R1.fastq.gz,data/ctrl_rep3_R2.fastq.gz
treated_rep1,treated,data/treated_rep1_R1.fastq.gz,data/treated_rep1_R2.fastq.gz
treated_rep2,treated,data/treated_rep2_R1.fastq.gz,data/treated_rep2_R2.fastq.gz
treated_rep3,treated,data/treated_rep3_R1.fastq.gz,data/treated_rep3_R2.fastq.gz
```

The config file holds everything else a new user might need to change:

```yaml
# config/config.yaml
samples: "config/samples.csv"
genome: "resources/genome/genome.fa"
annotation: "resources/genome/genes.gtf"
star_index: "resources/star_index"
read_length: 100
design: "~ condition"
contrast: ["condition", "treated", "control"]
```

The design and contrast keys are passed straight into DESeq2, so switching from a two-group comparison to a paired design is a config edit, not a code edit. This config-driven approach mirrors how RNA-Seq-Pop separates organism-specific reference data from the reusable workflow logic [1].

## Step 3: Build the main Snakefile

The main Snakefile parses the sample sheet, includes the rule modules, and defines the final targets. Snakemake runs jobs only if some file in the directed acyclic graph (DAG, the dependency graph of all jobs) requires them, and rule all defines the terminal nodes:

```python
# Snakefile
import pandas as pd

configfile: "config/config.yaml"

samples = pd.read_csv(config["samples"], dtype=str).set_index("sample_id")
SAMPLES = samples.index.tolist()

include: "workflow/rules/qc.smk"
include: "workflow/rules/align.smk"
include: "workflow/rules/count.smk"
include: "workflow/rules/de.smk"

rule all:
    input:
        "results/multiqc/multiqc_report.html",
        "results/counts/counts_matrix.csv",
        "results/de/deseq2_results.csv"
```

Running `snakemake` with no target runs rule all, and Snakemake works backward from those three files to construct the full job graph.

## Step 4: Quality control rules

Create workflow/rules/qc.smk with three rules. fastp performs adapter trimming and per-base quality filtering in a single pass, writing both cleaned reads and machine-readable QC reports. fastqc then profiles the trimmed reads, and multiqc aggregates every report into one page.

```python
# workflow/rules/qc.smk

rule fastp_pe:
    input:
        r1=lambda wc: samples.loc[wc.sample, "fastq_1"],
        r2=lambda wc: samples.loc[wc.sample, "fastq_2"]
    output:
        r1="results/qc/fastp/{sample}_R1.trimmed.fastq.gz",
        r2="results/qc/fastp/{sample}_R2.trimmed.fastq.gz",
        html="results/qc/fastp/{sample}.html",
        json="results/qc/fastp/{sample}.json"
    log:
        "logs/fastp/{sample}.log"
    threads: 8
    conda:
        "../envs/fastp.yaml"
    shell:
        "fastp -i {input.r1} -I {input.r2} "
        "-o {output.r1} -O {output.r2} "
        "-h {output.html} -j {output.json} "
        "-w {threads} > {log} 2>&1"

rule fastqc_trimmed:
    input:
        "results/qc/fastp/{sample}_{read}.trimmed.fastq.gz"
    output:
        "results/qc/fastqc/{sample}_{read}.trimmed_fastqc.html"
    log:
        "logs/fastqc/{sample}_{read}.log"
    threads: 2
    conda:
        "../envs/qc.yaml"
    shell:
        "fastqc -o results/qc/fastqc -t {threads} {input} > {log} 2>&1"

rule multiqc:
    input:
        fastp=expand("results/qc/fastp/{sample}.json", sample=SAMPLES),
        fastqc=expand("results/qc/fastqc/{sample}_{read}.trimmed_fastqc.html",
                      sample=SAMPLES, read=["R1", "R2"])
    output:
        html="results/multiqc/multiqc_report.html",
        stats="results/multiqc/multiqc_data/multiqc_general_stats.txt"
    log:
        "logs/multiqc.log"
    conda:
        "../envs/qc.yaml"
    shell:
        "multiqc results/qc -o results/multiqc -f > {log} 2>&1"
```

Three structural details deserve attention because they separate maintainable workflows from fragile ones. The lambda functions pull file paths from the sample sheet, so adding a sample means adding one CSV row, not editing code. Every rule declares a log file, so failures leave a per-sample trace instead of scrolling off your terminal. And the conda paths use `../envs/` because Snakemake resolves conda directives relative to the Snakefile declaring them, which is workflow/rules/qc.smk here.

The corresponding pinned environment, workflow/envs/fastp.yaml:

```yaml
channels:
  - conda-forge
  - bioconda
dependencies:
  - fastp =0.23.2
```

And workflow/envs/qc.yaml:

```yaml
channels:
  - conda-forge
  - bioconda
dependencies:
  - fastqc =0.12.1
  - multiqc =1.21
```

Pinning exact versions with the equals sign is what makes the workflow reproducible months later. Unpinned environments silently drift as Bioconda publishes updates.

## Step 5: Index building and alignment rules

![Calc card showing sjdbOverhang set to read length minus one and genomeSAindexNbases of 13 for a 200 to 300 megabase genome.](/article-images/illustrations/snakemake-rna-seq-workflow-structure-star-index-params.webp)

Create workflow/rules/align.smk. STAR builds a genome index once and then aligns each sample against it. The sjdbOverhang parameter should be one less than your read length, and genomeSAindexNbases follows the STAR recommendation of taking the minimum of 14 and log2 of the genome size divided by 2, minus 1. For genomes in the 200 to 300 megabase range, a value of 13 fits that formula.

```python
# workflow/rules/align.smk

rule star_index:
    input:
        genome=config["genome"],
        gtf=config["annotation"]
    output:
        touch("resources/star_index/.index_done"),
        sa="resources/star_index/sa",
        genome="resources/star_index/Genome"
    params:
        sa_index="13"
    log:
        "logs/star_index.log"
    threads: 8
    conda:
        "../envs/star.yaml"
    shell:
        "mkdir -p resources/star_index && "
        "STAR --runMode genomeGenerate --runThreadN {threads} "
        "--genomeDir resources/star_index "
        "--genomeFastaFiles {input.genome} "
        "--sjdbGTFfile {input.gtf} "
        "--sjdbOverhang {config[read_length] - 1} "
        "--genomeSAindexNbases {params.sa_index} > {log} 2>&1"

rule star_align:
    input:
        r1="results/qc/fastp/{sample}_R1.trimmed.fastq.gz",
        r2="results/qc/fastp/{sample}_R2.trimmed.fastq.gz",
        sa="resources/star_index/sa",
        genome="resources/star_index/Genome"
    output:
        bam="results/aligned/{sample}/Aligned.sortedByCoord.out.bam",
        counts="results/aligned/{sample}/ReadsPerGene.out.tab",
        log_final="results/aligned/{sample}/Log.final.out"
    params:
        prefix="results/aligned/{sample}/"
    log:
        "logs/star/{sample}.log"
    threads: 8
    conda:
        "../envs/star.yaml"
    shell:
        "mkdir -p results/aligned/{wildcards.sample} && "
        "STAR --genomeDir resources/star_index "
        "--readFilesIn {input.r1} {input.r2} "
        "--readFilesCommand zcat "
        "--outSAMtype BAM SortedByCoordinate "
        "--quantMode GeneCounts "
        "--runThreadN {threads} "
        "--outFileNamePrefix {params.prefix} > {log} 2>&1"
```

Note that the align rule lists index files among its inputs. That is the dependency edge that makes Snakemake build the index automatically if it is missing, and rebuild the index before realigning if the reference FASTA changes. This is exactly the behavior you want when a collaborator swaps in a new genome build.

## Step 6: Counting and matrix assembly rules

Create workflow/rules/count.smk. featureCounts from the subread package assigns reads to exons grouped by gene_id, handling paired-end reads with the -p flag and paired-read counting with, countReadPairs (subread 2.x syntax):

```python
# workflow/rules/count.smk
from pathlib import Path

rule featurecounts:
    input:
        bam="results/aligned/{sample}/Aligned.sortedByCoord.out.bam",
        annotation=config["annotation"]
    output:
        "results/counts/{sample}.featureCounts.txt"
    log:
        "logs/featurecounts/{sample}.log"
    threads: 8
    conda:
        "../envs/subread.yaml"
    params:
        extra="-p --countReadPairs -t exon -g gene_id"
    shell:
        "featureCounts -T {threads} -a {input.annotation} "
        "-o {output} {params.extra} {input.bam} > {log} 2>&1"

rule aggregate_counts:
    input:
        expand("results/counts/{sample}.featureCounts.txt", sample=SAMPLES)
    output:
        "results/counts/counts_matrix.csv"
    log:
        "logs/aggregate_counts.log"
    run:
        frames = []
        for f in input:
            name = Path(f).name.replace(".featureCounts.txt", "")
            table = pd.read_csv(f, sep="\t", comment="#", index_col=0)
            frames.append(table.iloc[:, -1].rename(name))
        matrix = pd.concat(frames, axis=1)
        matrix.to_csv(output)
        print(f"Wrote matrix with {matrix.shape} genes x {matrix.shape[1]} samples")
```

The aggregate rule is the first place where plain Python runs inside the workflow rather than a shell command. The expected output on six samples of a well-annotated genome looks like this in the log:

```
Wrote matrix with 14512 genes x 6 samples
```

## Step 7: Differential expression with the script directive

For the DE step, hand the job to R using the script directive, which injects a snakemake object into the R session so the script can read inputs and write outputs without hardcoded paths. Create workflow/rules/de.smk:

```python
# workflow/rules/de.smk

rule deseq2:
    input:
        counts="results/counts/counts_matrix.csv",
        samples=config["samples"]
    output:
        "results/de/deseq2_results.csv"
    log:
        "logs/deseq2.log"
    conda:
        "../envs/deseq2.yaml"
    script:
        "../scripts/deseq2.R"
```

Then workflow/scripts/deseq2.R:

```r
suppressPackageStartupMessages(library(DESeq2))

counts <- read.csv(snakemake@input[["counts"]], row.names = 1)
samples <- read.csv(snakemake@input[["samples"]], stringsAsFactors = FALSE)
samples <- samples[match(colnames(counts), samples$sample_id), ]
rownames(samples) <- samples$sample_id

dds <- DESeqDataSetFromMatrix(
    countData = round(counts),
    colData   = samples,
    design    = as.formula(snakemake@config[["design"]])
)
dds <- DESeq(dds)
contrast <- snakemake@config[["contrast"]]
res <- results(dds, contrast = contrast)
write.csv(as.data.frame(res), snakemake@output[[1]])
```

The design formula comes from config, so a time-course or paired experiment changes one YAML line. The corresponding environment, workflow/envs/deseq2.yaml, needs r-base, bioconductor-deseq2, and their channels, pinned the same way as the tool environments above.

## Step 8: Run the workflow, first dry, then wet

Always run a dry run first. The -n flag builds the DAG and reports every job it would run without executing anything:

```bash
snakemake -n --use-conda
```

Expected output:

```
Building DAG of jobs...
Job stats:
job               count
all                   1
aggregate_counts      1
deseq2                1
fastp_pe              6
fastqc_trimmed       12
featurecounts         6
multiqc               1
star_align            6
star_index            1
total                35
```

That job table is your structural audit. If a number looks wrong, for example 12 fastp jobs when you have 6 samples, the sample sheet has a duplicate or a parse error, and you caught it before wasting an hour of compute. When the dry run is clean, execute:

```bash
snakemake --cores 8 --use-conda
```

The first run spends several minutes creating each conda environment, cached under .snakemake/conda for all future runs. A completed run ends with:

```
Finished job 0.
100% (35 of 35) steps processed
35 of 35 steps (100%) done
Complete log: .snakemake/log/2026-11-11T143021.312757.snakemake.log
```

On a second run after touching nothing, Snakemake reports that nothing needs doing, because every output is newer than its inputs. Edit only deseq2.R and rerun, and Snakemake reruns the DE step while leaving the alignment untouched. Selective reruns of this kind are the core reason to use a workflow manager rather than a shell script.

## Core directives at a glance

The table below summarizes the Snakemake directives that carry the structural weight in an RNA-seq workflow:

| Directive | Purpose | Example from this workflow |
|--|--|--|
| input | Files the rule needs, resolved before the job runs | `r1=lambda wc: samples.loc[wc.sample, "fastq_1"]` |
| output | Files the rule must produce, triggers downstream jobs | `"results/counts/{sample}.featureCounts.txt"` |
| wildcards | Placeholder values matched from the requested output | `{sample}`, `{read}` in QC rules |
| threads | Parallelism, scaled automatically on cluster schedulers | `threads: 8` on alignment |
| params | Non-file parameters, supports wildcard substitution | `prefix="results/aligned/{sample}/"` |
| log | Per-job log file, enables targeted debugging | `"logs/fastp/{sample}.log"` |
| conda | Pinned software environment for the rule | `"../envs/fastp.yaml"` |
| script | External R or Python script injected with snakemake object | `"../scripts/deseq2.R"` |
| run | Inline Python executed inside the workflow | matrix assembly in count.smk |
| touch | Creates a zero-byte marker so a rule runs once | `.index_done` in star_index |
| expand | Builds a list of filenames across wildcards | `expand("...{sample}...", sample=SAMPLES)` |

## Environments, containers, and execution profiles

Snakemake offers three escalating levels of software isolation, and a robust structure uses at least the first. The, use-conda flag materializes each rule's declared environment on first use, which is how the workflows above guarantee that fastp, STAR, and featureCounts exist at pinned versions regardless of what else is on the machine. For stronger isolation, add, software-deployment-method apptainer (or the, containerize route to generate a container image), which runs each job inside a container built from the same environment definition. At the strongest level, each rule can declare a Docker or Apptainer image directly with the container directive.

Local execution uses, cores to cap total parallel threads across jobs. Cluster execution uses a profile, a directory of default flags applied to every invocation. Create .profiles/slurm/config.yaml for a Slurm system:

```yaml
executor: slurm
jobs: 20
use-conda: true
software-deployment-method: apptainer
latency-wait: 60
restart-times: 3
```

With Snakemake 8, install the executor plugin first:

```bash
pip install snakemake-executor-plugin-slurm
```

Then run from any directory inside the project and Snakemake submits each rule as a scheduler job, requesting the thread count each rule declares:

```bash
snakemake --profile .profiles/slurm
```

The latency-wait setting matters on shared filesystems. Network file systems sometimes report a freshly written output file as missing for a few seconds after job completion, and without latency-wait Snakemake can mistakenly declare a job's output missing and resubmit it. restart-times adds automatic resubmission for jobs that die from transient cluster faults, a common necessity on heavily used schedulers.

## Checkpoints for data-dependent branching

Most RNA-seq workflows have a known job graph, but some analyses only reveal their outputs at runtime. A classic example is an assembly or a per-sample peak/feature discovery step where you cannot know the output file names until the job finishes. Snakemake's checkpoint directive handles this by re-evaluating downstream rules after the checkpoint completes. SVNeoPP explicitly relies on this mechanism for restarts in its multi-omics neoantigen workflow [6]. A minimal pattern:

```python
checkpoint discover_transcripts:
    input:
        "results/aligned/{sample}/Aligned.sortedByCoord.out.bam"
    output:
        directory("results/assembly/{sample}")
    shell:
        "mkdir -p results/assembly/{wildcards.sample} && "
        "some_assembler --out results/assembly/{wildcards.sample} {input}"

rule collect_assemblies:
    input:
        lambda wc: expand(
            "results/assembly/{sample}/transcripts.fa",
            sample=glob_wildcards(os.path.join(
                "results/assembly", "{sample}")).sample
        )
    output:
        "results/assembly/all_transcripts.fa"
```

Use checkpoints sparingly. For the standard bulk RNA-seq chain shown in this guide, plain rules are simpler and easier to debug, and the published Snakemake pipelines cited earlier achieve end-to-end reproducibility with conventional rules plus careful environment pinning [1], [3], [2].

## Checking that the result is right

Structure makes the workflow run. Validation makes the results trustworthy, and it belongs in the structure as explicit outputs. Three checks belong in every RNA-seq run:

1. Read-level QC. Open results/multiqc/multiqc_report.html and confirm per-sample adapter content fell after trimming, per-base quality is acceptable across the read, and duplication is not extreme in every sample at once, which often indicates a library problem rather than a biological signal.
2. Alignment metrics. STAR writes Log.final.out per sample. On a well-annotated reference, uniquely mapped read percentages should sit comfortably high and be broadly similar across samples from the same protocol. One sample lagging far behind the others is a sample-quality flag, not a workflow flag. MultiQC parses these STAR logs automatically into its report.
3. Count matrix sanity. The aggregate log line showing gene and sample dimensions should match your annotation and sample sheet. In DESeq2, a principal component plot of variance-stabilized counts should show replicates grouping by condition. Replicates clustering by batch instead of condition is an experimental design finding to address, not a bug to patch in code.

When you need to choose between candidate tools or parameters at any step, benchmark against curated data rather than intuition. The Dex-Benchmark project provides curated RNA-seq datasets and code templates specifically for objectively comparing transcriptomics analysis tools [7], and its dexamethasone treatment data can serve as a portable test fixture for a new workflow before your own samples arrive.

A further check concerns reference choice itself. Reanalysis work on fungal pathogen transcriptomes has shown that inconsistent use of reference genomes and tools complicates cross-study comparisons, and that standardizing the reference and pipeline restores near-perfect correlation with published results even across annotation versions [8]. Record the exact genome FASTA and annotation file, and their versions, in your config and output reports, because the workflow structure alone does not capture which reference you fed it.

## Nextflow vs Snakemake, and other bioinformatics workflow managers

Snakemake and Nextflow are the two dominant open-source workflow managers in genomics, and both solve the same core problem well: dependency tracking, parallel execution, resumable runs, conda and container support, and cluster or cloud backends. The practical differences are of language and ecosystem. Snakemake rules are written in a Python-based DSL, so the learning curve is gentle for labs already working in Python, and inline Python for steps like matrix assembly feels native. Nextflow uses a Groovy-based DSL built around asynchronous channels that pass data between processes, which some teams find more natural for highly dynamic, streaming-style fan-out, and it has a very large ready-made ecosystem of community-maintained pipelines in the nf-core collection. Both are excellent choices. If your team is Python-first or you want rule-level conda pinning as shown above, Snakemake is the comfortable fit. If you want to adopt or extend a large existing community pipeline collection, Nextflow's ecosystem weight is the draw.

Alternatives deserve mention where they fit the team. systemPipeR provides workflow construction and automated report generation inside the R and Bioconductor environment, handling both R and command-line tools with a flexible sample annotation infrastructure for complex experimental designs [9], which suits labs whose entire analysis stack lives in R. Galaxy-based platforms handle provenance through their own execution framework and are increasingly integrated with reproducible analysis patterns, including recent work combining public data repositories, reproducible workflows, and AI-assisted tool invocation under strict provenance tracking [8]. The structure principles in this guide, config separated from code, one rule per step, pinned environments, explicit logs and validation outputs, transfer to any of these systems.

The breadth of Snakemake in the literature supports the structure taught here. The same architecture underlies bulk population RNA-seq with variant calling [1], RNA proximity ligation contact prediction [4], [5], single-cell analysis with more than 30 integrated tools [2], haplotype-resolved long-read proteogenomics [3], and structural variant neoantigen prediction [6]. Learn the structure once and it carries across assays.

## Common Mistakes and Limitations

- Writing a monolithic Snakefile. Defining all 20 rules in one file works until you try to reuse three of them in another project. Split by pipeline stage into rule files under workflow/rules/ from day one.
- Hardcoding sample names. A rule listing `"results/counts/ctrl_rep1.featureCounts.txt"` as input is a maintenance trap. Derive everything from the sample sheet.
- Skipping the dry run. Running `snakemake, cores 8` before `snakemake -n, use-conda` discovers path typos after STAR has already burned 40 minutes of cluster time. The dry run takes seconds.
- Writing outputs into input directories. If a trimming rule writes cleaned reads next to the raw FASTQ files with a name pattern that overlaps the input pattern, Snakemake raises a cyclic dependency or silently creates output-as-input loops. Always separate raw data, intermediate results, and final results into different directory trees.
- Leaving conda environments unpinned. A workflow that ran correctly six months ago can break after a dependency update. Pin exact versions in every environment file.
- Not declaring log files. When a job fails on a cluster, stdout often vanishes with the job. A log directive per rule keeps an on-disk trace per sample.
- Ignoring the sample-to-reference contract. The workflow guarantees reproducible processing, not correctness of your experimental design. A design formula that does not match the actual experiment, or samples from different genome builds mixed in one count matrix, produces confident-looking but wrong results. Cross-study comparisons additionally require consistent reference genomes and tools, as demonstrated in standardized fungal pathogen reanalysis [8].
- Assuming the structure substitutes for statistics. With too few biological replicates per condition, no workflow structure rescues the analysis. Differential expression tools require adequate replication, and the DESeq2 design must reflect blocking, pairing, or batch structure in the experiment.
- Underestimating index memory. STAR genome generation and alignment are memory-hungry on large genomes. If a cluster job dies with an out-of-memory error, add a resources directive with mem to the align rule so the scheduler allocates enough.
- Overusing checkpoints. Checkpoints add re-evaluation complexity. Use them only when output file names genuinely cannot be known in advance, as SVNeoPP does for restartable multi-omics steps [6], not for every step.

## Frequently Asked Questions

### What does a good Snakemake RNA-seq workflow structure look like?

A good structure separates code, configuration, static resources, results, and logs into distinct directories. The template in this guide uses a main Snakefile with the target rule, small rule files grouped by pipeline stage under workflow/rules/, a sample sheet and YAML config under config/, pinned conda environments under workflow/envs/, and all generated files under results/ and logs/ with provenance-encoding paths.

### Is there a ready-made Snakemake RNA-seq workflow structure template I can reuse?

Yes, the directory layout and rule files in this guide form a complete working template, and published pipelines offer mature starting points. RNA-Seq-Pop provides an end-to-end Snakemake RNA-seq workflow covering quality control, differential expression, and variant calling for any organism [1], and bollito offers a modular single-cell RNA-seq pipeline with more than 30 integrated tools [2]. Study both to see the same structure applied at production scale.

### How does Snakemake decide which jobs to rerun?

Snakemake compares each output against its inputs using rerun triggers that cover file modification times, input changes, parameter changes, code changes in scripts, and software environment changes by default. Run `snakemake -n` to see what would rerun before committing to execution, and adjust `-rerun-triggers` if you need finer control over which changes force recomputation.

### Do I need conda or containers to run a Snakemake RNA-seq workflow?

Conda with the, use-conda flag is the practical minimum for reproducibility because it installs exactly the pinned software each rule declares. Containers add a second layer of isolation and portability through the, software-deployment-method apptainer flag or the container directive. Published Snakemake pipelines rely on this environment pinning for end-to-end reproducibility [6].

### Can I run a Snakemake RNA-seq workflow on an HPC cluster or the cloud?

Yes. For Slurm, install the snakemake-executor-plugin-slurm package and create a profile directory with executor, jobs, and latency-wait settings, then run `snakemake, profile .profiles/slurm`. Each rule is submitted as a scheduler job requesting the threads it declares. Comparable profiles exist for other schedulers and cloud backends.

### Nextflow vs Snakemake, which should I pick for RNA-seq?

Both are mature bioinformatics workflow managers that handle dependency tracking, resumption, containers, and cluster execution well. Choose Snakemake if your team works in Python and you want the rule-based, config-driven structure shown here. Choose Nextflow if you plan to adopt or extend a large existing community pipeline ecosystem, which is where Nextflow's channel-based model has the most ready-made options.

### Can the same Snakemake structure handle small RNA-seq or single-cell data?

Yes, the skeleton is assay-independent. You swap the trimming strategy, aligner, and counting step while keeping the config, rules, envs, results, and logs layout. Small RNA-seq requires specialized adapter and isoform handling, as shown in small RNA pipelines built for poorly annotated species [10], and single-cell RNA-seq pipelines such as bollito demonstrate the same modular structure scaled to more than 30 tools including dimensionality reduction, clustering, and trajectory inference [2].

### How do I test a new Snakemake workflow before running it on real data?

Run `snakemake -n, use-conda` to validate the job graph, then execute the full workflow on one or two tiny test FASTQ files or a subset of real reads. For tool-level validation, benchmark against curated public datasets such as those provided by the Dex-Benchmark project, which supplies RNA-seq data and code templates built specifically for objective tool comparison [7].

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What does a good Snakemake RNA-seq workflow structure look like?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "A good structure separates code, configuration, static resources, results, and logs into distinct directories. The template in this guide uses a main Snakefile with the target rule, small rule files grouped by pipeline stage under workflow/rules, a sample sheet and YAML config under config, pinned conda environments under workflow/envs, and all generated files under results and logs with provenance-encoding paths."
      }
    },
    {
      "@type": "Question",
      "name": "Is there a ready-made Snakemake RNA-seq workflow structure template I can reuse?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes, the directory layout and rule files in this guide form a complete working template, and published pipelines offer mature starting points. RNA-Seq-Pop provides an end-to-end Snakemake RNA-seq workflow covering quality control, differential expression, and variant calling for any organism, and bollito offers a modular single-cell RNA-seq pipeline with more than 30 integrated tools."
      }
    },
    {
      "@type": "Question",
      "name": "How does Snakemake decide which jobs to rerun?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Snakemake compares each output against its inputs using rerun triggers that cover file modification times, input changes, parameter changes, code changes in scripts, and software environment changes by default. Run snakemake -n to see what would rerun before committing to execution, and adjust the rerun-triggers option if you need finer control over which changes force recomputation."
      }
    },
    {
      "@type": "Question",
      "name": "Do I need conda or containers to run a Snakemake RNA-seq workflow?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Conda with the, use-conda flag is the practical minimum for reproducibility because it installs exactly the pinned software each rule declares. Containers add a second layer of isolation and portability through the, software-deployment-method apptainer flag or the container directive. Published Snakemake pipelines rely on this environment pinning for end-to-end reproducibility."
      }
    },
    {
      "@type": "Question",
      "name": "Can I run a Snakemake RNA-seq workflow on an HPC cluster or the cloud?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. For Slurm, install the snakemake-executor-plugin-slurm package and create a profile directory with executor, jobs, and latency-wait settings, then run snakemake with that profile. Each rule is submitted as a scheduler job requesting the threads it declares. Comparable profiles exist for other schedulers and cloud backends."
      }
    },
    {
      "@type": "Question",
      "name": "Nextflow vs Snakemake, which should I pick for RNA-seq?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Both are mature bioinformatics workflow managers that handle dependency tracking, resumption, containers, and cluster execution well. Choose Snakemake if your team works in Python and you want the rule-based, config-driven structure shown here. Choose Nextflow if you plan to adopt or extend a large existing community pipeline ecosystem, where Nextflow's channel-based model has the most ready-made options."
      }
    },
    {
      "@type": "Question",
      "name": "Can the same Snakemake structure handle [small RNA-seq](/knowledge/molecular-biology/small-rna-seq) or single-cell data?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes, the skeleton is assay-independent. You swap the trimming strategy, aligner, and counting step while keeping the config, rules, envs, results, and logs layout. Small RNA-seq requires specialized adapter and isoform handling, and single-cell RNA-seq pipelines such as bollito demonstrate the same modular structure scaled to more than 30 tools including dimensionality reduction, clustering, and trajectory inference."
      }
    },
    {
      "@type": "Question",
      "name": "How do I test a new Snakemake workflow before running it on real data?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Run snakemake -n, use-conda to validate the job graph, then execute the full workflow on one or two tiny test FASTQ files or a subset of real reads. For tool-level validation, benchmark against curated public datasets such as those provided by the Dex-Benchmark project, which supplies RNA-seq data and code templates built specifically for objective tool comparison."
      }
    }
  ]
}
</script>

## Sources

1. [RNA-Seq-Pop: Exploiting the sequence in RNA sequencing-A Snakemake workflow reveals patterns of insecticide resistance in the malaria vector Anopheles gambiae.](https://pubmed.ncbi.nlm.nih.gov/36695302/)
2. [bollito: a flexible pipeline for comprehensive single-cell RNA-seq analyses.](https://pubmed.ncbi.nlm.nih.gov/34788788/)
3. [Sample-specific haplotype-resolved protein isoform characterization via long-read RNA-seq-based proteogenomics.](https://pubmed.ncbi.nlm.nih.gov/41835386/)
4. [Unveiling Functional Long-Range RNA Structures via RIC-seq Analysis.](https://pubmed.ncbi.nlm.nih.gov/41174205/)
5. [RNAcontacts: A Pipeline for Predicting Contacts from RNA Proximity Ligation Assays.](https://pubmed.ncbi.nlm.nih.gov/37153509/)
6. [SVNeoPP: A Workflow for Structural-Variant-Derived Neoantigen Prediction and Prioritization Using Multi-Omics Data.](https://pubmed.ncbi.nlm.nih.gov/41892252/)
7. [Dex-Benchmark: datasets and code to evaluate algorithms for transcriptomics data analysis.](https://pubmed.ncbi.nlm.nih.gov/37953774/)
8. [Standardizing RNA-seq Analysis of Fungal Pathogens Using BRC-Analytics and Agentic AI: A Candidozyma auris Case Study.](https://pubmed.ncbi.nlm.nih.gov/41509213/)
9. [systemPipeR: NGS workflow and report generation environment.](https://pubmed.ncbi.nlm.nih.gov/27650223/)
10. [Small RNA-seq analysis of single porcine blastocysts revealed that maternal estradiol-17beta exposure does not affect miRNA isoform (isomiR) expression.](https://pubmed.ncbi.nlm.nih.gov/30081835/)