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 Template variables and SpEL expressions
Inside action configurations, you can use Spring Expression Language (SpEL) templates.
Variables are wrapped with #{[...]}
for simple substitution or #{...}
for complex expressions.
Standard template variables:
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. |
userinfo | User information object with authorities (available in some steps). |
SpEL expressions examples:
- Generate UUID:
#{T(java.util.UUID).randomUUID()}
- String concatenation:
#{'prefix-' + [itemUri]}
- Conditional logic:
#{[status] == 'draft' ? 'unpublished' : 'published'}
- Access step results:
#{[steps].update.authorizationCollection}
- Collection operations:
#{T(org.springframework.util.StringUtils).collectionToDelimitedString(#groups, ', ')}
- User's OIDC attributes:
#{[oidc_email]}
,#{[oidc_name]}
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 booleanmessage: Error text | – (throws) |
stopOnConditionStep | Stops execution (without error) when condition is met.with: condition: SpEL boolean expressionask: SPARQL ASK query (alternative to condition)store: Target store ("settings" or "collection") | – (returns empty map to stop) |
httpRequestStep | Makes an HTTP call.with: method: HTTP methodurl: Target URLheaders: Map of headersparameters: Query parametersbody: Request bodyconnectTimeout: Connection timeout (default: 15s)readTimeout: Read timeout (default: 60s) | steps.<id>.request steps.<id>.response |
sparqlSelectQueryStep | Runs a SELECT over the in‑memory itemModel and expects exactly one binding.with: query: SPARQL SELECT (templated) | steps.<id>.result |
modelToStringStep | Serialises itemModel to RDF format.with: format: MIME type (text/turtle or application/rdf+xml ) | steps.<id>.modelString |
jsonPathStep | Extracts a value from JSON using JSON Pointer.with: json: JSON string or objectjsonPointer: JSON Pointer pathdefaultValue: Optional default | steps.<id>.result |
modelUpdateQueryStep | ⚙️ Hook‑time helper. Executes an UPDATE on itemModel , then overwrites the graph in the store.with: query: SPARQL UPDATE | – |
itemUpdateQueryStep | 🏷 Workflow‑time helper. Performs an UPDATE on itemModel , then calls ItemService.updateJsonLd(...) which re‑enters the full create‑update pipeline.with: query: SPARQL UPDATE | – |
settingsUpdateQueryStep | Updates the settings RDF store with new data.with: graph: Target graph URI (templated)model: RDF data in Turtle format (templated)params: Additional parameters for templatingreturnParams: Parameters to return | Returns specified params |
indexCollectionItemsStep | Triggers indexing for specific items in a collection.with: stepParam: Name of previous step containing datacollectionParam: Parameter name for collection URIurisParam: Parameter name for item URIs list | – |
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. 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. |
5. Security considerations
SPARQL Injection Protection
When using SpEL templates in SPARQL queries, be aware that user-provided values are automatically escaped to prevent SPARQL injection attacks. The platform uses SparqlEscapeUtils
to:
- Validate input for dangerous patterns (SPARQL keywords, comment sequences)
- Escape special characters in string literals
- Reject values with unbalanced quotes or braces
This protection is applied to:
- Username from authentication context
- OIDC attributes in user onboarding
- String parameters in workflow actions
Important: Never bypass the templating system by concatenating user input directly into queries.
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.