30  🏗 btw

Published

2026-05-12

CautionAlert

The contents for section are being revised. Thank you for your patience.

In the previous chapters, we used the chores package to develop helpers—pre-written prompts—for repetitive tasks such as creating roxygen2 documentation and updating testthat tests.1 In the gander chapter, we used the addin to send code and environment context to the LLM.2

chores and gander assist us in automating or enhancing prompts we’re sending to an LLM. They use addins and/or shortcuts to streamline interactions with the model, providing an experience that resembles a browser interface (as opposed to chatting with live_console(chat) or live_browser()).

I’ve created the shinypak R package in an effort to make each section accessible and easy to follow. Install shinypak using pak (or remotes):

install.packages('pak')
pak::pak('mjfrigaard/shinypak')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '^30')

Launch apps with launch()

launch(app = '30_llm-btw')

Download apps with get_app()

get_app(app = '30_llm-btw')

In this chapter we’ll focus on the btw package, which is fundamentally different than chores and gander because it,

provides a default set of tools to to peruse the documentation of packages you have installed, check out the objects in your global environment, and retrieve metadata about your session and platform.3

I’ll cover using btw to improve the documentation and contents of the downloadable report in our sap application.

30.1 Configuration

Install ellmer and btw:

install.packages('ellmer')
# or the dev version
pak::pak('tidyverse/ellmer')
install.packages("btw")
# or the dev version
# install.packages("pak")
pak::pak("posit-dev/btw")

We can place the btw configuration options in the .Rprofile (similar to other ellmer configurations).4

I’ve added a project-level .Rprofile file this branch of the sap package and included a system_prompt and model:

if (interactive()) {
  require(ellmer, quietly = TRUE)
}
if (interactive()) {
  require(btw, quietly = TRUE)
}
options(
  btw.client = ellmer::chat_anthropic(
    system_prompt =
    "You are an expert R/Python programmer who loves explaining complex topics to non-technical audiences. 
    ...",
    model = "claude-sonnet-4-5-20250929"
    )
)
1
Ensure ellmer package
2
Ensure btw package
3
btw config
4
System prompt for all conversations with chat
5
model argument for most current Claude model

I’ve included the full system prompt below in an easier to read format:

The instructions for returning Markdown/R Markdown/Quarto responses are important for this particular app, as you’ll see below.

You are an expert R/Python programmer who loves explaining complex topics to non-technical audiences.

  • When writing R code, use base R functions, but follow the tidyverse style guide.

  • Avoid using for loops and prefer functional programming patterns like apply() or purrr.

  • When creating graphs/plots, use ggplot2. - When returning bash/shell commands, give each command their own code chunk (see example below):

DON’T DO THIS:

# Check the user's shell
getent passwd abc1

# Check SSH daemon logs for connection details
sudo tail -50 /var/log/secure | grep abc1

# Check if rsync is installed and accessible
which rsync

DO THIS:

Check the user’s shell:

getent passwd abc1

Check SSH daemon logs for connection details:

sudo tail -50 /var/log/secure | grep abc1

Check if rsync is installed and accessible:

which rsync
  • If your response includes markdown, RMarkdown, or Quarto syntax, wrap the contents in tilde blocks (~~~). See the example below:

---
title: 'Report'
output: html_document
---

```/{r setup, include=FALSE/}
knitr::opts_chunk$set(echo = TRUE)
install.packages('tidyverse')
```

## Analysis

Example analysis...

```/{r analysis/}
library(tidyverse)
# ...
```
  • If writing R Shiny code, use bslib for all layout functions (unless explicitly instructed otherwise).
  • If writing Python Shiny code, use shiny core (not express) to build apps and include explanations in comments.

Launch app with the shinypak package:

launch('30_llm-btw')

30.2 Context files

Along with a system prompt, we’ll want to introduce a comprehensive context file.5 Context files are markdown documents located in the root directory (similar toREADME.md files) that agents read automatically before doing work. Generally speaking, these files answer the question, “What do I need to know to be productive?

Think of these docs as the LLM’s analogue of an onboarding document for a new developer on your team. Good things to include are the project’s purpose, audience, conventions and constraints (i.e., gotchas, “don’t touch X”, etc.). Without any context, the LLM has to “rediscover” the project on every session, which wastes tokens because the model has to spend at least a few turns exploring the project’s files. btw comes with it’s own context file, btw.md.6

TipVarious context files

Context files have different names depending on the tool, but the role is the same:

