Skip to content
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
3ba48a7
Add AI-assisted Groovy essentials
pinin4fjords Aug 14, 2025
f5fba63
Refine section 1.1
pinin4fjords Sep 4, 2025
45013e2
Add groovy essentials to nav
pinin4fjords Sep 4, 2025
0630074
Fix list rendering
pinin4fjords Sep 4, 2025
e171a4c
Emphasise departure to groovy
pinin4fjords Sep 4, 2025
db4bcdc
Shorten intro
pinin4fjords Sep 4, 2025
07dd457
Shorten intro
pinin4fjords Sep 4, 2025
5c92314
Fix highlight
pinin4fjords Sep 4, 2025
4ef9986
Refine section 1.2
pinin4fjords Sep 10, 2025
aa468a2
messy updates up to section 4
pinin4fjords Oct 8, 2025
6be8aa5
State after a good chat wiht Claude code
pinin4fjords Oct 9, 2025
28210b4
Tone down intro
pinin4fjords Oct 9, 2025
51245c8
Reset collect.nf to starting state
pinin4fjords Oct 9, 2025
df11a32
Tweaks
pinin4fjords Oct 9, 2025
cda7f76
Reset collect.nf to starting state
pinin4fjords Oct 9, 2025
e37d4ef
Tweaks
pinin4fjords Oct 9, 2025
2c5f0b6
Revert main.nf to starting point
pinin4fjords Oct 9, 2025
954ce99
Tweaks
pinin4fjords Oct 9, 2025
cd45c76
backticks fix?
pinin4fjords Oct 9, 2025
c0ec86a
Reset fastp module to starting point
pinin4fjords Oct 9, 2025
51e3e1d
Fix fastp
pinin4fjords Oct 9, 2025
e2f3681
tweaks
pinin4fjords Oct 9, 2025
5ae910d
Merge branch 'master' into groovy-essentials-side-quest
pinin4fjords Oct 9, 2025
5913c17
latest tweaks, simplify the variable interpretation part
pinin4fjords Oct 10, 2025
4bf4d05
Reorg subsections a bit
pinin4fjords Oct 10, 2025
4232b7c
Fix highlights
pinin4fjords Oct 10, 2025
93198e5
Fix up dynamic resources
pinin4fjords Oct 10, 2025
7e708a4
Fix up dynamic routing
pinin4fjords Oct 10, 2025
a9e3c28
Final tweaks
pinin4fjords Oct 10, 2025
ef9124c
Language/ clarity improvements
pinin4fjords Oct 10, 2025
9f1fa17
Prettier
pinin4fjords Oct 10, 2025
c4a6eaf
Apply suggestions from code review
pinin4fjords Oct 10, 2025
4d42b2a
Add teach time estimate for groovy
pinin4fjords Oct 11, 2025
124d15c
Merge branch 'groovy-essentials-side-quest' of github.com:nextflow-io…
pinin4fjords Oct 11, 2025
8914150
Fix formatting
pinin4fjords Oct 11, 2025
97bb707
Some section 2 fixes
pinin4fjords Oct 13, 2025
0ef30c6
Fix highlights
pinin4fjords Oct 14, 2025
fa8c04d
Fix tiny issues
pinin4fjords Oct 14, 2025
b9cd15a
More Groovy fixes
pinin4fjords Oct 14, 2025
b8e0c80
update time estimate
pinin4fjords Oct 14, 2025
3b5f11a
Refactor Groovy Essentials to Essential Scripting Patterns
pinin4fjords Oct 14, 2025
9c466b1
Prettier
pinin4fjords Oct 14, 2025
57b2505
Update title to Essential Nextflow Scripting Patterns
pinin4fjords Oct 14, 2025
9f332c5
Apply suggestions from code review
pinin4fjords Oct 14, 2025
2462885
Easy fixes for Ben
pinin4fjords Oct 14, 2025
4793e8d
Iterable -> List
pinin4fjords Oct 14, 2025
c6bc005
Fixes up to handlers
pinin4fjords Oct 14, 2025
5d530c9
Fix highlight
pinin4fjords Oct 14, 2025
abc25eb
Tighten intro
pinin4fjords Oct 14, 2025
be9538d
Merge branch 'master' into groovy-essentials-side-quest
pinin4fjords Oct 14, 2025
0d6022a
Tiny fixes
pinin4fjords Oct 14, 2025
9d56de2
Merge branch 'groovy-essentials-side-quest' of github.com:nextflow-io…
pinin4fjords Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,283 changes: 2,283 additions & 0 deletions docs/side_quests/groovy_essentials.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/side_quests/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Otherwise, select a side quest from the table below.
| Side Quest | Time Estimate for Teaching |
| ----------------------------------------------------------------- | -------------------------- |
| [Nextflow development environment walkthrough](./ide_features.md) | 45 mins |
| [Groovy Essentials for Nextflow](./groovy_essentials.md) | 2 hours |
| [Introduction to nf-core](./nf-core.md) | - |
| [Metadata in workflows](./metadata.md) | 45 mins |
| [Splitting and Grouping](./splitting_and_grouping.md) | 45 mins |
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ nav:
- side_quests/splitting_and_grouping.md
- side_quests/workflows_of_workflows.md
- side_quests/debugging.md
- side_quests/groovy_essentials.md
- side_quests/nf-test.md
- side_quests/nf-core.md
- Archive:
Expand Down
7 changes: 7 additions & 0 deletions side-quests/groovy_essentials/collect.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def sample_ids = ['sample_001', 'sample_002', 'sample_003']

