submitr (development version)
Breaking changes
The results folder written by
htc_gen_executable()is nowoutput/rather thanresults/, matching the folder convention used acrosstooleroandcontainr. Analysis scripts that write toresults/will produce an empty tarball until they are updated;toolero::save_output()handles this for you.Results tarballs are named
<script stem>[-<subset stem>]-results.tar.gz, with directories and extensions stripped from both stems. A single job runninganalysis.Rstill producesanalysis-results.tar.gz, andr_script = "R/analysis.R"now does too, but a multiple-mode job overadelie.csvproducesanalysis-adelie-results.tar.gzwhere it previously producedadelie.csv-results.tar.gz. This is a clean break: a job submitted with 0.1.0 and collected with this version will havehtc_download()looking for names that do not exist on the submit node. Download those results before upgrading, or passfilesexplicitly. See the README section “A note on the results naming change” for the rationale.htc_gen_submit()gains anr_scriptargument, positioned afterexecutable. It is used to derive the defaultoutput_filesname, which has to match the tarballhtc_gen_executable()tells the job to build, and to check that the script is among the uploaded inputs (see below). The function never reads the executable script or the Dockerfile, so the script’s name cannot be inferred and has to be supplied. In multiple mode, omitting it warns. Code callinghtc_gen_submit()with positional arguments pastexecutablewill need updating.Single-mode submit files now carry a
transfer_output_filesline derived fromr_script, where previously they carried only a placeholder comment unlessoutput_fileswas supplied. This is what makeshtc_download()bring back the results tarball in single mode rather than log files alone.The analysis script now travels to the execute node as an uploaded job input rather than being baked into the container image.
htc_gen_executable()runs it by bare name (e.g.Rscript analysis.R) rather than by an absolute,home_dir-prefixed path, because HTCondor’s file transfer does not preserve subdirectories: a script listed intransfer_input_filesalways lands flat in the scratch directory. List the script ininput_fileswhen callinghtc_gen_submit(), andhtc_upload()sends it along with everything else;htc_gen_submit()warns whenr_scriptis known but its basename is missing frominput_files.data_filesare unaffected: they remain baked into the image underhome_dirand are still read by absolute path. The practical benefit is that editing the analysis script now requires only re-uploading and resubmitting, not a container rebuild and registry push. A Dockerfile built withcontainr::generate_dockerfile(code_file = r_script)should drop that argument and add the script toinput_filesinstead; see the updated vignettes.The option controlling
htc_config()’s progress messages is renamed fromhtc_config_verbosetosubmitr.verbose, matching the namespacing ofsubmitr.configandsubmitr.check_server. It was undocumented, so this is unlikely to affect anyone; both options are now documented under?htc_config.The resource presets file is now
htc-resources.yml, following the family-wide rule (toolero’sCONVENTIONS.md, section 8) that every YAML file a family package names for itself ends in.yml. The copy shipped ininst/extdata/is renamed. A project’s ownhtc-resources.yaml, as submitr 0.1.0 documented it, is still read for this release, with a warning asking for it to be renamed; the old name will stop being read in the next release. When both names are present,htc-resources.ymlis used and the old file is ignored, also with a warning.
New features
htc_start()starts an HTC session by reading the project’shtc.cfgand storing it for the rest of the R session.htc_upload(),htc_submit(),htc_status(), andhtc_download()then use the stored config automatically whenconfig = NULL, soconfig = cfgno longer has to be passed on every call. Each of them checks for an explicitconfigfirst, falls back to the session, and errors with instructions if neither is available. Calloptions(submitr.config = NULL)to clear the session manually, or let it expire when R restarts.htc_config()gains aproject_configargument. Pass the path to a_toolero.ymlfile (the project configtoolero::init_project()writes to a project’s root) and itsfoldersandconventionssections are parsed once and folded into the returned list asconfig$project$foldersandconfig$project$conventions, kept separate from the connection detailsconfighas always held and never written intohtc.cfg.containr::generate_dockerfile()reads the same file through its ownconfigargument.htc_config()gains acheck_serverargument controlling whether it opens an SSH connection to report the server’s reachability. It previously did so unconditionally, which meant that merely reading a config file required a network, and that scripts, test suites, andR CMD checkall paid for a probe with no one to read it. The argument defaults to the newsubmitr.check_serveroption, so it can be set once for a session or a CI job rather than passed at every call, andhtc_start()accepts it through....htc_ssh_setup()writes the ControlMaster block described inhtc_config()’s setup guidance to~/.ssh/config, and creates the connections directory it references, without leaving R. It leaves the file untouched if a matchingHostblock already exists, and supportsdry_run = TRUEto preview the change first.submitr now keeps a submission state file,
htc-manifest.yml, in the project root besidehtc.cfg.htc_gen_submit(),htc_gen_executable(),htc_upload(), andhtc_submit()record what they know as they run (mode, submit and executable file names, input and output files, subset names, remote path, cluster ID), and every later step reads it back for its defaults, so values typed once flow through the rest of the workflow. The file lives on disk rather than in a session option, so it survives an R restart: a long job can be submitted in one session and collected in another. Every function that reads or writes it takes apathargument naming the directory that holds it, defaulting to".". The generators’pathis independent of theiroutputargument, so generated job files can be written to a subfolder while the submission state stays in the project root; to keep it elsewhere, pass the samepathto every call. Development versions before 0.2.0 wrote the file ashtc-manifest.yaml. It is nowhtc-manifest.yml, following the family’s.ymlrule, with no fallback to the old name since no release ever wrote it: in a project set up with a development version, rename the file or regenerate the job files.htc_gen_submit()andhtc_gen_executable()gain aconfigargument (a named list as returned byhtc_config()). Whenconfigwas built withproject_config,htc_gen_submit()’squeue_fromdefaults tofile.path(config$project$conventions$split_dir, "manifest.csv")andhtc_gen_executable()’sresults_folderdefaults toconfig$project$conventions$output_dir. Both remain fully optional: everything can still be passed explicitly.htc_gen_submit()’sexecutableargument andhtc_gen_executable()’soutput_fileargument default to the executable name the other generator already recorded in the submission state, so the name only has to be typed once, whichever generator runs first. If an explicit value disagrees with the recorded one, both functions warn rather than silently preferring one over the other; the explicit value is still used.htc_check()is a preflight check that catches, locally and in seconds, problems that would otherwise surface an hour later as a held or failed job on the cluster: a missing input or data file, a"multiple"-mode job whose subset files no longer matchsubdatasets.csv, an implausible resource request, and acontainer_imagetaggedlatest(or carrying no tag at all). Whenpodmanordockeris available locally, it also makes a best-effort attempt to confirm the image is pullable;check_image = FALSEskips that probe, which contacts the registry. Every other argument resolves from the submission state, so the common case ishtc_check()with no arguments, run after the generators and beforehtc_upload(). Returns a tibble of issues (possibly zero rows), each tagged"error"or"warning".htc_upload()gains afiles = NULLdefault. Whenfilesis omitted, the upload list is resolved from the submission state: the submit file, the executable script, any shared input files, and, in"multiple"mode,subdatasets.csvtogether with the individual subset files.remote_pathalso defaults toNULL, resolving from an explicit argument, then the value already recorded, then"~/", and is recorded after a successful (non-dry_run) upload.htc_upload()gains acheckargument (defaultFALSE). WhenTRUE, it runshtc_check()before uploading and aborts if any"error"-level issue is found;"warning"-level issues are reported but do not block the upload.htc_submit()reads the submission state for its own defaults.submit_fileandremote_pathboth default toNULLand resolve, in order, from an explicit argument, the recorded value, and finally the old hardcoded literal, so uploading to a non-defaultremote_pathno longer breaks the submit step. The cluster ID it prints is recorded for the functions that follow.htc_status()gains apathargument and resolvescluster_idfrom the submission state when omitted, so the cluster IDhtc_submit()just printed no longer has to be retyped.htc_status()gains ashow_hold_reasonargument (defaultTRUE). When any held jobs are present, it automatically runs a follow-upcondor_q -holdquery and prints the hold reason, so diagnosing a held job no longer means leaving R. Setshow_hold_reason = FALSEto skip the extra query.htc_cancel()andhtc_release()cancel a submitted cluster withcondor_rm, or release jobs HTCondor has held back into the queue withcondor_release. Both resolvecluster_idfrom the submission state when not supplied but, unlikehtc_status(), refuse to proceed when nocluster_idcan be resolved rather than acting on every job in the queue: canceling or releasing everything is a much more consequential default than merely showing everything.htc_cancel()also accepts areasonrecorded against the removed jobs. Both supportdry_runandverbose.htc_download()gains acluster_idargument and afiles = NULLdefault. Whenfilesis omitted, it builds the download list from whathtc_gen_submit(),htc_gen_executable(), andhtc_submit()recorded, for both single and multiple mode.remote_pathdefaults toNULL, resolving from an explicit argument, then the value recorded in the submission state, then"~/"; previously it assumed"~/"even when the job had been submitted from somewhere else.htc_collect()unpacks the results tarballshtc_download()brought back, one subfolder per job, and returns the job index: a tibble with one row per job giving its group, HTCondor process and cluster numbers, whether its tarball was extracted, how many files its results folder holds and what they are (a list column of paths relative tooutput_dir), whether it includes an output record, the paths to its.log,.err, and.outfiles, and the container image it ran in. It resolves the tarballs from the submission state, or accepts an explicit namedtarballsvector. It works for any analysis, whatever the script wrote, and never opens the files themselves: the output record (project-manifest.json) is only checked for, not read, since interpreting it istoolero’s job. A job whose tarball is missing or will not extract still gets a row, withextracted = FALSE, and one warning covers all such jobs, so a failed job no longer stops the collection. An existing extraction folder is an error raised before anything is extracted, unlessoverwrite = TRUE.
Minor improvements
htc_upload()andhtc_download()print a success confirmation after every successful transfer, rather than only whenverbose = TRUE.Remote commands in
htc_submit()andhtc_status()are assembled with a single POSIX quoting idiom rather thanshQuote(). This does not fix a known failure:shQuote()does escape a dollar sign when it switches to double quotes, so the previous arrangement held. But it relied on a choiceshQuote()makes from its own input, and on a dialect that follows the local platform, neither of which suits a command bound for the POSIX shell at the far end of an SSH connection and quoted twice on the way. Quoting is now the same whatever the input and whatever the local platform.
Bug fixes
htc_gen_submit()lists input files by basename intransfer_input_files.htc_upload()sends every file flat into one directory on the submit node, soinput_files = "R/analysis.R"put a path in the submit file that did not exist there, and the job failed before it started. The submission state still records the paths as given, which is howhtc_upload()finds the files on this machine. Two input files that share a basename (R/utils.Randscripts/utils.R) would overwrite each other on the submit node, so they are now an error, raised before anything is written.The script
htc_gen_executable()writes now packs the results folder even when the R script fails, and then exits with R’s own exit status. Previouslyset -euo pipefailstopped the script at the failingRscriptline, so no tarball was built: whatever the analysis had written was lost, and HTCondor held the job for a missingtransfer_output_filesentry rather than letting it finish. A failed job now returns its partial results and is reported by HTCondor as failed, not held.htc_gen_executable()always writes#!/bin/bashas the first line of the generated script. Withcomments = TRUE, the shebang section’s explanatory comment was written ahead of it, putting#!/bin/bashon line two, where the kernel does not look for it, so the script ran under whatever shell happened to invoke it. The shebang now carries no comment of its own, and the comment sits withset -euo pipefail, which is what it was describing all along.htc_gen_executable()includesset -euo pipefailafter the shebang, so the script exits on the first error instead of silently continuing.htc_gen_executable()changes to HTCondor’s scratch directory (cd "${_CONDOR_SCRATCH_DIR:-$PWD}") before any file operations, and reads baked-in data files by absolute path underhome_dir(default/home), wherecontainr::generate_dockerfile()put them. Outputs are therefore written where HTCondor looks fortransfer_output_files, while data is read from where the image actually holds it.htc_gen_submit()prependsdocker://tocontainer_imagewhen the prefix is missing. Previously, omitting it caused HTCondor to treat the image as a local file.htc_gen_submit()includesshould_transfer_files = YESandwhen_to_transfer_output = ON_EXITin the transfer section. HTCondor requires both for its file transfer mechanism to work.htc_status(watch = TRUE)decides when to stop polling by reading the job count fromcondor_q’s ownTotal for query:line, falling back to matching the cluster ID against theJOB_IDScolumn. It previously searched the whole report for the cluster ID as a substring, which could match the address or port in the schedd header, or a count in theTotal for all users:line. Because that test drives the loop’s exit, a false match did not give a wrong answer; it left the loop polling forever.htc_submit()quotesremote_pathandsubmit_filefor the remote shell rather than typing quote characters around the assembled command. A filename or path containing a space or an apostrophe previously truncated the command at that character, producing a shell syntax error or, in the worst case, running the remainder as separate commands. A leading~is held outside the quoting so the remote shell still expands it.htc_submit()printscondor_submitoutput withcat()rather than passing it tocli, which treats braces as inline markup and would try to evaluate anything an HTCondor message happened to wrap in them. Error details fromhtc_submit()andhtc_status()are escaped for the same reason. This matches howhtc_status()already printedcondor_qoutput.
Documentation
- Documentation, vignettes, and user-facing messages now use the family’s shared vocabulary (see toolero’s
CONVENTIONS.md).htc-manifest.ymlis the submission state andmanifest.csvfromtoolero::write_by_group()is the job manifest. Earlier development versions called the submission state the “job manifest”, which collided with the toolero file of the same name. Messages across the package now say “submission state” wherever they meanhtc-manifest.ymland “job manifest” only when they meanmanifest.csv. Function names and arguments are unchanged.
Testing
- New
tests/testthat/test-readme-workflow.R, a documentation-regression suite rather than a code-correctness one. It reproduces the documented code blocks inREADME.md(the first workflow, scaling to many jobs) with their literal argument values in a temporary directory, and checks the result against claims made elsewhere in the README: the submission state’s example YAML, the resource preset table, the results-naming table, and the quick function reference. It exists because the README has already gone stale once, after the output folder and the tarball naming convention changed.
submitr 0.1.0
CRAN release: 2026-05-19
submitr is the third package in the From the Notebook to the Cluster family, alongside toolero and containr. It provides a workflow for submitting containerized R analyses to the UW-Madison Center for High Throughput Computing (CHTC) from inside R.
Connection management
-
htc_config()– create or read a project-levelhtc.cfgconfiguration file. On first use, prompts interactively for username and server, displays ControlMaster SSH setup guidance to reduce Duo MFA prompts, writeshtc.cfg, and adds it to.gitignore. Subsequent calls read the existing file and validate server reachability. Returns a named list withusernameandserver. Errors informatively whenusernameorserverare supplied as empty strings.
Job scaffolding
htc_gen_submit()– generate an HTCondor.subsubmit file from project parameters. Supports single-job and multiple-job modes. Multiple mode reads the job manifest fromtoolero::write_by_group(manifest = TRUE), extracts filenames, writessubdatasets.csv, and emitsqueue file from subdatasets.csv. Resource presets (small,medium,large,custom) are loaded at runtime frominst/extdata/htc-resources.yaml; a local./htc-resources.yamltakes precedence over the package default. GPU support viagpu = TRUEandgpu_options.comments = TRUEannotates each section of the generated file with explanatory text.htc_gen_executable()– generate the.shexecutable script that HTCondor runs inside the container. Produces a four-element script: shebang,mkdir,Rscript, andtar. In multiple-job mode, passes${1}as a positional argument to the R script.r_scriptmust be supplied explicitly – there is no default.set_executable = TRUE(default) sets executable permissions viaSys.chmod().
File transfer and job control
htc_upload()– copy files to the CHTC submit node viascp. Accepts single files, vectors of files, directories (transferred recursively), and glob patterns.remote_pathdefaults to"~/".dry_run = TRUEpreviews the command without executing it.htc_submit()– runcondor_submiton the remote submit node via SSH from the remote directory where files were uploaded. Returns the cluster ID invisibly for use withhtc_status(). Supportsdry_run = TRUE.htc_status()– check job progress viacondor_q. Optionally filters by cluster ID.watch = TRUEpolls atintervalseconds (default 60) until the cluster ID leaves the queue. Returnscondor_qoutput invisibly as a character vector. Supportsdry_run = TRUE.htc_download()– copy result files back from the submit node viascp. Supports single filenames, vectors of filenames, and glob patterns ("*.tar.gz","job.*"). Glob patterns are single-quoted to prevent local shell expansion.local_pathdefaults to".". Supportsdry_run = TRUE.
Package infrastructure
inst/extdata/htc-resources.yamlships with the package and provides default resource presets forhtc_gen_submit().inst/extdata/hello-world.subandinst/extdata/hello-world.share included as test files for end-to-end workflow verification.inst/extdata/sample.Ris included as a sample R script for use in examples.
Testing
- The test suite uses a three-layer strategy to handle the fact that end-to-end testing requires a live HTCondor environment and SSH access. Layer 1 covers argument validation. Layer 2 covers command construction using
dry_run = TRUEand mocked bindings. Layer 3 integration tests are opt-in viaSys.setenv(CHTC_USERNAME = "your.netid")and never run on CRAN or CI. 153 tests passing across seven test files.
