Dataset-update workflow automation¶
An OCS instance can run an existing openEO workflow when a successful ingestion or sync operation changes stored data. This is event-driven: it does not guess that an operation has finished by scheduling a second cron expression.
Configure a trigger¶
Triggers are instance-owned bindings in climate-service.yaml:
automation:
workflow_triggers:
- id: chirps-to-chap
on_update_of: chirps3_precipitation_daily
workflow_id: aggregate_to_chap_csv
arguments:
dataset_id: $event.dataset_id
temporal_extent: [$event.previous_end, $event.current_end]
geometries: { from_features: districts }
method: mean
period_type: day
on_update_of names the managed source dataset and workflow_id names a workflow already
available through GET /process_graphs. Trigger IDs must be unique within an instance.
Arguments are the parameters passed to the workflow. Literal YAML values are preserved. These exact event references can be used at any nesting level:
$event.dataset_id$event.artifact_id$event.action:ingestfor an ingestion, or the sync planner'sappendorrematerialize$event.previous_end: the coverage end before the update, ornullwhen the whole store is new or rewritten$event.current_start: the coverage start after the update$event.current_end: the coverage end after the update
After an initial ingestion, previous_end is JSON/YAML null, because no periods were stored
before. A temporal interval of [$event.previous_end, $event.current_end] is therefore open at
the start and reads through current_end from the dataset's earliest available period. This
means the first triggered workflow run processes the complete ingested history. An
ingestion that rewrites the whole store, including overwrite, also uses null because every
period was rewritten. After an append, previous_end is the old coverage boundary, so that
interval includes the boundary and the newly appended periods. A workflow that always needs an
explicit, non-null start can use $event.current_start, the start of the complete coverage after
the update.
The workflow definition remains reusable and deployment-independent. Operational bindings such as output dataset IDs, geometries, and DHIS2 identifiers remain in instance configuration.
Referencing a feature collection¶
Within an automation trigger, geometries: { from_features: districts } names a feature
collection declared under plugins/vectors/ (see
Installable plugins) instead of embedding a
FeatureCollection inline. This shorthand is resolved by the automation service and is not a
general openEO process-graph argument. Direct process graphs should call load_features
explicitly. An inline FeatureCollection remains valid in either context.
The reference is never resolved into geometry at submission. Instead OCS rewrites it into a
sibling node in the process graph that the workflow calls load_features on:
{
"features_districts": {
"process_id": "load_features",
"arguments": { "id": "districts", "version": "2026-09-01T06:00:00+00:00" }
},
"workflow": {
"process_id": "aggregate_to_chap_csv",
"arguments": { "geometries": { "from_node": "features_districts" } },
"result": true
}
}
This keeps a country's whole org unit hierarchy out of the persisted job record. At submission,
OCS resolves the current feature record and stores only the collection id, its refresh timestamp,
and a load_features call. The timestamp pins execution to that record: if the collection is
refreshed while the job is queued, the job refuses the stale binding instead of silently running
against different geometry. The job description carries the same version (for example
against features districts@2026-09-01T06:00:00+00:00), so it accurately explains why one run
covered 47 districts and the next covered 48.
A malformed reference or an id that no template declares fails at startup, like an unknown
workflow_id. A declared collection must also be registered before an event can submit a job.
Delivery behavior¶
A workflow is considered only after a successful ingestion or sync has persisted a
dataset.updated event. Failed, cancelled and no-op runs do not trigger workflows, and neither
does re-ingesting a dataset that is already current. Ingestion and sync, whether queued,
scheduled, run inline, or started from the admin pages, all use the same path. Work run inline
is recorded as a completed job under /ingestions/jobs, because that record is what makes
its event durable after the operation returns. Unlike queued ingestion, inline execution has
no pre-write job checkpoint: if the process stops after committing data but before recording
the completed job, that update event cannot be recovered automatically.
An ingestion job notes in its checkpoint that it is about to change stored data, before fetching anything. If OCS stops after the data is committed but before the job completes, the recovered job finds the data current and still emits the event it owed, exactly once.
Each event and trigger pair produces a deterministic openEO job ID. OCS replays persisted events at startup, but an already created, queued, running, or completed job is not duplicated. If OCS stopped after creating a job but before queueing it, startup queues that existing job.
A trigger is activated the first time OCS starts with it configured. By default
(replay_existing: false), startup replay ignores events that predate the trigger's activation, so
adding a new trigger does not backfill the workflow over every historical update. Set
replay_existing: true to opt into replaying all persisted events for that trigger. Newly
persisted events are always dispatched immediately, regardless of this flag.
Workflow execution and status remain owned by the openEO batch-job service and are visible under
GET /jobs. Workflows may also be submitted directly without a preceding dataset sync.
Triggered jobs share the existing openEO job pool with manually submitted jobs — there is no separate automation queue or concurrency limit. A fan-out of several triggers therefore runs independently in that pool. Bounding total workflow concurrency is a general resource-policy concern deferred to CLIM-845; until then the per-store lock remains the write-safety boundary for workflows that publish managed datasets.
Retries and restarts¶
A triggered workflow job runs up to max_attempts times, the first attempt included. The
default is 3 and the bound is 10:
automation:
workflow_triggers:
- id: chirps-to-dhis2
on_update_of: chirps3_precipitation_daily
workflow_id: aggregate_to_dhis2_json
max_attempts: 3
arguments: { ... }
Every attempt runs under the same deterministic job ID, so a retry is still the one job for that event and trigger, and replaying the event does not create another.
- A failure while the workflow runs is retried after a backoff of 1, 2, then 4 minutes.
This covers an unreachable source, a timeout, a server error, a store another writer is
busy with, and also errors such as a truncated remote response, which look like invalid
input but may not repeat. While it waits the job is
queued, holding no worker, and no process runs it before its backoff has passed. An invalid argument discovered only while the graph runs may therefore use the full attempt budget; only graph validation and known save-result configuration errors can be classified as permanent before execution. - A permanent error is not retried, because it fails the same way on every attempt: an invalid process graph, a request a process refuses (an unknown collection, for example), or invalid configuration found while saving the result, such as an unknown export or a units mismatch.
- A restart during an attempt requeues the job if it has attempts left. The interruption counts as an attempt, so a job that keeps crashing the server still stops.
- A job still backing off at shutdown waits out the rest of its backoff after the next start.
- Cancelling during a backoff takes effect at once, and the attempt history records it.
Re-running a cancelled job with
POST /jobs/{job_id}/resultsruns it; the earlier cancellation does not carry over.
When the attempts run out, the job stays error. Its error names the attempt, for example
OSError: connection reset (attempt 3 of 3), and GET /jobs/{job_id}/results answers with it.
The job's logs field lists every attempt with its time and outcome.
Only an attempt that finishes can deliver, so a trigger with deliver imports the result of
the successful attempt, once. Re-running a failed job with POST /jobs/{job_id}/results
starts a fresh attempt budget.
Jobs submitted directly, not by a trigger, run once as before. A job still running in another OCS process, for example one that has not finished shutting down, is not marked failed at startup: it is left to that process, and taken over if the process exits without finishing it.
Deliver the result to DHIS2¶
A trigger can deliver its job's result once the job finishes. The workflow must save through a
named export, and deliver.export names that same export:
automation:
workflow_triggers:
- id: chirps-monthly-to-districts
on_update_of: chirps3_precipitation_monthly
workflow_id: aggregate_to_dhis2_json
arguments:
dataset_id: $event.dataset_id
temporal_extent: [$event.previous_end, $event.current_end]
geometries: { from_features: districts }
export: chirps-monthly-districts
deliver:
export: chirps-monthly-districts
dry_run: true # the default; set false to import into DHIS2
When a job created by the trigger finishes, OCS submits a delivery exactly as
POST /exports/{export_id} does. Verification,
chunking, checkpoints, the per-export lock, and the import report all come from that delivery
job. The source job lists it under usage.deliveries. A job that fails or is canceled
delivers nothing.
dry_run defaults to true, so writing to DHIS2 is an explicit opt-in. Inspect the dry-run
reports first, then set dry_run: false. Only jobs that finish after the change are imported.
Jobs that finished earlier are never imported live, whether or not their dry run was delivered.
Each finished job is delivered at most once per export and mode. The idempotency key is
auto:{job_id}:{export}:{dry-run|live}, so replaying an event or restarting OCS creates no
second delivery. A job records the delivery it owes in the same write that marks it finished.
At startup OCS submits any recorded delivery missed because the process stopped after a job
finished. If submission fails, the source job exposes the latest failure under
usage.delivery_error; OCS retries it during the next startup reconciliation, not on an
in-process timer. A workflow finishing during shutdown may likewise defer submission until
the next startup reconciliation. A job keeps its delivery links when re-run by hand, so a
delivered job is not delivered again, even after switching to live; submit the new result through
POST /exports/{export_id} instead.
Adding deliver to an existing trigger does not deliver its history. The delivery step has
its own activation time, and only jobs that finish after it are delivered. Changing
deliver.export or deliver.dry_run, or removing deliver and adding it back, starts a new
activation. A job that finishes while its trigger has no active delivery step, for example
while deliver is absent or the instance is read-only, owes nothing and is never delivered
later. replay_existing applies to workflow submission only, never to delivery.
Startup fails, naming the trigger, when deliver.export is not a configured export, is
configured more than once, its plugin is not dhis2, it has no connection, that connection
is not configured, its mapping is invalid, or a literal arguments.export differs from
deliver.export. A read-only instance validates the same configuration but leaves delivery
inactive, allowing writable and serving instances to share one tracked configuration.
The export delivers what exists at its period. To deliver a monthly export from a daily dataset, let one trigger derive and publish the monthly dataset, and a second trigger on that dataset's update run the export. See the export's period must be reachable.
Delivery needs the dhis2 extra (open-climate-service[dhis2]) and the connection's token in
the server environment. See
named connections.
Current boundary¶
This mechanism dispatches workflows owned by the same OCS instance. It does not provide workflow dependency graphs, cross-service retries, webhooks, or distributed event consumption. A delivery that ends partial, rejected, or unknown is reported on its delivery job but does not yet notify anyone (CLIM-919), and a workflow job that exhausts its attempts is not reported beyond its own status. Exactly one writable OCS process should perform automation until the stores and leadership model become shared and transactional.