The Styx Book

Welcome to the Styx ecosystem - where command-line tools become type-safe programming interfaces.

What is Styx?

Styx is a compiler that transforms structured tool descriptions into type-safe language bindings for Python, TypeScript, and R. While originally developed for neuroimaging research, Styx works with any command-line tool that can be described in a structured format.

At its core, Styx allows you to:

  • Write once, use anywhere - Define tool interfaces once, use them in multiple languages
  • Catch errors early - Leverage type checking to prevent runtime errors
  • Focus on your work - Forget complex command-line arguments and focus on your actual research

Quick Overview

The Styx ecosystem consists of:

  • Styx Compiler - Generates language bindings from structured tool descriptions
  • NiWrap - A collection of tool descriptions for popular neuroimaging software
  • Boutiques - The current frontend format for describing tools

note

Found a bug in a NiWrap interface? Report it at the NiWrap issue tracker.

Interested in Styx compiler development? Visit the Styx compiler repository.

Getting Started

New to Styx? Head to the Getting Started section to learn the basics.

Already familiar and want to do more? Check out:

When to Use Styx

Styx is particularly valuable when:

  • You're building reproducible research workflows
  • You're developing tools that should be accessible across programming languages
  • You want to provide consistent interfaces to complex command-line tools
  • You need type safety when working with command-line utilities

Citation

If Styx helps your research, please cite:

