Skip to main content

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:

  1. Cron scheduler (onCron) – ActionsCronRunner runs once per the actions.cronExpression setting (Spring @Scheduled). A collection‑wide empty RDF Model is supplied together with collectionUri in the template arguments.

  2. Create / Update flow – the platform calls the runner inside the item service in this strict order:

    beforeSave → onSave → afterSave
    PhaseWhen it firesWhat 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 onSave job is defined, the default rdfStore.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.
  3. Delete flow – called with the sequence:

    beforeDelete → onDelete → afterDelete

    onDelete follows the same override rule: if present, the platform does not call rdfStore.deleteGraph(...).

  4. Manual workflows (workflow) – kicked off by the user (UI button or API) by passing the action id to the runner.

Event summary

on valueTriggered byWhat happens if jobs exist?
onCronSchedulerJob is executed on its cron schedule.
beforeSaveCreate / UpdateStore will occur unless an onSave job overrides it.
onSaveCreate / UpdateJob is executed; it may skip or augment storage (the default replaceGraph is bypassed when an onSave job is present).
afterSaveCreate / UpdateJob can further modify the item model or trigger side-effects; the graph is already stored.
beforeDeleteDeleteDelete will occur unless an onDelete job overrides it.
onDeleteDeleteIf at least one job is found, you must handle graph removal (or an alternative). If none match, Hanami calls rdfStore.deleteGraph(graphUri) by default.
afterDeleteDeleteJob can post-process the deletion (e.g., cleanup, notifications).
workflowExplicit user actionJob executes whenever the user triggers the corresponding workflow action.

The runner first filters jobs by on and optional id. Each job runs serially; individual steps marked async: true are executed on the actionTaskExecutor thread‑pool via JobRunnerService.runJobAsync(...).

2. Top-level keys Top‑level keys

KeyTypeRequiredDescription
onstringOne of the values listed above.
idstringA globally unique identifier for the action. Use kebab case (e.g. dataset-publish-action).
asyncboolean✕ (default false)When true, the job is queued and processed in the background.
jobsobjectDeclarative 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:

KeyRequiredPurpose
idStep identifier; must be unique within this file.
kindThe 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.
withStep‑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:

VariableExpands to
itemUriAbsolute URI of the item being processed.
collectionUriURI of the collection that owns the item.
graphUriThe graph that stores the item (handy for updates).
userIdThe account that triggered the action.
userinfoUser 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.

kindDescription & typical inputsOutputs
declareConstantStepInjects a literal into the template context.
with:
  value: SpEL‑templated string.
steps.<id>.value
failOnConditionStepAborts the whole job when a boolean SpEL expression evaluates to true.
with:
  condition: SpEL returning boolean
  message: Error text
– (throws)
stopOnConditionStepStops execution (without error) when condition is met.
with:
  condition: SpEL boolean expression
  ask: SPARQL ASK query (alternative to condition)
  store: Target store ("settings" or "collection")
– (returns empty map to stop)
httpRequestStepMakes an HTTP call.
with:
  method: HTTP method
  url: Target URL
  headers: Map of headers
  parameters: Query parameters
  body: Request body
  connectTimeout: Connection timeout (default: 15s)
  readTimeout: Read timeout (default: 60s)
steps.<id>.request
steps.<id>.response
sparqlSelectQueryStepRuns a SELECT over the in‑memory itemModel and expects exactly one binding.
with:
  query: SPARQL SELECT (templated)
steps.<id>.result
modelToStringStepSerialises itemModel to RDF format.
with:
  format: MIME type (text/turtle or application/rdf+xml)
steps.<id>.modelString
jsonPathStepExtracts a value from JSON using JSON Pointer.
with:
  json: JSON string or object
  jsonPointer: JSON Pointer path
  defaultValue: 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
settingsUpdateQueryStepUpdates the settings RDF store with new data.
with:
  graph: Target graph URI (templated)
  model: RDF data in Turtle format (templated)
  params: Additional parameters for templating
  returnParams: Parameters to return
Returns specified params
indexCollectionItemsStepTriggers indexing for specific items in a collection.
with:
  stepParam: Name of previous step containing data
  collectionParam: Parameter name for collection URI
  urisParam: 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 all beforeSave/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

SettingBehaviourWhen 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: trueThe 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 allows workflow jobs provided wf-enabled is true.
  • wf-enabled – controls whether manual workflow actions are available.
  • cron-expression – schedule consumed by ActionsCronRunner.

The guard inside ActionsRunner requires at least one of enabled or wf-enabled to be true; otherwise runJobs(...) is a no‑op.