Handler Functions
A Spade block in R is an ordinary R function -- the handler -- plus metadata that tells the library how to wire it up. This page covers how to write handlers, attach type annotations, and handle single and multiple outputs.
Anatomy of a handler🔗
A handler is a plain R function whose parameter names match the block's input names and parameter names:
handler <- function(source, buffer) {
# processing logic
}The library resolves each argument at runtime:
- File inputs -- subdirectories under
inputs/are matched by name and delivered as S4 objects (File,RasterFile, etc.). - Scalar parameters -- values from
params.yamlare matched by name and delivered as plain R values (numeric,character,logical).
Inputs take precedence when a name appears in both inputs/ and params.yaml.
Secrets🔗
Blocks that need credentials -- a database connection string, an API key -- request them by a logical name with get_secret(), rather than reading them from params.yaml or the process environment directly.
library(spade)
handler <- function() {
dsn <- get_secret("db")
# ... connect with dsn and query ...
}
run(handler)get_secret(name) returns the secret value (a length-one character string) bound to the logical name. The logical name is part of your block's contract, documented like any other parameter: the pipeline author binds it to one of their stored secrets via a secrets: map alongside args: in the pipeline YAML.
If name was not declared in the pipeline's secrets: map, or the bound secret failed to resolve, get_secret() calls stop(). A declared-but-unresolvable secret is a real error, not a silently empty string -- wrap the call in tryCatch() if you need to handle it, otherwise let it propagate like any other error (see Error handling below).
get_secret() never talks to a keychain or key-management service itself. It only reads values the worker or CLI already injected into the process environment before your script ran.
Attaching type annotations🔗
Use the spade_types<- replacement function to attach a named list of type annotations. Each key is a parameter name (or .return for the return type), and each value is a type name string:
spade_types(handler) <- list(
source = "RasterFile",
buffer = "numeric",
.return = "RasterFile"
)Without type annotations, the library still works -- file inputs are delivered as generic File or FileCollection objects based on the number of files in each input directory. Type annotations let you request a specific subtype and enable manifest generation via build().
Attaching a description🔗
Set the spade_description attribute to provide a human-readable description for the block manifest:
attr(handler, "spade_description") <- "Buffer a raster by a given distance."The run() entry point🔗
run(handler) is the single entry point that drives execution:
- Loads scalar parameters from
params.yamlviayaml::read_yaml(). - Scans
inputs/subdirectories and constructs typed S4 objects based on your type annotations. - Merges the two sets of arguments into one named list (inputs override params on name collision).
- Filters the argument list to only those names present in your handler's formal parameters (unless your handler accepts
...). - Calls your handler via
do.call(fn, filtered_args). - Writes the handler's return value to
outputs/.
Your script should end with:
run(handler)Handlers with ...🔗
If your handler uses ... instead of named parameters, the library passes all available arguments without filtering:
handler <- function(...) {
args <- list(...)
# args$source, args$buffer, etc.
}This is occasionally useful for generic or forwarding blocks, but named parameters are preferred because they make the block's interface explicit and enable manifest generation.
Single output🔗
Return a single S4 object to produce one output. The library writes it to outputs/<name>/ where <name> is either inferred from the type or read from the block manifest:
handler <- function(source, buffer) {
r <- terra::rast(source@path)
buffered <- terra::buffer(r, width = buffer)
out_path <- file.path(tempdir(), "result.tif")
terra::writeRaster(buffered, out_path, overwrite = TRUE)
RasterFile(path = out_path)
}
spade_types(handler) <- list(
source = "RasterFile",
buffer = "numeric",
.return = "RasterFile"
)The inferred output directory for RasterFile is outputs/raster/. The full mapping is:
| Return type | Default output name |
|---|---|
File | file |
RasterFile | raster |
VectorFile | vector |
TabularFile | tabular |
JsonFile | json |
Directory | directory |
FileCollection | files |
RasterFileCollection | rasters |
VectorFileCollection | vectors |
TabularFileCollection | tables |
If the block manifest declares exactly one output, the library uses that declared name instead of the default.
Multiple outputs🔗
Return a named list to produce multiple outputs. Each key becomes the output directory name:
handler <- function(source) {
r <- terra::rast(source@path)
raster_path <- file.path(tempdir(), "processed.tif")
terra::writeRaster(r, raster_path, overwrite = TRUE)
stats_path <- file.path(tempdir(), "stats.json")
jsonlite::write_json(list(mean = 42), stats_path)
list(
raster_out = RasterFile(path = raster_path),
stats_out = JsonFile(path = stats_path)
)
}This writes to outputs/raster_out/ and outputs/stats_out/. The names in the returned list should match the output names declared in your block manifest.
No output🔗
If your handler returns NULL (or invisible(NULL)), the library writes nothing to outputs/. This is valid for blocks that produce side effects only, though most blocks return at least one output.
Error handling🔗
Errors raised inside your handler propagate normally. If your handler calls stop(), the block fails and the error message is reported by the Spade runtime:
handler <- function(source) {
if (!file.exists(source@path)) {
stop("Input file does not exist: ", source@path)
}
# ...
}There is no special error-handling API. Use standard R patterns (tryCatch, stop, warning) as needed.
Complete template🔗
Putting it all together, here is a minimal but complete block script:
library(spade)
handler <- function(source, threshold) {
# Your processing logic here
r <- terra::rast(source@path)
result <- terra::classify(r, cbind(-Inf, threshold, NA))
out_path <- file.path(tempdir(), "classified.tif")
terra::writeRaster(result, out_path, overwrite = TRUE)
RasterFile(path = out_path)
}
spade_types(handler) <- list(
source = "RasterFile",
threshold = "numeric",
.return = "RasterFile"
)
attr(handler, "spade_description") <- "Classify raster values below a threshold."
run(handler)