This is the function you schedule to run regularly (e.g. daily, via cron): it pulls in new laboratory data, checks every monitored stream for statistical aberrations with the configured detectors, reconciles the results into cluster dossiers for the board to assess, and records everything in the database. A run either completes in full or leaves no trace at all - it runs inside a single database transaction, so a failed run is always safe to simply retry.
Usage
episodic_run_cron(
cases,
denominators = NULL,
institution_activity = NULL,
episodic_config_path = Sys.getenv("EPISODIC_CONFIG", unset = NA),
pathogen_config_path = Sys.getenv("EPISODIC_PATHOGEN_CONFIG", unset = NA),
db_path = Sys.getenv("EPISODIC_DB"),
host = Sys.info()[["nodename"]],
account = Sys.info()[["user"]],
run_date = Sys.Date(),
backfill = NULL,
debug = FALSE
)Arguments
- cases
Your laboratory data: a data frame or
tibblein the shape episodic_case_data describes, or a zero-argument function that returns one. Defaults to the bundled synthetic generator, useful for demos and testing but not real surveillance.- denominators
Optional: your testing-volume data, in the same form as
cases- normally a data set, a function if it has to be produced at run time (seeepisodic_synthetic_denominators()for the expected shape). Leave asNULL(the default) if you have none to supply - positivity panels simply stay blank.- institution_activity
Optional: your hospital patient-days data (see
episodic_synthetic_institution_activity()for the expected shape), normally as a data set, or as a function taking the current institutions table. Itsinstitution_keyis the same identifier your case data uses - EpiSODIC hashes both on load, so a key taken from the institutions table this passes a function is already hashed and will match nothing. Leave asNULL(the default) if you have none - detection falls back to raw case counts.- episodic_config_path
The config path.
- pathogen_config_path
Path to an operator's own per-pathogen CSV, overlaid row-by-row on the shipped
inst/config/episodic_default_pathogen_config.csv. Defaults to theEPISODIC_PATHOGEN_CONFIGenvironment variable; if unset, only the shipped defaults are used. Set to a path that does not exist, this stops with an error. Seevignette("tuning-detection")for the column reference.- db_path
Path to the EpiSODIC database: a SQLite file (created automatically if it does not exist yet) or a MariaDB/MySQL DSN (see
episodic_db_dsn_mariadb()).- host, account
Recorded with the run for audit purposes; default to the current machine and account.
- run_date
The date to treat as "today". Defaults to the system date. Every detector anchors on it:
same_placeandrare_triggerreport only hits inside theirlookback_daysof it, and Farrington tests only weeks that are complete on or before it. Detection against a historical extract is therefore one run per date with arun_dateinside the extract's own window, not one run dated today holding the whole archive - a run dated today reports what is current, and the archive is not.episodic_validate_detection()replays exactly that way.- backfill
Whether this run reports the whole case history it is given rather than only what falls inside the detectors' lookback windows.
NULL, the default, decides from the database: a run with no completed run behind it is a backfill, so a first import of years of history arrives as years of clusters (the settled ones closed in that same run byreconciliation.stale_open_days, and so straight into the Archive) rather than as an empty dashboard. Every run after it is bounded again, so nothing is ever reported twice. PassFALSEto bound the first run too, which is what a prospective replay wants:episodic_validate_detection()measures detection delay, and a first replay run that reported its whole baseline at once would measure the import instead.TRUEforces it on, for a database whose history was imported by something other than a run.- debug
If
TRUE, print a good deal more than the phase-by-phase progress this function always writes:sessionInfo(), the versions of every package whose own behaviour a run depends on, memory snapshots at the start and end, and per-stream detail inside the detection loop - which stream, how many cases, whether it cleared the eligibility gate, what each detector did with it. Useful when a run's results are not what an operator expects and the question is which stream, or which phase, they diverged at. Noisy; leave off for routine scheduled runs.
Value
Invisibly, the run_id of the completed run. The run's row in
episodic_detection_run holds its status, the per-feed load counts,
and error_text if it failed. Case data that does not satisfy the
requirements throws instead of returning - the run row is still written,
with status = "failed" and the same message in error_text.
Details
EpiSODIC never connects to your laboratory system directly. You extract
and transform your own data beforehand, and hand it over as a plain data
frame or tibble: cases for the laboratory results themselves (see
episodic_case_data for the required columns and their allowed
values), and optionally denominators and institution_activity for
testing volume and hospital activity. A data set is the normal case; if
producing the data only makes sense at run time (a live database query,
for instance), a zero-argument function returning one is accepted just
as well.
The exact detection settings used are recorded with the run, so any past result can always be traced back to the configuration that produced it.
A database whose schema is behind the installed package is brought up
to date before the run starts, the way episodic_db_migrate() would,
so upgrading an instance is upgrading the package. Set
database.auto_migrate to false in your configuration where schema
changes go through a DBA instead; the run then refuses such a database
and records why. See vignette("deployment"), "Upgrading EpiSODIC".
So is what each feed delivered. Before the run writes anything, your
case data goes through episodic_check_cases(). Structural problems -
a missing column, a value outside the allowed set, a date that does not
read as a date - stop the run with an error naming every offending
column, its values and the rows they are in, and are recorded on the
run as well, so the reason is visible both where the run was started
and in the dashboard's activity screen. Advisory findings are mentioned
once and the run proceeds. Run episodic_check_cases() on your extract
yourself to see all of it without starting a run at all. A run that
fails later, for any other reason, records the reason and warns rather
than returning quietly. Rows
that are merely unmatched are counted rather than dropped in silence:
institution activity whose institution_key matches no known
institution is skipped with a warning, its count recorded, and the run
finishes "partial" instead of "success". Both are complete runs the
dashboard reads from; "partial" says go and look at why rows were
skipped. episodic_detection_run carries the counts (n_cases_supplied,
n_cases_inserted, n_activity_skipped, and the rest).
Check your data before you run anything
Do not find out from an empty dashboard. Run episodic_check_cases()
on your extract - it needs no database, changes nothing, and reports
every problem it finds at once, with the rows and values involved and
what to do about each:
cases <- my_extract_and_transform_function()
episodic_check_cases(cases)It also reports what is merely worth a look: one pathogen spelled two
ways (two streams instead of one), no ward on any hospital row (no
ward-level detection), a patient_key that never repeats (no
deduplication), postcodes the map cannot place, sample dates in the
future. episodic_check_cases(..., stop_on_problem = TRUE) runs the same checks but throws an error,
for use in a script. episodic_run_cron() runs that before every
run, and refuses to start on data it cannot use, naming what to fix.
See also
episodic_check_cases() to see what EpiSODIC makes of your
extract before you schedule anything, and episodic_case_data for
the requirements it checks against.
Examples
# \donttest{
db_path <- tempfile(fileext = ".sqlite")
cases <- episodic_synthetic_cases(
start_date = as.Date("2025-01-01"), end_date = as.Date("2025-03-31")
)
run_id <- episodic_run_cron(db_path = db_path, cases = cases)
#> 2026-09-30 10:43:09.943 | episodic_run_cron() starting (host=runnervmtr4k5, account=runner)
#> 2026-09-30 10:43:09.944 | Resolving configuration
#> 2026-09-30 10:43:09.950 | Configuration resolved (hash 7fef366a6991)
#> 2026-09-30 10:43:09.950 | Connecting to database
#> 2026-09-30 10:43:09.950 | No existing database found - creating one
#> 2026-09-30 10:43:09.982 | Database connected (SQLite 3.53.3)
#> 2026-09-30 10:43:09.983 | Run 1 started
#> 2026-09-30 10:43:09.983 | Resolving and checking case data
#> 2026-09-30 10:43:09.996 | Case data checked: 421 rows, 0 problems, 0 advisory finding(s)
#> 2026-09-30 10:43:09.997 | Beginning transaction
#> 2026-09-30 10:43:09.997 | Loading pathogen configuration
#> 2026-09-30 10:43:10.001 | Pathogen configuration loaded (39 pathogen(s))
#> 2026-09-30 10:43:10.001 | Loading case data into the database
#> 2026-09-30 10:43:10.111 | Case data loaded: supplied=421, deduplicated=403, inserted=403
#> 2026-09-30 10:43:10.112 | Fetching all known cases and institutions
#> 2026-09-30 10:43:10.114 | First run on this database: reporting the whole case history rather than only what falls inside the detectors' lookback windows. Clusters whose last case is more than 60 day(s) before this run's date close in this same run and go straight to the Archive; the rest open for assessment. Every run after this one is bounded again, so nothing here is reported twice.
#> 2026-09-30 10:43:10.116 | Case history on file spans 2025-01-01 to 2025-03-31, ending 548 day(s) before this run's date (2026-09-30)
#> 2026-09-30 10:43:10.117 | Enumerating lattice streams
#> ! 2026-09-30 10:43:10.144 | no province could be resolved for any of the 105 postcode values in this run - province-level (L4) detection has nothing to run on. EPISODIC_PC_PROVINCE_MAP is unset, and there is no built-in rule to fall back on - deriving a province from a postcode is country-specific. Point it at your own pc/province_code CSV, or leave it unset and the province level stays empty.
#> 2026-09-30 10:43:10.151 | Running same-place detector
#> 2026-09-30 10:43:10.228 | Same-place detector found 5 detection(s)
#> 2026-09-30 10:43:10.228 | Running rare-trigger detector
#> 2026-09-30 10:43:10.234 | Rare-trigger detector found 1 detection(s)
#> 2026-09-30 10:43:10.234 | Farrington tests every week its streams can carry this run, rather than the 8-week catch-up cap: there is no earlier run to catch up to
#> 2026-09-30 10:43:10.236 | Reconciling 320 stream(s) (Farrington/MEM detection, triangle update, cluster reconciliation)
#> ! 2026-09-30 10:43:10.720 | MEM: declined stream Bordetella pertussis/pathogen_region/REGION, disabled by mem_mode
#> ! 2026-09-30 10:43:10.726 | MEM: declined stream Campylobacter/pathogen_region/REGION, insufficient history for climatology
#> ! 2026-09-30 10:43:10.732 | MEM: declined stream Clostridioides difficile/pathogen_region/REGION, disabled by mem_mode
#> ! 2026-09-30 10:43:10.737 | MEM: declined stream Giardia lamblia/pathogen_region/REGION, insufficient history for climatology
#> ! 2026-09-30 10:43:10.744 | MEM: declined stream Influenza A/pathogen_region/REGION, insufficient history for climatology
#> ! 2026-09-30 10:43:10.750 | MEM: declined stream MRSA/pathogen_region/REGION, disabled by mem_mode
#> ! 2026-09-30 10:43:10.755 | MEM: declined stream Neisseria meningitidis/pathogen_region/REGION, disabled by mem_mode
#> ! 2026-09-30 10:43:10.760 | MEM: declined stream Norovirus/pathogen_region/REGION, insufficient history for climatology
#> ! 2026-09-30 10:43:10.766 | MEM: declined stream RSV/pathogen_region/REGION, insufficient history for climatology
#> ! 2026-09-30 10:43:10.779 | MEM: declined stream Salmonella/pathogen_region/REGION, insufficient history for climatology
#> 2026-09-30 10:43:10.815 | Stream loop time by stage: cases 0.0s, mem 0.0s, eligibility 0.2s, farrington 0.0s, trend 0.0s, detections 0.0s, reconciliation 0.0s, total 0.4s (320 stream(s))
#> 2026-09-30 10:43:10.815 | Stream reconciliation done: 6 detection(s), 6 new signal(s), 0 updated signal(s)
#> 2026-09-30 10:43:10.816 | Backfill: 6 cluster(s) opened from the case history, 6 of them already closed by the system and in the Archive, 0 left open for assessment
#> 2026-09-30 10:43:10.818 | Suppressing lattice: weighing 6 cluster(s) across 5 pathogen(s)
#> 2026-09-30 10:43:10.819 | Suppressing lattice: 42 case link(s) read for 6 cluster(s), 0 assessed cluster(s) exempt
#> 2026-09-30 10:43:10.826 | Suppressing lattice done: 0 cluster(s) suppressed (0 parent(s) behind a dominant child, 0 child(ren) behind a diffuse parent)
#> 2026-09-30 10:43:10.827 | Nowcast: no stream carries an open cluster
#> 2026-09-30 10:43:10.828 | Epidemic direction: no open epidemic
#> 2026-09-30 10:43:10.829 | Epidemic outlook: no open epidemic
#> 2026-09-30 10:43:10.831 | Outbreak-end forecast: 0 of 0 open outbreak(s) of a transmissible pathogen computed, 0 point source, 0 without the parameters or reporting history, 0 failed
#> 2026-09-30 10:43:10.831 | Committing transaction
#> 2026-09-30 10:43:10.833 | Finishing run 1 (status: success)
#> 2026-09-30 10:43:10.836 | episodic_run_cron() finished in 0.9s (status: success)
file.remove(db_path)
#> [1] TRUE
# }