Skip to content

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.

Terminal window
cp -r examples/plugin-template my-plugin

The 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.

src/index.tsx
export interface RatingBody { readonly stars: number } // what a reader answers
export interface RatingConfig { readonly max: number } // a task's settings

The 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 (undefined is unanswered), onChange, the task’s name as label, 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; defaultConfig is what a new task starts from.
  • The @jsxImportSource @yggdrasil/plugin-api pragma compiles JSX to calls into the app’s React. (A plugin built outside this repository can set "jsxImportSource": "@yggdrasil/plugin-api" in its tsconfig.json instead.)

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.

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.

Terminal window
pnpm --filter @yggdrasil/plugin-template build

dist/ 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.

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.