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) –ActionsCronRunnerruns once per theactions.cronExpressionsetting (Spring@Scheduled). A collection‑wide empty RDFModelis supplied together withcollectionUriin the template arguments.Create / Update flow – the platform calls the runner inside the item service in this strict order:
beforeSave → onSave → afterSavePhase When it fires What happens if jobs exist? beforeSaveBefore any data is persisted. You may mutate itemModel. The platform reloads the model after all jobs run.onSaveImmediately after beforeSave.If at least one onSavejob is defined, the defaultrdfStore.replaceGraph(...)step is skipped – your job is responsible for persisting the graph.afterSaveOnce 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 → afterDeleteonDeletefollows 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 actionidto 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
onand optionalid. Each job runs serially; individual steps markedasync: trueare executed on theactionTaskExecutorthread‑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. | 
userUri | The account URI that triggered the action. | 
userinfo | User information object with authorities (available in some steps). ([userinfo].username, [userinfo].displayName, [userinfo].authorities) | 
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>.requeststeps.<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
•modelUpdateQueryStepis surgical: it tweaks the current model and writes it back once. No extra hooks fire.
•itemUpdateQueryStepis recursive: after modifying the model it invokes the high‑level item service, so allbeforeSave/onSave/afterSaveactions 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 allowsworkflowjobs providedwf-enabledistrue. - wf-enabled – controls whether manual workflow actions are available.
 - cron-expression – schedule consumed by 
ActionsCronRunner. 
The guard inside
ActionsRunnerrequires at least one ofenabledorwf-enabledto betrue; otherwiserunJobs(...)is a no‑op.