Tool Filename
Claude Code CLAUDE.md
Cursor .cursorrules / .cursor/rules/*.md
OpenAI Codex CLI AGENTS.md
GitHub Copilot .github/copilot-instructions.md
btw btw.md

Let’s view the system and session context without a btw.md (or other context file). In the RStudio Console, we’ll use btw_client() to create a btw-enhanced chat client.7

chat <- btw_client()

We can view the btw enhancements by examining the chat object in the console:

chat
<Chat Anthropic/claude-sonnet-4-5-20250929 turns=1 input=0 output=0 cost=$0.00>
── system ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# System and Session Context

Please account for the following R session and system settings in all responses.

<system_info>
R_VERSION: R version 4.5.3 (2026-03-11)
OS: macOS Tahoe 26.4.1
SYSTEM: x86_64, darwin20
UI: RStudio
LANGUAGE: (EN)
LOCALE: en_US.UTF-8
ENCODING: en_US.UTF-8
TIMEZONE: America/Phoenix
DATE: Wednesday, April 29, 2026 (2026-04-29)
RSTUDIO: 2026.04.0+526 Globemaster Allium (desktop)
</system_info>


# Tools

You have access to tools that help you interact with the user's R session and 
workspace. Use these tools when they are helpful and appropriate to complete 
the user's request. These tools are available to augment your ability to help 
the user, but you are smart and capable and can answer many things on your 
own. It is okay to answer the user without relying on these tools.

## Skills

You have access to specialized skills that provide detailed guidance for 
specific tasks. Skills are loaded on-demand to provide domain-specific 
expertise without consuming context until needed.

### Using Skills

1. **Check available skills**: Review the `<available_skills>` listing below
2. **Load when relevant**: When you recognize that a task matches a skill's 
description, call `btw_tool_skill(name)` to load the full skill instructions
3. **Don't reload**: If a skill has already been loaded in this conversation, 
follow its instructions directly without loading it again
4. **Access resources**: After loading, use file read tools to access 
references

Skills may include bundled resources:
- **Scripts**: Code bundled with the skill. Scripts are not directly 
executable by btw; read them for reference or adapt their logic into R code 
for use with the R code execution tool.
- **References**: Additional documentation to consult as needed
- **Assets**: Templates and files for use in outputs

<available_skills>
<skill>
<name>skill-creator</name>
<description>Guide for creating effective skills. This skill should be used 
  when users want to create a new skill (or update an existing skill) that 
  extends Claude's capabilities with specialized knowledge, workflows, or 
  tool integrations.
</description>
<location>/Users/mjfrigaard/Library/Caches/org.R-project.R/R/renv/cache/v5/macos/R-4.5/x86_64-apple-darwin20/btw/1.2.1/9d2abfeb6859f7d079b14da1f8d84ad8/btw/skills/skill-creator/SKILL.md
</location>
</skill>
</available_skills>

*You are an expert R/Python programmer who loves explaining complex topics to non-technical audiences.*

- *When writing R code, use base R functions, but follow the tidyverse style guide.*

- *Avoid using `for` loops and prefer functional programming patterns like `apply()` or `purrr`.*

- *When creating graphs/plots, use `ggplot2`. - When returning bash/shell commands, give each command their own code chunk (see example below):*

**DON'T DO THIS**:

``` bash
# Check the user's shell
getent passwd abc1

# Check SSH daemon logs for connection details
sudo tail -50 /var/log/secure | grep abc1

# Check if rsync is installed and accessible
which rsync
```

**DO THIS**:

*Check the user's shell:*

``` bash
getent passwd abc1
```

*Check SSH daemon logs for connection details:*

``` bash
sudo tail -50 /var/log/secure | grep abc1
```

*Check if `rsync` is installed and accessible:*

``` bash
which rsync
```

- *If your response includes markdown, RMarkdown, or Quarto syntax, wrap the contents in tilde blocks (`~~~`). See the example below:*

```` markdown

---
title: 'Report'
output: html_document
---

```/{r setup, include=FALSE/}
knitr::opts_chunk$set(echo = TRUE)
install.packages('tidyverse')
```

## Analysis

Example analysis...

```/{r analysis/}
library(tidyverse)
# ...
```
````

- *If writing R Shiny code, use `bslib` for all layout functions (unless explicitly instructed otherwise).*
- *If writing Python Shiny code, use shiny core (not express) to build apps and include explanations in comments.*

We can see btw has added sections for tools and skills before the system prompt we provided in ellmer::chat_anthropic() (we will cover tools and skills in more depth below). We can already see the benefit of having the system prompt loaded in the context because the model already has our instructions on the first turn (and we’ve spent $0).

30.3 btw tools

Broadly speaking, tools are functions a model can call. Tools have a name, description, and will run code that returns a result (i.e., list files and folders, read a file, execute R). Tools are always present in the model’s context — the model sees the full list and picks one when needed.

btw comes with a set of pre-defined tools for examining package documentation, environments, directories and files, git and GitHub, our development environment, CRAN packages, R sessions, and general web searches.8

All btw functions follow the name convention below:

Prefix Group Name
btw_tool_ files_ code_search()
btw_tool_ files_ list_files()
btw_tool_ files_ read_text_file()
btw_tool_ files_ write_text_file()

The Group can be one of docs, env, git, github, ide, search, session or web.

30.4 btw.md

use_btw_md() function will create a boilerplate btw.md file.

use_btw_md(scope = "project")
ℹ See btw::btw_client for format details
ℹ See btw::btw_tools for available tools
ℹ Call `btw::btw_task_create_btw_md()` to use an LLM to help you initialize the project context.

This includes some default content for the project context:

Use btw.md to inform the LLM of your preferred code style, to provide domain-specific terminology or definitions, to establish project documentation, goals and constraints, to include reference materials such or technical specifications, or more. Storing this kind of information in btw.md may help you avoid repeating yourself and can be used to maintain coherence across many chat sessions.

The YAML header in our newly created btw.md is where can specify the client (along with the provider and model).


---
client: claude/claude-sonnet-4-5-20250929
---

The default values in btw.md will automatically use the latest Claude model from Anthropic. The YAML values above are similar to using ellmer’s chat_* functions.9

The tools section of the YAML header contains a list of the groups from btw_tools(). Each of these groups contains a collection of functions “that allow the chat to interface with your computational environment.

---
tools:
  - docs
  - env
  - files
  - git
  - github
  - ide
  - search
  - session
  - web
---

Additional instructions are also provided on code style:

Follow these important style rules when writing R code:

* Prefer solutions that use {tidyverse}
* Always use `<-` for assignment
* Always use the native base-R pipe `|>` for piped expressions

30.5 Creating btw.md

We’ll want to improve our generic btw.md context file. As the output above mentioned, we can call btw_task_create_btw_md() to start an interactive chat session to help us write our context file.10

btw_task_create_btw_md(client = "anthropic")

This opens an interactive chat in the Viewer pane:

30.6 How tool calling works

When we start the interactive chat, the model informs us it will be examining the sap package contents for more information.

I'll begin by exploring the project structure to understand 
what we're working with.

The model can’t execute R code, but btw has “registered” a collection of tools (i.e., R functions) with the model, these can be used to help provide additional information. In other words, the btw_task_create_btw_md() function uses the tools in btw to add more project context to the btw.md file.

The btw_tools() function lists all the tools we could register, but chat$get_tools() returns the tools that are registered with the model.

If we want to register a tool, we include the name of the function, a description of what the function does, and a list of function arguments with their type (boolean, integer, number, etc.). 11

30.6.1 Registered tools

Let’s back up and take a look at the tools btw has registered with get_tools():

chat$get_tools()

Each registered tool has a @name and @description parameter, with WHEN TO USE instructions written in markdown.

Recall the model was going to start by “exploring your project structure”, and we can see this aligns with the WHEN TO USE section of btw_tool_files_list():

$btw_tool_files_list
# <ellmer::ToolDef> btw_tool_files_list(path, type, regexp, `_intent`)
# @name: btw_tool_files_list
# @description: List files or directories in the project.

WHEN TO USE:
* Use this tool to discover the file structure of a project.
* When you want to understand the project structure, use `type = "directory"` 
  to list all directories.
* When you want to find a specific file, use `type = "file"` and `regexp` to
  filter files by name or extension.

CAUTION: Do not list all files in a project, instead prefer listing files in
a specific directory with a `regexp` to filter to files of interest.

The output from chat$get_tools() is rather lengthy, but we can use Ctrl/Cmd + F to locate btw_tool_files_list(), which is the first tool called by the LLM.

View the entire output from chat$get_tools() in inst/prompts/btw-chat-get-tools.md.

30.6.2 Tool calls

In the Viewer pane, we can review each tool call and the results. Below is the first call to btw_tool_files_list():

btw_tool_files_list(path = ".", `_intent` = "List root directory to identify project type and structure")

The function includes an _intent argument, where the model, “explain[s] why it called the tool.12

30.6.3 Tool results

The tool returns a markdown-formatted table of the project contents (file/folder names, their size, and when they were last changed):13

| path              | type      | size    | modification_time   |
| ----------------- | --------- | ------- | ------------------- |
| DESCRIPTION       | file      | 621     | 2026-04-29 10:34:14 |
| NAMESPACE         | file      | 65      | 2026-04-29 09:46:28 |
| R                 | directory | 352     | 2026-04-29 10:35:32 |
| R/app_server.R    | file      | 689     | 2026-04-29 09:25:01 |
| R/app_ui.R        | file      | 492     | 2026-04-29 10:46:21 |

...omitted...

30.6.4 Model response

The table result is sent to the model, which provides a summary of it’s contents:

Perfect! This is clearly an R Package with a Shiny application. 
Let me read the key files to understand the project.

30.6.5 Recap

I’ve created an overview of this tool call in the diagram below.14

Tool calls with btw

Tool calls with btw

The model context begins with our supplemented system prompt–which now includes the tools and skills at it’s disposal. When we begin, the model beings by exploring the project structure–matching the registered tool description for the btw_tool_files_list() function, which returns a table of the project contents. These tool results are sent to the model, and we see the overall summary.

30.7 Building the btw.md file

I strongly recommend reading the documentation on creating the btw.md.15 The instructions are broken into three phases:

  1. Phase 1: Strategic Exploration (Research & Discovery)
  2. Phase 2: Narrative Construction (User Collaboration)
  3. Phase 3: Documentation (File Writing)

Phase 1: Strategic Exploration

The first phase is the Strategic Exploration, and it’s goal is to efficiently understand the project’s structure, purpose, and technical architecture without overwhelming the context window. This includes using tools to identify key files, review documentation, and tailor the approach to the project type.

The phase 1 summary is what the model learned about the sap package using the btw tools (expand the callout box to view).

Phase 1 Summary: Project Understanding

Project Type: R Package with Shiny Application Primary Language: R Primary Framework: Shiny (with bslib for modern UI components)

Purpose: This is a demonstration Shiny app-package called sap (from the “Shiny App-Packages” book chapter on btw). It provides an interactive web application for exploring a movies dataset with filtering, visualization, and HTML report generation capabilities.

Key Findings:

  1. Structure: Classic Shiny app-package structure with modular design
    • Uses Shiny modules pattern (mod_*_ui and mod_*_server functions)
    • Four main modules: inputs, visualization, data table, download report
    • Single export: launch_app() function
  2. Data: Bundled movies dataset (651 movies with 34 variables from Rotten Tomatoes and IMDB)
  3. Architecture:
    • Centralized inputs module that returns reactive values consumed by other modules
    • Visualization and data table modules respond to the same input controls
    • Report generation uses R Markdown to create standalone HTML reports with the current view
  4. Dependencies:
    • Core: shiny, bslib (modern UI)
    • Visualization: ggplot2
    • Tables: reactable
    • Reports: rmarkdown
    • Dev environment: renv for reproducibility
  5. Purpose Context: This is educational/demonstration code from the book “Shiny App-Packages” specifically demonstrating the btw package integration (LLM tooling for R)
  6. Notable: The existing btw.md contains style rules but no comprehensive project documentation

The model primarily relied on btw_tool_files_read() and btw_tool_docs_available_vignettes() to gather context on the project.

Phase 2: Narrative Construction

After the project summary, the model starts with the Narrative Construction. The overall goal of phase 2 is to collaborate with the user to provide key architectural context and the reasoning behind important design decisions, using brief, developer-focused explanations that aren’t in the existing documentation.

I’ve included the phase 2 questions and answers below (expand the callout box to view):

PHASE 2: Gathering Context

I understand this is a demo Shiny app-package for teaching purposes, specifically from the “Shiny App-Packages” book chapter about btw (LLM tooling).

Before I write comprehensive documentation, I have a few questions to capture important context:

Question 1 of 5: What is the primary learning objective or teaching goal of this particular demo? In other words, what should developers understand about btw or app-package structure after working with this example?

This project is intended to demonstrate the features of the btw package in the context of an app-package.

Thank you! That’s helpful context.

Question 2 of 5: I noticed there’s a file inst/prompts/btw-chat-get-tools.md which suggests this demonstrates specific btw features. Are there particular btw capabilities (like the chat tools, documentation access, or AI-assisted development workflow) that this app showcases or that developers should pay attention to when exploring this project?

The ellmer authors recommend storing conversations with an LLM in inst/prompts, with a sensible naming convention

Perfect, that clarifies the purpose of that directory.

Question 3 of 5: I see this uses the Shiny modules pattern with a centralized inputs approach (one module provides inputs that other modules consume). Was this architectural choice made specifically to demonstrate best practices for app-packages, or is there another reason this pattern is important for btw integration or maintainability?

This architectural choice was made to demonstrate best practices for app-packages.

Excellent, thank you.

Question 4 of 5: The project includes both app.R (at the root) and R/launch_app.R as entry points. Is app.R kept for backward compatibility or local development convenience, while launch_app() is the recommended approach for installed packages? Should developers understand when to use each?

The app.R file (when fully developed) should be used for deployment to platforms like RStudio Connect, Shiny Server, or shinyapps.io. It will essentially be a “wrapper” script for loading the package (with pkgload::load_all()) and then calling the main execution function, launch_app().

That’s very helpful - the deployment context is important.

Question 5 of 5: Are there any non-obvious gotchas, conventions, or aspects of working with this demo that developers should be aware of? For example, things about the renv setup, the relationship between inst/extdata/movies.csv and data/movies.rda, or development workflow expectations?

No–this app-package is meant to be easy to understand (so users can focus on implementing btw).

Perfect! That’s very clear. Thank you for those answers.

Let me now draft the documentation sections. I’ll present them one at a time for your feedback.

Phase 3: Documentation

After completing the phase 2, btw gives us a chance to review each section of our btw.md. You can review the entire conversation in the inst/prompts/setup.md file.

When Phase 2 is complete, we’re told we have a “comprehensive btw.md file” that is “structured from narrative (purpose and architecture) to reference material (technical details and workflows), making it easy for new developers to get oriented quickly while also serving as a practical guide for working with the project”.

---
client: claude/claude-sonnet-4-5-20250929
tools:
  - docs
  - env
  - files
  - git
  - github
  - ide
  - search
  - session
  - web
---

sap

Demo Shiny app-package showcasing btw integration and app-package best practices

Overview

sap is a demonstration R package from the Shiny App-Packages book, specifically the btw chapter. It provides a working example of how to structure a Shiny application as an R package while integrating with the btw package for LLM-assisted development.

The application itself is a movies explorer: users can create scatter plots comparing different movie metrics (ratings, scores, runtime), filter by genre, and generate HTML reports. However, the primary purpose is educational—to demonstrate app-package architecture and btw tooling integration in a realistic, working application.

Purpose

This project teaches developers how to:

  • Structure a Shiny application as an installable R package

  • Use Shiny modules with centralized input patterns for maintainability

  • Integrate btw LLM tooling into their development workflow

  • Follow best practices for app-package organization

The intentional simplicity of the application lets developers focus on understanding the package structure and btw features rather than complex domain logic.

Architecture & Design

App-Package Structure

sap follows the standard R package layout with Shiny-specific organization:

  • R/ - All application code, organized by responsibility
  • data/ - Bundled dataset (movies.rda) loaded with the package
  • inst/ - Installation files including R Markdown templates and prompts
  • man/ - Generated documentation for functions and data
  • renv/ - Reproducible environment management

Shiny Modules Pattern

The application uses centralized input management to demonstrate scalable architecture:

  1. mod_inputs - Single source of truth for all user controls (x/y variables, color, alpha, size, genre filter). Returns a list of reactive values.

  2. mod_visualization - Consumes inputs to generate ggplot2 scatter plots

  3. mod_data_table - Consumes inputs to filter and display data with reactable

  4. mod_download_report - Generates parameterized R Markdown reports using current inputs and outputs

This pattern avoids prop-drilling and makes it easy to add new modules that respond to the same controls.

Entry Points

  • launch_app() - Exported function for running the app from an installed package (sap::launch_app())
  • app.R - Deployment wrapper for Shiny Server, RStudio Connect, or shinyapps.io. Uses pkgload::load_all() + launch_app() for platforms that expect a standalone script.

btw Integration

Following ellmer best practices, LLM conversation history is stored in inst/prompts/ with descriptive filenames. This makes it easy to reference previous AI-assisted development sessions and share context with collaborators.

Data

movies Dataset

The package includes a bundled dataset of 651 randomly sampled movies produced before 2016, sourced from Rotten Tomatoes and IMDB.

Key variables:

  • Identifiers: title, director, actors (1-5)

  • Classification: genre, title_type, mpaa_rating

  • Metrics: runtime, imdb_rating, imdb_num_votes, critics_score, audience_score

  • Ratings: critics_rating (Certified Fresh/Fresh/Rotten), audience_rating (Spilled/Upright)

  • Release dates: Theatrical and DVD release date components

  • Awards: Oscar nomination/win indicators (best picture, actor, actress, director)

  • Other: studio, top200_box, imdb_url, rt_url

Access:

# After loading/installing package
sap::movies

# Or in the app via reactive
movies_data <- reactive({ sap::movies })

Files:

  • data/movies.rda - Binary R data file loaded with package

  • inst/extdata/movies.csv - Source CSV (available after installation via system.file())

Full documentation: ?sap::movies

Development Workflow

Setup

Install dependencies and set up the environment:

# Install btw and ellmer
install.packages(c('ellmer', 'btw'))
# Or development versions:
# pak::pak('tidyverse/ellmer')
# pak::pak("posit-dev/btw")

# Restore renv environment
renv::restore()

Common Tasks

Load package for development:

devtools::load_all()

Run the application:

# During development (after load_all())
launch_app()

# Or directly
shiny::runApp()

Update documentation:

devtools::document()

Install package locally:

devtools::install(upgrade = FALSE)

Code Conventions

Per the existing project standards:

  • Prefer tidyverse solutions
  • Use <- for assignment (never =)
  • Use native pipe |> for piped expressions (not %>%)

Testing the App

After making changes:

  1. devtools::load_all() to reload code

  2. launch_app() to test interactively

  3. Try different variable combinations and genre filters

  4. Generate a report to verify R Markdown rendering works

Technical Details

Dependencies

Core Application:

  • shiny - Reactive web application framework

  • bslib - Modern Bootstrap 5 UI components (page_sidebar, cards)

  • ggplot2 - Static visualizations

  • reactable - Interactive data tables

  • rmarkdown - Report generation

  • tools - Utility functions

Development:

  • renv - Dependency management and reproducibility

  • roxygen2 - Documentation generation

  • devtools - Package development workflow

Directory Structure

sap/
├── R/                          # Application code
│   ├── launch_app.R           # Main entry point (exported)
│   ├── app_ui.R               # UI definition
│   ├── app_server.R           # Server logic coordinator
│   ├── mod_inputs.R           # Centralized input controls
│   ├── mod_visualization.R    # Scatter plot module
│   ├── mod_data_table.R       # Filtered table module
│   ├── mod_download_report.R  # Report generation module
│   ├── scatter_plot.R         # Plot helper function
│   └── data.R                 # Data documentation
├── data/                      # Package data
│   └── movies.rda            # Bundled dataset
├── inst/                      # Installed files
│   ├── extdata/              # External data files
│   │   └── movies.csv
│   ├── prompts/              # LLM conversation history
│   │   └── btw-chat-get-tools.md
│   └── rmd/                  # R Markdown templates
│       └── report.Rmd        # Report template
├── man/                       # Generated documentation
├── renv/                      # Environment management
├── app.R                      # Deployment entry point
├── DESCRIPTION               # Package metadata
└── NAMESPACE                 # Exported functions

Key Components

launch_app() - Main exported function that creates and returns a Shiny app object. Loads required libraries and calls shinyApp(ui = app_ui(), server = app_server).

app_ui() - Builds UI using bslib::page_sidebar() with inputs in the sidebar and visualization + table in a two-column layout.

app_server() - Coordinates all modules. Gets inputs from mod_inputs_server(), passes them to visualization and table modules, then provides results to the report module.

Module Pattern - Each module has a *_ui() function for UI elements and a *_server() function for reactive logic. All use namespaced IDs via NS(id).

Report Generation - Uses rmarkdown::render() with parameterized R Markdown. Template is located via system.file() with fallback to relative path for development.

Resources

View the complete updated btw.md file on GitHub.

To make sure btw reads the project context file, we’ll restart R, load, and install the sap package, then create another chat object with btw_client():

chat <- btw_client()
chat

Now we can see the chat object includes 1) the system and session info, 2) the tool access description, 3) the entire btw.md file, and 4) our system prompt.

30.8 btw App

We’re now ready to launch the btw app:16

btw_app()

If we expand the sidebar, we can see the registered tools provided by btw:

By default, the app includes all of the tools available from btw (with the exception of Subagent, Load skill, and Package Tools).17

30.8.1 Subagents

When I started using btw, I was somewhat new to LLMs, so it felt a bit like I’d been dropped into the deep end of the pool. Below I’ve tried to give some background on the questions I had when I started building subagents and skills.

Agents in btw_app()

Agents in btw_app()

What is the difference between an agent and a subagent?

As we can see from the App interface, we’re able to add a Subagent to the available tools.18

Generally speaking, an agent is an LLM chat session that uses tools autonomously to complete tasks. A subagent is a specialized tool the agent can delegate subtasks to—like a manager (the Agent) assigning work to a specialist (the Subagent).

%%{init: {'theme': 'neutral', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"16px"}}}%%

flowchart TD
    User(User) -->|prompt| Agt("Agent<br>(LLM + tools)")
    Agt -->|delegates subtask| SubAgt("Subagent<br>(tool-shaped agent)")
    SubAgt -->|result| Agt
    Agt -->|final response| User
    

Agents vs. subagents

In btw, subagents are separately configured chat sessions — with their own models, system prompts, and toolsets — that can be invoked from the main session.

Subagents are defined in a markdown file with YAML frontmatter — not in an .R file. The frontmatter configures the agent, and the markdown body becomes it’s system prompt. btw auto-discovers these files and exposes them as tools to the main agent. We’ll create a subagent file in .btw/agent-code_reviewer.md and give it some instructions for reviewing code.

Below is the frontmatter for the code_reviewer subagent:

---
name: code_reviewer
description: Reviews R code for best practices...
title: Code Reviewer
icon: magnifying-glass
client: anthropic/claude-sonnet-4-20250514
tools:
  - files
  - docs
---
1
How the subagent is invoked
2
Advertised to the parent
3
Subagent display name
4
UI hint
5
Which model runs it
6
Tool groups it can call

We’ll also include some instructions and examples for how to style the code that differs from the standard tidyverse conventions. Expand the callout box below to view the entire description.


```yaml
name: code_reviewer
description: Reviews R code for best practices, bugs, and performance issues.
title: Code Reviewer
icon: magnifying-glass
client: anthropic/claude-sonnet-4-20250514
tools:
  - files
  - docs
```

You are an expert R code reviewer. Analyze code for:

- Adherence to `grkstyle` style and best practices. Check package installation here: <https://github.com/gadenbuie/grkstyle>

  - unstyled

    ``` r
    fruits <- c(
      "apple",
      "banana",
      "mango"
    )
    ```

  - grkstyle

    ``` r
    fruits <- c(
        "apple",
        "banana",
        "mango"
    )
    ```

- Line Breaks Inside Function Calls

  - **unstyled**

    ``` r
    do_something_very_complicated(something = "that", requires = many,
                                  arguments = "some of which may be long")
    ```

  - **grkstyle**

    ``` r
    do_something_very_complicated(
        something = "that",
        requires = many,
        arguments = "some of which may be long"
    ) 
    ```

  - **styler::tidyverse_style**

    ``` r
    do_something_very_complicated(
      something = "that", requires = many,
      arguments = "some of which may be long"
    ) 
    ```

- Indentation of Function Arguments

  - **unstyled**

    ``` r
    long_function_name <- function(a = "a long argument",
                                   b = "another argument",
                                   c = "another long argument") {
      # As usual code is indented by two spaces.
    }
    ```

  - **grkstyle**

    ``` r
    long_function_name <- function(
        a = "a long argument",
        b = "another argument",
        c = "another long argument"
    ) {
        # As usual code is indented by two spaces.
    } 
    ```

  - **styler::tidyverse_style**

    ``` r
    long_function_name <- function(a = "a long argument",
                                   b = "another argument",
                                   c = "another long argument") {
      # As usual code is indented by two spaces.
    } 
    ```

- Also check for:

  - Potential bugs, edge cases, or logic errors

  - Performance considerations

  - Readability and maintainability

Provide specific, actionable feedback with short code examples where helpful.

Now when we launch the btw_app(), we can see our Code Reviewer subagent.

Agents in btw_app()

Agents in btw_app()

Using the subagent

After selecting our subagent in the app UI, we’ll ask the model to review the code using code_reviewer:

‘Review the code using the code_reviewer subagent.’

code_reviewer subagent in btw_app()

code_reviewer subagent in btw_app()

This prompt is sent from the parent chat, which sees the code_reviewer subagent as a single tool call. After sending the prompt, the code_reviewer subagent runs its own multi-turn loop using its own tools.

If we expand the Running Code Reviewer box, we can see the session ID (stable_sparrow) for the subagent context window and the prompt sent to the subagent:

Agent: code_reviewer
Session ID: stable_sparrow
Provider: Anthropic
Model: claude-sonnet-4-20250514
Tools: btw_tool_docs_package_news, btw_tool_docs_package_help_topics, btw_tool_docs_help_page, btw_tool_docs_available_vignettes, btw_tool_docs_vignette, btw_tool_files_edit, btw_tool_files_list, btw_tool_files_read, btw_tool_files_replace, btw_tool_files_search, btw_tool_files_write

Prompt

Please review all the R code in the sap Shiny app-package project. Focus on:

  1. Best Practices: Check adherence to Shiny module patterns, R package conventions, and tidyverse style guide
  2. Potential Bugs: Look for common issues in reactive programming, namespace collisions, or edge cases
  3. Performance: Identify any inefficiencies in data processing or reactive dependencies
  4. Maintainability: Assess code organization, documentation quality, and consistency
  5. Security: Check for any security concerns in user inputs or file operations

The project structure includes:

  • Main entry point: R/launch_app.R
  • UI and server: R/app_ui.R, R/app_server.R
  • Modules: R/mod_inputs.R, R/mod_visualization.R, R/mod_data_table.R, R/mod_download_report.R
  • Helper functions: R/scatter_plot.R
  • Data documentation: R/data.R
  • Deployment script: app.R

Please provide a comprehensive review with specific recommendations for improvements.

%%{init: {'theme': 'neutral', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"16px"}}}%%

flowchart TD
    User[User in main chat] --> ParentChat["Parent<br><code>btw_client</code>"]
    ParentChat -->|btw_tool_agent| Subagent["<code>code_reviewer</code><br>subagent"]
    Subagent -->|reads YAML + body| Spec["<code>agent-code_reviewer.md</code>"]
    Subagent -->|calls files/docs tools| Project["Project files & R docs"]
    Subagent -->|returns review text| ParentChat
    ParentChat --> User
    

Subagent call

The code_reviewer subagent invocation starts with a fresh context window. This keeps the parent chat’s history from leaking in, and the intermediate work performed by the code_reviewer doesn’t leak out. When used this way, the code_reviewer can read all the project’s files without bloating the parent chat’s context.

What is the subagent doing?

I’ve stored the tool calls from the code_reviewer subagent on GitHub,19 but the image below captures what’s happening:

How the subagent works

How the subagent works

The parent chat never sees the intermediate tool calls — only the final summary (which it summarizes again before returning a response).

code_reviewer subagent response in btw_app()

code_reviewer subagent response in btw_app()

See the full-code_review-2026-04-29.md file for the prompt, tool calls, and summary from the subagent.

In our .Rprofile, we’ll can add the following option for the default toolset if the orchestrator doesn’t specify:

options(
  btw.subagent.tools_default = c("agent", "cran", "docs", 
                                 "env", "files",  "git", 
                                 "github", "ide", "pkg",
                                 "run", "skills")
)

30.8.2 Skills

In the btw_app(), we can see we have an option to load a skill and it’s “bundled resources.”

Skills in btw_app()

Skills in btw_app()

If you recall, the initial btw.md referenced how skills can be used/accessed:

Skills

You have access to specialized skills that provide detailed guidance for specific tasks. Skills are loaded on-demand to provide domain-specific expertise without consuming context until needed.

Using Skills

  1. Check available skills: Review the <available_skills> listing below
  2. Load when relevant: When you recognize that a task matches a skill’s description, call btw_tool_skill(name) to load the full skill instructions
  3. Don’t reload: If a skill has already been loaded in this conversation, follow its instructions directly without loading it again
  4. Access resources: After loading, use file read tools to access references

Skills may include bundled resources:

  • Scripts: Code bundled with the skill. Scripts are not directly executable by btw; read them for reference or adapt their logic into R code for use with the R code execution tool.

  • References: Additional documentation to consult as needed

  • Assets: Templates and files for use in outputs

<available_skills>
<skill>
<name>skill-creator</name>
<description>Guide for creating effective skills. This skill should be used 
  when users want to create a new skill (or update an existing skill) that 
  extends Claude's capabilities with specialized knowledge, workflows, or 
  tool integrations.
</description>
<location>/Users/mjfrigaard/Library/Caches/org.R-project.R/R/renv/cache/v5/macos/R-4.5/x86_64-apple-darwin20/btw/1.2.1/9d2abfeb6859f7d079b14da1f8d84ad8/btw/skills/skill-creator/SKILL.md
</location>
</skill>
</available_skills>

Below we’ll cover what skills are, how they compare to tools and subagents, and how to create them.

What are skills?

Skills are markdown files with instructions for the model on how to approach a task (i.e., domain-specific expertise). Skills are loaded “on demand”–and don’t execute anything–so they don’t consume context until they’re needed.

This aligns with the skills section in the chat client–it has instructions and the location of skill-creator/SKILL.md to “provide detailed guidance for specific tasks.

How are skills different than tools and subagents?

I’ve found a good way to think about the differences between tools and skills is,

Tools pay rent, skills pay only when you visit.


  1. The prompt directory is where chores stores the helpers used in the addin.↩︎

  2. This is explained in What is gander actually doing?↩︎

  3. This description of btw actually comes from the mcptools documentation.↩︎

  4. Recall that the .Rprofile file can exist at the user and/or the project-level. You can easily open this file with usethis::edit_r_profile().↩︎

  5. There is a push to standardize these files across platforms.↩︎

  6. If you’re already using a context file (i.e., AGENTS.md or llms.txt), these can be configured with in btw_client().↩︎

  7. An ellmer::Chat client which provides a “sequence of user and assistant Turns sent to a specific Provider.↩︎

  8. Read a complete list of available tools from btw in the package documentation.↩︎

  9. For more information on client values, read the Chat Settings documentation.↩︎

  10. The prompts used in this project are stored in inst/prompts/ (as we learned in the ellmer chapter).↩︎

  11. We can register tools using ellmer’s create_tool_def() and tool() functions.↩︎

  12. The _intent argument is “An optional string describing the intent of the tool use. When the tool is used by an LLM, the model will use this argument to explain why it called the tool.↩︎

  13. I’ve cleaned up the formatting on this markdown table so it’s easier to read, but it’s an output you might expect from the fs package.↩︎

  14. This image was inspired by the tool calling overview presented in Garrick Aden-Buie’s genAI 2025: Using LLMs in Shiny presentation.↩︎

  15. Read the entire btw-init.md prompt here.↩︎

  16. To run the btw_app(), make sure you have the latest version of shinychat.↩︎

  17. Subagents are defined with btw_tool_agent_subagent(), Load skill uses btw_tool_skill(), and the Package Tools include functions for loading code, running R CMD check, generating documentation, running package tests, and computing test coverage.↩︎

  18. I recommend reading the documentation from btw on subagents.↩︎

  19. Both the comprehensive review from the subagent and the tool calls are in the inst/prompts/ folder.↩︎