Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,29 @@ Use this to learn about command-specific options, refresh your memory about synt
$ nextflow help run
```

(cli-machine-readable-help)=

### Machine-readable help

:::{versionadded} 26.06.0-edge
:::

The `-help-json` option prints the help and options for a command as JSON instead of the human-formatted help text. This provides a stable, structured description of the CLI for tools and AI agents to consume, without scraping the rendered `-help` output.

The option is contextual, so you can discover the CLI one command at a time. At the top level, it lists the global options and an index of every available command:

```console
$ nextflow -help-json
```

Add it to any command to get that command's full options and arguments:

```console
$ nextflow run -help-json
```

The output describes each option with its name, flags, type, help text, and default value (where applicable). The schema is derived directly from the CLI definitions, so it stays in sync with the available commands and options.

### Version information

The `-v` and `-version` options print Nextflow version information.
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ Available options:
`-h`
: Print available commands and options.

`-help-json`
: :::{versionadded} 26.06.0-edge
:::
: Print the help and options as JSON, for consumption by tools and LLMs (see {ref}`cli-machine-readable-help`).
: Works as a top-level option (`nextflow -help-json`) and on any command (e.g. `nextflow run -help-json`).

`-log`
: Set Nextflow log file path (default: `.nextflow.log`). Must be a local path.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ class CliOptions {
@Parameter(names = ['-h'], description = 'Print this help', help = true)
boolean help

@Parameter(names = ['-help-json'], description = 'Print the command help and options as JSON (for tools and LLMs)', help = true)
boolean helpJson

@Parameter(names = ['-q','-quiet'], description = 'Do not print information messages' )
boolean quiet

Expand Down
237 changes: 237 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/cli/CliSchema.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.cli

import java.lang.reflect.Field

import com.beust.jcommander.DynamicParameter
import com.beust.jcommander.Parameter
import com.beust.jcommander.Parameters
import groovy.json.JsonOutput
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic

/**
* Build a machine-readable (JSON) description of the Nextflow command line
* interface by introspecting the JCommander {@link Parameter} / {@link Parameters}
* annotations.
*
* This powers the global {@code -help-json} flag, which lets tools and LLMs
* discover the CLI one command at a time without scraping the rendered
* {@code -help} text:
*
* <pre>
* nextflow -help-json # global options + index of all commands
* nextflow run -help-json # full option/argument detail for `run`
* </pre>
*
* @author Phil Ewels <phil.ewels@seqera.io>
*/
@CompileStatic
class CliSchema {

/**
* Build the schema for the top-level program: the global options plus a
* name + description index of every available command.
*/
static String root(CliOptions options, List<CmdBase> commands) {
final schema = [
name : 'nextflow',
path : 'nextflow',
usage : 'nextflow [options] COMMAND [arg...]',
params : paramsOf(CliOptions),
subcommands : commandIndex(commands),
]
return render(schema)
}

/**
* Build the schema for a single command: its usage, options and positional
* arguments.
*/
static String command(CmdBase cmd) {
final clazz = cmd.getClass()
final params = paramsOf(clazz)
final schema = [
name : cmd.name,
path : "nextflow ${cmd.name}".toString(),
usage : usageOf(cmd, params),
params : params,
] as Map<String,Object>

final description = descriptionOf(clazz)
if( description )
schema.help = description
final aliases = aliasesOf(clazz)
if( aliases )
schema.aliases = aliases

return render(schema)
}

/**
* Map each command name to its description and aliases. Nextflow commands
* are flat (no nested groups), so a single index lists them all; drill into
* any one with {@code nextflow <command> -help-json}.
*/
@CompileDynamic
private static Map<String,Object> commandIndex(List<CmdBase> commands) {
final index = new TreeMap<String,Object>()
for( CmdBase cmd : commands ) {
final clazz = cmd.getClass()
final description = descriptionOf(clazz)
// only advertise commands that opt in with a description, matching `-help`
if( !description )
continue
final entry = [help: description] as Map<String,Object>
final aliases = aliasesOf(clazz)
if( aliases )
entry.aliases = aliases
index[cmd.name] = entry
}
return index
}

/**
* Introspect the {@link Parameter} / {@link DynamicParameter} annotations of
* a command (or options) class into a list of JSON-friendly maps. Inherited
* fields are included; hidden and meta options ({@code -h}, {@code -help},
* {@code -help-json}) are skipped.
*/
@CompileDynamic
private static List<Map<String,Object>> paramsOf(Class clazz) {
final instance = newInstance(clazz)
final result = []
for( Field field : declaredFields(clazz) ) {
final p = field.getAnnotation(Parameter)
final dp = field.getAnnotation(DynamicParameter)
if( p )
addParam(result, field, p.names() as List<String>, p.description(), p.hidden(), p.required(), p.arity(), instance)
else if( dp )
addParam(result, field, dp.names() as List<String>, dp.description(), dp.hidden(), false, -1, instance)
}
// sort for a stable, deterministic ordering: reflection field order is not
// guaranteed by the JVM, but this is a machine-readable contract
result.sort { it.opts ? it.opts[0] : it.name }
return result
}

@CompileDynamic
private static void addParam(List result, Field field, List<String> names, String description, boolean hidden, boolean required, int arity, Object instance) {
if( hidden )
return
if( names.any { it in META_OPTIONS } )
return

final isArgument = !names
final isFlag = field.getType() == boolean || field.getType() == Boolean || arity == 0
final entry = [
name : field.getName(),
kind : isArgument ? 'argument' : 'option',
type : typeName(field),
] as Map<String,Object>
if( names )
entry.opts = names
if( description )
entry.help = description
if( required )
entry.required = true
if( isFlag )
entry.is_flag = true

final defValue = defaultOf(field, instance)
if( defValue != null && !isFlag )
entry.default = defValue

result << entry
}

/** Build a usage string, e.g. {@code nextflow run [options] <args>}. */
private static String usageOf(CmdBase cmd, List<Map<String,Object>> params) {
final pieces = ["nextflow ${cmd.name}".toString()]
if( params.any { it.kind == 'option' } )
pieces << '[options]'
if( params.any { it.kind == 'argument' } )
pieces << '[args...]'
return pieces.join(' ')
}

@CompileDynamic
private static String descriptionOf(Class clazz) {
return clazz.getAnnotation(Parameters)?.commandDescription() ?: null
}

@CompileDynamic
private static List<String> aliasesOf(Class clazz) {
final names = clazz.getAnnotation(Parameters)?.commandNames()
return names ? (names as List<String>) : null
}

private static String typeName(Field field) {
return field.getType().getSimpleName()
}

/** Read a field's default value from a fresh instance, ignoring empty values. */
@CompileDynamic
private static Object defaultOf(Field field, Object instance) {
if( instance == null )
return null
try {
field.setAccessible(true)
final value = field.get(instance)
if( value == null || value == false )
return null
if( value instanceof Collection && value.isEmpty() )
return null
if( value instanceof Map && value.isEmpty() )
return null
return value instanceof CharSequence || value instanceof Number || value instanceof Boolean
? value
: value.toString()
}
catch( Exception e ) {
return null
}
}

private static Object newInstance(Class clazz) {
try {
return clazz.getDeclaredConstructor().newInstance()
}
catch( Exception e ) {
return null
}
}

/** Collect declared fields from the class and its superclasses. */
private static List<Field> declaredFields(Class clazz) {
final fields = new ArrayList<Field>()
Class current = clazz
while( current != null && current != Object ) {
fields.addAll(current.getDeclaredFields())
current = current.getSuperclass()
}
return fields
}

private static String render(Map schema) {
return JsonOutput.prettyPrint(JsonOutput.toJson(schema))
}

private static final List<String> META_OPTIONS = ['-h', '-help', '-help-json']

}
3 changes: 3 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/cli/CmdBase.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ abstract class CmdBase implements Runnable {

@Parameter(names=['-h','-help'], description = 'Print the command usage', arity = 0, help = true)
boolean help

@Parameter(names=['-help-json'], description = 'Print the command help and options as JSON (for tools and LLMs)', arity = 0, help = true)
boolean helpJson
}
15 changes: 12 additions & 3 deletions modules/nextflow/src/main/groovy/nextflow/cli/Launcher.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ class Launcher {
println "Usage: nextflow [options] COMMAND [arg...]\n"
printOptions(CliOptions)
printCommands(allCommands)
println "Tip: add -help-json to any command for machine-readable help as JSON.\n"
}

@CompileDynamic
Expand Down Expand Up @@ -478,6 +479,10 @@ class Launcher {
return this
}

private boolean isHelpJson() {
options.helpJson || command?.helpJson
}

protected void checkForHelp() {
if( options.help || !command || command.help ) {
if( command instanceof UsageAware ) {
Expand Down Expand Up @@ -519,6 +524,13 @@ class Launcher {
return 0
}

// -- print out the machine-readable JSON help, then exit
if( isHelpJson() ) {
final json = command ? CliSchema.command(command) : CliSchema.root(options, allCommands)
println json
return 0
}

// -- print out the program help, then exit
checkForHelp()

Expand Down Expand Up @@ -705,9 +717,6 @@ class Launcher {

}

/*
* The application 'logo'
*/
/*
* The application 'logo'
*/
Expand Down
Loading
Loading