// Nextflow collect() - groups multiple channel emissions into one
ch_input = Channel.fromList(sample_ids)
ch_input.view { "Individual channel item: ${it}" }
ch_collected = ch_input.collect()
ch_collected.view { "Nextflow collect() result: ${it} (${it.size()} items grouped into 1)" }
4 changes: 4 additions & 0 deletions side-quests/groovy_essentials/data/samples.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
sample_id,organism,tissue_type,sequencing_depth,file_path,quality_score
SAMPLE_001,human,liver,30000000,data/sequences/SAMPLE_001_S1_L001_R1_001.fastq,38.5
SAMPLE_002,mouse,brain,25000000,data/sequences/SAMPLE_002_S2_L001_R1_001.fastq,35.2
SAMPLE_003,human,kidney,45000000,data/sequences/SAMPLE_003_S3_L001_R1_001.fastq,42.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@sample_001_read_1
ATGCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATC
+
IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
@sample_001_read_2
GCATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGAT
+
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
@sample_001_read_3
TCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGA
+
JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJH
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@sample_002_read_1
CGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGAT
+
IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
@sample_002_read_2
ATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCG
+
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
@sample_002_read_3
GATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATC
+
JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@sample_003_read_1
GCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGC
+
IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
@sample_003_read_2
CGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCGCG
+
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
@sample_003_read_3
ATATATATATATATATATATATATATATATATATATATAT
+
JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
5 changes: 5 additions & 0 deletions side-quests/groovy_essentials/main.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
workflow {
ch_samples = Channel.fromPath("./data/samples.csv")
.splitCsv(header: true)
.view()
}
21 changes: 21 additions & 0 deletions side-quests/groovy_essentials/modules/fastp.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
process FASTP {
container 'community.wave.seqera.io/library/fastp:0.24.0--62c97b06e8447690'

input:
tuple val(meta), path(reads)

output:
tuple val(meta), path("*_trimmed*.fastq.gz"), emit: reads

script:
"""
fastp \\
--in1 ${reads[0]} \\
--in2 ${reads[1]} \\
--out1 ${meta.id}_trimmed_R1.fastq.gz \\
--out2 ${meta.id}_trimmed_R2.fastq.gz \\
--json ${meta.id}.fastp.json \\
--html ${meta.id}.fastp.html \\
--thread $task.cpus
"""
}
16 changes: 16 additions & 0 deletions side-quests/groovy_essentials/modules/generate_report.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
process GENERATE_REPORT {

publishDir 'results/reports', mode: 'copy'

input:
tuple val(meta), path(reads)

output:
path "${meta.id}_report.txt"

script:
"""
echo "Processing ${reads}" > ${meta.id}_report.txt
echo "Sample: ${meta.id}" >> ${meta.id}_report.txt
"""
}
37 changes: 37 additions & 0 deletions side-quests/groovy_essentials/modules/trimgalore.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
process TRIMGALORE {
container 'quay.io/biocontainers/trim-galore:0.6.10--hdfd78af_0'

input:
tuple val(meta), path(reads)

output:
tuple val(meta), path("*_trimmed*.fq"), emit: reads
path "*_trimming_report.txt" , emit: reports

script:
// Simple single-end vs paired-end detection
def is_single = reads instanceof List ? reads.size() == 1 : true

if (is_single) {
def input_file = reads instanceof List ? reads[0] : reads
"""
trim_galore \\
--cores $task.cpus \\
${input_file}

# Rename output to match expected pattern
mv *_trimmed.fq ${meta.id}_trimmed.fq
"""
} else {
"""
trim_galore \\
--paired \\
--cores $task.cpus \\
${reads[0]} ${reads[1]}

# Rename outputs to match expected pattern
mv *_val_1.fq ${meta.id}_trimmed_R1.fq
mv *_val_2.fq ${meta.id}_trimmed_R2.fq
"""
}
}
3 changes: 3 additions & 0 deletions side-quests/groovy_essentials/nextflow.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Nextflow configuration for Groovy Essentials side quest

