Spec-driven R wrappers for neuroimaging command-line tools — validated command building, execution, and provenance.
niflowr gives you typed, validated, provenance-tracked R functions for 300+ commands across FSL, AFNI, ANTs, FreeSurfer, FastSurfer, and dcm2niix — without writing a single system() call. Each tool interface is a JSON specification, so the wrappers are consistent, inspectable, and easy to extend.
result <- ni_fsl_bet(
in_file = "sub-01_T1w.nii.gz",
out_file = "sub-01_desc-brain_T1w.nii.gz",
frac = 0.5,
mask = TRUE
)
ni_outputs(result)
#> $out_file
#> [1] "sub-01_desc-brain_T1w.nii.gz"
That single call validated the inputs, built a safe argument vector (no shell string), ran bet via processx, confirmed the output exists, and wrote a provenance sidecar — with no paste() and no quoting bugs.
Neuroimaging pipelines stitch together dozens of command-line tools, each with its own flag syntax, validation quirks, and output-naming conventions. Wrapping them in R usually means brittle system() calls with paste()-assembled shell strings: no input checking, silent failures on a typo'd flag, shell-injection hazards, and no record of what actually ran.
processx as a character vector, without shell interpolation or word splitting.xor, requires).ni_result includes the exact command, engine/container, outputs, exit status, and timing; successful file-producing runs can also write a JSON sidecar.targets (and crew) handle scheduling, caching, and parallelism.For standard, end-to-end fMRI preprocessing of BIDS datasets, start with fMRIPrep and the broader NiPreps ecosystem. Those projects provide community-maintained preprocessing workflows, reports, BIDS App execution, and carefully chosen defaults for anatomical and functional MRI processing.
niflowr does not try to reimplement, replace, or outdo fMRIPrep/NiPreps. It
does not define a recommended preprocessing pipeline, choose algorithms for you,
or claim production-grade preprocessing. Its narrower job is to make individual
neuroimaging command-line calls from R safer, more inspectable, and easier to
compose when you are building custom analyses, wrapping a tool that is not part
of a NiPreps workflow, using targets, or reading/handoffing existing fMRIPrep
derivatives.
From the R-universe (recommended):
install.packages("niflowr", repos = "https://bbuchsbaum.r-universe.dev")
Or the development version from GitHub:
# install.packages("pak")
pak::pak("bbuchsbaum/niflowr")
niflowr wraps neuroimaging tools; it does not install them. FSL, AFNI, ANTs, FreeSurfer, etc. must be available on your
PATH, or supplied through a Docker/Apptainer image (see Reproducible execution). The pure-R parts below —ni_cmd(),ni_dry_run(), spec browsing, validation — work with no external binaries at all.
Preview the exact command without running it — pure R, no binaries needed:
library(niflowr)
ni_cmd(ni_call("fsl.bet",
in_file = "sub-01_T1w.nii.gz",
out_file = "sub-01_desc-brain_T1w.nii.gz",
frac = 0.5,
mask = TRUE
))
#> $command
#> [1] "bet"
#> $args
#> [1] "sub-01_T1w.nii.gz" "sub-01_desc-brain_T1w.nii.gz" "-f" "0.50" "-m"
#> ...
Dry-run to see the resolved command and expected outputs together:
ni_dry_run("fsl.bet",
in_file = "/data/sub-01_T1w.nii.gz",
out_file = "/out/sub-01_brain.nii.gz",
frac = 0.5
)
Two equivalent ways to run — a generated wrapper, or the generic ni_call() / ni_run():
# Ergonomic, tab-completable wrapper
res <- ni_fsl_bet(in_file = "T1w.nii.gz", out_file = "brain.nii.gz", frac = 0.5)
# Generic equivalent — handy for programmatic / metaprogrammed pipelines
res <- ni_run(ni_call("fsl.bet",
in_file = "T1w.nii.gz", out_file = "brain.nii.gz", frac = 0.5
))
ni_outputs(res) # named list of produced files
ni_provenance(res) # full record of what ran
Chain steps by piping one result into the next — niflowr pulls the right output path automatically:
ni_fsl_bet(in_file = "T1w.nii.gz", out_file = "brain.nii.gz") |>
ni_fsl_flirt(reference = "MNI152.nii.gz", out_file = "brain_mni.nii.gz")
A spec is a small JSON document. This is (abridged) the one behind ni_fsl_bet():
{
"id": "fsl.bet",
"command": "bet",
"inputs": {
"in_file": { "type": "file", "required": true, "cli": { "argstr": "%s", "position": 0 } },
"out_file": { "type": "file", "cli": { "argstr": "%s", "position": 1 } },
"frac": { "type": "double", "cli": { "argstr": "-f %.2f" } },
"mask": { "type": "flag", "cli": { "argstr": "-m" } }
},
"outputs": {
"out_file": { "type": "file", "path": { "from_input": "out_file" } }
}
}
From that single declaration niflowr derives the validated ni_fsl_bet() wrapper, the argument vector, and the output path. Because the behavior lives in data, the whole catalog is uniform — and adding or fixing a tool is a JSON edit, not a code change.
Outputs can be taken straight from an input (from_input), built from a glue template, or gated on a condition. For instance ants.registration_syn_quick derives every artifact from a single output_prefix and emits the warp fields only for deformable transforms — so ni_outputs() always reflects what the command actually produced (x_0GenericAffine.mat, x_Warped.nii.gz, x_1Warp.nii.gz, …).
Arguments are assembled as a vector and handed to processx, so spaces, special characters, and paths are passed as argument values rather than interpolated by a shell. Inputs are checked before execution against the spec — types, required fields, enum choices, numeric min/max, regex patterns, and relational constraints (xor, requires). A bad call fails loudly in R instead of producing a cryptic tool error (or worse, silently wrong output).
When ni_run() returns an ni_result, that result includes provenance metadata
for the execution. Failed commands can be returned by setting
error_on_status = FALSE; otherwise they raise an error. When provenance = TRUE (the default), the command exits successfully, and the spec has at least
one declared output, niflowr also writes a *_provenance.json sidecar next to
the primary output. The sidecar captures the spec id, full command and argument
vector, execution engine/profile, input-file checksums (xxhash64), output paths,
exit status, and timing. Read it back anytime:
ni_provenance_read("sub-01_desc-brain_T1w_provenance.json")
The same call runs natively or inside a container — niflowr handles path mounting and command rewriting:
ni_fsl_bet(in_file = "T1w.nii.gz", out_file = "brain.nii.gz",
.engine = "apptainer", .profile = "fsl")
.engine is one of "native", "docker", "apptainer", or "auto" (probe the host, then containers). A profile (defined in a project-level niflowr.yml) maps a tool suite to a container image:
profiles:
fsl:
docker_image: "brainlife/fsl:6.0.4"
# Many FSL images use a shell ENTRYPOINT that ignores the payload command.
entrypoint: ""
# Optional on Apple Silicon when the image is amd64-only:
# platform: "linux/amd64"
ants: { apptainer_uri: "docker://antsx/ants:v2.5.0" }
For stricter execution records, ni_pin() records resolved image digests in a lockfile, ni_lock_validate() checks your environment against it, and ni_doctor() reports on binaries, mounts, profiles, payload execution, and lock status.
Wrap any step as a targets file target so outputs are tracked and only stale steps rerun:
# _targets.R
library(targets); library(niflowr)
list(
tar_ni_step(brain, "fsl.bet",
in_file = "sub-01_T1w.nii.gz", out_file = "sub-01_brain.nii.gz", frac = 0.5)
)
niflowr includes a small BIDS-facing adapter layer and optional integrations with adjacent R packages:
ni_deriv_path() — build BIDS-derivatives output paths with the right entities.ni_bids_inputs() / ni_from_openneuro() — query a BIDS project (via bidser) or pull a dataset from OpenNeuro (via openneuroR).ni_fmriprep_preproc(), ni_fmriprep_confounds(), ni_fmriprep_derivatives() — locate and load outputs from an existing fMRIPrep derivatives directory.ni_bids_app() — scaffold a BIDS App (via bidsappr).ni_read_output() / ni_read_transform() — load results as neuroim2 images or metadata-typed neurotransform transforms.ni_convert_transform() — preserve a transform's geometric mapping while writing FSL, ITK/ANTs, AFNI, FreeSurfer LTA, generic, or X5 formats.These helpers are adapters around existing BIDS datasets, derivatives, and packages. They are not a substitute for running fMRIPrep or another NiPreps app when that is the appropriate preprocessing tool.
An under-converged registration runs, writes a transform, and does not fold; it is wrong only in a way you see by measuring it.
ni_ants_register_to_template() runs a pinned Rigid + Affine + SyN schedule
for T1w-to-template registration at about 1 mm. The default "precise"
schedule matched fMRIPrep 25.x's T1w-to-MNI152NLin2009cAsym transform within
noise, at about 17 minutes with 8 threads; "testing" exists only to
exercise pipelines. ni_ants_template_preset() shows each schedule with its
provenance and cost. Neither is a default of ni_ants_registration().ni_ants_registration_qa() reports ANTs similarity costs inside a mask,
brain-mask and label Dice after warping, and the Jacobian determinant's
minimum with counts of folded and non-finite voxels.ni_spec_list() # every available spec id
ni_inputs("ants.registration") # inputs: name, type, required, default, choices, ...
ni_constraints("ants.registration") # xor / requires / range constraints
ni_doctor() # environment & runtime health check
niflowr ships specs (and a matching ni_<tool>_<command>() wrapper) for 300+ commands:
| Suite | Commands | Example wrappers |
|---|---|---|
| FSL | 96 |
ni_fsl_bet(), ni_fsl_flirt(), ni_fsl_eddy()
|
| FreeSurfer | 92 |
ni_freesurfer_recon_all(), ni_freesurfer_synthstrip()
|
| AFNI | 80 |
ni_afni_calc(), ni_afni_volreg()
|
| ANTs | 34 |
ni_ants_registration(), ni_ants_apply_transforms(), ni_ants_transform_build()
|
| FastSurfer | 2 |
ni_fastsurfer_run(), ni_fastsurfer_segment()
|
| dcm2niix | 1 | ni_dcm2niix_convert() |
length(ni_spec_list())
#> [1] 305
Adding a tool is writing a JSON spec — no R required. Drop a mytool.command.json file in inst/specs/ (or point ni_spec_read() / ni_call() at any path), validate it, and you have a working interface:
ni_lint_specs("path/to/specs") # schema + sanity checks (shell metachars, position clashes, ...)
ni_call("path/to/mytool.command.json", input = "x.nii.gz") |> ni_cmd()
Specs that need bespoke argument grammar (e.g. ANTs' staged antsRegistration) can declare a render hook backed by a registered builder. See the Extending niflowr article for the full guide.
targets pipelines.targets/crew are for.niflowr is experimental (v0.1.0): the spec schema and API may still change.
(Run browseVignettes("niflowr"), or see the vignettes/ directory.)
MIT © Bradley Buchsbaum. See LICENSE.
ni_plan(call) and ni_run(call, dry_run = TRUE) return an inspectable
ni_execution_plan. It records the host and container payloads separately from
the executable/argv, effective environment overrides, host/container working
directories, mounts, profile settings, image identity when available, and spec
and package source hashes. Native processes inherit the host environment;
explicit settings take precedence in the order configuration, spec, call.
Container image environment defaults remain image-defined. Plans do not pull
Apptainer images or launch the workload; they may create mount directories or
stage explicitly permitted inputs.
call <- ni_call("ants.n4_bias_field_correction",
input_image = "input.nii.gz", output_image = "corrected.nii.gz",
save_bias = TRUE, .engine = "native")
plan <- ni_plan(call)
result <- ni_run(call, timeout = 300)
A zero process exit does not override a failed required-output check. Required
outputs must use fresh paths; an existing file is not an admitted cache entry.
Specs can explicitly mark an input role = "inout" for in-place processing;
its original content is hashed before execution and unchanged output is rejected.
Only outputs declared must_exist = TRUE are required. This enforcement is
currently enabled for the smoke subset (N4, BET, FAST, MCFLIRT, FLIRT, epi_reg),
not asserted for the entire imported catalog.
Each invocation keeps stdout, stderr, and (unless disabled) provenance under
.niflowr/runs beside the first output, or the effective cwd when there are no
outputs. Set log_dir to choose another location. Failure conditions of class
ni_execution_error contain $result and $stderr; the result includes log
and provenance paths. error_on_status = FALSE returns a failed result with a
warning, but return = "files" always rejects a failed execution. A wall-time
limit also removes the invocation's uniquely named Docker container. Container
naming and detached execution cannot be overridden through extra_run_args.
N4 maps both corrected-image and bias-field paths. save_bias = TRUE emits the
native two-output form; copy_header = TRUE raises an explicit unsupported
error. FAST returns partial-volume and requested probability/binary/restored/bias
collections. epi_reg returns the final image and matrix and treats supplied WM
segmentation as an input. These FSL output resolvers support NIFTI_GZ and
NIFTI; other formats fail explicitly.
The smoke harness runs a synthetic preprocessing chain with digest-pinned images and independently reads its products with NiBabel. This establishes mechanical interface behavior, not scientific equivalence or preprocessing efficacy. Unit CI and imaging smoke CI report separately.