@software{styx,
  author = {The Styx Contributors},
  title = {Styx: Type-safe command-line tool bindings},
  url = {https://github.com/styx-api/styx},
  year = {2023}
}

Getting Started

This section will guide you through your first steps with NiWrap, which provides type-safe language bindings for neuroimaging tools generated by the Styx compiler.

Installation

Start by installing the NiWrap package for your preferred language:

pip install niwrap
npm install niwrap
# or
yarn add niwrap
# Install from GitHub
devtools::install_github("styx-api/niwrap-r")

That's it! With this, you're ready to start using neuroimaging tools through NiWrap's bindings.

Your First Command

Let's run a simple brain extraction using FSL's BET tool:

from niwrap import fsl 

# Run brain extraction
result = fsl.bet(
    infile="subject_01/T1.nii.gz",
    outfile="subject_01/brain.nii.gz",
    binary_mask=True
)
import { fsl } from 'niwrap';

// Run brain extraction
const result = await fsl.bet({
    infile: "subject_01/T1.nii.gz",
    outfile: "subject_01/brain.nii.gz",
    binary_mask: true
});
library(niwrap)

# Run brain extraction
result <- fsl$bet(
    infile = "subject_01/T1.nii.gz",
    outfile = "subject_01/brain.nii.gz",
    binary_mask = TRUE
)

This will result into the following command being called:

bet subject_01/T1.nii.gz subject_01/brain.nii.gz -m

Notice how NiWrap automatically translates parameters like binary_mask=True into the appropriate command-line flags that the underlying tools expect.

Using Docker

Don't have FSL installed? No problem. You can run all tools in containers:

from niwrap import fsl
import niwrap

# Enable Docker for all subsequent commands
niwrap.use_docker()

# Now this will run inside an FSL Docker container
result = fsl.bet(
    infile="subject_01/T1.nii.gz",
    outfile="subject_01/brain.nii.gz",
    binary_mask=True
)
import { fsl, useDocker } from 'niwrap';

// Enable Docker for all subsequent commands
useDocker();

// Now this will run inside an FSL Docker container
const result = await fsl.bet({
    infile: "subject_01/T1.nii.gz",
    outfile: "subject_01/brain.nii.gz",
    binary_mask: true
});
library(niwrap)

# Enable Docker for all subsequent commands
niwrap::use_docker()

# Now this will run inside an FSL Docker container
result <- fsl$bet(
    infile = "subject_01/T1.nii.gz",
    outfile = "subject_01/brain.nii.gz",
    binary_mask = TRUE
)

Working with Results

Each command returns a result object containing all output files, making it easy to chain operations:

from niwrap import fsl

# Run brain extraction
bet_result = fsl.bet(
    infile="subject_01/T1.nii.gz",
    binary_mask=True
)

# Use the result in another NiWrap command
fsl.fast(infile=bet_result.outfile)

# Or use with other Python libraries
from nilearn.plotting import plot_anat
plot_anat(bet_result.outfile)
import { fsl } from 'niwrap';

async function processData() {
    // Run brain extraction
    const betResult = await fsl.bet({
        infile: "subject_01/T1.nii.gz",
        binary_mask: true
    });

    // Use the result in another NiWrap command
    await fsl.fast({
        infile: betResult.outfile
    });
}
library(niwrap)

# Run brain extraction
bet_result <- fsl$bet(
    infile = "subject_01/T1.nii.gz",
    binary_mask = TRUE
)

# Use the result in another NiWrap command
fsl$fast(infile = bet_result$outfile)

# Or use with other R packages
library(oro.nifti)
img <- readNIfTI(bet_result$outfile)

tip

NiWrap includes detailed documentation about every command, argument, and output file. You should be able to hover over any of them in your editor to view its documentation.

Discovering Available Tools

To explore what tools are available, use your editor's autocomplete functionality:

from niwrap import fsl

# Type "fsl." and use autocomplete to see available tools
# fsl.bet
# fsl.fast
# fsl.flirt
# ...
import { fsl } from 'niwrap';

// Type "fsl." and use autocomplete to see available tools
// fsl.bet
// fsl.fast
// fsl.flirt
// ...
library(niwrap)

# Type "fsl$" and use autocomplete to see available tools
# fsl$bet
# fsl$fast
# fsl$flirt
# ...

Each tool has type hints and documentation that describe its parameters and outputs.

Next Steps

Now that you've run your first commands, you might want to learn about:

Ready to see more complex examples? Check out the Examples section for real-world usage patterns.

Runners

Runners define how commands will be executed and how output files will be stored.

By default they will be executed with the system shell and outputs will be stored in a folder named styx_temp/ in the current working directory.

While this provides a good start, users may want more control where the data gets store or might not have all the software dependencies installed. The first step before packaging and deploying a pipeline should be to modify this behavior.

Official Runners

There are a of number official runners:

  • styxdefs.LocalRunner - This is the default Runner. It executes commands locally using the system shell.
  • styxdefs.DryRunner - This Runner dry-runs commands, useful when writing new wrappers to ensure commands are as expected.
  • styxdocker.DockerRunner - This Runner executes commands in a Docker container.
  • styxsingularity.SingularityRunner - This Runner executes commands in an Apptainer/Singularity container.
  • styxgraph.GraphRunner - This is a special Runner, capturing information about how commands are connected, returning a diagram.

Setting up a Runner

If you for example want to change where the LocalRunner stores data, we create a new instance of it and set it to be used globally:

from styxdefs import set_global_runner, LocalRunner

my_runner = LocalRunner()
my_runner.data_dir = "/some/folder"
set_global_runner(my_runner)

# Now you can use any Styx functions as usual

The same method can be used to set up other Runners:

from styxdefs import set_global_runner
from styxdocker import DockerRunner

my_runner = DockerRunner()
set_global_runner(my_runner)

# Now you can use any Styx functions as usual

important

Look at the individual Runners documentation to learn more about how they can be configured.

tip

For most users, configuring the global Runner once at the beginning of their script should be all they ever need.

Alternatively, if a specific function should be executed with a different Runner without modifying the global Runner, we can pass it as an argument to the wrapped command:

my_other_runner = DockerRunner()

fsl.bet(
    infile="my_file.nii.gz",
    runner=my_other_runner,
)

# Now you can use any Styx functions as usual

Middleware Runners

Middleware Runners are special runners that can be used on top of other runners. Currently, the GraphRunner, which creates a diagram by capturing how commands are connected, is the only official runner:

from styxdefs import set_global_runner, get_global_runner
from styxgraph import GraphRunner

my_runner = DockerRunner()
set_global_runner(GraphRunner(my_runner))  # Use GraphRunner middleware

# Use any Styx functions as usual
# ...

print(get_global_runner().mermaid())  # Print mermaid diagram

I/O: Runner File Handling

When you're using Styx wrapper functions with the default runners, you won't have to worry too much about I/O since a lot of it gets handled automatically for you.

Basic File Access

Take this example:

outputs = fsl.bet(infile="brain.nii", fractional_intensity=0.5)

outputs.mask_file  # File path to an individual output

The outputs object gives you structured access to generated files through properties that support autocompletion and include helpful documentation.

Dynamic File Access

Sometimes, when a descriptor hasn't been fully implemented or if the output structure is more complex, an output property may not be available.. For these cases, you can use the outputs.root property, which always points to the output directory:

outputs.root / "my_special_file.ext"

# Dynamic file access
for number in [1, 2, 3]:
    f = outputs.root / f"my_file_with_a_{number}.ext"

Default Runner Behavior

While custom Styx runners let you implement file I/O handling however you want, the default runners (LocalRunner, DockerRunner, and SingularityRunner) all work pretty similarly by default. They create a working directory (defaults to styx_temp/ in your current working directory) with individual folders for each Styx function call you make.

Directory Structure

79474bd248c4b2f1_5_bet/
^^^^^^^^^^^^^^^^ ^ ^^^
|                | |
|                | `-- Interface name (FSL BET)
|                `---- Execution number (5th call)
`--------------------- Unique session hash

Components:

  • Session hash: A unique random identifier generated per runner instance, preventing conflicts between pipeline executions
  • Execution number: Sequential counter for chronological ordering of function calls
  • Interface name: Human-readable identifier for the Styx function

This naming convention ensures unique, sortable, and identifiable output directories.

warning

You can clean up outputs using shutil.rmtree(outputs.root) for individual function outputs, or shutil.rmtree(get_global_runner().data_dir) to remove all outputs from the current session. Be careful - make sure you're not deleting any important data this way!

note

For advanced runner configuration and custom I/O behavior, see Subcommands and Writing Custom Runners.

Tips & best practices

Managing runner output

You may only want to keep certain outputs generated from your workflow. One strategy is to set the runner's output directory to temporary storage, copying only what should be saved to a more permanent location.

import shutil
from styxdefs import set_global_runner, LocalRunner

my_runner = LocalRunner()
my_runner.data_dir = "/some/temp/folder"
set_global_runner(my_runner)

# Perform some task
# ...

shutil.copy2(task_output.out_files, "/some/permanent/output/folder")

# Remove temporary directory for cleanup
shutil.rmtree(runner.data_dir) 

Workflow logging

All official runners have a logger available. To avoid having to setup a new (custom) logger, the runner's logger can be used.

import logging

from styxdefs import set_global_runner, LocalRunner

my_runner = LocalRunner()
set_global_runner(my_runner)

# Get and use the logger
logger = logging.getLogger(my_runner.logger_name)

Environment variables in runners

Environment variables can be passed onto the runners. These can be passed to via the environ attribute as a dictionary.

my_runner.environ = {"VARIABLE": str(variable_value)}

Styx on HPC clusters

Styx runners can also be used on High-Performance Computing (HPC) environments. The default LocalRunner can be used if the required software is available. However, in most cases, software will need to be installed or made available via container. In most HPC environments, Apptainer (formerly Singularity), is the container system of choice (in place of Docker). Styx provides an official Apptainer/Singularity runner, SingularityRunner, that can be used in HPC environments.

To use the SingularityRunner, the containers must first be downloaded such that they can be mapped for use. The key to map the container location to is the container metadata for each wrapped command. Let's take a look at an example:

First, we'll note that Mrtrix3 has the following container metadata:

{
    "container-image": {
        "image": "mrtrix3/mrtrix3:3.0.4",
        "type": "docker"
    }
}

We'll also download the container and install package associated with the runner.

apptainer pull docker://mrtrix3/mrtrix3:3.0.4 /container/directory/mrtrix3_3.0.4.sif

pip install styxsingularity

Now to use our runner:

from styxdefs import set_global_runner
from styxsingularity import SingularityRunner

my_runner = SingularityRunner(
    images={
        "mrtrix3/mrtrix3:3.0.4": "/container/directory/mrtrix3_3.0.4.sif"
    }
)
set_global_runner(my_runner)

# Now you can use Styx functions as usual

tip

If you wish to use a different downloaded container, you can map the key to the path of the other container. Note, commands may not be all supported if non-listed container used.

Local scratch storage

On HPC environments, local scratch storage is often made available on computing nodes. These often provide superior performance by using a locally-connected SSD instead of processing over network-attached storage. While not strictly necessary, runners can benefit by redirecting the temporary output to the local storage and copying the final outputs to the desired location. Take a look at the tips page for how to manage the runner output.

I Found a Bug

If you've encountered a problem, we'd love to help! The best place to report issues depends on what you're having trouble with.

NiWrap Issues (Most Common)

Most issues are related to specific neuroimaging tools or wrapper functions. For these, please use the NiWrap issue tracker:

🔗 Report NiWrap Issues

This includes:

  • Problems with specific tools (FSL, AFNI, ANTs, etc.)
  • Missing or incorrect wrapper functions
  • Tool-specific errors or unexpected behavior
  • Requests for new neuroimaging tools

The NiWrap repository has helpful issue templates to guide you:

  • 🐛 Report Problem with Tool - For bugs with specific interfaces
  • Request New Tool - For missing tools or packages
  • Question - For general questions about usage
  • 🙋‍♂️ Help Request - When you need assistance

Styx Core Issues

For problems with the core Styx framework (runners, file handling, etc.):

🔗 Report Styx Core Issues

This includes:

  • Problems with runners (LocalRunner, DockerRunner, etc.)
  • File I/O issues
  • Core framework bugs
  • Performance problems

Documentation Issues

Found a problem with this documentation or have suggestions for improvement?

🔗 Report Documentation Issues

This includes:

  • Typos, unclear explanations, or missing information
  • Broken links or formatting issues
  • Suggestions for new documentation topics
  • Examples that don't work as expected

Before Reporting

To get you help faster:

  1. Check existing issues - Someone might have already reported the same problem
  2. Include details - Error messages, code examples, and your environment details are super helpful
  3. Use the templates - They guide you through providing the right information

Thanks for helping make Styx better! 🎉

Advanced Concepts

This section covers more advanced topics for users who want to extend Styx beyond the standard functionality.

Writing Custom Runners

Learn how to build your own runners for specialized execution environments like remote clusters, cloud platforms, or custom logging systems. This is useful when the built-in runners don't quite fit your workflow needs.

Writing Custom Runners

While the official runners cover most common use cases, you might need to create your own custom runner for specific requirements - like running commands on a remote cluster, implementing custom logging, or handling specialized file systems.

Understanding the Runner Protocol

Custom runners need to implement two main protocols: Runner and Execution. Think of the Runner as a factory that creates Execution objects, and each Execution handles a single command run.

The Runner Protocol

Your runner class needs to implement just one method:

class MyRunner:
    def start_execution(self, metadata: Metadata) -> Execution:
        """Start an execution for a specific tool.
        
        Args:
            metadata: Information about the tool being executed (name, package, etc.)

        Returns:
            An Execution object that will handle the actual command execution
        """

The Execution Protocol

The Execution object does the heavy lifting with four key methods:

class MyExecution:
    def input_file(self, host_file: InputPathType, resolve_parent: bool = False, mutable: bool = False) -> str:
        """Handle input files - return where the command should find them"""

    def output_file(self, local_file: str, optional: bool = False) -> OutputPathType:
        """Handle output files - return where they'll be stored on the host"""

    def params(self, params: dict) -> dict:
        """Process or modify command parameters if needed"""

    def run(self, cargs: list[str], handle_stdout=None, handle_stderr=None) -> None:
        """Actually execute the command"""

Example: Simple Custom Runner

Let's build a custom runner that adds some logging and stores outputs in timestamped directories:

import pathlib
import subprocess
import logging
from datetime import datetime
from styxdefs import Runner, Execution, Metadata, InputPathType, OutputPathType

class TimestampedExecution(Execution):
    def __init__(self, output_dir: pathlib.Path, metadata: Metadata):
        self.output_dir = output_dir
        self.metadata = metadata
        self.logger = logging.getLogger(f"custom_runner.{metadata.name}")
        
        # Create output directory
        self.output_dir.mkdir(parents=True, exist_ok=True)

    def input_file(self, host_file: InputPathType, resolve_parent: bool = False, mutable: bool = False) -> str:
        # For simplicity, just return the absolute path
        return str(pathlib.Path(host_file).absolute())

    def output_file(self, local_file: str, optional: bool = False) -> OutputPathType:
        return self.output_dir / local_file

    def params(self, params: dict) -> dict:
        # Log parameters for debugging
        self.logger.info(f"Running {self.metadata.name} with params: {params}")
        return params

    def run(self, cargs: list[str], handle_stdout=None, handle_stderr=None) -> None:
        self.logger.info(f"Executing: {' '.join(cargs)}")
        
        # Run the command
        result = subprocess.run(
            cargs,
            cwd=self.output_dir,
            capture_output=True,
            text=True
        )
        
        # Handle output
        if handle_stdout and result.stdout:
            for line in result.stdout.splitlines():
                handle_stdout(line)
        if handle_stderr and result.stderr:
            for line in result.stderr.splitlines():
                handle_stderr(line)
                
        if result.returncode != 0:
            raise RuntimeError(f"Command failed with return code {result.returncode}")

class TimestampedRunner(Runner):
    def __init__(self, base_dir: str = "custom_outputs"):
        self.base_dir = pathlib.Path(base_dir)
        self.execution_counter = 0

    def start_execution(self, metadata: Metadata) -> Execution:
        # Create timestamped directory
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        output_dir = self.base_dir / f"{timestamp}_{self.execution_counter}_{metadata.name}"
        self.execution_counter += 1
        
        return TimestampedExecution(output_dir, metadata)

# Usage
from styxdefs import set_global_runner

custom_runner = TimestampedRunner(base_dir="my_custom_outputs")
set_global_runner(custom_runner)

Learning from Official Runners

The best way to understand custom runners is to look at how the official ones work:

LocalRunner Pattern

The LocalRunner is the simplest - it just runs commands directly on the host system. Notice how it:

  • Creates unique output directories using a hash and counter
  • Uses subprocess.Popen with threading for real-time output handling
  • Handles file paths by converting them to absolute paths

DockerRunner Pattern

The DockerRunner shows more complex file handling:

  • Maps host files to container paths using Docker mounts
  • Creates a run script inside the container
  • Handles the complexity of translating between host and container file systems

Advanced Patterns

Middleware Runners

You can create runners that wrap other runners to add functionality:

class LoggingRunner(Runner):
    def __init__(self, wrapped_runner: Runner):
        self.wrapped_runner = wrapped_runner
        self.logger = logging.getLogger("logging_runner")

    def start_execution(self, metadata: Metadata) -> Execution:
        self.logger.info(f"Starting execution of {metadata.name}")
        execution = self.wrapped_runner.start_execution(metadata)
        return LoggingExecution(execution, self.logger)

class LoggingExecution(Execution):
    def __init__(self, wrapped_execution: Execution, logger):
        self.wrapped = wrapped_execution
        self.logger = logger

    def run(self, cargs: list[str], handle_stdout=None, handle_stderr=None) -> None:
        self.logger.info(f"Running command: {' '.join(cargs)}")
        start_time = datetime.now()
        
        try:
            self.wrapped.run(cargs, handle_stdout, handle_stderr)
            duration = datetime.now() - start_time
            self.logger.info(f"Command completed in {duration}")
        except Exception as e:
            self.logger.error(f"Command failed: {e}")
            raise

    # Forward other methods to wrapped execution
    def input_file(self, *args, **kwargs): return self.wrapped.input_file(*args, **kwargs)
    def output_file(self, *args, **kwargs): return self.wrapped.output_file(*args, **kwargs)
    def params(self, *args, **kwargs): return self.wrapped.params(*args, **kwargs)

Remote Execution

For cluster or remote execution, you might implement SSH-based runners, SLURM job submission, or cloud-based execution.

Tips for Custom Runners

File Handling: Think carefully about where files live and how paths get translated. Input files need to be accessible to your execution environment, and output files need to end up where the user expects them.

Error Handling: Always check return codes and provide meaningful error messages. Consider implementing custom exception types like StyxDockerError for better debugging.

Logging: Good logging makes debugging much easier. Use structured logging with appropriate levels.

Threading: If you need real-time output handling (like showing progress), consider using threading like the official runners do.

Testing: Test your runner with various tools and edge cases. The DryRunner pattern is useful for testing command generation without actual execution.

tip

Start simple! Create a basic working runner first, then add advanced features like custom file handling or remote execution once you have the basics working.

tip

Look at the source code of official runners for inspiration. They handle many edge cases you might not think of initially.

Contributing

Thank you for your interest in contributing to the Styx ecosystem! This section provides detailed guides for contributing to different components of the project.

Contribution Areas

The Styx ecosystem consists of several components that work together:

  1. NiWrap - A collection of descriptors for neuroimaging tools
  2. Styx Compiler - Transforms descriptors into type-safe language bindings
  3. Language-specific packages - The compiled outputs for Python, TypeScript, and R
  4. Documentation - The Styx Book that you're reading right now

Guides in this Section

Why Contribute?

Your contributions help expand the ecosystem of type-safe neuroimaging tools across multiple programming languages. Whether you're fixing a bug, adding support for a new tool, or improving documentation, your work directly benefits researchers and developers in the neuroimaging community.

We strive to make contributing as accessible as possible, regardless of your technical background:

  • Documentation contributions are a great way to get started
  • NiWrap descriptor contributions are accessible to researchers with basic JSON knowledge
  • Compiler contributions welcome those with more programming experience

We value clear, approachable documentation and code that helps make neuroimaging analysis more accessible to all.

Getting Help

If you're not sure where to start or have questions about contributing:

  1. Check the NiWrap issue tracker or the Styx compiler issue tracker
  2. Look for issues labeled "good first issue" for beginner-friendly tasks
  3. Reach out to the maintainers through GitHub issues

We welcome contributors of all experience levels and backgrounds!

Contributing to NiWrap

Thank you for your interest in contributing to NiWrap! This guide will help you understand how NiWrap works and how you can contribute to make it even better.

Overview

NiWrap is a collection of Boutiques descriptors for neuroimaging tools. If you've found a bug in one of the interfaces or you're missing an interface, you can either report it in the NiWrap issue tracker or attempt to fix it yourself by following this guide.

Table of Contents

  1. Understanding NiWrap and Boutiques
  2. Repository Structure
  3. Development Environment Setup
  4. Contributing Descriptors
  5. Working with Package Configurations
  6. Source Extraction
  7. Testing Your Changes
  8. Contribution Workflow
  9. Advanced Topics
  10. Getting Help

Understanding NiWrap and Boutiques

NiWrap is a collection of Boutiques descriptors for neuroimaging tools. These descriptors are used by the Styx compiler to generate type-safe language bindings in Python, TypeScript, and R.

What is Boutiques?

Boutiques is a JSON-based descriptive standard for command-line tools. It allows you to specify:

  • Command-line arguments and their types
  • Input and output files
  • Dependencies between parameters
  • Container configurations
  • And much more

Boutiques descriptors serve as the "source of truth" for NiWrap, defining how each neuroimaging tool works and how users can interact with it.

Note: For a comprehensive guide to Boutiques concepts and advanced usage, see the Boutiques Guide section of this documentation.

Here's a simplified example of a Boutiques descriptor:

{
  "name": "example_tool",
  "description": "An example neuroimaging tool",
  "tool-version": "1.0.0",
  "command-line": "example_tool [INPUT] [OUTPUT] [OPTIONS]",
  "inputs": [
    {
      "id": "input_file",
      "name": "Input file",
      "type": "File",
      "value-key": "[INPUT]",
      "optional": false
    },
    {
      "id": "output_file",
      "name": "Output file",
      "type": "String",
      "value-key": "[OUTPUT]",
      "optional": false
    },
    {
      "id": "verbose",
      "name": "Verbose output",
      "type": "Flag",
      "command-line-flag": "-v",
      "optional": true
    }
  ],
  "output-files": [
    {
      "id": "output",
      "name": "Output file",
      "path-template": "[OUTPUT]"
    }
  ]
}

This descriptor defines a tool with an input file, output file, and a verbose flag. The Styx compiler transforms this into type-safe language bindings that users can call from their preferred programming language.

Repository Structure

The NiWrap repository is organized as follows:

niwrap/
├── build-templates/      # Templates used during the build process
├── build.py              # Main build script for generating language bindings
├── descriptors/          # Boutiques descriptors for each tool
│   ├── afni/             # AFNI tools
│   ├── ants/             # ANTs tools
│   ├── fsl/              # FSL tools
│   └── ...
├── extraction/           # Tools for extracting parameter information
├── packages/             # Package configuration files
├── schemas/              # JSON schemas for validation
├── scripts/              # Utility scripts for maintaining descriptors
├── tests/                # Tests for descriptors
└── update_readme.py      # Script to update the README with current tool coverage

Development Environment Setup

Setting up a proper development environment will make contributing to NiWrap more efficient.

Basic Setup

# Clone the repository
git clone https://github.com/styx-api/niwrap.git
cd niwrap

# Install required dependencies using pip
pip install pytest
pip install git+https://github.com/styx-api/styx.git

Note: A formal requirements.txt file may be added in the future. For now, installing pytest and the Styx compiler from GitHub should be sufficient for most development tasks.

VSCode JSON Schema Validation

Visual Studio Code users can enable automatic validation and autocompletion for Boutiques descriptors by configuring JSON schema validation:

  1. Create or open .vscode/settings.json in your NiWrap repository
  2. Add the following configuration:
{
    "json.schemas": [
        {
            "fileMatch": [
                "descriptors/**/*.json"
            ],
            "url": "./schemas/descriptor.schema.json"
        }
    ]
}

This setup provides real-time feedback as you edit descriptor files, highlighting potential errors and offering suggestions based on the Boutiques schema.

Contributing Descriptors

The most common contribution to NiWrap is adding or improving Boutiques descriptors for neuroimaging tools.

Finding the Right Location

Descriptors are organized by tool suite in the descriptors/ directory:

descriptors/
├── afni/        # AFNI tools
├── ants/        # ANTs tools
├── fsl/         # FSL tools
├── mrtrix3/     # MRTrix3 tools
└── ...

Place your descriptor in the appropriate directory, or create a new directory if you're adding a tool from a new suite.

Fixing an Existing Descriptor

If you've found a bug in an existing tool interface, you'll need to modify its descriptor.

Example: Fixing a Parameter Type

Let's say you discovered that the fractional_intensity parameter in FSL's BET tool should be a floating-point number between 0 and 1, but it's currently defined as an integer:

// Original descriptor (simplified)
{
  "name": "bet",
  "inputs": [
    {
      "id": "fractional_intensity",
      "name": "Fractional intensity threshold",
      "type": "Number",
      "integer": true,
      "minimum": 0,
      "maximum": 1
    }
  ]
}

To fix this, you'd change the descriptor to:

// Fixed descriptor
{
  "name": "bet",
  "inputs": [
    {
      "id": "fractional_intensity",
      "name": "Fractional intensity threshold",
      "type": "Number",
      "integer": false,
      "minimum": 0,
      "maximum": 1
    }
  ]
}

Adding a Missing Parameter

If a tool has a parameter that isn't exposed in NiWrap, you can add it to the descriptor.

Example: Adding a Missing Flag

Suppose FSL's FAST tool has a -N flag for no bias field correction, but it's missing from the descriptor:

// Original descriptor (simplified)
{
  "name": "fast",
  "inputs": [
    // existing parameters
  ]
}

You would add the new parameter:

// Updated descriptor
{
  "name": "fast",
  "inputs": [
    // existing parameters
    {
      "id": "no_bias_field_correction",
      "name": "No bias field correction",
      "type": "Flag",
      "command-line-flag": "-N",
      "description": "Turns off bias field correction"
    }
  ]
}

Creating a New Descriptor

If you want to add support for a completely new tool, you'll need to create a new descriptor from scratch.

Example: Creating a New Descriptor

Here's a simplified example of creating a descriptor for a fictional tool called brainanalyze:

{
  "name": "brainanalyze",
  "tool-version": "1.0.0",
  "description": "Analyzes brain structures in neuroimaging data",
  "command-line": "brainanalyze [INPUT] [OUTPUT] [OPTIONS]",
  "container-image": {
    "image": "neuroimaging/brainanalyze:1.0.0",
    "type": "docker"
  },
  "inputs": [
    {
      "id": "input_file",
      "name": "Input file",
      "type": "File",
      "value-key": "[INPUT]",
      "description": "Input neuroimaging file (NIFTI format)",
      "optional": false
    },
    {
      "id": "output_file",
      "name": "Output file",
      "type": "String",
      "value-key": "[OUTPUT]",
      "description": "Output file path",
      "optional": false
    },
    {
      "id": "smoothing",
      "name": "Smoothing factor",
      "type": "Number",
      "value-key": "[OPTIONS]",
      "command-line-flag": "-s",
      "integer": false,
      "minimum": 0,
      "maximum": 10,
      "description": "Smoothing factor (0-10)",
      "optional": true,
      "default-value": 2.5
    }
  ],
  "output-files": [
    {
      "id": "output",
      "name": "Output file",
      "path-template": "[OUTPUT]",
      "description": "Output analysis file"
    }
  ]
}

Working with Package Configurations

The packages/ directory contains package-level configuration files that define metadata for each neuroimaging tool suite.

Package Configuration Structure

A typical package configuration file looks like this:

{
  "name": "FSL",
  "author": "FMRIB Analysis Group, University of Oxford",
  "url": "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki",
  "approach": "Manual",
  "status": "Experimental",
  "container": "brainlife/fsl:6.0.4-patched2",
  "version": "6.0.5",
  "description": "FSL is a comprehensive library of analysis tools for FMRI, MRI and diffusion brain imaging data.",
  "id": "fsl",
  "api": {
    "endpoints": [
      {
        "target": "AnatomicalAverage",
        "status": "done",
        "descriptor": "descriptors/fsl/AnatomicalAverage.json"
      },
      // More endpoints...
    ]
  }
}

The status field in each endpoint entry can have one of three values:

  • "done": The descriptor is complete and ready for use
  • "missing": The tool is identified but the descriptor is not yet created
  • "ignore": The tool should be deliberately excluded from the API

Updating Package Configurations

When adding or modifying descriptors, remember to update the corresponding package configuration:

  1. For a new tool, add a new endpoint entry in the appropriate package file
  2. When updating a tool descriptor, ensure its status is set to "done"
  3. If adding a new tool suite, create a new package configuration file

Source Extraction

NiWrap's extraction directory contains specialized code that modifies the source code of neuroimaging toolboxes to programmatically extract tool information.

How Source Extraction Works

Source extraction involves:

  1. Modifying the original source code of a neuroimaging tool
  2. Adding instrumentation to dump tool information during compilation or runtime
  3. Capturing this information in a structured format that can be transformed into Boutiques descriptors

This is an advanced contribution area that typically requires:

  • Understanding of the tool's source code and architecture
  • Programming skills in the language the tool is written in
  • Familiarity with the tool's command-line interface

Using LLM Prompts for Initial Descriptor Creation

The extraction directory also contains LLM (Large Language Model) prompts that can help bootstrap the creation of descriptors:

  1. Capture the help text of a neuroimaging tool:

    mytool --help > tool_help.txt
    
  2. Use the provided LLM prompts with a model like Claude or GPT to generate an initial Boutiques descriptor:

    # Example prompt structure (check extraction directory for specifics)
    Given the following help text for a neuroimaging tool, create a Boutiques descriptor:
    
    [Paste help text]
    
  3. Review and refine the generated descriptor to ensure accuracy

  4. Place the final descriptor in the appropriate directory under descriptors/

This approach is particularly useful for tools without structured extraction capabilities and can significantly speed up the initial descriptor creation process.

For Tool Maintainers

If you're a maintainer of a neuroimaging tool and would like to collaborate on better integration with NiWrap:

  • We welcome direct collaboration on source extraction
  • This can ensure that your tool's interface is accurately represented in NiWrap
  • Contact the NiWrap team through GitHub issues to discuss collaboration opportunities

Testing Your Changes

After modifying or creating descriptors, you should test them to ensure they work correctly:

  1. Use the NiWrap test suite:

    # Run tests for a specific tool
    python -m pytest tests/test_descriptors.py::test_descriptor_validity::test_tool_descriptor
    
  2. Build the project to check if your descriptors can be processed by Styx:

    python build.py
    

Note: NiWrap does not use the original Boutiques runtime (bosh). All testing and validation should be done using NiWrap's own build and test utilities.

Contribution Workflow

Here's a step-by-step guide to contributing to NiWrap:

  1. Fork the repository: Create your own fork of NiWrap on GitHub
  2. Clone your fork:
    git clone https://github.com/your-username/niwrap.git
    cd niwrap
    
  3. Create a branch:
    git checkout -b fix-fsl-bet-descriptor
    
  4. Make changes: Modify or add descriptors in the appropriate directory
  5. Update package configuration: If necessary, update the corresponding package configuration file
  6. Test: Ensure your changes work correctly using the testing methods described above
  7. Commit your changes:
    git add descriptors/fsl/bet.json packages/fsl.json
    git commit -m "Fix: Update FSL BET fractional intensity parameter type"
    
  8. Push your changes:
    git push origin fix-fsl-bet-descriptor
    
  9. Submit a PR: Create a pull request on GitHub with a clear description of your changes

Advanced Topics

Adding a New Tool Suite

If you want to add support for an entirely new neuroimaging tool suite (e.g., adding a new package like SPM or BrainSuite):

  1. Create a new directory in descriptors/ for the tool suite
  2. Create descriptors for each tool you want to support
  3. Create a new package configuration file in packages/
  4. Consider creating extraction scripts in the extraction/ directory

Creating Helper Scripts

For complex tools, you might need to create helper scripts to streamline the creation of descriptors:

  1. Add your script to the scripts/ directory
  2. Document its usage in a comment at the top of the script
  3. Reference it in your pull request to help maintainers understand its purpose

Getting Help

If you're unsure about anything or need guidance, you can:

Feel free to ask specific questions in your issue or pull request. The maintainers are happy to help guide you through the process.

Thank you for contributing to NiWrap and helping make neuroimaging analysis more accessible across multiple programming languages!

Contributing to the Styx Compiler

The Styx compiler is the core technology that transforms Boutiques descriptors into type-safe language bindings. This guide provides a basic overview of the compiler architecture and how to contribute to its development.

Repository Structure

The Styx compiler is organized into several key components:

styx/
├── src/
│   └── styx/
│       ├── frontend/       # Parses input formats into IR
│       │   └── boutiques/  # Boutiques-specific frontend
│       ├── ir/             # Intermediate Representation
│       └── backend/        # Code generation for target languages
│           ├── generic/    # Language-agnostic utilities
│           ├── python/     # Python-specific code generation
│           ├── typescript/ # TypeScript-specific code generation
│           └── r/          # R-specific code generation
├── tests/                  # Unit and integration tests
└── docs/                   # Documentation

Compiler Architecture

The Styx compiler follows a traditional compiler design with three main phases:

  1. Frontend: Parses input formats (e.g., Boutiques descriptors) into an Intermediate Representation (IR)
  2. IR: A language-agnostic representation of the tool interface
  3. Backend: Generates code for target languages (Python, TypeScript, R) from the IR

Setting Up Your Development Environment

# Clone the repository
git clone https://github.com/styx-api/styx.git
cd styx

# Install development dependencies using uv
uv pip install -e .

# Run tests
python -m pytest tests/

Common Contribution Areas

Adding Features to Existing Language Backends

If you want to improve code generation for a specific language:

  1. Locate the language provider in src/styx/backend/<language>/languageprovider.py
  2. Make your changes to the code generation logic
  3. Add tests in the tests/ directory
  4. Run the test suite to ensure everything works as expected

Adding Support for a New Language

To add support for a new target language:

  1. Create a new directory in src/styx/backend/ for your language
  2. Implement a language provider that conforms to the interface in backend/generic/languageprovider.py
  3. Add language-specific code generation logic
  4. Add tests for your new language backend

Improving the IR

If you want to enhance the Intermediate Representation:

  1. Make changes to the IR structure in src/styx/ir/core.py
  2. Update the normalization and optimization passes if necessary
  3. Ensure all language backends can handle your IR changes
  4. Add tests for your IR modifications

Testing Your Changes

The Styx compiler has a comprehensive test suite:

# Run all tests
python -m pytest

# Run specific test files
python -m pytest tests/test_output_files.py

# Run with verbose output
python -m pytest -v

Documentation

Styx uses pdoc for API documentation:

# Install pdoc if needed
pip install pdoc

# Generate documentation
pdoc --html --output-dir docs/api src/styx

Getting Help

The Styx compiler is a complex piece of software. If you're having trouble:

  1. Check existing issues on GitHub
  2. Look at the test cases to understand how different components work
  3. Reach out to the maintainers through GitHub issues

Next Steps

While contributing to the Styx compiler requires more technical expertise than contributing to NiWrap descriptors, it's a rewarding way to improve the entire ecosystem. Start with small changes and work your way up to more complex features.

For most users interested in neuroimaging tools, contributing to NiWrap might be a more accessible starting point.

Contributing to the Book

This guide explains how to contribute to the Styx Book documentation. Good documentation is essential for making neuroimaging tools accessible to researchers, so your contributions here are highly valuable.

Documentation Style Guide

Friendly and Accessible Tone

The Styx Book aims to be approachable for all users, including those without extensive technical backgrounds. When writing:

  • Use a conversational, friendly tone - Write as if you're explaining concepts to a colleague
  • Avoid unnecessary jargon - When technical terms are needed, explain them
  • Consider the audience - Remember that many readers will be neuroscientists, not software developers
  • Use examples - Concrete examples help clarify abstract concepts

Technical Level by Section

Different sections of the book target different technical levels:

  • Getting Started & Examples: Written for neuroscientists with basic programming knowledge
  • Contributing to NiWrap: Accessible to researchers with minimal software development experience
  • Advanced Concepts & Styx Compiler: Can assume more technical background, but still aim for clarity

Book Structure

The Styx Book is built using mdBook, a tool for creating online books from Markdown files.

Repository Structure

book/
├── src/
│   ├── SUMMARY.md            # Book structure/table of contents
│   ├── README.md             # Book landing page
│   ├── getting_started/      # Getting started guides
│   ├── contributing/         # Contribution guides
│   ├── examples/             # Example workflows
│   ├── advanced_concepts/    # More technical topics
│   └── boutiques_guide/      # Boutiques reference
├── theme/                    # Custom styling (if applicable)
└── book.toml                 # Configuration file

Making Changes to the Book

Local Development

  1. Install mdBook (if not already installed):

    cargo install mdbook
    # Or use your system's package manager
    
    # Optional: Mermaid diagram rendering
    # cargo install mdbook-mermaid
    
  2. Clone the repository:

    git clone https://github.com/styx-api/styxbook.git
    cd styxbook
    
  3. Serve the book locally to see changes in real-time:

    mdbook serve
    # Open http://localhost:3000 in your browser
    
  4. Edit Markdown files in the src/ directory

    • Changes will automatically reload in your browser

Contributing Changes

  1. Create a branch for your changes:

    git checkout -b improve-getting-started
    
  2. Make your edits to the relevant Markdown files

  3. Commit and push your changes:

    git add .
    git commit -m "Improve getting started documentation"
    git push origin improve-getting-started
    
  4. Open a pull request on GitHub

Writing Guidelines

Content Structure

  • Use clear headings and subheadings
  • Break long sections into digestible chunks
  • Include a brief introduction at the beginning of each page
  • End complex sections with a summary or key takeaways

Code Examples

  • Always include complete, runnable examples when possible
  • Explain what the code does, not just show it
  • Use syntax highlighting for code blocks (e.g., ```python)
  • Include expected output where helpful