docker.enabled = true
18 changes: 18 additions & 0 deletions side-quests/solutions/groovy_essentials/collect.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
def sample_ids = ['sample_001', 'sample_002', 'sample_003']

// Nextflow collect() - groups multiple channel emissions into one
ch_input = Channel.fromList(sample_ids)
ch_input.view { "Individual channel item: ${it}" }
ch_collected = ch_input.collect()
ch_collected.view { "Nextflow collect() result: ${it} (${it.size()} items grouped into 1)" }

// Groovy collect - transforms each element, preserves structure
def formatted_ids = sample_ids.collect { id ->
id.toUpperCase().replace('SAMPLE_', 'SPECIMEN_')
}
println "Groovy collect result: ${formatted_ids} (${sample_ids.size()} items transformed into ${formatted_ids.size()})"

// Spread operator - concise property access
def sample_data = [[id: 's1', quality: 38.5], [id: 's2', quality: 42.1], [id: 's3', quality: 35.2]]
def all_ids = sample_data*.id
println "Spread operator result: ${all_ids}"
69 changes: 69 additions & 0 deletions side-quests/solutions/groovy_essentials/main.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
include { FASTP } from './modules/fastp.nf'
include { TRIMGALORE } from './modules/trimgalore.nf'
include { GENERATE_REPORT } from './modules/generate_report.nf'

def validateInputs() {
// Check input parameter is provided
if (!params.input) {
error("Input CSV file path not provided. Please specify --input <file.csv>")
}

// Check CSV file exists
if (!file(params.input).exists()) {
error("Input CSV file not found: ${params.input}")
}
}

def separateMetadata(row) {
def sample_meta = [
id: row.sample_id.toLowerCase(),
organism: row.organism,
tissue: row.tissue_type.replaceAll('_', ' ').toLowerCase(),
depth: row.sequencing_depth.toInteger(),
quality: row.quality_score?.toDouble()
]
def run_id = row.run_id?.toUpperCase() ?: 'UNSPECIFIED'
sample_meta.run = run_id
def fastq_path = file(row.file_path)

def m = (fastq_path.name =~ /^(.+)_S(\d+)_L(\d{3})_(R[12])_(\d{3})\.fastq(?:\.gz)?$/)
def file_meta = m ? [
sample_num: m[0][2].toInteger(),
lane: m[0][3],
read: m[0][4],
chunk: m[0][5]
] : [:]

def priority = sample_meta.quality > 40 ? 'high' : 'normal'

// Validate data makes sense
if (sample_meta.depth < 30000000) {
log.warn "Low sequencing depth for ${sample_meta.id}: ${sample_meta.depth}"
}

return [sample_meta + file_meta + [priority: priority], fastq_path]
}

