Why niflowr?
Neuroimaging pipelines call many command-line tools — FSL’s
bet, ANTs’ antsRegistration, FreeSurfer’s
recon-all — each with its own argument syntax, validation
quirks, and output conventions. Wrapping these in R typically means
writing brittle system() calls with
paste()-assembled shell strings, no input validation, and
no record of what ran. niflowr takes a different approach:
- Tool interfaces are defined as JSON specs, not executable code. Each spec declares inputs, outputs, CLI rendering rules, and validation constraints.
- Commands are built as argument vectors, never shell strings — no quoting bugs, no injection hazards.
- Execution captures provenance — what command ran, with what arguments, how long it took, and what it produced.
- Pipeline orchestration is left to {targets} — niflowr handles the interface layer; {targets} and {crew} handle scheduling, caching, and parallelism.
Quick example
Here’s what it looks like to run FSL’s brain extraction tool:
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"
#> $mask_file
#> [1] "sub-01_desc-brain_T1w.nii.gz_mask.nii.gz"That single call validated your inputs, built a safe argument vector,
ran bet via processx, checked that outputs
exist, and wrote a provenance sidecar — all without a single
paste() or system().
Browsing available specs
niflowr ships with specs for common tools across four major suites:
ni_spec_list()
#> [1] "afni.a_boverlap"
#> [2] "afni.afn_ito_nifti"
#> [3] "afni.align_epi_anat_py"
#> [4] "afni.allineate"
#> [5] "afni.auto_tcorrelate"
#> [6] "afni.auto_tlrc"
#> [7] "afni.autobox"
#> [8] "afni.automask"
#> [9] "afni.axialize"
#> [10] "afni.bandpass"
#> [11] "afni.blur_in_mask"
#> [12] "afni.blur_to_fwhm"
#> [13] "afni.brick_stat"
#> [14] "afni.bucket"
#> [15] "afni.calc"
#> [16] "afni.cat_matvec"
#> [17] "afni.cat"
#> [18] "afni.center_mass"
#> [19] "afni.clip_level"
#> [20] "afni.convert_dset"
#> [21] "afni.copy"
#> [22] "afni.deconvolve"
#> [23] "afni.degree_centrality"
#> [24] "afni.despike"
#> [25] "afni.detrend"
#> [26] "afni.dot"
#> [27] "afni.ecm"
#> [28] "afni.edge3"
#> [29] "afni.eval"
#> [30] "afni.fim"
#> [31] "afni.fourier"
#> [32] "afni.fwh_mx"
#> [33] "afni.gcor"
#> [34] "afni.hist"
#> [35] "afni.lfcd"
#> [36] "afni.local_bistat"
#> [37] "afni.localstat"
#> [38] "afni.mask_tool"
#> [39] "afni.maskave"
#> [40] "afni.means"
#> [41] "afni.merge"
#> [42] "afni.net_corr"
#> [43] "afni.notes"
#> [44] "afni.nwarp_adjust"
#> [45] "afni.nwarp_apply"
#> [46] "afni.nwarp_cat"
#> [47] "afni.one_d_tool_py"
#> [48] "afni.outlier_count"
#> [49] "afni.quality_index"
#> [50] "afni.qwarp_plus_minus"
#> [51] "afni.qwarp"
#> [52] "afni.re_ho"
#> [53] "afni.refit"
#> [54] "afni.remlfit"
#> [55] "afni.resample"
#> [56] "afni.retroicor"
#> [57] "afni.roi_stats"
#> [58] "afni.seg"
#> [59] "afni.skull_strip"
#> [60] "afni.svm_test"
#> [61] "afni.svm_train"
#> [62] "afni.synthesize"
#> [63] "afni.t_cat_sub_brick"
#> [64] "afni.t_cat"
#> [65] "afni.t_corr_map"
#> [66] "afni.t_corr1_d"
#> [67] "afni.t_correlate"
#> [68] "afni.t_norm"
#> [69] "afni.t_project"
#> [70] "afni.t_shift"
#> [71] "afni.t_smooth"
#> [72] "afni.t_stat"
#> [73] "afni.to3_d"
#> [74] "afni.undump"
#> [75] "afni.unifize"
#> [76] "afni.volreg"
#> [77] "afni.warp"
#> [78] "afni.z_cut_up"
#> [79] "afni.zcat"
#> [80] "afni.zeropad"
#> [81] "ants.affine_initializer"
#> [82] "ants.ai"
#> [83] "ants.ants_introduction"
#> [84] "ants.ants"
#> [85] "ants.apply_transforms_to_points"
#> [86] "ants.apply_transforms"
#> [87] "ants.atropos"
#> [88] "ants.average_affine_transform"
#> [89] "ants.average_images"
#> [90] "ants.brain_extraction"
#> [91] "ants.buildtemplateparallel"
#> [92] "ants.compose_multi_transform"
#> [93] "ants.composite_transform_util"
#> [94] "ants.convert_scalar_image_to_rgb"
#> [95] "ants.cortical_thickness"
#> [96] "ants.create_jacobian_determinant_image"
#> [97] "ants.create_tiled_mosaic"
#> [98] "ants.denoise_image"
#> [99] "ants.gen_warp_fields"
#> [100] "ants.image_math"
#> [101] "ants.joint_fusion"
#> [102] "ants.kelly_kapowski"
#> [103] "ants.label_geometry"
#> [104] "ants.laplacian_thickness"
#> [105] "ants.measure_image_similarity"
#> [106] "ants.multiply_images"
#> [107] "ants.n4_bias_field_correction"
#> [108] "ants.registration_syn_quick"
#> [109] "ants.registration"
#> [110] "ants.resample_image_by_spacing"
#> [111] "ants.threshold_image"
#> [112] "ants.transform_build"
#> [113] "ants.warp_image_multi_transform"
#> [114] "ants.warp_time_series_image_multi_transform"
#> [115] "dcm2niix.convert"
#> [116] "fastsurfer.run"
#> [117] "fastsurfer.segment"
#> [118] "freesurfer.add_x_form_to_header"
#> [119] "freesurfer.aparc2_aseg"
#> [120] "freesurfer.apas2_aseg"
#> [121] "freesurfer.apply_mask"
#> [122] "freesurfer.apply_vol_transform"
#> [123] "freesurfer.bb_register"
#> [124] "freesurfer.binarize"
#> [125] "freesurfer.ca_label"
#> [126] "freesurfer.ca_normalize"
#> [127] "freesurfer.ca_register"
#> [128] "freesurfer.check_talairach_alignment"
#> [129] "freesurfer.concatenate_lta"
#> [130] "freesurfer.concatenate"
#> [131] "freesurfer.contrast"
#> [132] "freesurfer.curvature_stats"
#> [133] "freesurfer.curvature"
#> [134] "freesurfer.dicom_convert"
#> [135] "freesurfer.edit_w_mwith_aseg"
#> [136] "freesurfer.em_register"
#> [137] "freesurfer.euler_number"
#> [138] "freesurfer.extract_main_component"
#> [139] "freesurfer.fit_ms_params"
#> [140] "freesurfer.fix_topology"
#> [141] "freesurfer.fuse_segmentations"
#> [142] "freesurfer.glm_fit"
#> [143] "freesurfer.gtm_seg"
#> [144] "freesurfer.gtmpvc"
#> [145] "freesurfer.image_info"
#> [146] "freesurfer.jacobian"
#> [147] "freesurfer.label2_annot"
#> [148] "freesurfer.label2_label"
#> [149] "freesurfer.label2_vol"
#> [150] "freesurfer.logan"
#> [151] "freesurfer.lta_convert"
#> [152] "freesurfer.make_average_subject"
#> [153] "freesurfer.make_surfaces"
#> [154] "freesurfer.mni_bias_correction"
#> [155] "freesurfer.mp_rto_mni305"
#> [156] "freesurfer.mr_is_ca_label"
#> [157] "freesurfer.mr_is_calc"
#> [158] "freesurfer.mr_is_combine"
#> [159] "freesurfer.mr_is_convert"
#> [160] "freesurfer.mr_is_expand"
#> [161] "freesurfer.mr_is_inflate"
#> [162] "freesurfer.mri_convert"
#> [163] "freesurfer.mri_coreg"
#> [164] "freesurfer.mri_fill"
#> [165] "freesurfer.mri_marching_cubes"
#> [166] "freesurfer.mri_pretess"
#> [167] "freesurfer.mri_tessellate"
#> [168] "freesurfer.mris_preproc_recon_all"
#> [169] "freesurfer.mris_preproc"
#> [170] "freesurfer.mrtm1"
#> [171] "freesurfer.mrtm2"
#> [172] "freesurfer.ms_lda"
#> [173] "freesurfer.normalize"
#> [174] "freesurfer.one_sample_t_test"
#> [175] "freesurfer.paint"
#> [176] "freesurfer.parcellation_stats"
#> [177] "freesurfer.parse_dicom_dir"
#> [178] "freesurfer.recon_all"
#> [179] "freesurfer.register_av_ito_talairach"
#> [180] "freesurfer.register"
#> [181] "freesurfer.relabel_hypointensities"
#> [182] "freesurfer.remove_intersection"
#> [183] "freesurfer.remove_neck"
#> [184] "freesurfer.resample"
#> [185] "freesurfer.robust_register"
#> [186] "freesurfer.robust_template"
#> [187] "freesurfer.sample_to_surface"
#> [188] "freesurfer.seg_stats_recon_all"
#> [189] "freesurfer.seg_stats"
#> [190] "freesurfer.segment_cc"
#> [191] "freesurfer.segment_wm"
#> [192] "freesurfer.smooth_tessellation"
#> [193] "freesurfer.smooth"
#> [194] "freesurfer.sphere"
#> [195] "freesurfer.spherical_average"
#> [196] "freesurfer.surface_smooth"
#> [197] "freesurfer.surface_snapshots"
#> [198] "freesurfer.surface_transform"
#> [199] "freesurfer.surface2_vol_transform"
#> [200] "freesurfer.synthesize_flash"
#> [201] "freesurfer.synthmorph_apply"
#> [202] "freesurfer.synthmorph_register"
#> [203] "freesurfer.synthstrip"
#> [204] "freesurfer.talairach_avi"
#> [205] "freesurfer.talairach_qc"
#> [206] "freesurfer.tkregister2"
#> [207] "freesurfer.unpack_sdicom_dir"
#> [208] "freesurfer.volume_mask"
#> [209] "freesurfer.watershed_skull_strip"
#> [210] "fsl.apply_mask"
#> [211] "fsl.apply_topup"
#> [212] "fsl.apply_warp"
#> [213] "fsl.apply_xfm"
#> [214] "fsl.ar1_image"
#> [215] "fsl.av_scale"
#> [216] "fsl.b0_calc"
#> [217] "fsl.bedpostx5"
#> [218] "fsl.bet"
#> [219] "fsl.binary_maths"
#> [220] "fsl.change_data_type"
#> [221] "fsl.cluster"
#> [222] "fsl.complex"
#> [223] "fsl.contrast_mgr"
#> [224] "fsl.convert_warp"
#> [225] "fsl.convert_xfm"
#> [226] "fsl.copy_geom"
#> [227] "fsl.dilate_image"
#> [228] "fsl.distance_map"
#> [229] "fsl.dti_fit"
#> [230] "fsl.dual_regression"
#> [231] "fsl.eddy_correct"
#> [232] "fsl.eddy_quad"
#> [233] "fsl.eddy"
#> [234] "fsl.epi_de_warp"
#> [235] "fsl.epi_reg"
#> [236] "fsl.erode_image"
#> [237] "fsl.extract_roi"
#> [238] "fsl.fast"
#> [239] "fsl.feat_model"
#> [240] "fsl.feat"
#> [241] "fsl.filmgls"
#> [242] "fsl.filter_regressor"
#> [243] "fsl.find_the_biggest"
#> [244] "fsl.first"
#> [245] "fsl.flameo"
#> [246] "fsl.flirt"
#> [247] "fsl.fnirt"
#> [248] "fsl.fugue"
#> [249] "fsl.glm"
#> [250] "fsl.ica_aroma"
#> [251] "fsl.image_maths"
#> [252] "fsl.image_meants"
#> [253] "fsl.image_stats"
#> [254] "fsl.inv_warp"
#> [255] "fsl.isotropic_smooth"
#> [256] "fsl.make_dyadic_vectors"
#> [257] "fsl.maths_command"
#> [258] "fsl.max_image"
#> [259] "fsl.maxn_image"
#> [260] "fsl.mcflirt"
#> [261] "fsl.mean_image"
#> [262] "fsl.median_image"
#> [263] "fsl.melodic"
#> [264] "fsl.merge"
#> [265] "fsl.min_image"
#> [266] "fsl.motion_outliers"
#> [267] "fsl.multi_image_maths"
#> [268] "fsl.overlay"
#> [269] "fsl.percentile_image"
#> [270] "fsl.plot_motion_params"
#> [271] "fsl.plot_time_series"
#> [272] "fsl.power_spectrum"
#> [273] "fsl.prelude"
#> [274] "fsl.prepare_fieldmap"
#> [275] "fsl.prob_track_x"
#> [276] "fsl.prob_track_x2"
#> [277] "fsl.proj_thresh"
#> [278] "fsl.randomise"
#> [279] "fsl.reorient2_std"
#> [280] "fsl.robust_fov"
#> [281] "fsl.sig_loss"
#> [282] "fsl.slice_timer"
#> [283] "fsl.slice"
#> [284] "fsl.slicer"
#> [285] "fsl.smm"
#> [286] "fsl.smooth_estimate"
#> [287] "fsl.smooth"
#> [288] "fsl.spatial_filter"
#> [289] "fsl.split"
#> [290] "fsl.std_image"
#> [291] "fsl.susan"
#> [292] "fsl.swap_dimensions"
#> [293] "fsl.temporal_filter"
#> [294] "fsl.text2_vest"
#> [295] "fsl.threshold"
#> [296] "fsl.topup"
#> [297] "fsl.tract_skeleton"
#> [298] "fsl.unary_maths"
#> [299] "fsl.vec_reg"
#> [300] "fsl.vest2_text"
#> [301] "fsl.warp_points_from_std"
#> [302] "fsl.warp_points_to_std"
#> [303] "fsl.warp_points"
#> [304] "fsl.warp_utils"
#> [305] "fsl.x_fibres5"Each spec is a JSON file under inst/specs/. You can
inspect any spec to see what inputs and outputs it defines:
spec <- ni_spec_read("fsl.bet")
spec
#>
#> ── FSL BET ──
#>
#> Command: `bet`
#> Spec version: 0.1.0
#> FSL BET wrapper for skull stripping
#>
#> ── Inputs (20)
#> args (string) - Additional parameters to the command
#> center (list) - center of gravity in voxels
#> frac (double) - fractional intensity threshold
#> functional (flag) - apply to 4D fMRI data
#> in_file (file) [required] - input file to skull strip
#> mask (flag) - create binary mask image
#> mesh (flag) - generate a vtk mesh brain surface
#> no_output (flag) - Don't generate segmented output
#> out_file (file) - name of output skull stripped image
#> outline (flag) - create surface outline image
#> padding (flag) - improve BET if FOV is very small in Z (by temporarily padding
#> end slices)
#> radius (int) - head radius
#> reduce_bias (flag) - bias field and neck cleanup
#> remove_eyes (flag) - eye & optic nerve cleanup (can be useful in SIENA)
#> robust (flag) - robust brain centre estimation (iterates BET several times)
#> skull (flag) - create skull image
#> surfaces (flag) - run bet2 and then betsurf to get additional skull and scalp
#> surfaces (includes registrations)
#> t2_guided (file) - as with creating surfaces, when also feeding in
#> non-brain-extracted T2 (includes registrations)
#> threshold (flag) - apply thresholding to segmented brain image and mask
#> vertical_gradient (double) - vertical gradient in fractional intensity
#> threshold (-1, 1)
#>
#> ── Outputs (1)
#> out_file (file)The core workflow
niflowr has three core objects that flow into each other:
-
ni_spec— the tool interface definition (from JSON) -
ni_call— a spec bound to concrete parameter values -
ni_result— the execution output with runtime info and provenance
Step 1: Build a call
ni_call() binds parameter values to a spec. It validates
inputs immediately — required parameters, file existence, numeric
ranges, mutual exclusion constraints:
# This fails: frac must be between 0 and 1
ni_call("fsl.bet",
in_file = "/tmp/t1.nii.gz",
out_file = "/tmp/brain.nii.gz",
frac = 1.5,
.validate = FALSE # skip file-exists check for this demo
)
#>
#> ── ni_call: fsl.bet
#> Command: `bet /tmp/t1.nii.gz /tmp/brain.nii.gz -f 1.50`
#>
#> ── Expected outputs
#> out_file: /tmp/brain.nii.gzWhen inputs are valid, you get back a call object you can inspect:
call <- ni_call("fsl.bet",
in_file = "/tmp/t1.nii.gz",
out_file = "/tmp/brain.nii.gz",
frac = 0.3,
mask = TRUE,
.validate = FALSE
)
call
#>
#> ── ni_call: fsl.bet
#> Command: `bet /tmp/t1.nii.gz /tmp/brain.nii.gz -f 0.30 -m`
#>
#> ── Expected outputs
#> out_file: /tmp/brain.nii.gzStep 2: Inspect the command
Before running anything, you can see exactly what command would be executed. niflowr never hides what it’s doing:
ni_cmd(call)
#> $command
#> [1] "bet"
#>
#> $args
#> [1] "/tmp/t1.nii.gz" "/tmp/brain.nii.gz" "-f"
#> [4] "0.30" "-m"
#>
#> $wd
#> NULL
#>
#> $env
#> NULL
#>
#> $engine
#> NULL
#>
#> $profile
#> NULL
#>
#> $stdout
#> NULL
#>
#> $stderr
#> NULLThe args vector is what gets passed to
processx::run() — each element is a separate argument,
never concatenated into a shell string.
Step 3: Execute
ni_run() runs the command and returns an
ni_result:
result <- ni_run(call)
result
#> -- ni_result: fsl.bet [success] --
#> Exit status: 0
#> Duration: 12.3s
#>
#> -- Outputs --
#> out_file: /tmp/brain.nii.gz
#> mask_file: /tmp/brain.nii.gz_mask.nii.gzDry-run mode
If you just want to preview the command without executing it, use
dry_run = TRUE or the shorthand
ni_dry_run():
ni_dry_run("fsl.bet",
in_file = "/tmp/t1.nii.gz",
out_file = "/tmp/brain.nii.gz",
frac = 0.5,
.engine = "native"
)
#> ℹ Dry run [native]: `bet /tmp/t1.nii.gz /tmp/brain.nii.gz -f 0.50`
#>
#> ── Expected outputs
#> out_file: /tmp/brain.nii.gzUsing the convenience wrappers
Every bundled spec has a corresponding wrapper function with typed arguments and documentation. These are the recommended way to call tools:
| Function | Tool |
|---|---|
ni_fsl_bet() |
FSL BET (brain extraction) |
ni_fsl_flirt() |
FSL FLIRT (linear registration) |
ni_fsl_applywarp() |
FSL applywarp (apply warp fields) |
ni_afni_3dcalc() |
AFNI 3dcalc (voxelwise math) |
ni_afni_3dresample() |
AFNI 3dresample (reslicing) |
ni_ants_registration() |
ANTs registration (SyN) |
ni_ants_apply_transforms() |
ANTs antsApplyTransforms |
ni_freesurfer_recon_all() |
FreeSurfer recon-all |
ni_freesurfer_mri_convert() |
FreeSurfer mri_convert |
Each wrapper calls ni_call() + ni_run()
internally, so you get the same validation, safe execution, and
provenance:
# Linear registration with FLIRT
result <- ni_fsl_flirt(
in_file = "func_mean.nii.gz",
reference = "MNI152_T1_2mm.nii.gz",
out_file = "func_mean_mni.nii.gz",
out_matrix_file = "func2mni.mat",
dof = 12,
cost = "corratio"
)Provenance
Every successful run writes a JSON sidecar next to the primary output. This records the exact command, arguments, timing, and (optionally) input file hashes:
# After running bet...
prov <- ni_provenance(result)
prov$command
#> [1] "bet"
prov$args
#> [1] "/data/sub-01_T1w.nii.gz" "/data/sub-01_brain.nii.gz" "-f" "0.5" "-m"
prov$duration_secs
#> [1] 12.3
# Or read a sidecar from disk
ni_provenance_read("sub-01_brain_provenance.json")Container support
niflowr can run tools inside Docker or Apptainer containers.
Container execution is profile-driven via niflowr.yml, with
stable container paths (/in, /out,
/work):
# Configure once per project (or rely on automatic niflowr.yml discovery):
ni_config(config_file = "niflowr.yml")
result <- ni_fsl_bet(
in_file = "/data/t1.nii.gz",
out_file = "/data/brain.nii.gz",
.engine = "auto", # native first, then container fallback by runtime.prefer
.profile = "fsl" # maps to profiles.fsl in niflowr.yml
)
# Runs natively if `bet` exists; otherwise uses configured container backend.Wrappers always accept host paths; path mapping into container paths
is handled inside ni_run().
FastSurfer: GPU-accelerated segmentation
niflowr includes specs for FastSurfer, a
GPU-accelerated alternative to FreeSurfer that runs brain segmentation
in ~5 minutes. FastSurfer runs primarily via container, so configure a
profile in niflowr.yml:
profiles:
fastsurfer:
docker_image: "deepmi/fastsurfer:cpu-v2.4.2"
apptainer_uri: "docker://deepmi/fastsurfer:cpu-v2.4.2"Then call the wrapper:
# Full pipeline: segmentation + surface reconstruction
result <- ni_fastsurfer_run(
t1 = "sub-01_T1w.nii.gz",
sid = "sub-01",
sd = "derivatives/fastsurfer",
device = "cuda",
.engine = "docker"
)
# Segmentation only (~5 min)
seg <- ni_fastsurfer_segment(
t1 = "sub-01_T1w.nii.gz",
sid = "sub-01",
sd = "derivatives/fastsurfer",
device = "cpu"
)
ni_outputs(seg)
#> $seg_file
#> [1] "derivatives/fastsurfer/sub-01/mri/aparc.DKTatlas+aseg.deep.mgz"
#> $mask_file
#> [1] "derivatives/fastsurfer/sub-01/mri/mask.mgz"Runtime diagnostics with ni_doctor()
Use ni_doctor() to check runtime binaries, mounted
roots, profile definitions, and lockfile consistency:
Pinning and validating lockfiles
Use ni_pin() to create/update a lockfile with resolved
runtime references for configured profiles, and
ni_lock_validate() to verify current config matches the
lock:
# Create/update lockfile (default: runtime.lockfile or niflowr.lock.yml)
lock <- ni_pin()
# Validate current config against lockfile
checks <- ni_lock_validate()
checksLock enforcement during execution
Enable lock enforcement to ensure containerized runs use lockfile-pinned references:
ni_config(config = list(
runtime = list(
lockfile = "niflowr.lock.yml",
lock_enforce = TRUE
)
))
# When lock_enforce = TRUE, ni_run() resolves container refs from the lockfile
# for the selected profile and errors if lock constraints are violated.
result <- ni_fsl_bet(
in_file = "/data/t1.nii.gz",
out_file = "/data/brain.nii.gz",
.engine = "docker",
.profile = "fsl"
)Ecosystem integration
niflowr is designed to work with three companion packages:
neuroim2 — read outputs as R objects
After running a tool, load its output directly into a neuroim2
NeuroVol or NeuroVec:
result <- ni_fsl_bet(in_file = "t1.nii.gz", out_file = "brain.nii.gz")
brain <- ni_read_output(result, "out_file")
# Returns a NeuroVol — you can slice, plot, do math on itneurotransform — work with spatial transforms
Registration outputs can be loaded as neurotransform morphism objects for composition and resampling:
reg <- ni_fsl_flirt(
in_file = "func.nii.gz", reference = "t1.nii.gz",
out_file = "func_t1.nii.gz", out_matrix_file = "func2t1.mat"
)
xfm <- ni_read_transform(reg, "out_matrix_file")
# Returns an Affine3DMorphism — compose, invert, resample with itbidser — BIDS-aware input discovery
Query a BIDS dataset for inputs and generate derivatives-style output paths:
library(bidser)
proj <- bids_project("/data/bids")
# Find T1w images for a subject
inputs <- ni_bids_inputs(proj, "fsl.bet", subid = "01", modality = "T1w")
# Generate a BIDS-derivatives output path
ni_deriv_path(inputs$path[1], desc = "brain", suffix = "T1w")
#> [1] "derivatives/niflowr/sub-01/anat/sub-01_desc-brain_T1w.nii.gz"Building pipelines with {targets}
For multi-step pipelines, niflowr integrates with {targets} for DAG-based execution with caching and parallelism. The key idea: niflowr handles the interface layer, {targets} handles the execution layer.
Using ni_run_files() in targets
The simplest pattern uses ni_run_files(), which returns
output paths suitable for format = "file" targets:
Using tar_ni_step() for less boilerplate
The tar_ni_step() target factory wraps this pattern:
list(
tar_ni_step(brain, "fsl.bet",
in_file = "data/sub-01_T1w.nii.gz",
out_file = "derivatives/sub-01_desc-brain_T1w.nii.gz",
frac = 0.5
)
)Dynamic branching over subjects
Process multiple subjects in parallel with {targets} dynamic branching and {crew} workers:
library(targets)
library(crew)
library(niflowr)
library(bidser)
tar_option_set(
packages = c("niflowr", "bidser"),
controller = crew_controller_local(workers = 8)
)
list(
tar_target(bids, bidser::bids_project("data/bids")),
tar_target(t1w_files,
bidser::search_files(bids, regex = "_T1w\\.nii", full_path = TRUE)
),
tar_target(brain, {
out <- ni_deriv_path(t1w_files, desc = "brain")
ni_run_files("fsl.bet", in_file = t1w_files, out_file = out, frac = 0.5)
}, pattern = map(t1w_files), format = "file")
){targets} skips subjects whose outputs are already up to date, and {crew} distributes the work across 8 parallel workers.
Writing your own specs
You can write specs for any command-line tool. The
vignette("extending-niflowr") vignette walks through the
full loop (spec → lint → wrapper → golden → test) using SynthStrip as a
worked example. The short version: create a JSON file following the
schema at inst/schema/niflowr-spec-0.1.0.json:
{
"spec_version": "0.1.0",
"id": "my.tool",
"title": "My Custom Tool",
"command": "mytool",
"inputs": {
"in_file": {
"type": "file",
"required": true,
"validate": { "exists": true },
"cli": { "argstr": "--input %s" }
},
"threshold": {
"type": "double",
"validate": { "min": 0, "max": 1 },
"cli": { "argstr": "--thresh %g" }
}
},
"outputs": {
"out_file": {
"type": "file",
"path": { "template": "{in_file}_processed.nii.gz" },
"must_exist": true
}
}
}Load it by path:
spec <- ni_spec_read("/path/to/my.tool.json")
result <- ni_run(ni_call(spec, in_file = "data.nii.gz", threshold = 0.5))Next steps
- Browse the bundled specs with
ni_spec_list()andni_spec_read()to see how real interfaces are defined - See
?ni_callfor the full set of validation and constraint options - See
?ni_runfor execution options (echo, provenance, container) - See
?tar_ni_stepfor {targets} integration details - See
?ni_read_outputand?ni_read_transformfor ecosystem helpers