Images and Diagrams

  • Use screenshots or diagrams to illustrate complex concepts
  • Ensure images have alt text for accessibility
  • Keep diagrams simple and focused on the key point

Example Improvement

Less Helpful:

## Command Execution
The execute_command function runs commands with appropriate argument handling.

More Helpful:

## Running Neuroimaging Tools

When you call a function like `fsl.bet()`, NiWrap handles all the complex command-line arguments for you behind the scenes. 

For example, this simple Python code:

```python
from niwrap import fsl

fsl.bet(infile="T1.nii.gz", outfile="brain.nii.gz", fractional_intensity=0.5)
```

Gets translated into the equivalent command-line call:

```bash
bet T1.nii.gz brain.nii.gz -f 0.5
```

This conversion happens automatically, saving you from remembering the exact command syntax.

Getting Help

If you're unsure about how to document something or have questions about the book structure:

  1. Open an issue in the Styx Book repository
  2. Ask for guidance in your pull request
  3. Look at existing documentation for similar features as a reference

Thank you for helping improve the Styx Book! Your contributions make these tools more accessible to the neuroimaging community.

Examples

Short examples

Styx in the wild

Example: MRI anatomical preproc

The following is a toy implementation of a minimalistic MRI T1 anatomical preprocessing.

from niwrap import fsl, set_global_runner, DockerRunner
import os

