jamovi Module Development
An ordinary R package with a declarative layer on top: options and results are described in YAML, and the GUI, the R API and the option validation are all generated from that description rather than written.
What it is
A jamovi module is an R package. It has a DESCRIPTION, a NAMESPACE, R/, man/ and data/, installs like any other package, and depends on jmvcore, R6 and magrittr. What makes it a module is a jamovi/ directory of YAML files describing what the analysis takes and what it produces, and a build step that compiles the whole thing into a single .jmo file that jamovi installs.
Each analysis is up to five files, and the split is the thing worth understanding:
| File | What it declares |
|---|---|
jamovi/<name>.a.yaml |
The options — every input, its type, which variable types are suggested and which are permitted, plus where the analysis appears in the menus |
jamovi/<name>.r.yaml |
The results schema — every table, plot and value the analysis can output, with each table’s columns and their types declared up front |
jamovi/<name>.u.yaml |
The UI layout — how the options are arranged in the panel |
R/<name>.b.R |
The backend: an R6 class implementing private$.run(). The only file you write the logic in |
R/<name>.h.R |
Generated. Its first line reads “This file is automatically generated, you probably don’t want to edit this” — it contains the Options class and the Base class that .b.R inherits from |
A module-level jamovi/0000.yaml lists every analysis and its menu placement, and jamovi/00refs.yaml holds citations. Three separately versioned schema keys appear — jas on the analysis, jrs on the results, jms on the module — so the framework can evolve each independently.
The consequence: you declare first, then implement
This is the part that surprises people arriving from plain R, and it cuts both ways.
What you get free. Declaring an option in .a.yaml produces the GUI control, the R API argument, the type checking, and the documentation stub — all of it, without writing interface code. suggested and permitted mean the variable picker will offer the sensible types and refuse the impossible ones before the analysis runs, so a whole class of error is handled by declaration rather than by defensive code in .b.R.
What you pay. A result you have not declared cannot be filled. Adding a column to an output table means editing .r.yaml and regenerating, not appending to a data frame. The loop is edit YAML → regenerate → rebuild → reinstall → click through the GUI, which is markedly slower than changing a line in an R script and re-sourcing it.
.r.yaml is optional, incidentally — an analysis that produces no declared output can omit it entirely, and several of the worked examples in the template do exactly that.
A module cannot read the environment it is running in
Worth knowing before designing anything that talks to an authenticated service: the jamovi engine sanitises process environment variables, and its HOME is a build-time placeholder rather than the user’s home directory. So the ordinary Sys.getenv() route to an API key returns nothing, and ~/.Renviron does not resolve to the file the user edited.
askLLM works around it with an explicit lookup chain — process environment first, then the Windows registry (HKCU\Environment, then HKLM), with ~/.Renviron only as a last resort. That is a real constraint on module design rather than a quirk of that module, and it means “just put it in an environment variable” is not an available answer for secrets here.
clearWith: — the idea worth stealing
Inside .r.yaml, each result carries a clearWith: list naming the options that invalidate it:
- name: pyramidTable
type: Table
clearWith: [age, gender, female]That is a declared dependency graph for caching. Change the age variable and this table is discarded and recomputed; change something not on the list and it survives untouched. jamovi recomputes only what the change actually affected, which is what makes a GUI over expensive analyses feel responsive.
It is also a maintenance trap in the honest direction: forget to list an option and the result goes stale rather than wrong-looking — the number on screen was computed with the old setting and nothing announces it. The general lesson generalises past jamovi: when invalidation is declared by hand, the failure mode is silence.
Why it matters for my work
It is how an analysis reaches someone who does not write R. That is the whole argument. ClinicoPath jamovi Module states the rationale directly — lower friction than raw R, each function implemented once, publication-ready output — and the practical effect is that a trainee or a clinician runs a defensible survival analysis without a console.
Reproducibility is a side effect of the file format, not a discipline anyone has to maintain. A .omv file stores the data and the analysis chain that produced the output, so reopening it reconstructs how a number was reached. That is a stronger guarantee than a script sitting beside a spreadsheet, and it is free. jmvReadWrite in ClinicoPath Org Collection makes those files readable from R, which is the only practical route to diffing or regression-testing an analysis outside the GUI.
The bottleneck is documentation, not code. ClinicoPath jamovi Module records 388 analyses against 32 vignettes in the umbrella, while the much smaller sub-modules carry 35 to 63 vignettes each. The declarative layer makes adding an analysis cheap enough that the writing falls behind — which is worth knowing before treating “add one more analysis” as the low-cost option. Volume is not the constraint here; explaining what the 388 do is.
Wrapping beats reimplementing, and the collection is organised around that. jjstatsplot is ggstatsplot behind a jamovi GUI, and ggstatsplot is mirrored in ClinicoPath Org Collection alongside the packages behind Descriptive Tables and Survival Analysis Workflow. The normal shape of a new analysis here is: find the R package that already does it, declare the options and results, and let .b.R be thin.
How it connects
ClinicoPath jamovi Module — the live module this is all in service of, and where the documentation-debt numbers come from.
ClinicoPath Org Collection — the reference library: the framework’s own plumbing, the templates this page was read from, and around fifty other people’s modules to learn the conventions from.
jjstatsplot — the clearest worked example of the wrap-don’t-reimplement pattern.
Descriptive Tables — the analyses most often asked for, and the ones whose R implementations are already mirrored in the collection.
Survival Analysis Workflow — the same, for time-to-event work, and the module’s documented smoke test is a Kaplan-Meier run on a bundled dataset.
LLM Report-Quality Checking — the adjacent question of putting a model rather than a statistical method behind a GUI; askLLM in the collection is someone else’s attempt at exactly that inside jamovi.
Open questions
- Is there a regression test that runs outside the GUI? The umbrella carries 811 test files, but nothing here records whether they exercise
.b.Rlogic directly or whether output tables are compared against stored expectations.jmvReadWritemakes the second possible.[unverified] - Is
clearWith:audited anywhere? A missing entry produces a silently stale result, and with 388 analyses a manual check is not realistic — but it is mechanically checkable, since every option a.b.Rreads viaself$options$...should appear in theclearWith:of every result it affects. Nothing records whether that has ever been run.[unverified] - What is the actual release route — the jamovi library, or sideloading a
.jmo? The template commits a built.jmoalongside its source, which suggests sideloading is at least routine. Not recorded anywhere in this wiki. - Would generating vignettes from the
.a.yaml/.r.yamldeclarations close some of the documentation gap? The option names, types and result schemas are already machine-readable, so a skeleton per analysis is derivable; only the interpretation would need writing. Speculative, but cheap to test on one module.