Actions Configuration for Collections
This document explains how to configure actions for a collection in Hanami. Actions are small, declarative jobs that run automatically in response to lifecycle events on an item or when a user explicitly triggers a workflow action. Every collection may declare its own set of actions under
ext/collections/<collection‑id>/actions/
Each action is a single YAML file (extension: *.yml
). The filename is arbitrary
but should be meaningful (e.g. dataset-publish.yml
).
1. Lifecycle events (on
)
Actions are executed by the ActionsRunner service, which is invoked from three different code paths:
Cron scheduler (
onCron
) –ActionsCronRunner
runs once per theactions.cronExpression
setting (Spring@Scheduled
). A collection‑wide empty RDFModel
is supplied together withcollectionUri
in the template arguments.Create / Update flow – the platform calls the runner inside the item service in this strict order:
beforeSave → onSave → afterSave
Phase When it fires What happens if jobs exist? beforeSave
Before any data is persisted. You may mutate itemModel
. The platform reloads the model after all jobs run.onSave
Immediately after beforeSave
.If at least one onSave
job is defined, the defaultrdfStore.replaceGraph(...)
step is skipped – your job is responsible for persisting the graph.afterSave
Once data is safely stored (either by you or by the platform). Perfect for side‑effects such as notifications or extra writes; the model is reloaded so changes get indexed. Delete flow – called with the sequence:
beforeDelete → onDelete → afterDelete
onDelete
follows the same override rule: if present, the platform does not callrdfStore.deleteGraph(...)
.Manual workflows (
workflow
) – kicked off by the user (UI button or API) by passing the actionid
to the runner.
Event summary
on value | Triggered by | What happens if jobs exist? |
---|---|---|
onCron | Scheduler | Job is executed on its cron schedule. |
beforeSave | Create / Update | Store will occur unless an onSave job overrides it. |
onSave | Create / Update | Job is executed; it may skip or augment storage (the default replaceGraph is bypassed when an onSave job is present). |
afterSave | Create / Update | Job can further modify the item model or trigger side-effects; the graph is already stored. |
beforeDelete | Delete | Delete will occur unless an onDelete job overrides it. |
onDelete | Delete | If at least one job is found, you must handle graph removal (or an alternative). If none match, Hanami calls rdfStore.deleteGraph(graphUri) by default. |
afterDelete | Delete | Job can post-process the deletion (e.g., cleanup, notifications). |
workflow | Explicit user action | Job executes whenever the user triggers the corresponding workflow action. |
The runner first filters jobs by
on
and optionalid
. Each job runs serially; individual steps markedasync: true
are executed on theactionTaskExecutor
thread‑pool viaJobRunnerService.runJobAsync(...)
.
2. Top-level keys Top‑level keys
Key | Type | Required | Description |
---|---|---|---|
on | string | ✓ | One of the values listed above. |
id | string | ✓ | A globally unique identifier for the action. Use kebab case (e.g. dataset-publish-action ). |
async | boolean | ✕ (default false ) | When true , the job is queued and processed in the background. |
jobs | object | ✓ | Declarative job definition (see below). |
Example skeleton:
on: afterSave # lifecycle hook
id: sample-action # action id
async: false # run synchronously with the transaction
jobs:
steps:
- id: update-status
kind: itemUpdateQueryStep
with:
query: |
# SPARQL UPDATE goes here
3. jobs.steps
jobs
holds a pipeline executed in order. Each element under steps
has:
Key | Required | Purpose |
---|---|---|
id | ✓ | Step identifier; must be unique within this file. |
kind | ✓ | The step implementation. Common kinds are: • itemUpdateQueryStep – SPARQL UPDATE scoped to the current item. • modelUpdateQueryStep – SPARQL UPDATE against the entire triplestore. • notifyStep – send an HTTP/webhook or platform notification. More kinds can be added by the platform. |
with | ✓ | Step‑specific parameters (free structure). |
3.1 Place‑holders available in SPARQL
Inside query
strings you may reference templating variables wrapped with
#{[ ... ]}
:
Variable | Expands to |
---|---|
itemUri | Absolute URI of the item being processed. |
collectionUri | URI of the collection that owns the item. |
graphUri | The graph that stores the item (handy for updates). |
userId | The account that triggered the action. |
3.2 Step‑kind catalog
Each kind
corresponds to a Spring bean implementing app.hanami.actions.steps.Step
.
Below is the built‑in palette. You can add your own by wiring a bean with the
same interface and referencing its name in YAML.
kind | Description & typical inputs | Outputs |
---|---|---|
declareConstantStep | Injects a literal into the template context. with: value: SpEL‑templated string. | steps.<id>.value |
failOnConditionStep | Aborts the whole job when a boolean SpEL expression evaluates to true . with: condition: SpEL returning boolean message: Error text | – (throws) |
httpRequestStep | Makes an HTTP call. Fields support templating: method , url , headers , parameters , body , connectTimeout , readTimeout . | steps.<id>.response.body (and headers) |
sparqlSelectQueryStep | Runs a SELECT over the in‑memory itemModel and expects exactly one binding. with.query: SPARQL (templated). | steps.<id>.result |
modelToStringStep | Serialises itemModel to text/turtle or application/rdf+xml . with.format: MIME type | steps.<id>.modelString |
jsonPathStep | Extracts a value from arbitrary JSON using a JSON Pointer. with: json , jsonPointer , [defaultValue] | steps.<id>.result |
modelUpdateQueryStep | ⚙️ Hook‑time helper. Executes an UPDATE on itemModel , then overwrites the graph in the store. Use this within beforeSave / afterSave etc. | – |
itemUpdateQueryStep | 🏷 Workflow‑time helper. Performs an UPDATE on itemModel , then calls ItemService.updateJsonLd(...) which re‑enters the full create‑update pipeline (including triggering its own actions!). Ideal for publish/retract flows. | – |
Model vs. Item update
•modelUpdateQueryStep
is surgical: it tweaks the current model and writes it back once. No extra hooks fire.
•itemUpdateQueryStep
is recursive: after modifying the model it invokes the high‑level item service, so allbeforeSave
/onSave
/afterSave
actions of the same collection will run again. Use it when you want the normal validation/indexing lifecycle to happen.
4. End‑to‑end examples
4.1 Workflow: Publish a dataset
This action is wired to a button (or API call) and flips the catalog record to
adms:current
.
on: workflow
id: dataset-publish-action
async: false
jobs:
steps:
- id: update
kind: itemUpdateQueryStep
with:
query: |
PREFIX adms: <http://www.w3.org/ns/adms#>
PREFIX dcat: <http://www.w3.org/ns/dcat#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX status: <https://data.in.ep.europa.eu/def/record-status/>
DELETE {
?record adms:status ?oldStatus .
}
INSERT {
?record adms:status status:current .
}
WHERE {
<#{[itemUri]}> a dcat:Dataset .
?record a dcat:CatalogRecord ;
foaf:primaryTopic <#{[itemUri]}> ;
adms:status ?oldStatus .
}
4.2 Workflow: Revert to draft
on: workflow
id: dataset-back-to-draft-action
async: false
jobs:
steps:
- id: update
kind: itemUpdateQueryStep
with:
query: |
PREFIX adms: <http://www.w3.org/ns/adms#>
PREFIX dcat: <http://www.w3.org/ns/dcat#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
DELETE {
?catalogRecord adms:status ?oldStatus .
}
INSERT {
?catalogRecord adms:status <https://data.in.ep.europa.eu/def/record-status/draft> .
}
WHERE {
?catalogRecord a dcat:CatalogRecord ;
foaf:primaryTopic <#{[itemUri]}> ;
adms:status ?oldStatus .
}
4.3 Hook: Reset status automatically after each save
on: afterSave
id: dataset-reset-onsave
async: false
jobs:
steps:
- id: update
kind: modelUpdateQueryStep
with:
query: |
PREFIX adms: <http://www.w3.org/ns/adms#>
PREFIX dcat: <http://www.w3.org/ns/dcat#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
DELETE {
?catalogRecord adms:status ?oldStatus .
}
INSERT {
?catalogRecord adms:status <https://data.in.ep.europa.eu/def/record-status/draft> .
}
WHERE {
?catalogRecord a dcat:CatalogRecord ;
foaf:primaryTopic <#{[itemUri]}> ;
adms:status ?oldStatus .
}
5. Synchronous vs. asynchronous actions
Setting | Behaviour | When to choose |
---|---|---|
async: false (default) | The action runs within the same transaction as the triggering event. If it fails, the whole operation rolls back. | Fast updates, critical integrity checks. |
async: true | The action is queued. Users do not wait for completion. Failures are logged but do not affect the primary operation. | Long‑running SPARQL updates, calls to external APIs, heavy indexing. |
6. Feature flags & cron schedule
Actions are globally toggled via Spring configuration properties:
hanami:
features:
actions:
enabled: true # master switch for ALL hooks (save / delete / cron)
wf-enabled: true # allow manual `workflow` jobs even when hooks are disabled
cron-expression: "0 0 * * * *" # Spring cron syntax (here: every hour)
- enabled – if
false
, Hanami ignores every automatic hook (on*
) but still allowsworkflow
jobs providedwf-enabled
istrue
. - wf-enabled – controls whether manual workflow actions are available.
- cron-expression – schedule consumed by
ActionsCronRunner
.
The guard inside
ActionsRunner
requires at least one ofenabled
orwf-enabled
to betrue
; otherwiserunJobs(...)
is a no‑op.