def anatomical_preprocessing(input_file):
    # Step 1: Reorient to standard space
    reorient_output = fsl.fslreorient2std(
        input_image=input_file,
    )

    # Step 2: Robustly crop the image
    robustfov_output = fsl.robustfov(
        input_file=reorient_output.output_image,
    )

    # Step 3: Brain extraction
    bet_output = fsl.bet(
        infile=robustfov_output.output_roi_volume,
        fractional_intensity=0.5,  # Fractional intensity threshold
        robust_iters=True,
        binary_mask=True,
        approx_skull=True,
    )

    # Step 4: Tissue segmentation
    seg_output = fsl.fast(
        in_files=[bet_output.outfile],
        img_type=3  # 3 tissue classes
    )

    print("Anatomical preprocessing completed.")
    return bet_output, seg_output

if __name__ == "__main__":
    input_file = "path/to/your/input/T1w.nii.gz"
    output_dir = "my_output"

    # Set up the Docker runner
    set_global_runner(DockerRunner(data_dir=output_dir))

    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)

    # Run the anatomical preprocessing
    brain, segmentation = anatomical_preprocessing(input_file)

Full source.

Example: Generate mermaid graph

This example takes the previous example (anatomical preprocessing) and shows how the GraphRunner can be used to reconstruct an execution graph to then generate a mermaid graph.

note

The GraphRunner is still in development and not ready for wide-spread use. At this point this example serves more as a tech-demo.

from niwrap import fsl, set_global_runner, DockerRunner, GraphRunner
import os

def anatomical_preprocessing(input_file):
    # Step 1: Reorient to standard space
    reorient_output = fsl.fslreorient2std(
        input_image=input_file,
    )

    # Step 2: Robustly crop the image
    robustfov_output = fsl.robustfov(
        input_file=reorient_output.output_image,
    )

    # Step 3: Brain extraction
    bet_output = fsl.bet(
        infile=robustfov_output.output_roi_volume,
        fractional_intensity=0.5,  # Fractional intensity threshold
        robust_iters=True,
        binary_mask=True,
        approx_skull=True,
    )

    # Step 4: Tissue segmentation
    seg_output = fsl.fast(
        in_files=[bet_output.outfile],
        img_type=3  # 3 tissue classes
    )

    print("Anatomical preprocessing completed.")
    return bet_output, seg_output

if __name__ == "__main__":
    input_file = "path/to/your/input/T1w.nii.gz"
    output_dir = "my_output"

    # Set up the Docker runner
    runner = DockerRunner(data_dir=output_dir)
    graph_runner = GraphRunner(base=runner)
    set_global_runner(graph_runner)

    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)

    # Run the anatomical preprocessing
    brain, segmentation = anatomical_preprocessing(input_file)

    print(graph_runner.node_graph_mermaid())

Full source.

graph TD
  fslreorient2std
  robustfov
  bet
  fast
  fslreorient2std --> robustfov
  robustfov --> bet
  bet --> fast

Example: Dynamic runners

A common pattern in processing pipelines with Styx is dynamically choosing what runner Styx should use. This allows the same pipeline to run e.g. both on your local machine for testing as well as on your HPC cluster.

from styxdefs import set_global_runner, LocalRunner
from styxdocker import DockerRunner
from styxsingularity import SingularityRunner

runner_type = "docker"  # You could read this from a CLI argument,
                        # config file, or check what system you are
                        # running on.

if runner_type == "docker":
    print("Using docker runner.")
    runner = DockerRunner()

elif runner_type == "singularity":
    print("Using singularity runner.")
    runner = SingularityRunner()

else:
    print("Using local runner.")
    runner = LocalRunner()

set_global_runner(runner)

Full source.

Boutiques Format in the Styx Ecosystem

What is Boutiques?

Boutiques is a JSON-based descriptive standard for command-line tools. It provides a structured way to specify:

  • Command-line arguments and their types
  • Input and output files
  • Dependencies between parameters
  • Container configurations
  • And much more

In the Styx ecosystem, Boutiques descriptors serve as the "source of truth" that defines how each tool works and how users can interact with it.

Role in the Styx Ecosystem

Styx is a general-purpose compiler that transforms structured tool descriptions (currently using Boutiques format) into type-safe language bindings for Python, TypeScript, and R. While NiWrap focuses specifically on neuroimaging tools, Styx itself works with any command-line tool that can be described in a structured format.

The workflow is:

  1. Tool interfaces are defined in Boutiques descriptors
  2. Styx processes these descriptors to generate language bindings
  3. Users access the tools through type-safe interfaces in their preferred language

This approach allows you to:

  • Define a tool interface once and use it in multiple languages
  • Leverage type-checking to catch errors at compile time
  • Focus on your work instead of wrestling with command-line arguments

Extensions to the Standard

While Styx is compatible with the core Boutiques format, we've added several extensions to better support complex tools:

  • Subcommands: Hierarchical command structures with different parameter sets
  • Stdout/Stderr as outputs: Capturing command output as structured data
  • Enhanced parameter types: Additional validation and type information
  • Path extension handling: Smart handling of file extensions

These extensions are being proposed for inclusion in the core Boutiques standard.

Guide Structure

This guide is organized into several sections:

  1. Basic Structure - Core fields, parameter types, and command-line formation
  2. Subcommands - Detailed explanation of the subcommand extension
  3. File Handling - Input/output file handling, path templates, extensions
  4. Advanced Features - Additional fields, extensions, and configuration options
  5. Examples - Complete examples showing different patterns

Quick Example

Here's a simplified example of a Boutiques descriptor:

{
  "name": "example_tool",
  "description": "An example tool",
  "tool-version": "1.0.0",
  "command-line": "example_tool [INPUT] [OUTPUT] [OPTIONS]",
  "inputs": [
    {
      "id": "input_file",
      "name": "Input file",
      "type": "File",
      "value-key": "[INPUT]",
      "optional": false
    },
    {
      "id": "output_file",
      "name": "Output file",
      "type": "String",
      "value-key": "[OUTPUT]",
      "optional": false
    },
    {
      "id": "verbose",
      "name": "Verbose output",
      "type": "Flag",
      "command-line-flag": "-v",
      "value-key": "[OPTIONS]",
      "optional": true
    }
  ],
  "output-files": [
    {
      "id": "output",
      "name": "Output file",
      "path-template": "[OUTPUT]"
    }
  ]
}

This descriptor defines a tool with an input file, output file, and a verbose flag. The Styx compiler transforms this into type-safe language bindings that users can call from their preferred programming language.

Next Steps

Start with the Basic Structure section to learn the core concepts of the Boutiques format as used in the Styx ecosystem.

Basic Structure of Boutiques Descriptors

A Boutiques descriptor is a JSON file with a specific structure. This page explains the core components and how they fit together.

