Writing a task plugin
A task plugin adds an annotation type: what a reader answers (the body), how a project admin sets up a task of the type (its configuration), the reader’s control and the wizard’s editor. This guide builds the template plugin, org.example.rating@1: a rating from one star to a task’s maximum. Read Plugins and the plugin catalog first for what a plugin is trusted with.
1. Copy the template
Section titled “1. Copy the template”cp -r examples/plugin-template my-pluginThe template is a package of the repository’s workspace: it needs @yggdrasil/plugin-api (the contract’s types and the authoring kit) and Vite to build. Change, in src/manifest.json and src/index.tsx, the ID org.example.rating to one in your own namespace (the reverse of a domain you control); the ygg namespace is reserved.
2. Define the body and the configuration
Section titled “2. Define the body and the configuration”export interface RatingBody { readonly stars: number } // what a reader answersexport interface RatingConfig { readonly max: number } // a task's settingsThe app never compiles a schema at runtime (its Content Security Policy forbids eval), so a plugin checks bodies and configurations with functions. schemaCheck turns a function that lists problems (a JSON pointer and a code) into the contract’s SchemaCheck:
body: h.api.schemaCheck<RatingBody>("urn:org.example.rating:body:1", bodyProblems),config: h.api.schemaCheck<RatingConfig>("urn:org.example.rating:config:1", configProblems),Checks that need the configuration go in validate (here, at most max stars). Bodies are encrypted: only members’ apps check them, never the server.
3. Write the reader’s control and the wizard’s editor
Section titled “3. Write the reader’s control and the wizard’s editor”Components get React, React Intl and the design system from the host, so they share the app’s React and look like the app:
/** @jsxImportSource @yggdrasil/plugin-api */import { definePlugin, host } from "@yggdrasil/plugin-api/author";
function RatingInput({ config, value, onChange, label, required, disabled, errorMessage }) { const { RadioGroup, Radio } = host().ui; // …one radio button per star count; onChange(undefined) when cleared}- The control gets the task’s configuration, the current value (
undefinedis unanswered),onChange, the task’s name aslabel, whether it is required, and the app’s own error message. It reports every change: the app autosaves each one, to an encrypted device draft and then to the server, and submits the task when the reader does. There is no Save button. - The editor gets the configuration and reports every edit;
defaultConfigis what a new task starts from. - The
@jsxImportSource @yggdrasil/plugin-apipragma compiles JSX to calls into the app’s React. (A plugin built outside this repository can set"jsxImportSource": "@yggdrasil/plugin-api"in itstsconfig.jsoninstead.)
4. Describe the type
Section titled “4. Describe the type”definePlugin gives the factory the host and returns what the plugin registers:
export default definePlugin((h) => ({ annotationTypes: [ratingType(h)] }));The type (AnnotationTypePlugin) also says:
| Member | What it does |
|---|---|
levels |
The task levels it fits: patient, study, series, asset, region. |
modalities |
Built-in modality plugins (ygg.image2d@1, …) whose series-level tasks may use it. Without it, the type is offered at patient and study level, and on modalities that list it. |
selector |
Whether an answer carries a shape. A type with a shape names its geometry and labels (shape), and the viewers draw it. |
conditionValue |
The value show-if conditions compare. |
aggregation, consensus |
How several readers’ answers combine (here the median). |
agreement |
How the statistics page compares readers (here an interval scale). |
extent, compare |
How large an answer is (a volume, an area, a duration, a count…), and how two readers’ answers compare pair by pair (overlap or distance) when no scale fits. |
display |
The answer as text, which the task panel shows outside annotation mode (plugin API 1.8.0). Without it, the panel shows the control, disabled. |
numberKeys |
What the digit keys pick. |
references, guidelines |
The configuration’s IDs that answers use, and per-item guidelines, for types with options or labels. |
5. Test it
Section titled “5. Test it”pnpm --filter @yggdrasil/plugin-template test renders the control and the editor with the plugin API’s test harness and a host built from the real libraries, and checks that the plugin provides exactly what its manifest declares.
6. Build the package
Section titled “6. Build the package”pnpm --filter @yggdrasil/plugin-template builddist/ then holds the package: plugin.js (one ES module of a few kilobytes, with no React inside) and yggdrasil-plugin.json (src/manifest.json with the module’s integrity, written by scripts/package.ts). Each rebuild that changes the module changes the integrity: give each release its own version.
7. Add it to an installation
Section titled “7. Add it to an installation”A superadmin adds both files in the console’s Plugins section, checks the details and enables the plugin (Plugins and the plugin catalog). Reload the app; the type is then in the setup wizard’s task types, and a project’s manifest can name it:
{ "id": "quality", "name": "Image quality", "type": "org.example.rating@1", "level": "patient", "config": { "max": 5 } }The platform’s own end-to-end test does exactly this: it builds the template, adds and enables it in the console, and a reader answers its task in a project.