workflow {
validateInputs()

ch_samples = Channel.fromPath(params.input)
.splitCsv(header: true)
.map{ row -> separateMetadata(row) }

// Filter out invalid or low-quality samples
ch_valid_samples = ch_samples
.filter { meta, reads ->
meta.id && meta.organism && meta.depth > 25000000
}

trim_branches = ch_valid_samples
.branch { meta, reads ->
fastp: meta.organism == 'human' && meta.depth >= 30000000
trimgalore: true
}

ch_fastp = FASTP(trim_branches.fastp)
ch_trimgalore = TRIMGALORE(trim_branches.trimgalore)
GENERATE_REPORT(ch_samples)
}
37 changes: 37 additions & 0 deletions side-quests/solutions/groovy_essentials/modules/fastp.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
process FASTP {
container 'community.wave.seqera.io/library/fastp:0.24.0--62c97b06e8447690'

input:
tuple val(meta), path(reads)

output:
tuple val(meta), path("*_trimmed*.fastq.gz"), emit: reads
path "*.{json,html}" , emit: reports

script:
// Simple single-end vs paired-end detection
def is_single = reads instanceof List ? reads.size() == 1 : true

if (is_single) {
def input_file = reads instanceof List ? reads[0] : reads
"""
fastp \\
--in1 ${input_file} \\
--out1 ${meta.id}_trimmed.fastq.gz \\
--json ${meta.id}.fastp.json \\
--html ${meta.id}.fastp.html \\
--thread $task.cpus
"""
} else {
"""
fastp \\
--in1 ${reads[0]} \\
--in2 ${reads[1]} \\
--out1 ${meta.id}_trimmed_R1.fastq.gz \\
--out2 ${meta.id}_trimmed_R2.fastq.gz \\
--json ${meta.id}.fastp.json \\
--html ${meta.id}.fastp.html \\
--thread $task.cpus
"""
}
}
19 changes: 19 additions & 0 deletions side-quests/solutions/groovy_essentials/modules/generate_report.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
process GENERATE_REPORT {

publishDir 'results/reports', mode: 'copy'

input:
tuple val(meta), path(reads)

output:
path "${meta.id}_report.txt"

script:
"""
echo "Processing ${reads}" > ${meta.id}_report.txt
echo "Sample: ${meta.id}" >> ${meta.id}_report.txt
echo "Processed by: \${USER}" >> ${meta.id}_report.txt
echo "Hostname: \$(hostname)" >> ${meta.id}_report.txt
echo "Date: \$(date)" >> ${meta.id}_report.txt
"""
}
37 changes: 37 additions & 0 deletions side-quests/solutions/groovy_essentials/modules/trimgalore.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
process TRIMGALORE {
container 'quay.io/biocontainers/trim-galore:0.6.10--hdfd78af_0'

input:
tuple val(meta), path(reads)

output:
tuple val(meta), path("*_trimmed*.fq"), emit: reads
path "*_trimming_report.txt" , emit: reports

script:
// Simple single-end vs paired-end detection
def is_single = reads instanceof List ? reads.size() == 1 : true

if (is_single) {
def input_file = reads instanceof List ? reads[0] : reads
"""
trim_galore \\
--cores $task.cpus \\
${input_file}

# Rename output to match expected pattern
mv *_trimmed.fq ${meta.id}_trimmed.fq
"""
} else {
"""
trim_galore \\
--paired \\
--cores $task.cpus \\
${reads[0]} ${reads[1]}

# Rename outputs to match expected pattern
mv *_val_1.fq ${meta.id}_trimmed_R1.fq
mv *_val_2.fq ${meta.id}_trimmed_R2.fq
"""
}
}
23 changes: 23 additions & 0 deletions side-quests/solutions/groovy_essentials/nextflow.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Nextflow configuration for Groovy Essentials side quest

docker.enabled = true

workflow.onComplete = {
println ""
println "Pipeline execution summary:"
println "=========================="
println "Completed at: ${workflow.complete}"
println "Duration : ${workflow.duration}"
println "Success : ${workflow.success}"
println "workDir : ${workflow.workDir}"
println "exit status : ${workflow.exitStatus}"
println ""

if (workflow.success) {
println "✅ Pipeline completed successfully!"
println "Results are in: ${params.outdir ?: 'results'}"
} else {
println "❌ Pipeline failed!"
println "Error: ${workflow.errorMessage}"
}
}