Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d03bfa7
add poc for remote module inclusion
jorgee Jan 30, 2026
c766cbb
add remote inclusion in syntax parser v2 with SPI
jorgee Feb 9, 2026
c38b4a8
fix lint issue
jorgee Feb 9, 2026
97225f7
add poc for remote module inclusion
jorgee Jan 30, 2026
993c9fe
add remote inclusion in syntax parser v2 with SPI
jorgee Feb 9, 2026
a483d3e
fix lint issue
jorgee Feb 9, 2026
ce323b8
Merge branch '251117-module-system-implementation-with-remote-inclusi…
pditommaso Feb 19, 2026
15ac3c9
Merge branch 'master' into 251117-module-system-implementation-with-r…
pditommaso Feb 19, 2026
b224da7
Fix compilation issue [ci skip]
pditommaso Feb 19, 2026
a325e43
add review comments and fix compilation
jorgee Feb 25, 2026
f9a30a0
Merge branch '251117-module-system-implementation' into 251117-module…
jorgee Feb 25, 2026
53e8ad0
Merge branch '251117-module-system-implementation' into 251117-module…
jorgee Feb 26, 2026
1addadf
remove duplicated code when merge
jorgee Feb 26, 2026
751926c
look for checksum in redirected headers
jorgee Feb 26, 2026
d876ce4
Merge branch '251117-module-system-implementation' into 251117-module…
jorgee Feb 26, 2026
2a51213
Merge branch '251117-module-system-implementation' into 251117-module…
jorgee Feb 27, 2026
7de6423
add remote module inclusion in docs and update speckit files
jorgee Feb 27, 2026
fb7673c
Merge branch '251117-module-system-implementation' into 251117-module…
pditommaso Mar 3, 2026
11e3400
wip
pditommaso Mar 3, 2026
e677b26
Merge branch '251117-module-system-implementation' into 251117-module…
jorgee Mar 5, 2026
72ec5d9
update docs [ci skip]
bentsherman Mar 6, 2026
2fa01e8
Remote module inclusion without @ (#6898)
jorgee Mar 6, 2026
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
173 changes: 172 additions & 1 deletion docs/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,179 @@ This feature requires the use of a local or shared file system for the pipeline

## Sharing modules

Modules are designed to be easy to share and re-use across different pipelines, which helps eliminate duplicate work and spread improvements throughout the community. While Nextflow does not provide an explicit mechanism for sharing modules, there are several ways to do it:
Modules are designed to be easy to share and re-use across different pipelines, which helps eliminate duplicate work and spread improvements throughout the community. There are several ways to share modules:

- Use the Nextflow module registry (recommended, see below)
- Simply copy the module files into your pipeline repository
- Use [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to fetch modules from other Git repositories without maintaining a separate copy
- Use the [nf-core](https://nf-co.re/tools#modules) CLI to install and update modules with a standard approach used by the nf-core community

(module-registry)=

## Registry-based modules

:::{versionadded} 26.04.0
:::

Nextflow provides a module registry that enables you to install, share, and manage modules from centralized registries. This system provides version management, integrity checking, and seamless integration with the Nextflow DSL.

### Installing modules from a registry

Use the `module install` command to download modules from a registry:

```console
$ nextflow module install nf-core/fastqc
$ nextflow module install nf-core/fastqc -version 1.0.0
```

Installed modules are stored in the `modules/` directory and can be included using the registry syntax with the `@` prefix:

```nextflow
include { FASTQC } from '@nf-core/fastqc'

workflow {
reads = Channel.fromFilePairs('data/*_{1,2}.fastq.gz')
FASTQC(reads)
}
```

### Running modules directly

For ad-hoc tasks or testing, you can run a module directly without creating a workflow:

```console
$ nextflow module run nf-core/fastqc --input 'data/*.fastq.gz'
```

This command accepts all standard Nextflow options (`-profile`, `-resume`, etc.) and automatically downloads the module if not already installed.

### Managing module versions

Module versions are tracked in `nextflow_spec.json` in your project directory:

```json
{
"modules": {
"@nf-core/fastqc": "1.0.0",
"@nf-core/bwa-align": "1.2.0"
}
}
```

When you run your workflow, Nextflow automatically installs or updates modules to match the specified versions.

### Discovering modules

Search for available modules using the `module search` command:

```console
$ nextflow module search alignment
$ nextflow module search "quality control" -limit 10
```

List installed modules in your project:

```console
$ nextflow module list
```

### Module checksum verification

Nextflow automatically verifies module integrity using checksums. If you modify a module locally, Nextflow will detect the change and prevent accidental overwrites:

```console
$ nextflow module install nf-core/fastqc -version 1.1.0
Warning: Module @nf-core/fastqc has local modifications. Use -force to override.
```

Use the `-force` flag to override local modifications when needed.

### Removing modules

Use the `module remove` command to uninstall a module:

```console
$ nextflow module remove nf-core/fastqc
```

By default, both the local module files and the entry in `nextflow_spec.json` are removed. Use the flags below to control this behaviour:

- `-keep-files` — Remove the entry from `nextflow_spec.json` but keep the local module files
- `-keep-config` — Remove the local module files but keep the entry in `nextflow_spec.json`

### Viewing module information

Use the `module info` command to display metadata and a usage template for a module:

```console
$ nextflow module info nf-core/fastqc
$ nextflow module info nf-core/fastqc -version 1.0.0
```

The output includes the module description, authors, keywords, tools, inputs, outputs, and a ready-to-use command-line template. Use `-json` to get machine-readable output.

### Publishing modules

To share your own modules, use the `module publish` command:

```console
$ nextflow module publish myorg/my-module
```

The argument can be either a `scope/name` reference (for an already-installed module) or a local directory path containing the module files.

Your module directory must include:

- `main.nf` - The module entry point
- `meta.yaml` - Module metadata (name, description, version, etc.)
- `README.md` - Module documentation

Authentication is required for publishing and can be provided via the `NXF_REGISTRY_TOKEN` environment variable or in your configuration:

```groovy
registry {
apiKey = 'YOUR_REGISTRY_TOKEN'
}
```

Use `-dry-run` to validate your module structure without uploading:

```console
$ nextflow module publish myorg/my-module -dry-run
```

### Registry configuration

By default, Nextflow uses the public registry at `https://registry.nextflow.io`. You can configure alternative or additional registries:

```groovy
registry {
url = [
'https://private.registry.myorg.com',
'https://registry.nextflow.io'
]
apiKey = '${MYORG_TOKEN}'
}
```

Registries are queried in the order specified until a module is found. The `apiKey` is used only for the primary (first) registry.

### Module directory structure

Registry modules follow a standard directory structure:

```
modules/
└── @scope/
└── module-name/
├── .checksum # Integrity checksum (generated automatically)
├── README.md # Documentation (required for publishing)
├── main.nf # Module entry point (required)
├── meta.yaml # Module metadata (required for publishing)
├── resources/ # Optional: module binaries and resources
└── templates/ # Optional: process templates
```

The `modules/` directory should be committed to your Git repository to ensure reproducibility.

See the {ref}`cli-page` documentation for complete details on all module commands.
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ class CmdModuleInfo extends CmdBase {
throw new AbortOperationException("Incorrect number of arguments")
}

def moduleRef = '@' + args[0]
def reference = ModuleReference.parse(moduleRef)
def reference = ModuleReference.parse(args[0])

// Get config
def baseDir = root ?: Paths.get('.').toAbsolutePath().normalize()
Expand All @@ -117,10 +116,10 @@ class CmdModuleInfo extends CmdBase {
log.warn "Failed to fetch metadata from registry: ${e.message}"
}
if( !release ) {
throw new AbortOperationException("No release information available for ${reference.nameWithoutPrefix}")
throw new AbortOperationException("No release information available for ${reference}")
}
if( !release.metadata ) {
log.info("No metadata found for $reference.nameWithoutPrefix ${release.version ? "($release.version)" : ''}")
log.info("No metadata found for $reference ${release.version ? "($release.version)" : ''}")
}
def moduleUrl = buildModuleUrl(registryConfig.url, reference, release.version)
if( !output || output == 'text' ) {
Expand All @@ -135,7 +134,7 @@ class CmdModuleInfo extends CmdBase {
private void printFormattedInfo(ModuleReference reference, ModuleRelease release, String moduleUrl) {
ModuleMetadata metadata = release.metadata
println ""
println "Module: ${reference.nameWithoutPrefix}"
println "Module: ${reference}"
println "Version: ${release.version}"
println "URL: ${moduleUrl}"
println "Description: ${metadata.description ?: release.description ?: 'N/A'}"
Expand Down Expand Up @@ -218,7 +217,7 @@ class CmdModuleInfo extends CmdBase {

private List<String> generateUsageTemplate(ModuleReference reference, ModuleMetadata metadata) {
def template = new ArrayList<String>()
template.add("nextflow module run ${reference.nameWithoutPrefix}".toString())
template.add("nextflow module run ${reference}".toString())
if( version )
template.add(" -version $version".toString())

Expand Down Expand Up @@ -281,7 +280,7 @@ class CmdModuleInfo extends CmdBase {
private void printJsonInfo(ModuleReference reference, ModuleRelease release, String moduleUrl) {
def metadata = release?.metadata
def info = [
name : reference.nameWithoutPrefix,
name : reference.toString(),
fullName : reference.fullName,
version : release.version,
url : moduleUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ class CmdModuleInstall extends CmdBase {
throw new AbortOperationException("Incorrect number of arguments")
}

def moduleRef = '@' + args[0]

def reference = ModuleReference.parse(moduleRef)
def reference = ModuleReference.parse(args[0])

// Get config
def baseDir = root ?: Paths.get('.').toAbsolutePath().normalize()
Expand All @@ -96,7 +94,7 @@ class CmdModuleInstall extends CmdBase {
def installedVersion = version ?: resolver.resolveVersion(reference)
specFile.addModuleEntry(reference.fullName, installedVersion)

println "Module ${reference.nameWithoutPrefix}@${installedVersion} installed and configured successfully"
println "Module ${reference}@${installedVersion} installed and configured successfully"
}
catch( AbortOperationException e ) {
throw e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,15 @@ class CmdModuleList extends CmdBase {

installed.each { module ->
def status = getStatusString(module.integrity)
println "${module.reference.nameWithoutPrefix.padRight(40)}${(module.installedVersion ?: 'unknown').padRight(15)}${status}"
println "${module.reference.toString().padRight(40)}${(module.installedVersion ?: 'unknown').padRight(15)}${status}"
}
println ""
}

private void printJsonList(List<InstalledModule> installed) {
def modules = installed.collect { module ->
[
name : module.reference.nameWithoutPrefix,
name : module.reference.toString(),
version : module.installedVersion ?: 'unknown',
integrity: module.integrity.toString(),
directory: module.directory.toString()
Expand All @@ -136,8 +136,8 @@ class CmdModuleList extends CmdBase {
return 'OK'
case ModuleIntegrity.MODIFIED:
return 'MODIFIED'
case ModuleIntegrity.MISSING_CHECKSUM:
return 'NO CHECKSUM'
case ModuleIntegrity.NO_REMOTE_MODULE:
return 'LOCAL'
case ModuleIntegrity.CORRUPTED:
return 'CORRUPTED'
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import nextflow.cli.CmdBase
import nextflow.config.ConfigBuilder
import nextflow.config.RegistryConfig
import nextflow.exception.AbortOperationException
import nextflow.module.ModuleChecksum
import nextflow.module.ModuleSpec
import nextflow.module.ModuleReference
import nextflow.module.ModuleRegistryClient
Expand Down Expand Up @@ -60,6 +61,9 @@ class CmdModulePublish extends CmdBase {
@TestOnly
protected ModuleRegistryClient client

//Flag if publish is invoked from a scope/name. In this case we should create/update the .module-info with the correct checksum
private boolean useModuleReference = false

@Override
String getName() {
return 'publish'
Expand Down Expand Up @@ -115,14 +119,10 @@ class CmdModulePublish extends CmdBase {

private void publishModule(Path moduleDir, RegistryConfig registryConfig, ModuleSpec manifest){
log.info "Creating module bundle..."
def storage = new ModuleStorage(moduleDir.parent)
def tempBundleFile = Files.createTempFile("nf-module-publish-", ".tar.gz")

try {
storage.createBundle(moduleDir, tempBundleFile)

// Compute bundle checksum
def checksum = storage.computeBundleChecksum(tempBundleFile)
def checksum = ModuleStorage.createBundle(moduleDir, tempBundleFile)
log.info "Bundle checksum: ${checksum}"

// Read bundle content as bytes
Expand All @@ -139,6 +139,14 @@ class CmdModulePublish extends CmdBase {
def registryClient = new ModuleRegistryClient(registryConfig)
def response = registryClient.publishModule(manifest.name, request, registryUrl)

if (useModuleReference) {
// If publish is performed using the module reference we should create/update the .module-info with the correct checksum
try {
ModuleChecksum.save(moduleDir, ModuleChecksum.compute(moduleDir))
}catch (Exception e){
log.warn("Unable to save the checksum - ${e.message}")
}
}
println "✓ Module published successfully!"
println ""
println "Module details:"
Expand Down Expand Up @@ -246,13 +254,13 @@ class CmdModulePublish extends CmdBase {
return Paths.get(module).toAbsolutePath().normalize()
}

final ref = ModuleReference.parse('@' + module)
final ref = ModuleReference.parse(module)
final localStorage = new ModuleStorage(root ?: Paths.get('.').toAbsolutePath().normalize())

if (!localStorage.isInstalled(ref)){
throw new AbortOperationException("No module diretory found for $module")
}

useModuleReference = true
return localStorage.getModuleDir(ref)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ class CmdModuleRemove extends CmdBase {
throw new AbortOperationException("Cannot use both -keep-config and -keep-files flags together")
}

def moduleRef = '@' + args[0]

def reference = ModuleReference.parse(moduleRef)
def reference = ModuleReference.parse(args[0])

// Get config
def baseDir = root ?: Paths.get('.').toAbsolutePath().normalize()
Expand All @@ -87,15 +85,15 @@ class CmdModuleRemove extends CmdBase {

// Remove local files unless -keep-files is set
if( !keepFiles ) {
println "Removing module files for ${reference.nameWithoutPrefix}..."
println "Removing module files for ${reference}..."
filesRemoved = storage.removeModule(reference)
if( filesRemoved ) {
println "Module files removed successfully"
} else {
println "Module ${reference.nameWithoutPrefix} was not installed locally"
println "Module ${reference} was not installed locally"
}
} else {
println "Keeping module files for ${reference.nameWithoutPrefix} (due to -keep-files flag)"
println "Keeping module files for ${reference} (due to -keep-files flag)"
}

// Remove config entry unless -keep-config is set
Expand All @@ -105,17 +103,17 @@ class CmdModuleRemove extends CmdBase {
if( configRemoved ) {
println "Module entry removed from configuration"
} else {
println "Module ${reference.nameWithoutPrefix} was not configured in nextflow_spec.json"
println "Module ${reference} was not configured in nextflow_spec.json"
}
} else {
println "Keeping module entry in nextflow_spec.json (due to -keep-config flag)"
}

// Summary
if( filesRemoved || configRemoved ) {
println "\nModule ${reference.nameWithoutPrefix} removal completed"
println "\nModule ${reference} removal completed"
} else {
println "\nModule ${reference.nameWithoutPrefix} was not found"
println "\nModule ${reference} was not found"
}
}
catch( AbortOperationException e ) {
Expand Down
Loading
Loading