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:
- Examples for real-world usage patterns
- Advanced Concepts for custom deployment options
- Contributing to help improve the ecosystem
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:
- Runners - Control how commands get executed and files get stored
- I/O: Where are my files? - Understand how file paths work in NiWrap
- Tips & best practices - Guidelines for efficient workflows
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:
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.):
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?
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:
- Check existing issues - Someone might have already reported the same problem
- Include details - Error messages, code examples, and your environment details are super helpful
- 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:
- NiWrap - A collection of descriptors for neuroimaging tools
- Styx Compiler - Transforms descriptors into type-safe language bindings
- Language-specific packages - The compiled outputs for Python, TypeScript, and R
- Documentation - The Styx Book that you're reading right now
Guides in this Section
- Contributing to NiWrap - Learn how to add or improve neuroimaging tool descriptors
- Contributing to the Styx compiler - Help enhance the core compiler functionality
- Contributing to the Book - Improve the documentation you're reading right now
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:
- Check the NiWrap issue tracker or the Styx compiler issue tracker
- Look for issues labeled "good first issue" for beginner-friendly tasks
- 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
- Understanding NiWrap and Boutiques
- Repository Structure
- Development Environment Setup
- Contributing Descriptors
- Working with Package Configurations
- Source Extraction
- Testing Your Changes
- Contribution Workflow
- Advanced Topics
- 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:
- Create or open
.vscode/settings.json
in your NiWrap repository - 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:
- For a new tool, add a new endpoint entry in the appropriate package file
- When updating a tool descriptor, ensure its status is set to
"done"
- 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:
- Modifying the original source code of a neuroimaging tool
- Adding instrumentation to dump tool information during compilation or runtime
- 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:
-
Capture the help text of a neuroimaging tool:
mytool --help > tool_help.txt
-
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]
-
Review and refine the generated descriptor to ensure accuracy
-
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:
-
Use the NiWrap test suite:
# Run tests for a specific tool python -m pytest tests/test_descriptors.py::test_descriptor_validity::test_tool_descriptor
-
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:
- Fork the repository: Create your own fork of NiWrap on GitHub
- Clone your fork:
git clone https://github.com/your-username/niwrap.git cd niwrap
- Create a branch:
git checkout -b fix-fsl-bet-descriptor
- Make changes: Modify or add descriptors in the appropriate directory
- Update package configuration: If necessary, update the corresponding package configuration file
- Test: Ensure your changes work correctly using the testing methods described above
- Commit your changes:
git add descriptors/fsl/bet.json packages/fsl.json git commit -m "Fix: Update FSL BET fractional intensity parameter type"
- Push your changes:
git push origin fix-fsl-bet-descriptor
- 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):
- Create a new directory in
descriptors/
for the tool suite - Create descriptors for each tool you want to support
- Create a new package configuration file in
packages/
- 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:
- Add your script to the
scripts/
directory - Document its usage in a comment at the top of the script
- 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:
- Open an issue in the NiWrap issue tracker
- Check the Boutiques documentation
- Consult the Styx Book
- Review existing descriptors for similar tools as examples
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:
- Frontend: Parses input formats (e.g., Boutiques descriptors) into an Intermediate Representation (IR)
- IR: A language-agnostic representation of the tool interface
- 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:
- Locate the language provider in
src/styx/backend/<language>/languageprovider.py
- Make your changes to the code generation logic
- Add tests in the
tests/
directory - Run the test suite to ensure everything works as expected
Adding Support for a New Language
To add support for a new target language:
- Create a new directory in
src/styx/backend/
for your language - Implement a language provider that conforms to the interface in
backend/generic/languageprovider.py
- Add language-specific code generation logic
- Add tests for your new language backend
Improving the IR
If you want to enhance the Intermediate Representation:
- Make changes to the IR structure in
src/styx/ir/core.py
- Update the normalization and optimization passes if necessary
- Ensure all language backends can handle your IR changes
- 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:
- Check existing issues on GitHub
- Look at the test cases to understand how different components work
- 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
-
Install mdBook (if not already installed):
cargo install mdbook # Or use your system's package manager
# Optional: Mermaid diagram rendering # cargo install mdbook-mermaid
-
Clone the repository:
git clone https://github.com/styx-api/styxbook.git cd styxbook
-
Serve the book locally to see changes in real-time:
mdbook serve # Open http://localhost:3000 in your browser
-
Edit Markdown files in the
src/
directory- Changes will automatically reload in your browser
Contributing Changes
-
Create a branch for your changes:
git checkout -b improve-getting-started
-
Make your edits to the relevant Markdown files
-
Commit and push your changes:
git add . git commit -m "Improve getting started documentation" git push origin improve-getting-started
-
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:
- Open an issue in the Styx Book repository
- Ask for guidance in your pull request
- 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)
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())
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)
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:
- Tool interfaces are defined in Boutiques descriptors
- Styx processes these descriptors to generate language bindings
- 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:
- Basic Structure - Core fields, parameter types, and command-line formation
- Subcommands - Detailed explanation of the subcommand extension
- File Handling - Input/output file handling, path templates, extensions
- Advanced Features - Additional fields, extensions, and configuration options
- 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
Field | Description | Example |
---|---|---|
name | Short name of the tool | "bet" |
description | Detailed description of what the tool does | "Automated brain extraction tool for FSL" |
tool-version | Version of the tool being described | "6.0.4" |
schema-version | Version of the Boutiques schema | "0.5" |
command-line | Template for the command with placeholders | "bet [INFILE] [MASKFILE] [OPTIONS]" |
inputs | Array of input parameters | [{ "id": "infile", ... }] |
Common Optional Fields
Field | Description | Example |
---|---|---|
author | Author of the tool | "FMRIB Analysis Group, University of Oxford" |
url | URL for the tool's documentation | "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki" |
container-image | Container configuration | { "type": "docker", "image": "..." } |
output-files | Array of output files | [{ "id": "outfile", ... }] |
stdout-output | Capture stdout as an output | { "id": "stdout_data", ... } |
stderr-output | Capture 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 settests
: Sample invocations for testingonline-platform-urls
: URLs to platforms where tool is availableinvocation-schema
: Custom schema for tool-specific invocation validationsuggested-resources
: Computational resources neededtags
: Categorization tagserror-codes
: Tool-specific error codescustom
: 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
Field | Description | Required | Example |
---|---|---|---|
id | Unique identifier (alphanumeric + underscores) | Yes | "input_file" |
name | Human-readable name | Yes | "Input file" |
description | Detailed description | No | "The input image in NIFTI format" |
value-key | Placeholder in command-line template | Yes | "[INPUT_FILE]" |
optional | Whether parameter is required | Yes | true |
command-line-flag | Command-line option prefix | No | "-i" |
default-value | Default value if not specified | No | "standard.nii.gz" |
value-choices | Array of allowed values | No | ["small", "medium", "large"] |
Parameter Types
Basic Types
Type | Description | Attributes | Example |
---|---|---|---|
File | File path | N/A | { "type": "File", "id": "input_image" } |
String | Text string | N/A | { "type": "String", "id": "output_prefix" } |
Number | Numeric value | integer : boolean, minimum , maximum | { "type": "Number", "integer": false, "minimum": 0, "maximum": 1 } |
Flag | Boolean 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 requiredmax-list-entries
: Maximum number of elements allowedlist-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:
Field | Description | Required | Example |
---|---|---|---|
id | Unique identifier | Yes | "brain_mask" |
name | Human-readable name | Yes | "Brain Mask Image" |
description | Detailed description | No | "Binary mask of the brain" |
path-template | Template for output file path | Yes | "[OUTPUT_DIR]/[PREFIX]_mask.nii.gz" |
optional | Whether file might not be produced | No | true |
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:
- An object (for a single subcommand type)
- 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
- Use subcommands for mutually exclusive options instead of groups
- Keep subcommand IDs unique across the entire descriptor
- Use descriptive names for each subcommand option
- Consider output files carefully - each subcommand can have different outputs
- Nest subcommands when it makes logical sense for the tool's structure
- Use value-choices for fixed option sets within subcommands
- 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 - For detailed input/output file handling
- Advanced Features - For additional capabilities
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
- Always use
"type": "File"
for actual file paths - File inputs will be validated to ensure they exist when the tool is called
- 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 requiredvalue-choices
: A list of predefined file optionsdefault-value
: Default file path if not specifiedlist
: 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
Field | Description | Required | Example |
---|---|---|---|
id | Unique identifier | Yes | "brain_mask" |
name | Human-readable name | Yes | "Brain Mask Image" |
description | Detailed description | No | "Binary mask of the brain" |
path-template | Template for output file path | Yes | "[PREFIX]_mask.nii.gz" |
optional | Whether file might not be produced | No | true |
path-template-stripped-extensions | Extensions to remove from input paths | No | [".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
- Use
File
type for input files andString
type for output file paths - Keep output paths flexible by using placeholders from inputs
- Use
path-template-stripped-extensions
to handle file extension changes - Consider subcommand-specific outputs when different modes produce different files
- Use
stdout-output
andstderr-output
for tools that output data to the terminal - Make output files
optional: true
if they might not be produced in all cases
Container Considerations
When a tool runs in a container:
- Input file paths are automatically mapped from the host to the container
- Output files are mapped back from the container to the host
- 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
Field | Description | Example |
---|---|---|
name | Human-readable package name | "FSL" |
author | Author or organization | "FMRIB Analysis Group, University of Oxford" |
url | Documentation URL | "https://fsl.fmrib.ox.ac.uk/fslwiki" |
approach | How interfaces were created | "Manual" or "Extracted" |
status | Overall package status | "Experimental" or "Stable" |
container | Default container image | "brainlife/fsl:6.0.4-patched2" |
version | Package version | "6.0.5" |
description | Package description | "FSL is a comprehensive library..." |
id | Unique package identifier | "fsl" |
api.endpoints | Tool definitions | Array 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
Property | Description | Example |
---|---|---|
id | Unique identifier | "required_params" |
name | Human-readable name | "Required Parameters" |
description | Detailed description | "Parameters that must be specified" |
members | Array of parameter IDs | ["input_file", "output_prefix"] |
mutually-exclusive | Only one member can be used | true |
all-or-none | Either all or no members must be used | true |
one-is-required | At least one member must be specified | true |
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:
-
JSON Schema validation:
# In NiWrap repository python -m pytest tests/test_descriptors.py::test_descriptor_validity
-
Visual Studio Code validation: Add this to
.vscode/settings.json
:{ "json.schemas": [ { "fileMatch": ["descriptors/**/*.json"], "url": "./schemas/descriptor.schema.json" } ] }
-
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:
-
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 } ]
-
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:
- Make sure you're using
"type": "File"
for input files - Check if paths are relative to the current working directory
- 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:
-
Verify the container exists in the specified registry
-
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 apath-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:
- Check existing descriptors in the NiWrap repository for examples
- Examine the Boutiques documentation
- 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.