EpiSODIC supports plugins: small add-ons, written by other developers or by yourself, that let EpiSODIC work with the rest of your world. Every laboratory and public health service has its own systems, its own partners and its own way of working, and no single package can know them all. A plugin closes that gap without anyone having to change EpiSODIC itself.
A plugin can put a button on an outbreak or epidemic dossier, or quietly do its work after every detection run. A few examples of what that makes possible:
Plugins can be written in R, Python or Bash, so whoever in your team knows any of these can make one. And because a plugin only uses what EpiSODIC has already found, it never changes what EpiSODIC detects.
EpiSODIC ships with one ready-made plugin, which sends an outbreak’s summary to a web address of your choice. It is a good first plugin to try, and a good template for a plugin of your own:
Everything happens on the Plugins screen, which an administrator reaches from its card on the Instance screen.
From then on, a button plugin appears on the dossiers it belongs to, for every epidemiologist. A plugin that runs after each detection run simply starts doing its work.
Disabling, updating and uninstalling are one click each on the same screen. An update that asks for more than you allowed before waits for your approval rather than quietly taking it.
Each plugin on the Plugins screen has a status:
The same screen keeps a record of every time a plugin ran: who started it, on which outbreak, and how it went.
A plugin runs its own code on your server, so treat installing one the way you would treat installing any software:
Everything the Plugins screen does is also available from R, for an administrator who prefers scripts:
episodic_plugin_install("https://github.com/org/plugins/blob/v1.0.0/share-regional.yaml")
episodic_plugin_list()
episodic_plugin_enable("share-regional")
episodic_plugin_run("share-regional", cluster_id = 12)
episodic_plugin_disable("share-regional")
episodic_plugin_uninstall("share-regional")Plugins are kept in a folder: the one named by
EPISODIC_PLUGIN_DIR, or otherwise a plugins
folder beside your configuration file. Which plugins are switched on is
written in that configuration file, under plugins, so it is
visible and editable in the same place as all your other settings.
A plugin is one YAML file. It says what the plugin is, what it needs, and which steps it runs. Here is a complete one:
name: Share outbreak with a regional platform
version: 1.0.0
api: 1
trigger: button
placement: outbreak
label:
en: Share with regional platform
nl: Delen met regionaal platform
permissions: [cluster_summary]
secrets: [SHARE_ENDPOINT_URL, SHARE_ENDPOINT_TOKEN]
requires:
packages: [httr2, jsonlite]
timeout: 60
steps:
- name: Send the outbreak
shell: r
run: |
payload <- jsonlite::read_json(Sys.getenv("EPISODIC_PAYLOAD"))
# ... send it ...
cat("message=Shared ", payload$cluster$label, "\n",
sep = "", file = Sys.getenv("EPISODIC_RESULT"))The file’s name is the plugin’s id:
share-outbreak-http.yaml is the plugin
share-outbreak-http. An id uses lowercase letters, digits
and hyphens.
The easiest way to start is to copy the shipped plugin, give it a name of its own and change its steps. Install it from its path on the server, enable it, and push its button.
EpiSODIC checks every key. A key it does not know is reported rather than ignored, so a typo shows up on the Plugins screen straight away.
| Key | Required | What it holds |
|---|---|---|
name |
yes | The plugin’s name, as the Plugins screen shows it |
version |
yes | The plugin’s own version |
api |
yes | The plugin API version it was written for; this EpiSODIC provides
1 |
trigger |
yes | button, or run_committed to run after
every detection run |
steps |
yes | The steps it runs, in order |
placement |
for a button | outbreak or epidemic: the dossier the
button is on |
label |
for a button | The button’s text per language; en is required and used
when a language is missing |
description |
no | A description per language, with en |
permissions |
no | The parts of the data it receives (see below) |
destination |
with cluster_cases |
Where it sends data, in words, shown before every send |
secrets |
no | Names of the server’s environment variables it may read |
requires |
no | packages (R packages) and commands
(programs on the PATH) it needs |
timeout |
no | Seconds the whole plugin may run, 1 to 600; 60 by default |
source |
no | An https:// address of the plugin itself, from which
the Plugins screen updates it |
Requirements are checked, never installed: a plugin whose package or program is missing is listed as unavailable, with the reason, until the server has what it needs.
Each step has a shell (r, bash
or python) and the code it runs, and may have
a name and plain settings in env:
steps:
- name: Prepare
shell: r
run: |
writeLines("prepared", "state.txt")
- name: Upload
shell: bash
env:
MODE: production
run: |
curl --fail --silent -T state.txt "$UPLOAD_TARGET"
echo "message=Uploaded" >> "$EPISODIC_RESULT"r runs with the same R installation and packages as
EpiSODIC; bash and python use the programs on
the server. Steps share one working folder, so one step can leave a file
for the next. A step that fails ends the plugin there, and its error is
recorded.
A step’s env can take values from the payload with
placeholders, which saves reading the payload file for a value or
two:
A placeholder names a path in the payload (below), and only parts the
plugin receives: cluster needs
cluster_summary, run needs
run_summary, and cases, being a list, cannot
be used. A placeholder whose value is missing in a run stops that run
and says which one, rather than passing an empty value.
Placeholders work in env only, never in
run. The filled value reaches your code as an environment
variable, as data, so a ward or pathogen name can never change what the
code does. A ${{ }} in run is reported as a
mistake.
Each step runs in a fresh process with:
EPISODIC_PAYLOAD: the path of a JSON
file with the data the plugin may see (below).EPISODIC_RESULT: the path of the file
it reports back to (below).EPISODIC_PLUGIN_ID and
EPISODIC_LANGUAGE: the plugin’s id, and
the language of whoever pushed the button, so a message can be written
in that language.secrets, once enabled, and the step’s own
env.PATH, HOME, the locale, proxy settings and
certificate stores.Nothing else from the server’s environment is passed, and none of
EpiSODIC’s own variables ever is: a secret whose name starts with
EPISODIC_ is refused.
The payload always holds api, plugin
(id, version), trigger,
instance_id, language and
dashboard_url, and for a button the user who
pushed it (username, role). The rest depends
on the plugin’s permissions; a part it has no permission for is simply
absent:
| Permission | Trigger | Adds |
|---|---|---|
cluster_summary |
button | cluster: its id and label
(O-12, in the reader’s language), scale, origin, pathogen,
level, care line, region, institution, ward, first and last day, counts,
expected, excess, ratio, priority score, detector agreement, opening
time and the latest classification |
cluster_cases |
button | cases: the cluster’s cases as stored, pseudonymised,
one object per case |
run_summary |
run_committed | run: its id, date, status, whether it was the first run
on the database, its counts, and new_clusters with the
summary of each cluster it opened |
cluster_cases is the one permission that sends
case-level data. A plugin that asks for it must say where the data goes
in destination, and EpiSODIC asks for confirmation, naming
that destination, every time its button is pushed.
A step reports by adding lines to the file in
EPISODIC_RESULT:
status=success
message=O-12 was shared.
link=https://regional.example/outbreaks/123
status is success (the default) or
error, when the plugin wants to say that what it tried did
not work. message is one line of text, and
link an http or https address the
dashboard offers as a link. A later line overrides an earlier one, and
any other line is reported as a mistake in the plugin. A plugin that
reports nothing and finishes cleanly has succeeded without a
message.
The dashboard shows the message as plain text and the link as a plain link, never as markup.
A plugin that fails, or runs longer than its timeout, is
stopped and recorded, with the end of its error output. Whoever pushed
the button sees that output straight away, and an administrator finds it
on the Plugins screen, always with the values of the plugin’s secrets
masked. A plugin that runs after detection runs can never make a run
fail, and one failing plugin never stops the others.
What a plugin cannot do, by design:
requires must
already be on the server.destination says.The plugin API version changes only when the file format, the payload
or the result changes incompatibly. A plugin written for another version
is listed as unavailable with the reason, rather than handed data it
might misread. When you publish a plugin, point its source
at a release tag or commit rather than a branch, so an update is a
deliberate step for everyone who uses it.
EPISODIC_PLUGIN_DIR and every
other EPISODIC_* variable.?episodic_plugin_install,
?episodic_plugin_enable and
?episodic_plugin_run for the details of each function.