Top-level Fields

Required Fields

FieldDescriptionExample
nameShort name of the tool"bet"
descriptionDetailed description of what the tool does"Automated brain extraction tool for FSL"
tool-versionVersion of the tool being described"6.0.4"
schema-versionVersion of the Boutiques schema"0.5"
command-lineTemplate for the command with placeholders"bet [INFILE] [MASKFILE] [OPTIONS]"
inputsArray of input parameters[{ "id": "infile", ... }]

Common Optional Fields

FieldDescriptionExample
authorAuthor of the tool"FMRIB Analysis Group, University of Oxford"
urlURL for the tool's documentation"https://fsl.fmrib.ox.ac.uk/fsl/fslwiki"
container-imageContainer configuration{ "type": "docker", "image": "..." }
output-filesArray of output files[{ "id": "outfile", ... }]
stdout-outputCapture stdout as an output{ "id": "stdout_data", ... }
stderr-outputCapture stderr as an output{ "id": "error_log", ... }

Unused Boutiques Fields

The following standard Boutiques fields are not currently used in the Styx ecosystem:

  • environment-variables: Environment variables to set
  • tests: Sample invocations for testing
  • online-platform-urls: URLs to platforms where tool is available
  • invocation-schema: Custom schema for tool-specific invocation validation
  • suggested-resources: Computational resources needed
  • tags: Categorization tags
  • error-codes: Tool-specific error codes
  • custom: Custom tool-specific metadata

Input Parameters

Input parameters define the arguments that can be passed to a tool. They're specified in the inputs array.

Common Parameter Fields

FieldDescriptionRequiredExample
idUnique identifier (alphanumeric + underscores)Yes"input_file"
nameHuman-readable nameYes"Input file"
descriptionDetailed descriptionNo"The input image in NIFTI format"
value-keyPlaceholder in command-line templateYes"[INPUT_FILE]"
optionalWhether parameter is requiredYestrue
command-line-flagCommand-line option prefixNo"-i"
default-valueDefault value if not specifiedNo"standard.nii.gz"
value-choicesArray of allowed valuesNo["small", "medium", "large"]

Parameter Types

Basic Types

TypeDescriptionAttributesExample
FileFile pathN/A{ "type": "File", "id": "input_image" }
StringText stringN/A{ "type": "String", "id": "output_prefix" }
NumberNumeric valueinteger: boolean, minimum, maximum{ "type": "Number", "integer": false, "minimum": 0, "maximum": 1 }
FlagBoolean flag (no value)N/A{ "type": "Flag", "id": "verbose" }

List Variants

Any basic type can be made into a list by adding "list": true:

{
  "id": "coordinates", 
  "type": "Number",
  "list": true,
  "min-list-entries": 3,
  "max-list-entries": 3
}

Optional list-related fields:

  • min-list-entries: Minimum number of elements required
  • max-list-entries: Maximum number of elements allowed
  • list-separator: Character(s) used to join list values (default is space)

Example with a custom separator:

{
  "id": "tags",
  "type": "String",
  "list": true,
  "list-separator": ","
}

This would result in a command-line argument like: --tags tag1,tag2,tag3

Command-Line Formation

The command-line is formed by replacing value-keys in the command-line template with actual parameter values.

Value Keys

Value keys connect parameters to positions in the command line. In the Styx ecosystem, they should:

  • Be enclosed in square brackets: [LIKE_THIS]
  • Use only uppercase letters, numbers, and underscores
  • Match exactly in the command-line template

Example:

"command-line": "bet [INFILE] [OUTFILE] [OPTIONS]",
"inputs": [
  {
    "id": "infile",
    "value-key": "[INFILE]",
    "type": "File"
  }
]

This replaces [INFILE] in the command-line with the actual file path.

Command-Line Flags

Command-line flags are specified with the command-line-flag field:

{
  "id": "verbose",
  "command-line-flag": "-v",
  "value-key": "[VERBOSE]",
  "type": "Flag"
}

For flags with values, you can control the separator between the flag and value:

{
  "id": "threshold",
  "command-line-flag": "--threshold",
  "command-line-flag-separator": "=",
  "value-key": "[THRESHOLD]",
  "type": "Number"
}

This would result in --threshold=0.5 rather than --threshold 0.5.

Output Files

Output files are defined in the output-files array. Unlike inputs, these aren't actually files passed to the command line but rather specifications of what files will be produced:

"output-files": [
  {
    "id": "brain_mask",
    "name": "Brain Mask Image",
    "description": "Binary mask of the brain",
    "path-template": "[OUTDIR]/[PREFIX]_mask.nii.gz",
    "optional": false
  }
]

The path-template uses the same value keys from inputs to construct the output path.

Common output file fields:

FieldDescriptionRequiredExample
idUnique identifierYes"brain_mask"
nameHuman-readable nameYes"Brain Mask Image"
descriptionDetailed descriptionNo"Binary mask of the brain"
path-templateTemplate for output file pathYes"[OUTPUT_DIR]/[PREFIX]_mask.nii.gz"
optionalWhether file might not be producedNotrue

Container Configuration

Container configurations help ensure reproducibility:

"container-image": {
  "type": "docker",
  "image": "brainlife/fsl:6.0.4-patched2"
}

In the Styx ecosystem, primarily the type and image fields are used, with Docker as the only container type.

Cross-References

Now that you understand the basic structure, learn more about:

  • Subcommands - For tools with complex structure and hierarchical parameters
  • File Handling - For detailed file input/output, path templates, and extension handling
  • Advanced Features - For command output capture, package configuration, and more
  • Troubleshooting - For common issues and their solutions
  • Examples - For complete descriptor examples showing these concepts in action

Subcommands in Boutiques

Subcommands are a powerful extension to the Boutiques format in the Styx ecosystem. They allow describing tools with complex, hierarchical command structures where different "modes" or "algorithms" have different parameter sets.

Basic Concept

A subcommand is specified by making the type field of a parameter either:

  1. An object (for a single subcommand type)
  2. An array of objects (for a union of subcommand types)

Each subcommand object defines its own set of inputs, command-line, and outputs.

Subcommand vs. Groups

While standard Boutiques uses groups with options like mutually-exclusive to handle related parameters, the Styx ecosystem favors subcommands because they:

  • Create proper type hierarchies in generated bindings
  • Allow for nested parameter structures
  • Enable clear validation at compile-time rather than runtime
  • Support different output files per subcommand

Subcommand Union (Selection)

The most common use of subcommands is to represent different "modes" or "algorithms" where the user must select exactly one option:

{
  "id": "algorithm",
  "name": "Algorithm",
  "description": "Select processing algorithm",
  "value-key": "[ALGORITHM]",
  "type": [
    {
      "id": "fast",
      "name": "Fast Algorithm",
      "description": "Quick but less accurate",
      "command-line": "fast [FAST_INPUT] [FAST_OUTPUT]",
      "inputs": [
        {
          "id": "input",
          "name": "Input File",
          "value-key": "[FAST_INPUT]",
          "type": "File",
          "optional": false
        },
        {
          "id": "output",
          "name": "Output File",
          "value-key": "[FAST_OUTPUT]",
          "type": "String",
          "optional": false
        }
      ],
      "output-files": [
        {
          "id": "output",
          "name": "Output",
          "path-template": "[FAST_OUTPUT]"
        }
      ]
    },
    {
      "id": "accurate",
      "name": "Accurate Algorithm",
      "description": "Slower but more accurate",
      "command-line": "accurate [ACCURATE_INPUT] [ACCURATE_OUTPUT] [PRECISION]",
      "inputs": [
        {
          "id": "input",
          "name": "Input File",
          "value-key": "[ACCURATE_INPUT]",
          "type": "File",
          "optional": false
        },
        {
          "id": "output",
          "name": "Output File",
          "value-key": "[ACCURATE_OUTPUT]",
          "type": "String",
          "optional": false
        },
        {
          "id": "precision",
          "name": "Precision",
          "value-key": "[PRECISION]",
          "command-line-flag": "-p",
          "type": "Number",
          "integer": true,
          "minimum": 1,
          "maximum": 10,
          "optional": true,
          "default-value": 5
        }
      ],
      "output-files": [
        {
          "id": "output",
          "name": "Output",
          "path-template": "[ACCURATE_OUTPUT]"
        },
        {
          "id": "metrics",
          "name": "Performance Metrics",
          "path-template": "[ACCURATE_OUTPUT].metrics.json"
        }
      ]
    }
  ]
}

In this example:

  • The user must choose either the "fast" or "accurate" algorithm
  • Each algorithm has its own specific parameters
  • The "accurate" algorithm produces an additional output file

Single Subcommand (Configuration)

Sometimes you need a group of related parameters that are always used together. A single subcommand (where type is an object, not an array) can represent this configuration:

{
  "id": "config",
  "name": "Configuration",
  "value-key": "[CONFIG]",
  "command-line-flag": "--config",
  "type": {
    "id": "config_options",
    "command-line": "[KEY] [VALUE]",
    "inputs": [
      {
        "id": "key",
        "name": "Key",
        "value-key": "[KEY]",
        "type": "String",
        "optional": false
      },
      {
        "id": "value",
        "name": "Value",
        "value-key": "[VALUE]",
        "type": "String",
        "optional": false
      }
    ]
  },
  "optional": true
}

Repeatable Subcommands

Subcommands can be made repeatable by adding "list": true:

{
  "id": "transformations",
  "name": "Transformations",
  "list": true,
  "type": {
    "id": "transform",
    "command-line": "--transform [TYPE] [PARAMETERS]",
    "inputs": [
      {
        "id": "type",
        "name": "Type",
        "value-key": "[TYPE]",
        "type": "String",
        "value-choices": ["rotate", "scale", "translate"],
        "optional": false
      },
      {
        "id": "parameters",
        "name": "Parameters",
        "value-key": "[PARAMETERS]",
        "type": "Number",
        "list": true,
        "optional": false
      }
    ]
  },
  "optional": true
}

This allows specifying multiple transformations with different parameters, which would result in something like:

--transform rotate 0 0 90 --transform scale 2 2 1

Nested Subcommands

Subcommands can be nested multiple levels deep to represent complex tool hierarchies:

