Task Templates
Task Templates help you configure BPMN tasks quickly and consistently in tSM Process Designer. Instead of setting many technical properties by hand (topics, mappings, listeners, script parameters), you select a template and fill in a guided form.
A Task Template is design-time only: it helps you model faster, but it does not introduce a new runtime concept. After applying a template, you still end up with a standard BPMN element — fully inspectable, exportable, and deployable.
Why Task Templates matter
Many BPMN tasks require precise technical configuration to work correctly:
- Service Tasks with Script Binding need a
scriptCode,paramsJson, and a delegate expression. - External Tasks must use the exact topic and input variables that the worker expects.
- Kafka Tasks must match the message contract of the consumer (topics, payload structure, headers).
Configuring these by hand is error-prone and requires knowledge of the underlying implementation. Task Templates solve this by packaging the correct configuration into a reusable, form-driven experience.
How you use Task Templates
Applying a template to an existing element
- Select an existing BPMN element (Service Task, User Task, Receive Task, …).
- In the Properties panel, choose a Task Template.
- When the template is applied, all template-managed defaults are written into the element — they overwrite any previous values for the properties the template manages.
- The standard properties panel is extended with the template's form — use it to edit the template-specific properties.
- For properties that map to standard BPMN/Camunda attributes (e.g. task name, candidate groups, due date), you can
also edit them using the standard tools in the properties panel. However, be aware that properties bound via
from(form-driven bindings) will be overwritten by the template the next time you change the template form. Only properties set viaset(one-time static bindings) or properties not managed by the template at all will retain your manual edits.
Favorite templates in the palette
You can mark frequently used templates as Favorites. Favorite templates appear directly in the task palette, so you can drag them into the diagram without opening the template catalog first.
Editing a template-configured task
If a task is already bound to a template, selecting it shows the template form in the properties panel. You can change the template-specific properties at any time. tSM reads the current values from the BPMN element and shows them in the form (round-trip editing).
What happens when you apply a template
When you apply a Task Template, tSM writes configuration into the selected BPMN element, including:
- standard BPMN fields (e.g. element name)
- extension attributes (e.g. external task type/topic, async flags, delegate expressions)
- extension elements (e.g. input/output mappings, execution listeners, script binding fields)
- optional tSM-specific metadata (e.g. color, description)
All template defaults are applied immediately. Properties the template does not manage remain unchanged.
The result is still plain BPMN XML. You can inspect it, export it, deploy it, and edit it manually.
Template binding and versioning
Binding to the element
When a template is applied, the BPMN element is bound to the template via the tsm:templateCode attribute. This allows
tSM to:
- recognize which template was used to configure the element
- show the correct template form when the element is selected
- support round-trip editing (read values from BPMN back into the form)
Change propagation
When a template definition is updated, existing process instances are not modified automatically. The BPMN elements retain the configuration that was written when the template was last applied.
In future tSM releases, a dedicated tool is planned to:
- search all processes containing tasks with a specific
tsm:templateCode - show differences between the current template version and the applied configuration
- allow selective re-application of the updated template
- automatically create a new version of the affected process and deploy it
Currently, you can search for all process tasks with a given template ID and re-apply the template manually to update them.
Template types
Templates are not limited to one execution style. Common types include:
Preconfigured Task
A template for a complex BPMN element (e.g. a User Task with listeners, due dates, documentation, or tSM metadata). It does not require scripts or external workers — just a reusable configuration pattern.
Script-backed task (Script Binding)
A template that configures a task and/or listeners to run an existing tSM script
using Script Binding. The template provides a form for script parameters and
writes the scriptCode + paramsJson fields into the BPMN element.
External Task
A template that configures a Service Task as an External Task, delegating execution to an external worker. The template sets the correct topic and configures input mapping variables expected by the worker. Because external workers define a strict contract, templates are the recommended way to configure them.
Kafka Task
A template that configures a Receive Task as a Kafka Task, setting up the request/response topics, payload, and the Kafka listener delegate. Like External Tasks, Kafka consumers expect a specific message contract, making templates the natural fit.
Project-specific patterns
Templates can encode any repeatable integration pattern — tSM Connector calls (REST/SOAP/TCP), multi-step configurations, or project-specific combinations.
Defining a Task Template
A Task Template definition is a JSON object with the following structure:
TaskTemplate properties
| Property | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique identifier (UUID) |
code | string | yes | Unique code used as tsm:templateCode on BPMN elements |
name | string | yes | Human-readable name shown in the template catalog |
description | string | no | Description shown in the catalog |
version | string | no | Semantic version (e.g. "1.0.0") |
appliesTo | string[] | no | BPMN element types this template can be applied to (e.g. ["userTask"]) |
formCode | string | no | Code of a tSM Form (JSON Schema-based) displayed in the properties panel when the template is active |
bindings | TemplateBinding[] | no | Array of bindings that map form fields to BPMN storage locations (see below) |
color | string | no | Color associated with the template (e.g. hex code "#FF5733", max 128 chars) |
icon | string | no | Icon identifier or URL for the template (max 2048 chars) |
showInPalette | boolean | no | Whether this template is displayed in the modeler palette (default false) |
dataTags | string[] | no | Tags / categories for filtering in the catalog |
config | object | no | Arbitrary configuration metadata |
localizationData | object | no | Localized strings for name, description, etc. |
validityFrom | string | no | ISO date — template is valid from this date |
validityTo | string | no | ISO date — template is valid until this date |
Configuration form
When formCode is set, the properties panel shows the referenced tSM Form below the standard fields. The form defines *
what the user edits* — form field values are then mapped to BPMN storage locations via bindings.
- The form is a standard tSM Form (JSON Schema-based, rendered by fluent-forms).
- Form fields may accept SpEL expressions in the form
#{...}, evaluated at runtime.
Bindings
Bindings are the core of the template system. Each binding maps a value to a BPMN storage location. Bindings work in both directions:
- Write (apply): when you fill in the form or select the template, values are written into the BPMN element.
- Read (extract): when you open the template form on an existing element, values are read back from the BPMN into the form.
Value sources
Every binding that carries a value uses one of three sources:
| Source | Syntax | Behavior |
|---|---|---|
set | "set": "value" | Static value, written once when the template is applied. Not updated on form changes. |
from | "from": "/path" | Dynamic value read from the template form. Updated every time the form changes. Path uses JSON Pointer-like syntax: "/field" or "/nested/field". |
setJson | "setJson": { ... } | JSON object serialized to a string. Available for camundaField and delegate field values (scriptCode, params, fields[].value). |
set and from are mutually exclusive on the same binding — use one or the other.
set valuesStatic set values may contain #{...} SpEL expressions. These are not evaluated at design time — they are written
literally into the BPMN XML and evaluated by the Camunda engine at runtime.
{
"type": "inputParameter",
"name": "ticketId",
"set": "#{ticket.id}"
}
Binding types
attribute — BPMN / Camunda attribute
Sets a direct attribute on the BPMN element or its Camunda extension.
{
"type": "attribute",
"name": "<attributeName>",
"set": "value"
},
{
"type": "attribute",
"name": "<attributeName>",
"from": "/formField"
}
| Property | Type | Required | Description |
|---|---|---|---|
type | "attribute" | yes | |
name | string | yes | Attribute name (see table below) |
set / from | string | yes (one of) | Value source |
Available attribute names:
| Attribute name | TaskDefinition field | Applies to | Notes |
|---|---|---|---|
name | name | all | Element display name |
description | description | all | HTML description |
documentation | documentation | all | HTML documentation |
color | color | all | Element color in designer |
icon | icon | all tasks | PrimeIcons class (e.g. pi pi-user) |
taskSpecification | taskSpecification | all tasks | |
asyncBefore | asyncBefore | all tasks | Boolean as string |
asyncAfter | asyncAfter | all tasks | Boolean as string |
exclusive | exclusive | all tasks | Boolean as string |
jobPriority | jobPriority | all tasks | |
failedJobRetryTimeCycle | failedJobRetryTimeCycle | all tasks | ISO 8601 duration (e.g. R3/PT10M) |
hideInInstanceGraph | hideInInstanceGraph | ServiceTask, SendTask | Boolean as string |
showInActiveTasks | showInActiveTasks | ReceiveTask | Boolean as string |
assignee | assignee | UserTask | SpEL expression |
candidateGroups | candidateUserGroups | UserTask | Comma-separated or array |
defaultUserGroup | defaultUserGroup | UserTask | |
skills | skills | UserTask | Comma-separated or array |
formKey | formKey | UserTask | |
taskPriority | taskPriority | UserTask | |
disableChangeUser | disableChangeUser | UserTask | Boolean |
disableChangeUserGroup | disableChangeUserGroup | UserTask | Boolean |
hideDefaultDoneStatus | hideDefaultDoneStatus | UserTask | Boolean |
defaultOpenStatus | defaultOpenStatus | UserTask | Status code |
defaultCloseStatus | defaultCloseStatus | UserTask | Status code |
expression | conditionExpression | ServiceTask, SendTask | Main expression |
resultVariable | resultVariable | ServiceTask, SendTask | |
implementationType | implementationType | ServiceTask, SendTask | expression / external |
implementationTopic | implementationTopic | ServiceTask, SendTask | External task topic |
decisionRef | decisionRef | BusinessRuleTask | DMN decision key |
mapDecisionResult | mapDecisionResult | BusinessRuleTask | singleEntry / singleResult / collectEntries / resultList |
calledElement | callActivityProcessDefinitionId | CallActivity | Process definition code or expression |
nameFromCalledSubprocess | nameFromCalledSubprocess | CallActivity | Boolean |
privilegeSkipTask | config.privilegeSkipTask | tasks | Privilege role code |
privilegeRestartTask | config.privilegeRestartTask | tasks | Privilege role code |
privilegeInterruptTask | config.privilegeInterruptTask | tasks | Privilege role code |
privilegeSetTimeJob | config.privilegeSetTimeJob | tasks | Privilege role code |
privilegeExecuteJobNow | config.privilegeExecuteJobNow | tasks | Privilege role code |
privilegeNotAssignUser | config.privilegeNotAssignUser | UserTask | Privilege role code |
privilegeNotAssignUserGroup | config.privilegeNotAssignUserGroup | UserTask | Privilege role code |
Example:
[
{
"type": "attribute",
"name": "assignee",
"set": "#{tsmExpressionService.currentUser()}"
},
{
"type": "attribute",
"name": "candidateGroups",
"from": "/userGroup"
},
{
"type": "attribute",
"name": "formKey",
"set": "FORM_TASK_DETAIL"
},
{
"type": "attribute",
"name": "color",
"set": "BLUE"
},
{
"type": "attribute",
"name": "asyncBefore",
"set": "true"
},
{
"type": "attribute",
"name": "privilegeSkipTask",
"set": "ROLE_ADMIN"
}
]
inputParameter — Camunda Input Parameter
Adds or updates a camunda:inputParameter in the camunda:inputOutput extension element.
[
{
"type": "inputParameter",
"name": "paramName",
"set": "#{expression}"
},
{
"type": "inputParameter",
"name": "paramName",
"from": "/formField"
}
]
| Property | Type | Required | Description |
|---|---|---|---|
type | "inputParameter" | yes | |
name | string | yes | Parameter name |
set / from | string | yes (one of) | Value source |
Example:
[
{
"type": "inputParameter",
"name": "ticketId",
"set": "#{ticket.id}"
},
{
"type": "inputParameter",
"name": "emailTo",
"from": "/recipientEmail"
}
]
outputParameter — Camunda Output Parameter
Adds or updates a camunda:outputParameter. Same structure as inputParameter.
[
{
"type": "outputParameter",
"name": "paramName",
"set": "#{expression}"
},
{
"type": "outputParameter",
"name": "paramName",
"set": "script"
}
]
| Property | Type | Required | Description |
|---|---|---|---|
type | "outputParameter" | yes | |
name | string | yes | Parameter name |
set / from | string | yes (one of) | Value source |
camundaField — Top-level Camunda Field
Adds or updates a top-level camunda:field in extension elements (outside of listeners).
[
{
"type": "camundaField",
"name": "fieldName",
"set": "value"
},
{
"type": "camundaField",
"name": "fieldName",
"setJson": { "key": "value" }
}
]
| Property | Type | Required | Description |
|---|---|---|---|
type | "camundaField" | yes | |
name | string | yes | Field name (e.g. scriptCode, paramsJson, retryCount) |
set / from / setJson | string / object | yes (one of) | Value source. from reads from the template form. setJson is serialized to a JSON string. |
setJson instead of escaped stringsInstead of writing escaped JSON in set:
{
"set": "{\"comment\":\"Hello world\"}",
"name": "paramsJson",
"type": "camundaField"
}
use setJson for readability:
{
"setJson": {
"comment": "Hello world"
},
"name": "paramsJson",
"type": "camundaField"
}
Both produce the same camunda:field with <camunda:string>{"comment":"Hello world"}</camunda:string> (the paramsJson field always uses the camunda:string child element; other fields use the stringValue attribute).
Example — Script Binding via camundaField:
[
{
"type": "camundaField",
"name": "scriptCode",
"set": "SCRIPT_EMAIL_SENDER"
},
{
"type": "camundaField",
"name": "paramsJson",
"setJson": {
"recipient": "#{ticket.chars.email}",
"template": "welcome-email"
}
}
]
executionListener — Execution Listener
Creates a camunda:executionListener on the BPMN element. Two variants exist: expression-based and **delegate-based
**.
Expression variant
Simple listener that evaluates a SpEL expression.
{
"type": "executionListener",
"event": "start",
"set": "#{logService.info('Task started', ticket.id)}",
"name": "log-on-start",
"documentation": "Logs task start"
}
| Property | Type | Required | Description |
|---|---|---|---|
type | "executionListener" | yes | |
event | "start" | "end" | yes | Listener event |
set / from | string | yes (one of) | SpEL expression |
name | string | no | Listener name (used for identification and upsert) |
documentation | string | no | Human-readable description |
Delegate variant
Listener that calls a delegate bean, optionally with scriptCode, params, and custom fields.
{
"type": "executionListener",
"event": "start",
"name": "notify-on-start",
"delegateExpression": "#{tsmScriptListenerExecutor}",
"scriptCode": {
"set": "notification.create"
},
"params": {
"setJson": {
"code": "task.assigned",
"payload": {
"message": "#{task.name} - #{execution.processBusinessKey}",
"recipientGroup": "#{execution.getVariable('candidateGroup')}"
}
}
},
"fields": [
{
"name": "targetSystem",
"value": {
"from": "/integrationTarget"
}
},
{
"name": "action",
"value": {
"set": "FINALIZE"
}
},
{
"name": "config",
"value": {
"setJson": {
"retry": 3,
"timeout": 5000
}
}
}
]
}
| Property | Type | Required | Description |
|---|---|---|---|
type | "executionListener" | yes | |
event | "start" | "end" | yes | Listener event |
name | string | no | Listener name |
documentation | string | no | Description |
delegateExpression | string | no | Delegate bean. If omitted and scriptCode is present, defaults to #{tsmScriptListenerExecutor} |
scriptCode | BindingFieldValue | no | Script code (set, from, or setJson) → written as camunda:field name="scriptCode" |
params | ParamBinding[] | BindingFieldValue | no | Parameters → written as camunda:field name="paramsJson". Can be an array of named params (new) or a single BindingFieldValue (legacy). See Params formats below. |
fields | DelegateFieldBinding[] | no | Additional named fields → written as camunda:field elements |
Each DelegateFieldBinding has:
| Property | Type | Description |
|---|---|---|
name | string | Field name |
value | BindingFieldValue | Value — { "set": "..." }, { "from": "/..." }, or { "setJson": { ... } } |
delegateExpression auto-defaultIf you specify scriptCode but omit delegateExpression, the system automatically uses #{tsmScriptListenerExecutor}.
This is the standard tSM script executor delegate.
Params formats
The params property supports two formats. Both produce a single camunda:field name="paramsJson" containing a JSON
string.
Array format (recommended) — define each parameter individually with its own value source:
{
"type": "executionListener",
"event": "start",
"name": "notify-on-start",
"scriptCode": { "set": "Notify.Script.Comment" },
"params": [
{ "name": "orderId", "value": { "from": "/orderId" } },
{ "name": "comment", "value": { "from": "/comment" } },
{ "name": "data", "setJson": { "version": "1" } }
]
}
Each ParamBinding has:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | yes | Key in the resulting paramsJson object. Always used as the key, regardless of from. |
value | BindingValue | one of these | { "set": "..." } or { "from": "/..." } — resolved at apply time |
setJson | any | one of these | Static JSON value — written directly (no quoting). Mutually exclusive with value. |
The key in the resulting JSON object is always the name property of the param binding.
The from path only determines which form field the value is read from — it does NOT affect the key name.
For example, { "name": "comment", "value": { "from": "/commentMessage" } } reads the value from form field commentMessage but writes it as key "comment" in paramsJson.
Example with nested paths and name ≠ from:
{
"params": [
{ "name": "orderId", "value": { "from": "/orderId" } },
{ "name": "comment", "value": { "from": "/commentMessage" } },
{ "name": "region", "value": { "from": "/address/region" } },
{ "name": "data", "setJson": { "version": "1" } }
]
}
Given form values { "orderId": 123, "commentMessage": "hello", "address": { "region": "EU" } }, the resulting
paramsJson is:
{"orderId": 123, "comment": "hello", "region": "EU", "data": {"version": "1"}}
Note how comment comes from form field commentMessage, and region comes from the nested path address.region.
The from path determines where to read from the form; name determines the key in paramsJson.
With the array format, each from binding is individually extractable — when you reopen the element, each param
value is read back into its corresponding form field.
Object format (legacy) — a single BindingFieldValue that produces the entire paramsJson at once:
{
"params": {
"setJson": {
"code": "task.assigned",
"payload": { "message": "#{task.name}" }
}
}
}
Both formats are fully supported. Use the array format when individual parameters should be editable via the
template form, and the object format when paramsJson is a static blob or comes from a single form field.
How params remapping works
When a delegate listener (execution/task/status) uses the array params format, each ParamBinding with
value.from acts as a renaming rule between form field names and paramsJson keys. Understanding this mechanism is
essential for designing templates correctly.
The rules are:
- No
value.fromparams → form values flow through unchanged. The form field name IS the paramsJson key. value.fromparams → thenamebecomes the paramsJson key, thefrompath determines which form field to read. This allows renaming form fields to different paramsJson keys.setJsonparams → always injected as static values, regardless of the form.- Unmapped form fields → any form field NOT referenced by a
value.frombinding passes through as-is (the form field name becomes the paramsJson key directly).
These rules apply both when writing (form → paramsJson) and when reading (paramsJson → form).
params with value.from are renaming rules, not filters. They don't remove unmapped fields — they only rename
the ones they explicitly target. Everything else passes through unchanged.
Example — no params at all (pure pass-through):
Template binding:
{
"type": "executionListener",
"event": "start",
"scriptCode": { "set": "Notify.Script.Comment" }
}
When params is omitted entirely, there are no ParamBinding entries at all. The script's form provides the fields
(e.g. {orderId: string, commentMessage: string}) and all form values flow directly into paramsJson without any
transformation — form field names ARE the paramsJson keys.
Given form values { "orderId": "ORD-42", "commentMessage": "hello" }, the resulting paramsJson is:
{"orderId": "ORD-42", "commentMessage": "hello"}
<camunda:field name="scriptCode" stringValue="Notify.Script.Comment" />
<camunda:field name="paramsJson">
<camunda:string>{"orderId":"ORD-42","commentMessage":"hello"}</camunda:string>
</camunda:field>
This is the simplest case — no remapping, no static injection, just the raw form output.
Example — only setJson params (pass-through + static injection):
Template binding:
{
"type": "executionListener",
"event": "start",
"scriptCode": { "set": "Notify.Script.Comment" },
"params": [
{ "name": "data", "setJson": { "version": "1" } }
]
}
The script Notify.Script.Comment has a form with fields {orderId: string, commentMessage: string}.
Since there are no value.from bindings, the form field names are used directly as paramsJson keys.
The setJson param data is always injected.
Given form values { "orderId": "ORD-42", "commentMessage": "hello" }, the resulting paramsJson is:
{"data": {"version": "1"}, "commentMessage": "hello", "orderId": "ORD-42"}
<camunda:field name="scriptCode" stringValue="Notify.Script.Comment" />
<camunda:field name="paramsJson">
<camunda:string>{"data":{"version":"1"},"commentMessage":"hello","orderId":"ORD-42"}</camunda:string>
</camunda:field>
Example — full remapping (all form fields have value.from):
Template binding:
{
"type": "executionListener",
"event": "start",
"scriptCode": { "set": "Notify.Script.Comment" },
"params": [
{ "name": "orderId", "value": { "from": "/orderId" } },
{ "name": "comment", "value": { "from": "/commentMessage" } },
{ "name": "data", "setJson": { "version": "1" } }
]
}
Here, form field commentMessage is renamed to paramsJson key comment. The orderId mapping keeps the same name
(identity mapping). Since all form fields are covered by value.from bindings, nothing passes through unmapped.
Given form values { "orderId": "ORD-42", "commentMessage": "hello" }, the resulting paramsJson is:
{"orderId": "ORD-42", "comment": "hello", "data": {"version": "1"}}
<camunda:field name="scriptCode" stringValue="Notify.Script.Comment" />
<camunda:field name="paramsJson">
<camunda:string>{"orderId":"ORD-42","comment":"hello","data":{"version":"1"}}</camunda:string>
</camunda:field>
Note: commentMessage from the form became comment in paramsJson — that's the renaming effect.
Example — partial remapping (some fields renamed, others pass through):
Template binding:
{
"type": "executionListener",
"event": "start",
"scriptCode": { "set": "Notify.Script.Comment" },
"params": [
{ "name": "comment", "value": { "from": "/commentMessage" } },
{ "name": "data", "setJson": { "version": "1" } }
]
}
Here, only commentMessage is remapped to comment. The form field orderId has no corresponding value.from
binding, so it passes through unchanged — the form field name orderId becomes the paramsJson key directly.
Given form values { "orderId": "ORD-42", "commentMessage": "hello" }, the resulting paramsJson is:
{"comment": "hello", "data": {"version": "1"}, "orderId": "ORD-42"}
<camunda:field name="scriptCode" stringValue="Notify.Script.Comment" />
<camunda:field name="paramsJson">
<camunda:string>{"comment":"hello","data":{"version":"1"},"orderId":"ORD-42"}</camunda:string>
</camunda:field>
Note: orderId kept its form field name because no value.from binding targets it.
When you reopen the element, the reverse mapping applies:
- Params with
value.from: the paramsJson key (name) is read, and the value is placed at the form path (from). E.g. paramsJson{"comment": "hello"}→ form value{"commentMessage": "hello"}. setJsonparams: actively blocked during read — thesetJsonkey is excluded from pass-through, so its value does not leak into the form. This is correct becausesetJsonvalues are static and not form-driven.- Unmapped keys: passed through as-is (paramsJson key = form field name).
This ensures the form always shows the correct values regardless of any renaming.
setJson param nameIf a form field has the same name as a setJson param's name, the form field value wins — it overwrites the
setJson static value in the resulting paramsJson. This is because setJson is injected first, and the pass-through
loop writes over it. Avoid naming form fields the same as setJson param names.
Use value.from remapping when:
- The script expects different parameter names than what your form fields are called.
- You want to decouple the form field naming from the script's parameter contract.
If the form field names already match the script's expected parameter names, you don't need value.from —
just use setJson for static values and let form values pass through directly.
taskListener — Task Listener
Same structure as executionListener, but for task lifecycle events. Only available on User Tasks.
| Property | Difference from executionListener |
|---|---|
type | "taskListener" |
event | "create" | "complete" | "assignment" | "delete" |
Example:
[
{
"type": "taskListener",
"event": "create",
"set": "#{notificationService.notifyAssignee(task)}",
"name": "notify-assignee"
},
{
"type": "taskListener",
"event": "complete",
"name": "custom-delegate-complete",
"delegateExpression": "#{myCustomDelegateBean}",
"fields": [
{
"name": "action",
"value": {
"set": "FINALIZE"
}
},
{
"name": "targetSystem",
"value": {
"from": "/integrationTarget"
}
}
]
}
]
statusListener — Status Listener (tSM-specific)
Listener that fires when a tSM task transitions to one of the specified statuses. Same expression/delegate variants as
execution listeners, but with statuses instead of event.
| Property | Type | Required | Description |
|---|---|---|---|
type | "statusListener" | yes | |
statuses | string[] | yes | Status codes that trigger the listener (e.g. ["IN_PROGRESS", "DONE"]) |
name | string | no | Listener name |
documentation | string | no | Description |
| + expression or delegate fields | Same as executionListener |
Example:
[
{
"type": "statusListener",
"statuses": [
"IN_PROGRESS"
],
"set": "#{notificationService.notifyStatusChange(ticket, 'IN_PROGRESS')}"
},
{
"type": "statusListener",
"statuses": [
"ESCALATED"
],
"name": "escalation-handler",
"delegateExpression": "#{escalationHandlerDelegate}",
"fields": [
{
"name": "escalationLevel",
"value": {
"set": "L2"
}
},
{
"name": "notifyGroup",
"value": {
"from": "/escalationGroup"
}
}
]
}
]
inputVariable — Call Activity Input Variable
Adds a camunda:in element for passing variables into a called subprocess. Only for Call Activity.
{
"type": "inputVariable",
"source": "ticket",
"target": "parentTicket",
"sourceType": "source",
"local": false
}
| Property | Type | Required | Description |
|---|---|---|---|
type | "inputVariable" | yes | |
source | string | yes | Source variable name or expression |
target | string | yes | Target variable name in the subprocess |
sourceType | "source" | "sourceExpression" | no | Default "source". Use "sourceExpression" when source is a SpEL expression. |
local | boolean | no | If true, the variable is local to the execution scope |
Example:
[
{
"type": "inputVariable",
"source": "ticket",
"target": "parentTicket",
"sourceType": "source"
},
{
"type": "inputVariable",
"source": "#{ticket.chars.customerName}",
"target": "customerName",
"sourceType": "sourceExpression"
},
{
"type": "inputVariable",
"source": "#{tsmExpressionService.currentUser()}",
"target": "initiatorUser",
"sourceType": "sourceExpression",
"local": true
}
]
outputVariable — Call Activity Output Variable
Adds a camunda:out element. Same structure as inputVariable, but maps variables from the subprocess back to the
parent.
[
{
"type": "outputVariable",
"source": "childResult",
"target": "subprocessResult",
"sourceType": "source"
},
{
"type": "outputVariable",
"source": "#{childTicket.id}",
"target": "createdChildTicketId",
"sourceType": "sourceExpression"
}
]
status — Status Definition (tSM-specific)
Defines a status and its allowed transitions in the tSM task state machine. Only for User Tasks.
{
"type": "status",
"statusCode": "IN_PROGRESS",
"transitions": [
{
"statusCode": "DONE"
},
{
"statusCode": "WAITING",
"name": "Wait for customer",
"condition": "#{ticket.chars.customerContact != null}"
},
{
"statusCode": "ESCALATED",
"name": "Escalate",
"hasAuthority": "ROLE_SUPERVISOR",
"confirmMessage": "Are you sure?"
}
]
}
| Property | Type | Required | Description |
|---|---|---|---|
type | "status" | yes | |
statusCode | string | yes | Status code |
transitions | StatusTransition[] | no | Allowed transitions from this status |
Each StatusTransition has:
| Property | Type | Description |
|---|---|---|
statusCode | string | Target status code (required) |
name | string | Transition button label |
description | string | Tooltip description |
formKey | string | Form to show during transition |
charsPropertyName | string | Property name for form data |
condition | string | SpEL condition — transition is only available if it evaluates to true |
hasAuthority | string | Required privilege role |
confirmMessage | string | Confirmation dialog message |
Example — full state machine:
[
{
"type": "status",
"statusCode": "NEW",
"transitions": [
{
"statusCode": "IN_PROGRESS"
},
{
"statusCode": "WAITING",
"name": "Wait for customer",
"condition": "#{ticket.chars.customerContact != null}"
}
]
},
{
"type": "status",
"statusCode": "IN_PROGRESS",
"transitions": [
{
"statusCode": "DONE"
},
{
"statusCode": "WAITING",
"formKey": "FORM_WAIT_REASON",
"charsPropertyName": "waitReason"
},
{
"statusCode": "ESCALATED",
"hasAuthority": "ROLE_SUPERVISOR",
"confirmMessage": "Are you sure you want to escalate?"
}
]
},
{
"type": "status",
"statusCode": "WAITING",
"transitions": [
{
"statusCode": "IN_PROGRESS"
}
]
},
{
"type": "status",
"statusCode": "ESCALATED",
"transitions": [
{
"statusCode": "IN_PROGRESS"
},
{
"statusCode": "DONE"
}
]
},
{
"type": "status",
"statusCode": "DONE"
}
]
kpi — KPI Configuration (tSM-specific)
Sets deadline calculation parameters on the task.
{
"type": "kpi",
"minTime": 60,
"maxTime": 1440,
"manualDueDate": false
}
| Property | Type | Required | Description |
|---|---|---|---|
type | "kpi" | yes | |
minTime | number | no | Minimum expected duration in minutes |
maxTime | number | no | Maximum expected duration (deadline) in minutes |
manualDueDate | boolean | no | If true, allows manual due date override |
Binding types summary
| Binding type | Value sources | Applies to | BPMN target |
|---|---|---|---|
attribute | set / from | all | BPMN attribute or tSM extension attribute |
inputParameter | set / from | all tasks | camunda:inputParameter |
outputParameter | set / from | all tasks | camunda:outputParameter |
camundaField | set / from / setJson | all tasks | camunda:field (top-level) |
executionListener | set / from + delegate fields | all tasks | camunda:executionListener |
taskListener | set / from + delegate fields | UserTask | camunda:taskListener |
statusListener | set / from + delegate fields | UserTask | tsm:statusListener |
inputVariable | static (source/target) | CallActivity | camunda:in |
outputVariable | static (source/target) | CallActivity | camunda:out |
status | static (statusCode/transitions) | UserTask | tsm:status |
kpi | static (minTime/maxTime/manualDueDate) | all tasks | tsm:kpi |
Complete template example
A User Task template with a form, attribute bindings, listeners, statuses, and KPI:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "task.user.backoffice-review",
"name": "Backoffice Review",
"version": "1.0.0",
"description": "User task for backoffice ticket review with SLA and notifications",
"appliesTo": [
"userTask"
],
"formCode": "forms.backoffice-review",
"dataTags": [
"backoffice",
"review"
],
"bindings": [
{
"type": "attribute",
"name": "assignee",
"set": "#{tsmExpressionService.currentUser()}"
},
{
"type": "attribute",
"name": "candidateGroups",
"from": "/userGroup"
},
{
"type": "attribute",
"name": "formKey",
"set": "FORM_TASK_DETAIL"
},
{
"type": "attribute",
"name": "color",
"set": "BLUE"
},
{
"type": "attribute",
"name": "asyncBefore",
"set": "true"
},
{
"type": "attribute",
"name": "privilegeSkipTask",
"set": "ROLE_ADMIN"
},
{
"type": "inputParameter",
"name": "ticketId",
"set": "#{ticket.id}"
},
{
"type": "inputParameter",
"name": "emailTo",
"from": "/recipientEmail"
},
{
"type": "outputParameter",
"name": "taskResult",
"set": "#{taskResultVariable}"
},
{
"type": "executionListener",
"event": "start",
"name": "notify-on-start",
"scriptCode": {
"set": "notification.create"
},
"params": {
"setJson": {
"code": "task.assigned",
"payload": {
"message": "#{task.name} - #{execution.processBusinessKey}",
"recipientGroup": "#{execution.getVariable('candidateGroup')}"
}
}
}
},
{
"type": "taskListener",
"event": "complete",
"name": "custom-delegate-complete",
"delegateExpression": "#{myCustomDelegateBean}",
"fields": [
{
"name": "action",
"value": {
"set": "FINALIZE"
}
},
{
"name": "targetSystem",
"value": {
"from": "/integrationTarget"
}
}
]
},
{
"type": "camundaField",
"name": "notificationType",
"set": "EMAIL"
},
{
"type": "camundaField",
"name": "paramsJson",
"setJson": {
"channel": "email",
"template": "backoffice-review-notification"
}
},
{
"type": "status",
"statusCode": "NEW",
"transitions": [
{
"statusCode": "IN_PROGRESS"
}
]
},
{
"type": "status",
"statusCode": "IN_PROGRESS",
"transitions": [
{
"statusCode": "DONE"
},
{
"statusCode": "ESCALATED",
"hasAuthority": "ROLE_SUPERVISOR",
"confirmMessage": "Escalate?"
}
]
},
{
"type": "status",
"statusCode": "ESCALATED",
"transitions": [
{
"statusCode": "IN_PROGRESS"
},
{
"statusCode": "DONE"
}
]
},
{
"type": "status",
"statusCode": "DONE"
},
{
"type": "statusListener",
"statuses": [
"ESCALATED"
],
"name": "escalation-handler",
"delegateExpression": "#{escalationHandlerDelegate}",
"fields": [
{
"name": "escalationLevel",
"value": {
"set": "L2"
}
},
{
"name": "notifyGroup",
"value": {
"from": "/escalationGroup"
}
}
]
},
{
"type": "kpi",
"minTime": 60,
"maxTime": 1440,
"manualDueDate": false
}
]
}
Notes:
- The same bindings are used for reading (extract) and writing (apply).
setvalues may contain#{...}SpEL expressions — these are stored literally and evaluated at runtime.frompaths use JSON Pointer-like syntax (e.g."/userGroup","/nested/field").tsm:templateCodeis written automatically when a template is applied — you do not need a binding for it.inputVariable/outputVariable/status/kpiare static bindings — they do not useset/fromand are applied once when the template is selected.
Examples
The following examples show different template types and the BPMN XML they produce. In each case the user fills in a simple form — the template writes all the technical configuration.
AI Agent Call (Script Binding)
A Service Task that calls an AI agent via Script Binding. The user provides the agent ID, a prompt, and the variable to store the result.
Bindings:
[
{
"type": "camundaField",
"name": "scriptCode",
"set": "ai.agent.call"
},
{
"type": "camundaField",
"name": "paramsJson",
"setJson": {
"agentId": "agent-support-l1",
"prompt": "#{prompt}",
"resultVariable": "agentResponse"
}
}
]
Resulting BPMN XML:
<bpmn:serviceTask id="Activity_ai_agent" name="AI Agent Call"
camunda:delegateExpression="#{tsmScriptDelegateExecutor}"
tsm:templateCode="task.script.ai-agent/1.0.0">
<bpmn:extensionElements>
<camunda:field name="scriptCode" stringValue="ai.agent.call"/>
<camunda:field name="paramsJson">
<camunda:string>{"agentId":"agent-support-l1","prompt":"#{prompt}","resultVariable":"agentResponse"}</camunda:string>
</camunda:field>
</bpmn:extensionElements>
</bpmn:serviceTask>
Notify on Start (Execution Listener)
An execution listener template that sends a notification when a task starts. It can be combined with any User Task.
Bindings:
[
{
"type": "attribute",
"name": "candidateGroups",
"set": "backoffice"
},
{
"type": "executionListener",
"event": "start",
"name": "notify-on-create",
"scriptCode": {
"set": "notification.create"
},
"params": {
"setJson": {
"code": "task.assigned",
"payload": {
"message": "#{task.name}",
"recipientGroup": "#{execution.getVariable('candidateGroup')}"
}
}
}
}
]
Resulting BPMN XML:
<bpmn:userTask id="Activity_review" name="Review Request"
camunda:candidateGroups="backoffice"
tsm:templateCode="listener.notify-on-create/1.0.0">
<bpmn:extensionElements>
<camunda:executionListener event="start"
delegateExpression="#{tsmScriptListenerExecutor}"
tsm:name="notify-on-create">
<camunda:field name="scriptCode" stringValue="notification.create"/>
<camunda:field name="paramsJson">
<camunda:string>{"code":"task.assigned","payload":{"message":"#{task.name}","recipientGroup":"#{execution.getVariable('candidateGroup')}"}}</camunda:string>
</camunda:field>
</camunda:executionListener>
</bpmn:extensionElements>
</bpmn:userTask>
Comment Script with Named Params (Array params)
A Service Task that calls a script with individually-mapped form parameters using the array params format.
Bindings:
[
{
"type": "executionListener",
"event": "start",
"name": "add-comment",
"scriptCode": {
"set": "Notify.Script.Comment"
},
"params": [
{
"name": "orderId",
"value": { "from": "/orderId" }
},
{
"name": "comment",
"value": { "from": "/comment" }
},
{
"name": "metadata",
"setJson": { "source": "template", "version": 1 }
}
]
}
]
Resulting BPMN XML (assuming form values orderId = "ORD-42", comment = "Please review"):
<bpmn:serviceTask id="Activity_comment" name="Add Comment"
tsm:templateCode="task.script.comment/1.0.0">
<bpmn:extensionElements>
<camunda:executionListener event="start"
delegateExpression="#{tsmScriptListenerExecutor}"
tsm:name="add-comment">
<camunda:field name="scriptCode" stringValue="Notify.Script.Comment"/>
<camunda:field name="paramsJson">
<camunda:string>{"orderId":"ORD-42","comment":"Please review","metadata":{"source":"template","version":1}}</camunda:string>
</camunda:field>
</camunda:executionListener>
</bpmn:extensionElements>
</bpmn:serviceTask>
Each from param is individually extracted when you reopen the element, so changing one form field only updates that
parameter in the paramsJson — the rest are preserved.
JSON ↔ XML Binding Examples
The following examples show the bindings JSON and the resulting BPMN XML it produces.
Use these as a reference when creating new templates.
Example 1: UserTask — attributes, statuses, statusListener, input/output
A comprehensive UserTask showing attribute bindings, status definitions with full transition properties, a statusListener, and input/output parameters.
Bindings JSON:
[
{"type": "attribute", "name": "assignee", "set": "#{tsmExpressionService.currentUser()}"},
{"type": "attribute", "name": "candidateGroups", "set": "SUPPORT_L1"},
{"type": "attribute", "name": "defaultUserGroup", "set": "SUPPORT_L1"},
{"type": "attribute", "name": "formKey", "set": "Ticket.Detail"},
{"type": "attribute", "name": "defaultOpenStatus", "set": "NEW"},
{
"type": "status",
"statusCode": "NEW",
"transitions": [
{
"statusCode": "IN_PROGRESS",
"name": "Start processing",
"formKey": "Ticket.AcceptForm",
"condition": "#{ticket.chars.priority != null}",
"hasAuthority": "ROLE_SUPPORT",
"confirmMessage": "Accept this ticket and start processing?"
}
]
},
{
"type": "status",
"statusCode": "IN_PROGRESS",
"transitions": [{"statusCode": "DONE"}]
},
{
"type": "statusListener",
"statuses": ["NEW"],
"name": "log-new-ticket",
"set": "#{auditService.logStatusChange(ticket, 'NEW')}"
},
{"type": "inputParameter", "name": "ticketId", "set": "#{ticket.id}"},
{"type": "outputParameter", "name": "resolution", "set": "#{taskResult}"}
]
Resulting BPMN XML:
<bpmn:userTask id="Activity_review" name="Ticket Review"
camunda:assignee="#{tsmExpressionService.currentUser()}"
camunda:candidateUserGroups="SUPPORT_L1"
camunda:formKey="Ticket.Detail"
tsm:templateCode="task.ticket-review"
tsm:defaultUserGroup="SUPPORT_L1"
tsm:defaultOpenStatus="NEW"
tsm:color="WHITE">
<bpmn:extensionElements>
<tsm:status status="NEW">
<tsm:transition status="IN_PROGRESS"
name="Start processing"
formKey="Ticket.AcceptForm"
condition="#{ticket.chars.priority != null}"
hasAuthority="ROLE_SUPPORT"
confirmMessage="Accept this ticket and start processing?" />
</tsm:status>
<tsm:status status="IN_PROGRESS">
<tsm:transition status="DONE" />
</tsm:status>
<tsm:statusListener expression="#{auditService.logStatusChange(ticket, 'NEW')}"
statuses="NEW"
name="log-new-ticket" />
<camunda:inputOutput>
<camunda:inputParameter name="ticketId">#{ticket.id}</camunda:inputParameter>
<camunda:outputParameter name="resolution">#{taskResult}</camunda:outputParameter>
</camunda:inputOutput>
</bpmn:extensionElements>
</bpmn:userTask>
Example 2: ServiceTask — executionListener with scriptCode + array params
A ServiceTask that sends a notification using an execution listener with tsmScriptListenerExecutor.
Uses the array params format where individual parameters are mapped from form fields (from) or
set as static JSON (setJson).
Bindings JSON:
[
{
"type": "executionListener",
"event": "start",
"name": "send-notification",
"scriptCode": {"set": "Notification.Send"},
"params": [
{"name": "recipientId", "value": {"from": "/recipientId"}},
{"name": "message", "value": {"from": "/message"}},
{"name": "config", "setJson": {"channel": "email", "priority": "high"}}
]
}
]
Notice the binding does not specify delegateExpression. Because scriptCode is present,
the system automatically uses #{tsmScriptListenerExecutor}.
Resulting BPMN XML (assuming form values recipientId = "user-123", message = "Order shipped"):
<bpmn:serviceTask id="Activity_notify" name="Send Notification"
tsm:templateCode="task.notification"
tsm:color="WHITE"
tsm:icon="tsm-icon-notification">
<bpmn:extensionElements>
<camunda:executionListener delegateExpression="#{tsmScriptListenerExecutor}"
event="start"
tsm:name="send-notification">
<camunda:field name="scriptCode" stringValue="Notification.Send" />
<camunda:field name="paramsJson">
<camunda:string>{"recipientId":"user-123","message":"Order shipped","config":{"channel":"email","priority":"high"}}</camunda:string>
</camunda:field>
</camunda:executionListener>
</bpmn:extensionElements>
</bpmn:serviceTask>
The three params are merged into a single paramsJson field:
{"recipientId": <from form>, "message": <from form>, "config": {"channel":"email","priority":"high"}}
(the key for each param is the name property — e.g. name: "message" → key "message" in paramsJson).
Each from param is individually extractable — changing one form field updates only that key.
Example 3: ServiceTask — camundaField bindings (scriptCode + paramsJson)
A ServiceTask using tsmScriptDelegateExecutor with top-level camunda:field bindings.
This is the simpler approach when all parameters are static.
Bindings JSON:
[
{"type": "camundaField", "name": "scriptCode", "set": "Order.CreateInvoice"},
{
"type": "camundaField",
"name": "paramsJson",
"setJson": {
"orderId": "#{order.id}",
"invoiceType": "standard"
}
}
]
Resulting BPMN XML:
<bpmn:serviceTask id="Activity_invoice" name="Create Invoice"
camunda:delegateExpression="#{tsmScriptDelegateExecutor}"
tsm:templateCode="task.create-invoice"
tsm:color="WHITE"
tsm:icon="tsm-icon-invoice">
<bpmn:extensionElements>
<camunda:field name="scriptCode" stringValue="Order.CreateInvoice" />
<camunda:field name="paramsJson">
<camunda:string>{"orderId":"#{order.id}","invoiceType":"standard"}</camunda:string>
</camunda:field>
</bpmn:extensionElements>
</bpmn:serviceTask>
Use camundaField for top-level fields on the task element itself (e.g. tsmScriptDelegateExecutor).
Use executionListener/taskListener delegate when the fields belong inside a listener element.
Example 4: ServiceTask — inputParameter / outputParameter
A ServiceTask demonstrating inputParameter and outputParameter bindings with both set (static SpEL)
and from (form field) value sources.
Bindings JSON:
[
{"type": "inputParameter", "name": "ticketId", "set": "#{ticket.id}"},
{"type": "inputParameter", "name": "targetSystem", "from": "/targetSystem"},
{"type": "outputParameter", "name": "responseCode", "set": "#{responseCode}"},
{"type": "outputParameter", "name": "resultVariable", "from": "/resultVariable"}
]
Resulting BPMN XML (assuming form values targetSystem = "CRM", resultVariable = "#{crmResponse}"):
<bpmn:serviceTask id="Activity_process" name="Process Request"
tsm:templateCode="task.process-request"
tsm:color="WHITE">
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="ticketId">#{ticket.id}</camunda:inputParameter>
<camunda:inputParameter name="targetSystem">CRM</camunda:inputParameter>
<camunda:outputParameter name="responseCode">#{responseCode}</camunda:outputParameter>
<camunda:outputParameter name="resultVariable">#{crmResponse}</camunda:outputParameter>
</camunda:inputOutput>
</bpmn:extensionElements>
</bpmn:serviceTask>
"set": "#{ticket.id}" writes the literal string #{ticket.id} — it is evaluated by Camunda at runtime.
"from": "/targetSystem" reads the current value from the template form field /targetSystem and writes
it literally into the XML. The value CRM in the XML above came from the form at apply time.
Example 5: UserTask — status-only bindings
A minimal UserTask with only status bindings — a simple state machine (OPEN → IN_PROGRESS → RESOLVED). The first transition demonstrates all optional transition properties.
Bindings JSON:
[
{
"type": "status",
"statusCode": "OPEN",
"transitions": [
{
"statusCode": "IN_PROGRESS",
"name": "Start work",
"formKey": "Ticket.AssignForm",
"condition": "#{ticket.chars.assignee != null}",
"hasAuthority": "ROLE_OPERATOR",
"confirmMessage": "Start working on this ticket?"
}
]
},
{
"type": "status",
"statusCode": "IN_PROGRESS",
"transitions": [{"statusCode": "RESOLVED"}]
}
]
Resulting BPMN XML:
<bpmn:userTask id="Activity_support" name="Support Ticket"
tsm:templateCode="task.support-ticket"
tsm:color="WHITE">
<bpmn:extensionElements>
<tsm:status status="OPEN">
<tsm:transition status="IN_PROGRESS"
name="Start work"
formKey="Ticket.AssignForm"
condition="#{ticket.chars.assignee != null}"
hasAuthority="ROLE_OPERATOR"
confirmMessage="Start working on this ticket?" />
</tsm:status>
<tsm:status status="IN_PROGRESS">
<tsm:transition status="RESOLVED" />
</tsm:status>
</bpmn:extensionElements>
</bpmn:userTask>
Backoffice Review (External Task)
An External Task configured for backoffice review. The template sets the topic and maps the required input variables.
Bindings:
[
{
"type": "attribute",
"name": "implementationType",
"set": "external"
},
{
"type": "attribute",
"name": "implementationTopic",
"set": "backoffice.review"
},
{
"type": "inputParameter",
"name": "ticketId",
"set": "#{ticketId}"
},
{
"type": "inputParameter",
"name": "priority",
"set": "#{priority}"
},
{
"type": "inputParameter",
"name": "assigneeGroup",
"set": "#{assigneeGroup}"
}
]
Resulting BPMN XML:
<bpmn:serviceTask id="Activity_backoffice" name="Backoffice Review"
camunda:type="external"
camunda:topic="backoffice.review"
tsm:templateCode="task.external.backoffice-review/1.0.0">
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="ticketId">#{ticketId}</camunda:inputParameter>
<camunda:inputParameter name="priority">#{priority}</camunda:inputParameter>
<camunda:inputParameter name="assigneeGroup">#{assigneeGroup}</camunda:inputParameter>
</camunda:inputOutput>
</bpmn:extensionElements>
</bpmn:serviceTask>
Kafka Integration (Kafka Task)
A Receive Task configured as a Kafka Task. The template sets the topics, payload, and the Kafka listener delegate.
Bindings:
[
{
"type": "executionListener",
"event": "start",
"delegateExpression": "#{tsmKafkaTaskExecutor}",
"fields": [
{
"name": "requestTopic",
"value": {
"set": "eligibility-requests"
}
},
{
"name": "responseTopic",
"value": {
"set": "tsm-process-response"
}
},
{
"name": "key",
"value": {
"set": "#{execution.processBusinessKey}"
}
},
{
"name": "payload",
"value": {
"set": "#{execution.getVariable('eligibilityPayload')}"
}
}
]
}
]
Resulting BPMN XML:
<bpmn:receiveTask id="Activity_kafka" name="Request Eligibility Check"
tsm:templateCode="task.kafka.eligibility-check/1.0.0">
<bpmn:extensionElements>
<camunda:executionListener event="start"
delegateExpression="#{tsmKafkaTaskExecutor}">
<camunda:field name="requestTopic" stringValue="eligibility-requests"/>
<camunda:field name="responseTopic" stringValue="tsm-process-response"/>
<camunda:field name="key" stringValue="#{execution.processBusinessKey}"/>
<camunda:field name="payload" stringValue="#{execution.getVariable('eligibilityPayload')}"/>
</camunda:executionListener>
</bpmn:extensionElements>
</bpmn:receiveTask>
Summary
Task Templates provide a consistent, guided way to configure BPMN tasks:
- Templates are applied to existing BPMN elements and extend the properties panel with a configuration form.
- Favorite templates can be accessed directly from the palette.
- Templates use two-way bindings — configured values can be read back and edited at any time.
- The element is bound to the template via
tsm:templateCodefor traceability and round-trip editing. - Template updates do not propagate automatically to existing processes — re-application is an explicit action.
- The resulting BPMN remains standard, editable, and deployable.