Create a custom block
A function block that recurs across boards can be turned into a regular block with its own entry in the picker. This tutorial shows how to write one, either with a coding agent (Claude Code, Cursor, Positron with Copilot) or by hand in R. blockr handles state, reactivity and saving. Your code turns the input data into a plot or a new dataset. Both paths start from a starter package: a small working block, tests included, that you change into the block you want.
With a coding agent
Clone the docs repo so your agent has the starter packages and guides:
git clone --depth 1 https://github.com/cynkra/blockr.docsGive your agent this prompt, with your own visualization in place of the timeline:
Read blockr.docs/agents/skills/blockr-block/SKILL.md and follow it to create a new package blockr.timeline with an event timeline block: one horizontal lane per subject, a point per event positioned by a time column, colored by event type. Use the JS-driven pattern.
The skill tells the agent how to proceed: copy the starter package, rename it, rewrite the plot and the controls, keep the tests passing, and check the block in a running board. Data loading, filtering, and state handling are not part of the job; blockr and the upstream blocks cover that.
Try it
pkgload::load_all("blockr.timeline")
blockr::run_app()Loading the package registers the block. run_app() starts the app you know from Build your first app; right-click the canvas and you will find your block in the picker under Plots. Connect a data block and configure it. There is also a ready-made board in the package, left over from the agent's verification step: shiny::runApp("blockr.timeline").
R-driven or JS-driven?
The prompt above asks for the JS-driven pattern: the controls are written in JavaScript, like those of blockr's built-in blocks. That is more code, but the agent writes it and the tests cover it, including a browser test. Write "Use the R-driven pattern" instead if you want plain Shiny inputs and less machinery.
By hand, in R
Copy the R-driven starter package, which builds the same block with plain Shiny inputs and no JavaScript:
git clone --depth 1 https://github.com/cynkra/blockr.docs
cp -r blockr.docs/scaffolds/rblock blockr.timelineRename the package as described in blockr.docs/scaffolds/README.md. Two files do the actual work:
R/expr-builders.Rturns the user's choices into the R code the block generates. For a new visualization, this is where you start:
make_myplot_expr <- function(x, y) {
bbquote(
ggplot2::ggplot(.(data), ggplot2::aes(factor(.(x)), .(y))) +
ggplot2::geom_boxplot(),
list(x = as.name(x), y = as.name(y))
)
}R/myplot_block.Rwraps that in a Shiny module: the inputs, thestateused for saving and restoring, and the constructor that registers the block.
Run the tests as you edit, and the app to see the result:
pkgload::load_all("blockr.timeline")
testthat::test_dir("blockr.timeline/tests/testthat")
blockr::run_app()For the full picture read Create a block; Testing blocks explains the testServer() setup used by the starter's tests.
Going further
Some ideas for a second step, with or without an agent:
- Handle empty data, missing values, and columns that disappear upstream
- Add an option to the block, and a test for it
- Rewrite the controls of an R-driven block as JS-driven
Building blocks regularly?
Install the skill locally so it triggers on prompts like "add a timeline block":
cp -r blockr.docs/agents/skills/blockr-block ~/.claude/skills/Share it
The result is an ordinary R package. Push it to GitHub and others can install it:
pak::pak("you/blockr.timeline")After installing and loading, the block appears in their block picker as well.
Further reading
- Block anatomy: detailed technical reference
- Extend blockr: plugins and the block registry
- Testing blocks: testing patterns with
testServer()