Your First R Block
In this guide, you will create a custom block in R that reads a CSV file, groups the data by a column, and writes a summary as JSON. The entire block is a plain R function — no framework classes to inherit and no lifecycle hooks to implement.
This guide uses R. For other languages, see the guides for Python, TypeScript, Go, and Rust.
Prerequisites🔗
Before starting, make sure you have:
- R 4.0 or later installed
- The
spadeR package:install.packages("spade") - The
jsonliteR package:install.packages("jsonlite") - The Spade CLI installed (Installation guide)
Step 1: Create a block collection🔗
A collection is a repository of related blocks that share a language. Create one:
mkdir field-stats && cd field-stats
spade init --language rThis scaffolds an R project:
field-stats/
renv.lock # renv dependency lockfile
R/ # Handler scripts go here
blocks/ # Block manifests go hereStep 2: Add a block🔗
spade add count-by-groupThis creates two files:
blocks/count-by-group.yaml— The block manifest declaring inputs, outputs, and parametersR/count_by_group.R— The handler function you will implement
Step 3: Write the handler🔗
Open R/count_by_group.R and replace its contents with:
library(spade)
library(jsonlite)
handler <- function(data, group_by) {
# Read the CSV via the @path slot
df <- read.csv(data@path, stringsAsFactors = FALSE)
if (!(group_by %in% names(df))) {
stop("Column '", group_by, "' not found in data.")
}
# Count rows per group using base R
counts <- as.list(table(df[[group_by]]))
result <- list(
total_rows = nrow(df),
group_column = group_by,
n_groups = length(counts),
counts = counts
)
out_path <- file.path(tempdir(), "summary.json")
jsonlite::write_json(result, out_path, auto_unbox = TRUE, pretty = TRUE)
JsonFile(path = out_path)
}
# Attach type annotations so the library knows how to load each argument
spade_types(handler) <- list(
data = "TabularFile", # file input: delivered as a TabularFile S4 object
group_by = "character", # scalar: read from params.yaml
.return = "JsonFile" # return type
)
attr(handler, "spade_description") <- "Count CSV records grouped by a column."
# Entry point: run() loads inputs, calls handler, writes outputs
run(handler)Key points:
datais declared as"TabularFile", so the library delivers aTabularFileS4 object. Access the file path through its@pathslot.group_byis declared as"character", so the library reads it fromparams.yamland passes it as a plain R character string..return = "JsonFile"tells the library what kind of output the block produces.run(handler)is the entry point. It readsparams.yaml, scansinputs/, calls your handler viado.call(), and writes outputs automatically. This one line is the only Spade-specific wiring.
Step 4: Define the block manifest🔗
Open blocks/count-by-group.yaml and replace its contents:
id: field-stats.count-by-group
version: 0.1.0
kind: standard
network: false
description: Count CSV records grouped by a column.
inputs:
data:
type: file
format: CSV
description: The input CSV file to analyze
group_by:
type: string
description: Name of the column to group by
outputs:
json:
type: json
description: JSON file with row counts per groupThis tells Spade that the block:
- Accepts a CSV file input named
data - Accepts a string parameter named
group_by - Produces a JSON output named
json - Does not need network access
- Is a standard (non-map, non-reduce) block
Step 5: Validate🔗
Check that the manifest and source file are consistent:
spade checkExpected output:
Collection 'field-stats' (r) is valid.
1 block found: field-stats.count-by-groupspade check verifies that all required manifest fields are present, that the input and output types are valid, and that the handler file exists.
Step 6: Install locally🔗
Install the collection from the local directory:
spade install file://.This registers the block in ~/.spade/blocks/field-stats/0.1.0/. You can now reference field-stats.count-by-group in any pipeline.
Step 7: Use in a pipeline🔗
Create a pipeline that uses your new block. Create test-pipeline.yaml:
name: field-stats-test
version: "1.0"
description: Test the count-by-group block
blocks:
- id: "@count"
name: field-stats.count-by-group
inputs: []
args:
group_by: speciesRun it:
spade run test-pipeline.yamlWhat you built🔗
Here is the complete set of files you created:
R/count_by_group.R — the handler:
library(spade)
library(jsonlite)
handler <- function(data, group_by) {
df <- read.csv(data@path, stringsAsFactors = FALSE)
if (!(group_by %in% names(df))) {
stop("Column '", group_by, "' not found in data.")
}
counts <- as.list(table(df[[group_by]]))
result <- list(
total_rows = nrow(df),
group_column = group_by,
n_groups = length(counts),
counts = counts
)
out_path <- file.path(tempdir(), "summary.json")
jsonlite::write_json(result, out_path, auto_unbox = TRUE, pretty = TRUE)
JsonFile(path = out_path)
}
spade_types(handler) <- list(
data = "TabularFile",
group_by = "character",
.return = "JsonFile"
)
attr(handler, "spade_description") <- "Count CSV records grouped by a column."
run(handler)blocks/count-by-group.yaml — the manifest:
id: field-stats.count-by-group
version: 0.1.0
kind: standard
network: false
description: Count CSV records grouped by a column.
inputs:
data:
type: file
format: CSV
description: The input CSV file to analyze
group_by:
type: string
description: Name of the column to group by
outputs:
json:
type: json
description: JSON file with row counts per groupNext steps🔗
- Types (R) — all available Spade types for inputs and outputs
- Handler Functions (R) — patterns for single and multiple outputs
- Manifest Generation (R) — auto-generate manifests from type annotations
- Examples (R) — complete worked examples including raster processing
- Building a Block — a comprehensive end-to-end tutorial (Python, with notes for other languages)