{
  "id": "mode",
  "type": [
    {
      "id": "analysis",
      "command-line": "analysis [METHOD]",
      "inputs": [
        {
          "id": "method",
          "value-key": "[METHOD]",
          "type": [
            {
              "id": "parametric",
              "command-line": "parametric [MODEL]",
              "inputs": [
                {
                  "id": "model",
                  "value-key": "[MODEL]",
                  "type": "String",
                  "value-choices": ["linear", "quadratic", "exponential"],
                  "optional": false
                }
              ]
            },
            {
              "id": "nonparametric",
              "command-line": "nonparametric [KERNEL]",
              "inputs": [
                {
                  "id": "kernel",
                  "value-key": "[KERNEL]",
                  "type": "String",
                  "value-choices": ["gaussian", "uniform"],
                  "optional": false
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "visualization",
      "command-line": "visualization [VIZ_OPTIONS]",
      "inputs": [
        {
          "id": "type",
          "value-key": "[VIZ_OPTIONS]",
          "command-line-flag": "--type",
          "type": "String",
          "value-choices": ["2d", "3d", "interactive"],
          "optional": false
        }
      ]
    }
  ]
}

Real-World Example: MRTrix3 5ttgen

Here's a simplified version of how MRTrix3's 5ttgen tool is described with subcommands:

{
  "name": "5ttgen",
  "description": "Generate a 5TT image suitable for ACT.",
  "command-line": "5ttgen [ALGORITHM] [OPTIONS]",
  "inputs": [
    {
      "id": "algorithm",
      "name": "algorithm",
      "value-key": "[ALGORITHM]",
      "description": "Select the algorithm to be used",
      "type": [
        {
          "id": "freesurfer",
          "name": "freesurfer",
          "description": "Generate the 5TT image based on a FreeSurfer parcellation",
          "command-line": "freesurfer [INPUT] [OUTPUT] [OPTIONS_LUT]",
          "inputs": [
            {
              "id": "input",
              "name": "input",
              "value-key": "[INPUT]",
              "description": "The input FreeSurfer parcellation image",
              "type": "File",
              "optional": false
            },
            {
              "id": "output",
              "name": "output",
              "value-key": "[OUTPUT]",
              "description": "The output 5TT image",
              "type": "String",
              "optional": false
            },
            {
              "id": "lut",
              "name": "lut",
              "command-line-flag": "-lut",
              "value-key": "[OPTIONS_LUT]",
              "description": "Lookup table path",
              "type": "File",
              "optional": true
            }
          ],
          "output-files": [
            {
              "id": "output",
              "name": "output",
              "path-template": "[OUTPUT]",
              "description": "The output 5TT image"
            }
          ]
        },
        {
          "id": "fsl",
          "name": "fsl",
          "description": "Use FSL commands to generate the 5TT image",
          "command-line": "fsl [INPUT] [OUTPUT] [OPTIONS]",
          "inputs": [
            {
              "id": "input",
              "name": "input",
              "value-key": "[INPUT]",
              "description": "The input T1-weighted image",
              "type": "File",
              "optional": false
            },
            {
              "id": "output",
              "name": "output",
              "value-key": "[OUTPUT]",
              "description": "The output 5TT image",
              "type": "String",
              "optional": false
            },
            {
              "id": "t2",
              "name": "t2",
              "command-line-flag": "-t2",
              "value-key": "[OPTIONS]",
              "description": "Provide a T2-weighted image",
              "type": "File",
              "optional": true
            }
          ],
          "output-files": [
            {
              "id": "output",
              "name": "output",
              "path-template": "[OUTPUT]",
              "description": "The output 5TT image"
            }
          ]
        }
      ]
    },
    {
      "id": "nocrop",
      "name": "nocrop",
      "value-key": "[OPTIONS]",
      "command-line-flag": "-nocrop",
      "description": "Do NOT crop the resulting 5TT image",
      "type": "Flag",
      "optional": true
    }
  ]
}

Generated Bindings

When Styx compiles a descriptor with subcommands, it creates type-safe bindings that reflect the hierarchical structure. For example, in TypeScript:

// For the algorithm example
type AlgorithmOptions = 
  | { algorithm: "fast", input: string, output: string }
  | { algorithm: "accurate", input: string, output: string, precision?: number };

function processData(options: AlgorithmOptions): void {
  // Implementation
}

This ensures users can only provide valid parameter combinations.

Best Practices for Subcommands

  1. Use subcommands for mutually exclusive options instead of groups
  2. Keep subcommand IDs unique across the entire descriptor
  3. Use descriptive names for each subcommand option
  4. Consider output files carefully - each subcommand can have different outputs
  5. Nest subcommands when it makes logical sense for the tool's structure
  6. Use value-choices for fixed option sets within subcommands
  7. Add list: true for repeatable elements when the same subcommand can appear multiple times

Next Steps

Now that you understand subcommands, learn about:

File Handling in Boutiques

Handling input and output files is a core part of the Boutiques descriptor format. This page explains how to properly define file inputs and outputs in your descriptors.

Cross-reference: For the basics of descriptor structure and parameters, see Basic Structure.
Cross-reference: For examples of file handling in complete descriptors, see Examples.

Input Files

Input files are specified using the File type in the inputs array:

{
  "id": "image",
  "name": "Input Image",
  "description": "The input neuroimaging file",
  "type": "File",
  "value-key": "[INPUT]",
  "optional": false
}

Key Considerations for Input Files

  1. Always use "type": "File" for actual file paths
  2. File inputs will be validated to ensure they exist when the tool is called
  3. In containerized environments, file paths are automatically mapped to the container's filesystem

Optional File Fields

Some additional fields that can be used with file inputs:

  • optional: Whether the file is required
  • value-choices: A list of predefined file options
  • default-value: Default file path if not specified
  • list: Whether multiple files can be provided (creates a file list)

Output Files

Unlike inputs, output files are not parameters passed to the command line. Rather, they're specifications of what files will be produced by the tool. They're defined in the output-files array:

"output-files": [
  {
    "id": "brain_mask",
    "name": "Brain Mask Image",
    "description": "Binary mask of the brain",
    "path-template": "[OUTPUT].mask.nii.gz",
    "optional": false
  }
]

Path Templates

The path-template field defines where the output file will be created. It can use value-key placeholders from the inputs to construct dynamic paths:

"inputs": [
  {
    "id": "output_dir",
    "name": "Output Directory",
    "type": "String",
    "value-key": "[OUTDIR]"
  },
  {
    "id": "subject_id",
    "name": "Subject ID",
    "type": "String",
    "value-key": "[SUBJECT]"
  }
],
"output-files": [
  {
    "id": "processed_image",
    "name": "Processed Image",
    "path-template": "[OUTDIR]/sub-[SUBJECT]/anat/image.nii.gz"
  }
]

Extension Handling with path-template-stripped-extensions

A common pattern is to produce output files that have a similar name to input files but with different extensions. The path-template-stripped-extensions field helps with this:

"inputs": [
  {
    "id": "input_image",
    "name": "Input Image",
    "type": "File",
    "value-key": "[INPUT]"
  }
],
"output-files": [
  {
    "id": "output_mask",
    "name": "Output Mask",
    "path-template": "[INPUT]_mask.nii.gz",
    "path-template-stripped-extensions": [".nii.gz", ".nii", ".img", ".hdr"]
  }
]

If the input is subject1.nii.gz, this would produce subject1_mask.nii.gz (not subject1.nii.gz_mask.nii.gz).

Output File Fields

FieldDescriptionRequiredExample
idUnique identifierYes"brain_mask"
nameHuman-readable nameYes"Brain Mask Image"
descriptionDetailed descriptionNo"Binary mask of the brain"
path-templateTemplate for output file pathYes"[PREFIX]_mask.nii.gz"
optionalWhether file might not be producedNotrue
path-template-stripped-extensionsExtensions to remove from input pathsNo[".nii.gz", ".nii"]

Subcommand Output Files

Each subcommand can have its own set of output files, which is particularly useful when different algorithms produce different outputs:

{
  "id": "algorithm",
  "type": [
    {
      "id": "standard",
      "inputs": [...],
      "output-files": [
        {
          "id": "standard_output",
          "path-template": "[OUTPUT].nii.gz"
        }
      ]
    },
    {
      "id": "advanced",
      "inputs": [...],
      "output-files": [
        {
          "id": "advanced_output",
          "path-template": "[OUTPUT].nii.gz"
        },
        {
          "id": "quality_metrics",
          "path-template": "[OUTPUT]_qc.json"
        }
      ]
    }
  ]
}

Capture Command Output

Sometimes tools output important data to stdout or stderr instead of files. The Styx ecosystem extends Boutiques with stdout-output and stderr-output fields to capture this data:

"stdout-output": {
  "id": "coordinates",
  "name": "Extracted Coordinates", 
  "description": "Tab-separated coordinate values"
}

This is useful for tools that output structured data to stdout rather than files. The captured output is made available as a string in the generated bindings.

Best Practices for File Handling

  1. Use File type for input files and String type for output file paths
  2. Keep output paths flexible by using placeholders from inputs
  3. Use path-template-stripped-extensions to handle file extension changes
  4. Consider subcommand-specific outputs when different modes produce different files
  5. Use stdout-output and stderr-output for tools that output data to the terminal
  6. Make output files optional: true if they might not be produced in all cases

Container Considerations

When a tool runs in a container:

  1. Input file paths are automatically mapped from the host to the container
  2. Output files are mapped back from the container to the host
  3. Relative paths are resolved relative to the working directory

The Styx execution environment handles these mappings transparently, but it's important to be aware of them when designing descriptors.

Example: Complete File Handling

{
  "name": "image_processor",
  "description": "Process neuroimaging files",
  "command-line": "process_image [INPUT] [OUTPUT] [OPTIONS]",
  "inputs": [
    {
      "id": "input_file",
      "name": "Input File",
      "description": "Input neuroimaging file",
      "type": "File",
      "value-key": "[INPUT]",
      "optional": false
    },
    {
      "id": "output_prefix",
      "name": "Output Prefix",
      "description": "Prefix for output files",
      "type": "String",
      "value-key": "[OUTPUT]",
      "optional": false
    },
    {
      "id": "verbose",
      "name": "Verbose Output",
      "description": "Enable detailed log messages",
      "type": "Flag",
      "command-line-flag": "-v",
      "value-key": "[OPTIONS]",
      "optional": true
    }
  ],
  "output-files": [
    {
      "id": "main_output",
      "name": "Processed Image",
      "description": "The main processed output image",
      "path-template": "[OUTPUT].nii.gz",
      "optional": false
    },
    {
      "id": "mask",
      "name": "Binary Mask",
      "description": "Binary mask from the processing",
      "path-template": "[OUTPUT]_mask.nii.gz",
      "optional": true
    },
    {
      "id": "report",
      "name": "Processing Report",
      "description": "HTML report with quality metrics",
      "path-template": "[OUTPUT]_report.html",
      "optional": true
    }
  ],
  "stdout-output": {
    "id": "processing_log",
    "name": "Processing Log",
    "description": "Detailed log of the processing steps"
  }
}

Advanced Features in Boutiques

This page covers advanced features and extensions of the Boutiques format in the Styx ecosystem.

Cross-reference: For the core structure and basic fields, see Basic Structure.
Cross-reference: For subcommand hierarchies, see Subcommands.
Cross-reference: For file input/output handling, see File Handling.

Package Configuration Files

NiWrap uses separate package configuration files to organize tools by suite:

{
  "name": "FSL",
  "author": "FMRIB Analysis Group, University of Oxford",
  "url": "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki",
  "approach": "Manual",
  "status": "Experimental",
  "container": "brainlife/fsl:6.0.4-patched2",
  "version": "6.0.5",
  "description": "FSL is a comprehensive library of analysis tools for FMRI, MRI and diffusion brain imaging data.",
  "id": "fsl",
  "api": {
    "endpoints": [
      {
        "target": "AnatomicalAverage",
        "status": "done",
        "descriptor": "descriptors/fsl/AnatomicalAverage.json"
      },
      {
        "target": "Text2Vest",
        "status": "missing"
      },
      {
        "target": "Runtcl",
        "status": "ignore"
      }
    ]
  }
}

Package Configuration Fields

FieldDescriptionExample
nameHuman-readable package name"FSL"
authorAuthor or organization"FMRIB Analysis Group, University of Oxford"
urlDocumentation URL"https://fsl.fmrib.ox.ac.uk/fslwiki"
approachHow interfaces were created"Manual" or "Extracted"
statusOverall package status"Experimental" or "Stable"
containerDefault container image"brainlife/fsl:6.0.4-patched2"
versionPackage version"6.0.5"
descriptionPackage description"FSL is a comprehensive library..."
idUnique package identifier"fsl"
api.endpointsTool definitionsArray of endpoint objects

Endpoint Status Values

The status field in each endpoint tracks implementation:

  • "done": Descriptor is complete and ready to use
  • "missing": Tool is identified but descriptor not yet created
  • "ignore": Tool should be deliberately excluded from the API

While these files are primarily used for tracking coverage and generating documentation, some metadata (name, author, description) is used in the generated language bindings.

Command Output Capture

For tools that output important data to stdout or stderr, the Styx ecosystem extends Boutiques with special fields:

"stdout-output": {
  "id": "calculation_results",
  "name": "Calculation Results", 
  "description": "Output of the numerical calculation"
},
"stderr-output": {
  "id": "warning_messages",
  "name": "Warning Messages",
  "description": "Warnings and errors during processing"
}

These fields make the command output available as strings in the generated bindings, useful for tools that:

  • Output data tables to the terminal
  • Provide processing statistics on stderr
  • Generate simple text outputs without writing files

Groups

While the standard Boutiques format uses groups to organize related parameters, the Styx ecosystem generally favors subcommands for this purpose. However, the groups field is still part of the schema:

"groups": [
  {
    "id": "required_params",
    "name": "Required Parameters",
    "description": "Parameters that must be specified",
    "members": ["input_file", "output_prefix"]
  },
  {
    "id": "exclusive_options",
    "name": "Processing Options",
    "description": "Choose only one processing option",
    "members": ["fast_mode", "accurate_mode", "balanced_mode"],
    "mutually-exclusive": true
  },
  {
    "id": "debug_options",
    "name": "Debug Options",
    "description": "Debugging parameters",
    "members": ["verbose", "debug", "trace"],
    "one-is-required": false
  }
]

Group Properties

PropertyDescriptionExample
idUnique identifier"required_params"
nameHuman-readable name"Required Parameters"
descriptionDetailed description"Parameters that must be specified"
membersArray of parameter IDs["input_file", "output_prefix"]
mutually-exclusiveOnly one member can be usedtrue
all-or-noneEither all or no members must be usedtrue
one-is-requiredAt least one member must be specifiedtrue

Command-Line Flag Separators

By default, command-line flags and their values are separated by a space. You can change this with the command-line-flag-separator field:

{
  "id": "threshold",
  "name": "Threshold",
  "command-line-flag": "--threshold",
  "command-line-flag-separator": "=",
  "value-key": "[THRESHOLD]",
  "type": "Number"
}

This would produce --threshold=0.5 instead of --threshold 0.5.

List Separators

For list parameters, you can control how the values are joined with the list-separator field:

{
  "id": "coordinates",
  "name": "Coordinates",
  "type": "Number",
  "list": true,
  "list-separator": ",",
  "value-key": "[COORDS]"
}

With values [1, 2, 3], this would produce 1,2,3 instead of the default 1 2 3.

Container Configurations

The container-image field defines container information:

"container-image": {
  "type": "docker",
  "image": "brainlife/fsl:6.0.4-patched2",
  "index": "docker.io"
}

In the Styx ecosystem, primarily the type and image fields are used, with Docker as the main container type.

Value Constraints

Several fields help constrain parameter values:

For Numeric Values

{
  "id": "threshold",
  "type": "Number",
  "integer": false,
  "minimum": 0,
  "maximum": 1,
  "exclusive-minimum": false,
  "exclusive-maximum": false
}

For String Values with Fixed Choices

{
  "id": "mode",
  "type": "String",
  "value-choices": ["fast", "balanced", "accurate"]
}

For Files with Pattern Matching

{
  "id": "image",
  "type": "File",
  "value-choices": ["*.nii", "*.nii.gz"]
}

Validation and Testing

When creating or modifying descriptors, use these validation methods:

  1. JSON Schema validation:

    # In NiWrap repository
    python -m pytest tests/test_descriptors.py::test_descriptor_validity
    
  2. Visual Studio Code validation: Add this to .vscode/settings.json:

    {
      "json.schemas": [
        {
          "fileMatch": ["descriptors/**/*.json"],
          "url": "./schemas/descriptor.schema.json"
        }
      ]
    }
    
  3. Build testing:

    # Test if Styx can process your descriptor
    python build.py
    

Future Extensions

The Styx ecosystem continues to evolve, with several planned extensions:

  • Additional parameter types for more complex data structures
  • Enhanced dependency modeling between parameters
  • Improved container configuration options
  • Custom frontend formats beyond Boutiques

NiWrap extensions are being proposed for inclusion in the core Boutiques standard, helping to standardize these improvements across the community.

Cross-References

Now that you've explored advanced features, you might find these pages helpful:

  • Examples - Complete descriptors demonstrating these concepts in action
  • Troubleshooting - Solutions to common problems with descriptors
  • Subcommands - More on hierarchical command structures

Troubleshooting Boutiques Descriptors

This guide covers common issues when creating or modifying Boutiques descriptors and their solutions.

Cross-reference: For the core structure of descriptors, see Basic Structure.
Cross-reference: For issues specific to subcommands, see Subcommands.
Cross-reference: For file handling problems, see File Handling.

Schema Validation Errors

Missing Required Fields

Problem: ERROR: 'name' is a required property

Solution: Ensure all required top-level fields are present:

{
  "name": "tool_name",
  "description": "Tool description",
  "command-line": "command [ARGS]",
  "inputs": [...],
  "schema-version": "0.5",
  "tool-version": "1.0.0"
}

Invalid Value Types

Problem: ERROR: 'string' is not of type 'number'

Solution: Check that values match their declared types. For numeric parameters:

{
  "id": "threshold",
  "type": "Number",
  "default-value": 0.5  // Not "0.5" as a string
}

Invalid IDs

Problem: ERROR: 'input-file' does not match pattern '^[0-9,_,a-z,A-Z]*$'

Solution: IDs must contain only alphanumeric characters and underscores:

{
  "id": "input_file",  // Not "input-file"
  "name": "Input File"
}

Command-Line Formation Issues

Value-Key Placeholders Not Working

Problem: Value-key placeholders aren't replaced in the command line.

Solution:

  1. Ensure the value-key in the parameter matches exactly what's in the command-line:

    "command-line": "tool [INPUT_FILE]",
    "inputs": [
      {
        "id": "input",
        "value-key": "[INPUT_FILE]"  // Must match exactly, including case
      }
    ]
    
  2. Verify that value-keys follow the formatting rules (uppercase, underscores, enclosed in square brackets).

Command-Line Flags Not Appearing

Problem: Command-line flags aren't included in the generated command.

Solution: Make sure you're using the correct fields:

{
  "id": "verbose",
  "command-line-flag": "-v",  // The flag itself
  "value-key": "[VERBOSE]"    // Where it appears in the command-line
}

List Parameters Not Formatted Correctly

Problem: List values aren't formatted as expected in the command.

Solution: Use the list-separator field to control how values are joined:

{
  "id": "coordinates",
  "type": "Number",
  "list": true,
  "list-separator": ",",  // Will join values with commas
  "value-key": "[COORDS]"
}

Subcommand Issues

Subcommand Parameters Not Available

Problem: Parameters inside subcommands aren't accessible in the generated bindings.

Solution: Check your subcommand structure:

{
  "id": "algorithm",
  "type": [
    {
      "id": "method1",
      "inputs": [
        {
          "id": "param1",  // Make sure IDs are unique
          "type": "String"
        }
      ]
    }
  ]
}

Mutually Exclusive Options Not Working

Problem: The descriptor doesn't enforce mutually exclusive options.

Solution: Instead of using groups with mutually-exclusive, use subcommands:

{
  "id": "mode",
  "type": [
    { "id": "mode1", "inputs": [...] },
    { "id": "mode2", "inputs": [...] }
  ]
}

This creates a proper union type in the generated bindings.

File Handling Issues

Input Files Not Found

Problem: Input files are reported as not found even though they exist.

Solution:

  1. Make sure you're using "type": "File" for input files
  2. Check if paths are relative to the current working directory
  3. For containerized runs, verify file paths are accessible in the container

Output Files Not Created Where Expected

Problem: Output files appear in unexpected locations.

Solution: Check your path-template in output-files:

"output-files": [
  {
    "id": "output",
    "path-template": "[OUTPUT_DIR]/[PREFIX].nii.gz"
  }
]

Ensure all value-keys ([OUTPUT_DIR], [PREFIX]) are defined in your inputs.

File Extensions Not Handled Correctly

Problem: Output files have double extensions like file.nii.gz.nii.gz.

Solution: Use path-template-stripped-extensions:

"output-files": [
  {
    "id": "output",
    "path-template": "[INPUT]_processed.nii.gz",
    "path-template-stripped-extensions": [".nii.gz", ".nii"]
  }
]

Container Issues

Container Not Found

Problem: The container image cannot be pulled or found.

Solution:

  1. Verify the container exists in the specified registry

  2. Ensure the image name and tag are correct:

    "container-image": {
      "type": "docker",
      "image": "organization/image:tag"
    }
    

Missing Dependencies in Container

Problem: The tool reports missing dependencies inside the container.

Solution: Use a container that includes all required dependencies. You may need to build a custom container with a Dockerfile:

FROM base/image:tag
RUN apt-get update && apt-get install -y additional-dependency

Common Pitfalls

Value-Keys vs. Command-Line-Flags

Problem: Confusion between value-keys and command-line-flags.

Solution:

  • value-key is a placeholder in the command-line template

  • command-line-flag is the actual flag used in the command (e.g., -v, --verbose)

  • Both are often needed:

    {
      "id": "threshold",
      "command-line-flag": "--threshold",
      "value-key": "[THRESHOLD]"
    }
    

Input vs. Output File Types

Problem: Confusion about how to define input and output files.

Solution:

  • Input files use "type": "File" in the inputs section
  • Output files are defined in the output-files section with a path-template
  • For parameters that specify output paths, use "type": "String" (not "File")

Inconsistent Naming

Problem: Similar parameters have inconsistent naming across descriptors.

Solution: Follow consistent naming conventions:

// Good:
"id": "input_file"
"id": "output_dir"
"id": "threshold"

// Avoid mixing styles:
"id": "inputFile"
"id": "output-dir"
"id": "THRESHOLD"

Debugging Techniques

Validating Descriptors

Always validate your descriptors before using them:

# Using NiWrap validation
python -m pytest tests/test_descriptors.py::test_descriptor_validity

# Using VSCode with JSON schema
# Configure .vscode/settings.json as described in the docs

Printing Command Line

When testing, print the full command line to see if it's formed correctly:

# Python
from niwrap.tool import function
cmd = function.get_command(param1="value", param2=123)
print(cmd)

Using Verbose Mode

Many tools have verbose or debug modes that can help identify issues:

{
  "id": "verbose",
  "name": "Verbose",
  "type": "Flag",
  "command-line-flag": "-v",
  "value-key": "[VERBOSE]"
}

Getting Help

If you're still having issues:

  1. Check existing descriptors in the NiWrap repository for examples
  2. Examine the Boutiques documentation
  3. Open an issue in the NiWrap issue tracker

Boutiques Descriptor Examples

This page provides complete examples of Boutiques descriptors for different types of tools, showcasing various features of the format.

Basic Tool Example

A simple tool with input file, output file, and a few parameters:

{
  "name": "image_converter",
  "description": "Converts between image formats with optional compression",
  "tool-version": "1.0.0",
  "schema-version": "0.5",
  "author": "Example Author",
  "url": "https://example.org/tool",
  "command-line": "convert_image [INPUT] [OUTPUT] [COMPRESSION] [VERBOSE]",
  "container-image": {
    "type": "docker",
    "image": "example/image_converter:1.0.0"
  },
  "inputs": [
    {
      "id": "input_file",
      "name": "Input Image",
      "description": "The input image file to convert",
      "type": "File",
      "value-key": "[INPUT]",
      "optional": false
    },
    {
      "id": "output_file",
      "name": "Output Image",
      "description": "The output image file path",
      "type": "String",
      "value-key": "[OUTPUT]",
      "optional": false
    },
    {
      "id": "compression_level",
      "name": "Compression Level",
      "description": "Level of compression (0-9)",
      "type": "Number",
      "integer": true,
      "minimum": 0,
      "maximum": 9,
      "command-line-flag": "-c",
      "value-key": "[COMPRESSION]",
      "optional": true,
      "default-value": 5
    },
    {
      "id": "verbose",
      "name": "Verbose Output",
      "description": "Enable verbose logging",
      "type": "Flag",
      "command-line-flag": "-v",
      "value-key": "[VERBOSE]",
      "optional": true
    }
  ],
  "output-files": [
    {
      "id": "converted_image",
      "name": "Converted Image",
      "description": "The output converted image",
      "path-template": "[OUTPUT]",
      "optional": false
    }
  ],
  "stdout-output": {
    "id": "conversion_log",
    "name": "Conversion Log",
    "description": "Log of the conversion process"
  }
}

Tool with Subcommands

A more complex tool with different algorithms, each having specific parameters:

{
  "name": "brain_segmentation",
  "description": "Performs brain segmentation using different algorithms",
  "tool-version": "2.1.0",
  "schema-version": "0.5",
  "author": "Neuroimaging Lab",
  "url": "https://example.org/brain_segmentation",
  "command-line": "segment_brain [ALGORITHM] [GLOBAL_OPTIONS]",
  "container-image": {
    "type": "docker",
    "image": "neuroimaging/segmentation:2.1.0"
  },
  "inputs": [
    {
      "id": "algorithm",
      "name": "Algorithm",
      "description": "Segmentation algorithm to use",
      "value-key": "[ALGORITHM]",
      "optional": false,
      "type": [
        {
          "id": "atlas",
          "name": "Atlas-Based",
          "description": "Atlas-based segmentation",
          "command-line": "atlas [INPUT] [OUTPUT] [ATLAS_FILE] [ATLAS_OPTIONS]",
          "inputs": [
            {
              "id": "input",
              "name": "Input Image",
              "description": "Input brain image to segment",
              "type": "File",
              "value-key": "[INPUT]",
              "optional": false
            },
            {
              "id": "output",
              "name": "Output Directory",
              "description": "Output directory for segmentation results",
              "type": "String",
              "value-key": "[OUTPUT]",
              "optional": false
            },
            {
              "id": "atlas_file",
              "name": "Atlas File",
              "description": "Reference atlas file",
              "type": "File",
              "value-key": "[ATLAS_FILE]",
              "optional": false
            },
            {
              "id": "non_linear",
              "name": "Non-linear Registration",
              "description": "Use non-linear registration",
              "type": "Flag",
              "command-line-flag": "--nonlinear",
              "value-key": "[ATLAS_OPTIONS]",
              "optional": true
            }
          ],
          "output-files": [
            {
              "id": "segmentation",
              "name": "Segmentation Result",
              "description": "Segmented brain regions",
              "path-template": "[OUTPUT]/segmentation.nii.gz",
              "optional": false
            },
            {
              "id": "labels",
              "name": "Label Map",
              "description": "Label map for the segmentation",
              "path-template": "[OUTPUT]/labels.csv",
              "optional": false
            }
          ]
        },
        {
          "id": "deep",
          "name": "Deep Learning",
          "description": "Deep learning-based segmentation",
          "command-line": "deep [INPUT] [OUTPUT] [MODEL] [DEEP_OPTIONS]",
          "inputs": [
            {
              "id": "input",
              "name": "Input Image",
              "description": "Input brain image to segment",
              "type": "File",
              "value-key": "[INPUT]",
              "optional": false
            },
            {
              "id": "output",
              "name": "Output Directory",
              "description": "Output directory for segmentation results",
              "type": "String",
              "value-key": "[OUTPUT]",
              "optional": false
            },
            {
              "id": "model",
              "name": "Model Type",
              "description": "Deep learning model to use",
              "type": "String",
              "value-key": "[MODEL]",
              "value-choices": ["unet", "segnet", "densenet"],
              "optional": false
            },
            {
              "id": "batch_size",
              "name": "Batch Size",
              "description": "Processing batch size",
              "type": "Number",
              "integer": true,
              "minimum": 1,
              "maximum": 64,
              "command-line-flag": "--batch",
              "value-key": "[DEEP_OPTIONS]",
              "optional": true,
              "default-value": 8
            },
            {
              "id": "device",
              "name": "Computing Device",
              "description": "Device for computation",
              "type": "String",
              "command-line-flag": "--device",
              "value-key": "[DEEP_OPTIONS]",
              "value-choices": ["cpu", "cuda"],
              "optional": true,
              "default-value": "cpu"
            }
          ],
          "output-files": [
            {
              "id": "segmentation",
              "name": "Segmentation Result",
              "description": "Segmented brain regions",
              "path-template": "[OUTPUT]/segmentation.nii.gz",
              "optional": false
            },
            {
              "id": "probability_maps",
              "name": "Probability Maps",
              "description": "Probability maps for each region",
              "path-template": "[OUTPUT]/probabilities.nii.gz",
              "optional": false
            },
            {
              "id": "metrics",
              "name": "Performance Metrics",
              "description": "Model performance metrics",
              "path-template": "[OUTPUT]/metrics.json",
              "optional": false
            }
          ]
        }
      ]
    },
    {
      "id": "threads",
      "name": "Number of Threads",
      "description": "Number of CPU threads to use",
      "type": "Number",
      "integer": true,
      "minimum": 1,
      "command-line-flag": "--threads",
      "value-key": "[GLOBAL_OPTIONS]",
      "optional": true,
      "default-value": 4
    },
    {
      "id": "verbose",
      "name": "Verbose Output",
      "description": "Enable verbose logging",
      "type": "Flag",
      "command-line-flag": "--verbose",
      "value-key": "[GLOBAL_OPTIONS]",
      "optional": true
    }
  ]
}

Tool with Repeatable Subcommand

A tool where a subcommand can be repeated multiple times:

{
  "name": "image_processor",
  "description": "Apply multiple image processing operations sequentially",
  "tool-version": "1.2.0",
  "schema-version": "0.5",
  "command-line": "process_image [INPUT] [OUTPUT] [OPERATIONS]",
  "container-image": {
    "type": "docker",
    "image": "example/image_processor:1.2.0"
  },
  "inputs": [
    {
      "id": "input_file",
      "name": "Input Image",
      "description": "Input image to process",
      "type": "File",
      "value-key": "[INPUT]",
      "optional": false
    },
    {
      "id": "output_file",
      "name": "Output Image",
      "description": "Output processed image",
      "type": "String",
      "value-key": "[OUTPUT]",
      "optional": false
    },
    {
      "id": "operations",
      "name": "Processing Operations",
      "description": "Operations to apply (in order)",
      "type": {
        "id": "operation",
        "command-line": "--op [OPERATION] [PARAMS]",
        "inputs": [
          {
            "id": "operation_type",
            "name": "Operation Type",
            "description": "Type of image operation",
            "type": "String",
            "value-key": "[OPERATION]",
            "value-choices": ["blur", "sharpen", "resize", "rotate", "contrast"],
            "optional": false
          },
          {
            "id": "parameters",
            "name": "Operation Parameters",
            "description": "Parameters for the operation",
            "type": "Number",
            "list": true,
            "list-separator": ",",
            "value-key": "[PARAMS]",
            "optional": false
          }
        ]
      },
      "value-key": "[OPERATIONS]",
      "list": true,
      "optional": true
    }
  ],
  "output-files": [
    {
      "id": "processed_image",
      "name": "Processed Image",
      "description": "The output processed image",
      "path-template": "[OUTPUT]",
      "optional": false
    }
  ]
}

In this example, multiple operations can be specified:

process_image input.jpg output.jpg --op blur 3,3 --op rotate 90

Tool with Nested Subcommands

Example with deeply nested subcommands:

{
  "name": "data_analyzer",
  "description": "Analyze data with multiple methods and options",
  "tool-version": "3.0.0",
  "schema-version": "0.5",
  "command-line": "analyze [MODE] [GLOBAL_OPTIONS]",
  "inputs": [
    {
      "id": "mode",
      "name": "Analysis Mode",
      "description": "The type of analysis to perform",
      "value-key": "[MODE]",
      "optional": false,
      "type": [
        {
          "id": "statistical",
          "name": "Statistical Analysis",
          "description": "Perform statistical analysis",
          "command-line": "statistical [DATA] [STATS_OUTPUT] [STATS_METHOD]",
          "inputs": [
            {
              "id": "data_file",
              "name": "Data File",
              "description": "Input data file",
              "type": "File",
              "value-key": "[DATA]",
              "optional": false
            },
            {
              "id": "output_dir",
              "name": "Output Directory",
              "description": "Directory for output files",
              "type": "String",
              "value-key": "[STATS_OUTPUT]",
              "optional": false
            },
            {
              "id": "method",
              "name": "Statistical Method",
              "description": "Method for statistical analysis",
              "value-key": "[STATS_METHOD]",
              "optional": false,
              "type": [
                {
                  "id": "parametric",
                  "name": "Parametric Tests",
                  "description": "Parametric statistical tests",
                  "command-line": "parametric [PARAM_TEST] [PARAM_OPTIONS]",
                  "inputs": [
                    {
                      "id": "test_type",
                      "name": "Test Type",
                      "description": "Type of parametric test",
                      "type": "String",
                      "value-key": "[PARAM_TEST]",
                      "value-choices": ["ttest", "anova", "regression"],
                      "optional": false
                    },
                    {
                      "id": "alpha",
                      "name": "Alpha Level",
                      "description": "Significance level",
                      "type": "Number",
                      "integer": false,
                      "minimum": 0.001,
                      "maximum": 0.1,
                      "command-line-flag": "--alpha",
                      "value-key": "[PARAM_OPTIONS]",
                      "optional": true,
                      "default-value": 0.05
                    }
                  ],
                  "output-files": [
                    {
                      "id": "parametric_results",
                      "name": "Parametric Test Results",
                      "description": "Results of the parametric test",
                      "path-template": "[STATS_OUTPUT]/parametric_results.csv",
                      "optional": false
                    }
                  ]
                },
                {
                  "id": "nonparametric",
                  "name": "Non-parametric Tests",
                  "description": "Non-parametric statistical tests",
                  "command-line": "nonparametric [NONPARAM_TEST] [NONPARAM_OPTIONS]",
                  "inputs": [
                    {
                      "id": "test_type",
                      "name": "Test Type",
                      "description": "Type of non-parametric test",
                      "type": "String",
                      "value-key": "[NONPARAM_TEST]",
                      "value-choices": ["wilcoxon", "kruskal", "friedman"],
                      "optional": false
                    },
                    {
                      "id": "exact",
                      "name": "Exact Test",
                      "description": "Use exact test calculations",
                      "type": "Flag",
                      "command-line-flag": "--exact",
                      "value-key": "[NONPARAM_OPTIONS]",
                      "optional": true
                    }
                  ],
                  "output-files": [
                    {
                      "id": "nonparametric_results",
                      "name": "Non-parametric Test Results",
                      "description": "Results of the non-parametric test",
                      "path-template": "[STATS_OUTPUT]/nonparametric_results.csv",
                      "optional": false
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          "id": "visualization",
          "name": "Data Visualization",
          "description": "Create data visualizations",
          "command-line": "visualization [DATA] [VIZ_OUTPUT] [VIZ_TYPE] [VIZ_OPTIONS]",
          "inputs": [
            {
              "id": "data_file",
              "name": "Data File",
              "description": "Input data file",
              "type": "File",
              "value-key": "[DATA]",
              "optional": false
            },
            {
              "id": "output_dir",
              "name": "Output Directory",
              "description": "Directory for output files",
              "type": "String",
              "value-key": "[VIZ_OUTPUT]",
              "optional": false
            },
            {
              "id": "viz_type",
              "name": "Visualization Type",
              "description": "Type of visualization",
              "type": "String",
              "value-key": "[VIZ_TYPE]",
              "value-choices": ["boxplot", "histogram", "scatterplot", "heatmap"],
              "optional": false
            },
            {
              "id": "colormap",
              "name": "Color Map",
              "description": "Color map for the visualization",
              "type": "String",
              "command-line-flag": "--colormap",
              "value-key": "[VIZ_OPTIONS]",
              "value-choices": ["viridis", "plasma", "inferno", "magma", "cividis"],
              "optional": true,
              "default-value": "viridis"
            },
            {
              "id": "dpi",
              "name": "DPI",
              "description": "Resolution in dots per inch",
              "type": "Number",
              "integer": true,
              "minimum": 72,
              "maximum": 1200,
              "command-line-flag": "--dpi",
              "value-key": "[VIZ_OPTIONS]",
              "optional": true,
              "default-value": 300
            }
          ],
          "output-files": [
            {
              "id": "visualization_file",
              "name": "Visualization File",
              "description": "Output visualization image",
              "path-template": "[VIZ_OUTPUT]/plot.png",
              "optional": false
            }
          ]
        }
      ]
    },
    {
      "id": "verbose",
      "name": "Verbose Output",
      "description": "Enable verbose output",
      "type": "Flag",
      "command-line-flag": "--verbose",
      "value-key": "[GLOBAL_OPTIONS]",
      "optional": true
    }
  ]
}

Real-World Examples

For real-world examples check out the descriptors in NiWrap.