---
name: jamovi-module
description: Use when working in a jamovi module — R packages that define analyses through jamovi/*.a.yaml, *.r.yaml and *.u.yaml files, generated R/*.h.R classes and R/*.b.R R6 analysis bodies, jmvcore, or the jmc compiler. Covers module file anatomy, the development workflow, the .init()/.run() lifecycle, compiling, and cross-file conventions. The repo's own CLAUDE.md still governs project-specific details.
---

# Developing jamovi modules

This skill describes conventions common to every jamovi module. The current
repository's `CLAUDE.md` covers anything specific to *this* module (what it
does, its test commands, its analysis inheritance, its commit rules) and takes
precedence where the two differ.

For the official, authoritative way to do things — file formats, option and
results types, the compiler, UI definitions — consult the jamovi developer
documentation at <https://dev.jamovi.org/>. When this skill and the official
docs disagree, follow the docs and update the skill.

## What a jamovi module is

A jamovi module is an R package that is **also** a jamovi module. Each analysis
is therefore reachable two ways, and both must work:

- called directly from R (e.g. `jmv::ttestIS(data, ...)`), and
- driven interactively through the jamovi UI.

Keep this dual interface in mind: an analysis cannot rely on the UI being
present, and error handling must read sensibly both as a jamovi notice and as a
plain R error.

## Isolated dev environment

Develop each module in an isolated R environment (for example with `renv`) so
you test against known dependency versions rather than whatever happens to be
installed globally. `DESCRIPTION` stays the source of truth for the package's
dependencies (`Imports` / `Suggests`).

An isolated dev library only covers your dev/test loop (`testthat`,
`R CMD check`). The module ultimately runs inside jamovi's *own* bundled R and
library, so the real integration check is still installing into jamovi and
running it there.

## The development loop

A change to an analysis usually moves through these steps:

1. **Define options and results in yaml** — options in `<name>.a.yaml`,
   results in `<name>.r.yaml`. (See *Per-analysis file anatomy*.)
2. **Compile** — run `jmvtools::prepare()` (the `jmvtools` R package wraps the
   `jmc` compiler) to regenerate `R/<name>.h.R` and update the UI file
   `<name>.u.yaml`. (See *The compile step*.)
3. **Refine the UI** in `<name>.u.yaml` — adjust the layout so it reads clearly
   and doesn't overwhelm the user with options. In tame mode these hand tweaks
   survive later recompiles.
4. **Implement the body** in `R/<name>.b.R` — build the skeleton in `.init()`,
   compute and populate in `.run()`. (See *The .init() / .run() lifecycle*.)
5. **Test** the exported analysis function under `tests/testthat/`. (See
   *Testing*.)
6. **Install into jamovi and verify** — `jmvtools::install()` builds the module
   into jamovi; this is the only check that exercises the real bundled-R runtime
   and the UI. (See *Isolated dev environment*.)

The loop is iterative: adding or changing an option sends you back to step 1 →
recompile (tame mode adds the new control without clobbering your UI tweaks) →
adjust. Editing only `.b.R` logic skips the compile.

## Per-analysis file anatomy

An analysis named `<name>` is defined across several files that belong together:

- `jamovi/<name>.a.yaml` — **options** (the analysis arguments).
- `jamovi/<name>.r.yaml` — **results** definition: tables, columns, images,
  groups, and their visibility rules.
- `jamovi/<name>.u.yaml` (+ `jamovi/js/<name>.events.js`) — the UI layout and
  its interactive events.
- `R/<name>.h.R` — **generated** R6 `Options` and `Base` classes. Never edit
  these by hand; they are overwritten on every compile.
- `R/<name>.b.R` — the analysis **body**: an R6 class inheriting `<name>Base`.
  This is where you write code.

When you change options or results, you edit the `.yaml`, not the `.h.R`.

## The compile step

The `.h.R` files are regenerated from the yaml definitions by the jamovi
compiler (`jmc`). In practice you drive this through the **`jmvtools` R
package**, which wraps `jmc`: `jmvtools::prepare()` regenerates the generated
classes in place, and `jmvtools::install()` compiles and installs the module
into jamovi. (Check the module's `CLAUDE.md` / dev.jamovi.org for the exact
invocation.) Treat `R/*.h.R` as build output: review it, but don't hand-edit it.

The compiler also maintains the UI file `<name>.u.yaml`. Unlike `R/*.h.R`, the
`.u.yaml` is yours to hand-tune, and it offers two update modes:

- **tame (recommended)** — adds controls for newly added options but leaves your
  existing layout and tweaks untouched, so deliberate UI design survives
  recompiles.
- **aggressive** — regenerates the UI to mirror the options, overwriting manual
  layout changes.

Use tame so your UI work isn't lost; see dev.jamovi.org for the exact flag.

## The .init() / .run() lifecycle

The body class implements two phases driven by jmvcore:

- `.init()` — runs **before** data is available. Build the result *skeleton*
  here: create table rows/columns based on the chosen options and factor
  levels. Anything whose *shape* is known without the data belongs in `.init()`.
- `.run()` — data is available. Validate it, compute, and **populate** the
  skeleton, e.g. `table$setRow(rowKey = ..., values = list(...))`.

A common mistake is computing in `.init()` or building structure in `.run()`;
keep the split clean so partial/empty states render correctly.

## Cross-file conventions

These hold across analyses in a typical jamovi module:

- **Factor levels are base64-encoded** with `toB64()` before they go into R
  formulas / model fitting, so special characters survive. Decode with
  `fromB64()` for anything user-facing (table labels, messages).
- **Data problems are rejected**, not stopped:
  `jmvcore::reject(.("message"), code = exceptions$dataError, ...)`. Rejections
  surface as ordinary R errors for R users, so they are testable with
  `expect_error(...)`.
- **Non-fatal warnings** use `jmvcore::Notice` (often via a helper such as
  `setAnalysisNotice()`), not `warning()`.
- **All user-facing strings are wrapped in `.()`** for translation:
  `.("Some text")`. The `.()` lookup needs a `self` in scope — which is why
  package-level helper functions often take a `self` argument even when they
  otherwise wouldn't need one. Translation catalogs usually live in a
  `jamovi/i18n` git submodule and are regenerated separately, so flag any new
  translatable strings you introduce.

## Code style

jamovi modules vary in style; the safest default is to **match the surrounding
code** (indentation, spacing, naming). No formatter is mandated at the jamovi
level — if the repo's `CLAUDE.md`, an `air.toml`, or an installed standards
plugin (e.g. `jamovi-dev-standards`) specifies one, defer to that.

## Testing

Add tests for newly implemented functionality. Tests live under
`tests/testthat/` and use `testthat`. Prefer testing the actual exported
analysis functions (the way an R user calls them, e.g.
`jmv::ttestIS(data, ...)`), since that exercises the real interface; helper
functions can be tested directly where useful. Because data problems are
rejected as ordinary R errors (see Cross-file conventions), they are testable
with `expect_error()`.

The exact test layout and comment style may be governed by your repo's
`CLAUDE.md` or an installed standards plugin (e.g. `jamovi-dev-standards`).

## Where to look it up

dev.jamovi.org is the authoritative reference; reach for it on specifics rather
than guessing. Roughly where things live:

- **Option & results types** (field types, table/column/image elements) —
  Reference (the YAML definitions and the R Options / Results APIs).
- **UI controls** (the control types and their properties) — Reference (UI
  Controls).
- **Project structure & module manifest** — Resources (project structure /
  cheat sheets).
- **Translation / i18n** — Learn (intermediate).
- **Plots & images** (performance, themes) — Learn (plotting).
- **Distribution** (packaging and publishing) — Learn (advanced).
