Skip to content

Templates

Stephan Pelikan edited this page Sep 13, 2026 · 2 revisions

The cockpit shows a title for every user task and every business case, and a text the search box looks into. You can set those in your details provider, and then you write one string per language in Java. Templates are the other way: a file per text, rendered from the aggregate, kept out of the code.

If you set no title at all and configure no templates, the cockpit shows the name from the BPMN file. That works, and for a while it is enough.

  1. Switching templates on
    The one key, and the four spellings it accepts.
  2. The five templates
    Which file renders which text.
  3. Where a template is looked for
    The three directories, most specific first, and what template-path changes about them.
  4. Languages
    How one template serves several, and what happens without templates.
  5. What a template is handed
    The template context, and the one dependency a date needs.
  6. Naming a template from your code
    Setting a value which is read as a template name.

Switching templates on

Two things: Freemarker on the classpath, and a directory to load from.

vanillabp:
  cockpit:
    template-loader-path: classpath:cockpit-templates

The value says where the directory is. classpath:cockpit-templates reads the templates from the workflow module itself, file:/etc/my-application/cockpit-templates from the file system. classpath*: is accepted as well and loads the same templates as classpath:, because Freemarker asks the class loader per template and that searches every jar anyway.

A value without one of those prefixes ends the boot. Version 1 read a path without a prefix as a directory of the file system, and the cockpit server read the same kind of value as a classpath location, so one text meant two things depending on which side of the cockpit you wrote it on. Now neither side guesses. The message repeats your value with both prefixes in front of it, so an application which upgrades copies the one it meant.

A directory which cannot be read ends the boot as well, with a message naming the key, rather than showing up as a missing title once the first task arrives.

The five templates

File What it renders
workflow-title.ftl The title of a business case, in the list of cases and above its status site
task-title.ftl The title of one user task
task-definition-title.ftl The name of the kind of task, which is what a list groups and filters by
task-fulltext-search.ftl The text the search box matches a user task against
workflow-fulltext-search.ftl The same for a business case

None of them has to exist. A text with no template and no value from your code falls back to the BPMN name, and the two search texts simply stay empty.

Where a template is looked for

Three directories are tried, most specific first, and the first file found wins:

<module>/<workflow>/<task>/task-title.ftl
<module>/<workflow>/task-title.ftl
<module>/task-title.ftl

Each of the three segments is the id of that level: the workflow module id, the BPMN process id, the task definition. So a module which configures nothing keeps its templates in a directory named after itself, and a template put directly there serves every process of the module.

template-path renames a segment. It may be given for the module, for one workflow and for one user task, which is how two processes share one directory of templates:

vanillabp:
  workflow-modules:
    taxi-ride:
      cockpit:
        template-path: rides
      workflows:
        TaxiRide:
          cockpit:
            template-path: taxi
          user-tasks:
            approve:
              cockpit:
                template-path: approval

With that, task-title.ftl for the task approve of TaxiRide is looked for in rides/taxi/approval, then in rides/taxi, then in rides.

Languages

A title is reported once per language named in i18n-languages. For each of them the same template name is asked for with that language as the locale, so Freemarker picks task-title_de.ftl where it exists and task-title.ftl otherwise. One file therefore serves every language until you need a second one, and then you add it next to the first.

The two search texts are rendered once, in the first language of the list. Search matches text a user typed, not a text a user reads, so a second copy of it in another language would only make the index bigger.

Without templates the cockpit reports the names written in the BPMN files. Those names exist in one language, and bpmn-description-language says which. That is why the key is mandatory when you configure no templates and optional when you do.

What a template is handed

Whatever your details provider passed to setTemplateContext:

@UserTaskDetailsProvider
public UserTaskDetails retrievePayment(
        final Ride ride,
        final PrefilledUserTaskDetails details) {
    details.setTemplateContext(Map.of("customer", ride.getCustomerName()));
    return details;
}
Retrieve payment for ${customer}

A map, a plain object or a record all work. A record works because the object wrapper is configured to expose its accessors, and a record is usually the right shape for the small view object a title needs.

A java.time value is the one thing which needs help. Put no.api.freemarker:freemarker-java8 on the classpath and a template can read a LocalDate or an OffsetDateTime; without it a template touching one fails and says so in the log. Version 1 required that library from everybody. Here it is your choice, because most titles are text.

A template which fails to render leaves the text as it was and writes the reason to the log. It does not stop the report.

Naming a template from your code

A value you set is first treated as the name of a template:

details.setTitle(Map.of("en", "payment-reminder.ftl"));

If payment-reminder.ftl exists in one of the three directories, it is rendered and the result becomes the title. If no template of that name exists, the value stays what it is and becomes the title itself. So a fixed title for one task needs no file at all, and a special template for one task needs no configuration.

Clone this wiki locally