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 | – |
triplestoreSparqlStep | Executes SPARQL queries against a specific triplestore.with:triplestore: Target store - use admin (or settings) for the admin triplestore, or the full RDF store URI for data triplestores (e.g., https://data.hanami.app/config/rdfstore/2)querySelect: SPARQL SELECT query (use one of querySelect/queryConstruct/queryAsk)queryConstruct: SPARQL CONSTRUCT queryqueryAsk: SPARQL ASK queryoutputFormat: Result format (json-ld, turtle, json) | steps.<id>.outputs.result |
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.
7. Cross-triplestore queries and HTTP notifications
The triplestoreSparqlStep enables fetching data from either the admin (settings) or data triplestore and passing the results to subsequent steps. This is particularly useful for building notification systems where you need to:
- Fetch item metadata and user information from the admin database
- Send that data to an external notification service via HTTP
7.1 Triplestore selection
Hanami maintains separate triplestores. When specifying the triplestore parameter:
| Value | Description | Example |
|---|---|---|
admin or settings | Shortcut for the admin/settings triplestore (in-memory) | triplestore: admin |
| Full RDF store URI | Direct reference to any configured RDF store | triplestore: "https://data.hanami.app/config/rdfstore/2" |
Note: There is no
datashortcut. To query the data triplestore, you must use the full RDF store URI. You can find the correct URI in the Hanami admin UI under Settings → RDF Stores, or in your boot configuration files (typicallyhttps://data.hanami.app/config/rdfstore/2for the data store).
Common triplestores:
| Triplestore | Contains | Use case |
|---|---|---|
| Admin store | User accounts, groups, permissions, collection metadata | Fetching user info, group memberships, authorization data |
| Data store | RDF items managed by collections | Fetching item content, related data |
7.2 Example: Notification on item update
This example demonstrates a workflow that:
- Fetches the updated item's metadata and the user's group membership from the admin triplestore
- Sends a JSON-LD payload to an external notification service
on: afterSave
id: notify-users-on-update
async: true
jobs:
steps:
# Step 1: Gather notification data from admin triplestore
- id: gatherData
kind: triplestoreSparqlStep
with:
triplestore: admin
queryConstruct: |
PREFIX hanami: <https://hanami.app/ontology#>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT {
<#{[itemUri]}> a hanami:UpdateNotification ;
dct:modified ?now ;
hanami:updatedBy <#{[userUri]}> .
<#{[userUri]}> a foaf:Person ;
foaf:name ?userName ;
hanami:memberOf ?group .
?group rdfs:label ?groupName .
}
WHERE {
OPTIONAL {
<#{[userUri]}> foaf:name ?userName .
}
OPTIONAL {
<#{[userUri]}> hanami:memberOf ?group .
?group rdfs:label ?groupName .
}
BIND(NOW() as ?now)
}
outputFormat: json-ld
# Step 2: Fetch item data from the data triplestore (using full URI)
- id: fetchItemData
kind: triplestoreSparqlStep
with:
triplestore: "https://data.hanami.app/config/rdfstore/2" # Full RDF store URI required
queryConstruct: |
PREFIX dcat: <http://www.w3.org/ns/dcat#>
PREFIX dct: <http://purl.org/dc/terms/>
CONSTRUCT {
<#{[itemUri]}> a dcat:Dataset ;
dct:title ?title .
}
WHERE {
<#{[itemUri]}> dct:title ?title .
}
outputFormat: json-ld
# Step 3: Send to external notification service
- id: sendNotification
kind: httpRequestStep
with:
method: POST
url: "https://notifications.example.com/api/notify"
headers:
Content-Type: "application/ld+json"
Authorization: "Bearer ${NOTIFICATION_API_KEY}"
X-Event-Type: "item-updated"
X-Collection: "#{[collectionUri]}"
body: "[#{[steps][gatherData][outputs][result]}, #{[steps][fetchItemData][outputs][result]}]"
connectTimeout: 5
readTimeout: 10
7.3 Query types
The triplestoreSparqlStep supports three query types:
| Parameter | Returns | Best for |
|---|---|---|
querySelect | JSON array of bindings | Extracting specific values |
queryConstruct | RDF graph (in specified format) | Building structured payloads for HTTP requests |
queryAsk | Boolean | Conditional logic |
7.4 Output formats
When using queryConstruct, specify the outputFormat:
| Format | Content-Type | Description |
|---|---|---|
json-ld | application/ld+json | JSON-LD serialization (recommended for HTTP APIs) |
turtle | text/turtle | Turtle serialization |
json | application/json | JSON representation of results |
7.5 Accessing step results
Results from triplestoreSparqlStep are available to subsequent steps via:
#{[steps][<step-id>][outputs][result]}
For example, if your step has id: gatherData, access its output with:
#{[steps][gatherData][outputs][result]}