Skip to content
Draft
22 changes: 19 additions & 3 deletions adr/module-spec-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@
"type": "string",
"description": "Module name. Can be a simple identifier (e.g., 'fastqc', 'bwa_mem') for local/nf-core modules, or a fully qualified scoped name (e.g., 'nf-core/fastqc', 'myorg/custom') for registry modules.",
"examples": ["fastqc", "bwa_mem", "nf-core/fastqc", "myorg/salmon-quant"],
"pattern": "^([a-z0-9][a-z0-9-]*/)?[a-z][a-z0-9_-]*$"
"pattern": "^([a-z0-9][a-z0-9._-]*/)?[a-z][a-z0-9._-]*(/[a-z][a-z0-9._-]*)*$"
},
"version": {
"type": "string",
"description": "Semantic version of the module (MAJOR.MINOR.PATCH). Required for registry publication",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$",
"examples": ["1.0.0", "2.1.3", "1.0.0-beta.1"]
},
"kind": {
"type": "string",
"description": "Module kind: 'Process' (standalone process, default when absent) or 'Workflow' (standalone workflow / subworkflow). Metadata only; does not affect storage location.",
"enum": ["Process", "Workflow"]
},
"description": {
"type": "string",
"description": "Brief description of what the module does",
Expand Down Expand Up @@ -64,6 +69,15 @@
"description": "Nextflow version constraint using comparison operators",
"examples": [">=24.04.0", ">=24.04.0,<25.0.0"],
"pattern": "^[<>=!]+[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?(,\\s*[<>=!]+[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?)*$"
},
"modules": {
"type": "array",
"description": "Direct module dependencies (workflow modules): 'scope/name@<version>' references pinned to an exact semver, including the git-sha pre-release form (e.g. 0.0.0-4e3e10e). Version constraints/ranges are NOT supported — under nested per-module vendoring each dependency is installed at its pinned version in isolation.",
"items": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9_/-]*@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-[0-9A-Za-z-.]+)?$"
},
"uniqueItems": true
}
},
"additionalProperties": false
Expand Down Expand Up @@ -218,7 +232,7 @@
},
"type": {
"type": "string",
"description": "Data type of the parameter value",
"description": "Data type of the parameter value. The generic tags 'channel' (a typed workflow channel, e.g. Channel<T>) and 'custom-record' (a user-defined record type) are documentation only for statically-typed processes/workflows -- the authoritative types are declared in the source take:/emit: and typed input/output, and module run parses them from the code.",
"enum": [
"boolean",
"float",
Expand All @@ -227,7 +241,9 @@
"list",
"map",
"file",
"directory"
"directory",
"channel",
"custom-record"
]
},
"description": {
Expand Down
11 changes: 11 additions & 0 deletions docs/modules/developing-modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ The command creates a module directory with the following files:
- `meta.yml`: The module spec describing metadata, inputs, and outputs.
- `README.md`: Documentation for the module.

By default this scaffolds a *process* module. Use `-kind Workflow` to scaffold a *workflow* module, and `-typed` to generate statically-typed declarations. A typed workflow module can be executed directly with `nextflow module run`; an untyped one can only be included in a pipeline:

```console
$ nextflow module create myorg/my-workflow -kind Workflow -typed
```

See [module create][cli-module-create] for the full command reference.

## Module structure
Expand Down Expand Up @@ -185,6 +191,11 @@ $ nextflow module spec \

When updating an existing module spec, it is incorporated into the new file.

For a workflow module, the `input` and `output` sections are derived from the workflow's `take:` and
`emit:` declarations. Types are inferred when the workflow is statically typed; for an untyped workflow
the parameter names are captured but the types are left as `TODO` placeholders, since they cannot be
inferred from the source.

See [module spec][cli-module-spec] for the full command reference.

## Validating a module
Expand Down
16 changes: 16 additions & 0 deletions docs/modules/using-modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ Use the `-force` flag to overwrite local changes:
$ nextflow module install nf-core/fastqc -version 0.0.0-c9h0bv4 -force
```

### Updating a workflow module's dependencies

A workflow module declares its dependencies in `meta.yml` (`requires.modules`), and they are vendored
under the module's own nested `modules/` directory. If you edit the declared dependency versions of an
already-installed module by hand, use `-update-deps` to re-vendor them to match the edited `meta.yml`,
without reinstalling the module itself:

```console
$ nextflow module install nf-core/my-workflow -update-deps
```

This installs newly declared dependencies, updates changed versions, and removes dependencies that are no
longer declared. A vendored dependency with local modifications is not overwritten or removed; an error is
raised instead. The module itself is left untouched, so its local (unpublished) modification status is
preserved. The flag is ignored if the module is not installed, and cannot be combined with `-force`.

## Removing modules

Use the `module remove` command to uninstall a module from your project:
Expand Down
14 changes: 12 additions & 2 deletions docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1337,10 +1337,20 @@ The `module` command provides a comprehensive system for managing registry-based

#### Subcommands

##### `create [namespace/name]` {#module-create}
##### `create [options] [namespace/name]` {#module-create}

Create a new module with a basic `main.nf`, `meta.yml`, and `README.md`.

The following options are available:

###### `-kind` (`Process`)

The kind of module to scaffold: `Process` (default) or `Workflow`.

###### `-typed`

Scaffold a statically-typed module: adds `nextflow.enable.types = true` and typed `input`/`output` (process) or `take`/`emit` (workflow) declarations using basic types. A typed workflow module can be executed directly with `nextflow module run`; an untyped workflow module can only be included in a pipeline.

##### `install [options] [namespace/name]` {#module-install}

Install a module from the registry into your project.
Expand All @@ -1360,7 +1370,7 @@ Force reinstall even if the module exists locally with modifications. Without th
##### `list [options]` {#module-list}

List all modules currently installed in your project.
Shows each module's name, version, and integrity status (whether it has been modified locally).
Shows each module's name, version, kind (`Process` or `Workflow`), and integrity status (whether it has been modified locally).

The following options are available:

Expand Down
4 changes: 2 additions & 2 deletions modules/nextflow/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ dependencies {
api 'io.seqera:lib-trace:0.1.0'
api 'com.fasterxml.woodstox:woodstox-core:7.1.1'
api 'org.apache.commons:commons-compress:1.27.1' // For tar.gz extraction
api 'io.seqera:npr-api:0.22.0'
api 'io.seqera:npr-client:0.22.0'
api 'io.seqera:npr-api:0.24.10'
api 'io.seqera:npr-client:0.24.10'
api 'com.networknt:json-schema-validator:1.5.6'

testImplementation 'org.subethamail:subethasmtp:3.1.7'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ class CmdModuleCreate extends CmdBase {
@Parameter(description = "[namespace/name]")
List<String> args

@Parameter(names = ['-kind'], description = "Module kind: Process (default) or Workflow")
String kind

@Parameter(names = ['-typed'], description = "Generate a statically-typed module (usable directly with `nextflow module run`)", arity = 0)
boolean typed

@Override
String getName() {
return 'create'
Expand Down Expand Up @@ -86,8 +92,18 @@ class CmdModuleCreate extends CmdBase {

validateSegment('namespace', namespace)
validateSegments('name', name)
createModule(namespace, name)
createModule(namespace, name, normalizeKind(kind), typed)
}

static private String normalizeKind(String value) {
if( !value )
return 'Process'
final k = value.toLowerCase().capitalize()
if( k != 'Process' && k != 'Workflow' )
throw new AbortOperationException("Invalid module kind '${value}' -- must be 'Process' or 'Workflow'")
return k
}

static private void validateSegment(String field, String value) {
if( !value.matches('[a-zA-Z0-9][a-zA-Z0-9._\\-]*') )
throw new AbortOperationException("Invalid module $field '${value}' -- only alphanumeric characters, hyphens, underscores and dots are allowed, and must start with an alphanumeric character")
Expand All @@ -103,7 +119,7 @@ class CmdModuleCreate extends CmdBase {
return Path.of('modules')
}

protected void createModule(String namespace, String name) {
protected void createModule(String namespace, String name, String kind = 'Process', boolean typed = false) {
final moduleDir = modulesBase().resolve(namespace).resolve(name)
if( Files.exists(moduleDir) )
throw new AbortOperationException("Module directory already exists: $moduleDir")
Expand All @@ -112,22 +128,31 @@ class CmdModuleCreate extends CmdBase {
Files.createDirectories(moduleDir)

// create main.nf
moduleDir.resolve('main.nf').text = mainNf(namespace, name)
moduleDir.resolve('main.nf').text = mainNf(namespace, name, kind, typed)

// create README.md
moduleDir.resolve('README.md').text = readmeMd(namespace, name)

// create meta.yml
moduleDir.resolve('meta.yml').text = metaYml(namespace, name)
moduleDir.resolve('meta.yml').text = metaYml(namespace, name, kind, typed)

// create .module-info so it's recognised as a Nextflow managed module
Files.createFile(moduleDir.resolve(ModuleInfo.MODULE_INFO_FILE))

final defName = name.replaceAll('[^a-zA-Z0-9_]', '_').toUpperCase()
println "Module created successfully at path: $moduleDir"
println ""
println "To run the module:"
println ""
println " nextflow module run $namespace/$name --greeting 'Hello world!'"
// an untyped workflow module cannot be run directly (`module run` requires typed take:/emit:)
if( kind == 'Workflow' && !typed ) {
println "Include the workflow module in a pipeline:"
println ""
println " include { $defName } from '$namespace/$name'"
}
else {
println "To run the module:"
println ""
println " nextflow module run $namespace/$name --greeting 'Hello world!'"
}
}

static private String readLine() {
Expand All @@ -137,13 +162,84 @@ class CmdModuleCreate extends CmdBase {
: new BufferedReader(new InputStreamReader(System.in)).readLine()
}

static String mainNf(String namespace, String name) {
"""\
static String mainNf(String namespace, String name, String kind = 'Process', boolean typed = false) {
final defName = name.replaceAll('[^a-zA-Z0-9_]', '_').toUpperCase()

if( kind == 'Workflow' && typed ) {
return """\
/*
* Workflow module: ${namespace}/${name}
* TODO: rename the workflow, replace the example take/emit and types, and implement the logic.
*/

nextflow.enable.types = true

workflow ${defName} {
take:
greeting: String

main:
// TODO: implement the workflow logic
message = greeting

emit:
result: String = message
}
""".stripIndent()
}

if( kind == 'Workflow' ) {
return """\
/*
* Workflow module: ${namespace}/${name}
* TODO: rename the workflow, replace the example take/emit, and implement the logic.
*/

workflow ${defName} {
take:
ch_input

main:
// TODO: implement the workflow logic
ch_output = ch_input

emit:
output = ch_output
}
""".stripIndent()
}

if( typed ) {
return """\
/*
* Module: ${namespace}/${name}
* TODO: rename the process, replace the example input/output and types, and implement the script.
*/

nextflow.enable.types = true

process ${defName} {
input:
greeting: String

output:
message: String = stdout()

script:
\"\"\"
echo '\${greeting}'
\"\"\"
}
""".stripIndent()
}

return """\
/*
* Module: ${namespace}/${name}
* TODO: rename the process, replace the example input/output, and implement the script.
*/

process ${name.replaceAll('[^a-zA-Z0-9_]', '_').toUpperCase()} {
process ${defName} {
input:
val greeting

Expand All @@ -158,8 +254,69 @@ class CmdModuleCreate extends CmdBase {
""".stripIndent()
}

static String metaYml(String namespace, String name) {
"""\
static String metaYml(String namespace, String name, String kind = 'Process', boolean typed = false) {
if( kind == 'Workflow' && typed ) {
// typed workflow: derive input/output from the scaffold's take:/emit:
// (typed workflows require Nextflow 26.04.0+)
return """\
name: ${namespace}/${name}
version: 1.0.0
kind: Workflow
description: A brief description of the ${namespace}/${name} workflow module
license: Apache-2.0
requires:
nextflow: ">=26.04.0"
input:
- name: greeting
type: string
description: A greeting string
output:
- name: result
type: string
description: The greeting message
""".stripIndent()
}
if( kind == 'Workflow' ) {
// untyped workflow: take/emit have no declared types, but the generated scaffold's
// take/emit are channels, so the interface is documented with the generic channel type
return """\
name: ${namespace}/${name}
version: 1.0.0
kind: Workflow
description: A brief description of the ${namespace}/${name} workflow module
license: Apache-2.0
requires:
nextflow: ">=24.04.0"
input:
- name: ch_input
type: channel
description: The input channel
output:
- name: output
type: channel
description: The output channel
""".stripIndent()
}
if( typed ) {
// typed process (requires Nextflow 25.10.0+)
return """\
name: ${namespace}/${name}
version: 1.0.0
description: A brief description of the ${namespace}/${name} module
license: Apache-2.0
requires:
nextflow: ">=25.10.0"
input:
- name: greeting
type: string
description: A greeting string
output:
- name: message
type: string
description: The greeting message
""".stripIndent()
}
return """\
name: ${namespace}/${name}
version: 1.0.0
description: A brief description of the ${namespace}/${name} module
Expand Down
Loading
Loading