Skip to content

Supported BPMN elements

Orvanta’s BPMN engine executes a defined subset of BPMN 2.0 — not the full specification. This page lists exactly what runs. An element outside this list fails deploy-time validation with an explicit error naming the element; the engine never silently drops or mis-executes a construct it doesn’t recognise.

This table is derived from the engine, not hand-maintained. A test in orvanta-bpmn renders every ElementKind and recognised tag, plus its support status, to a committed element-support.json and asserts the two stay byte-identical — adding or changing a construct’s support fails that test until the record (and, in turn, this page) is updated. If you find a mismatch, the code and element-support.json are right and this page is wrong.

BPMN constructBehaviour
startEvent (no event definition)Starts the process, advances to its single outgoing flow.
startEvent + timerEventDefinition (process level)Starts instances of the deployed definition on its own. A cron timeCycle becomes a workspace schedule owned by the flow (listed on the schedules page, pausable there but not editable there); a timeDate or timeDuration arms a single firing. Each fire is a separate instance of the latest deployed version with no start variables, running with the permissions of the user who last deployed it. Missed occurrences catch up once and are then skipped, and recurring instances may overlap. Redeploying replaces the arming, deleting the flow or removing the timer removes it, archiving disarms it. Refused at deploy: an ISO-8601 repeating interval (R3/PT10M), flowable:endDate, and a timer start inside an embedded sub-process. flowable:timeZone is not read, so a cron is evaluated in UTC.
startEvent + messageEventDefinition (process level)Starts a new top-level instance when its message is sent to the workspace, with the request’s variables as start variables. It never correlates to a running instance. See Message start events. Refused at deploy: a message start inside an embedded sub-process, one with no messageRef, a messageRef that names no top-level <message>, and a message whose name is missing, has leading or trailing whitespace, or is a ${...}/#{...} expression.
endEvent (no event definition)Consumes the token.
endEvent + terminateEventDefinitionEnds the scope it sits in: every other token in that scope (and nested scopes) is discarded and their in-flight child jobs are cancelled. At process level the instance completes.
endEvent + errorEventDefinitionRaises an error carrying its resolved error code. Caught by the nearest enclosing interrupting boundary error event; if uncaught, parks the process as an incident.
intermediateCatchEvent + timerEventDefinitionParks the token until the timer fires.
intermediateCatchEvent + messageEventDefinitionParks the token waiting for an external event.
intermediateCatchEvent + signalEventDefinitionParks the token on a workspace-wide signal subscription. A signal is a broadcast: one delivery releases every token subscribed to that name, in this instance and every other instance in the workspace.
intermediateThrowEvent + signalEventDefinitionBroadcasts the signal and advances. flowable:scope is honoured: global (the default) reaches every subscribed instance in the workspace, processInstance only subscribers inside the throwing instance.
intermediateThrowEvent (no event definition)Pass-through — a bare throw event has nothing to throw, so it just advances.
boundaryEvent + errorEventDefinition, interrupting (cancelActivity true or absent)Receives a caught error and routes the token into the handler branch.
boundaryEvent + timerEventDefinition, messageEventDefinition or signalEventDefinition, interruptingArmed when the host activity is entered. When it fires it tears the host down (cancelling an in-flight child job) and routes a token out of the boundary. Timers honour timeDuration and timeDate, not timeCycle.
boundaryEvent with cancelActivity="false" (non-interrupting) and a timer, message or signal definitionForks a token out of the boundary and leaves the host activity running. An error boundary with cancelActivity="false" is accepted but executes as an interrupting one, because BPMN defines error boundaries as always interrupting.
intermediateThrowEvent + escalationEventDefinitionNotifies the nearest escalation catcher and, unless that catcher is interrupting, carries on along its outgoing flow. An escalation nobody catches is dropped, and the thrower continues. Needs an escalationRef. See Escalation.
endEvent + escalationEventDefinitionNotifies the nearest escalation catcher, then ends like a none end event. Unlike an error end event it is never a failure, and an uncaught escalation is not an incident. Needs an escalationRef.
boundaryEvent + escalationEventDefinition, interruptingCatches an escalation thrown inside the sub-process it is attached to, or by the instance a call activity started. Tears the host down (cancelling a called instance) and routes a token out of the boundary, carrying the code as results.<boundaryId>.
boundaryEvent + escalationEventDefinition, cancelActivity="false"Forks a token out of the boundary and leaves the host running, as well as the thrower. Unlike an error boundary it really is non-interrupting. Fires once per throw.
intermediateCatchEvent + conditionalEventDefinitionParks the token until a condition over the instance’s variables becomes true, then advances it. See Conditional events.
boundaryEvent + conditionalEventDefinition, interruptingArmed while the host runs. The first time the condition is true it cancels the host and routes a token out of the boundary, recording the firing as results.<boundaryId>.
boundaryEvent + conditionalEventDefinition, cancelActivity="false"Forks a token out of the boundary each time the condition becomes true (false to true) and leaves the host running.
dataObject (typed)Not a flow node. A typed declaration that initialises the process variable named by name when its process or sub-process scope starts. See Data objects.
scriptTaskDispatches the bound script as a child job.
serviceTaskDispatches the bound script as a child job (same binding mechanism as scriptTask).
serviceTask + flowable:type="external-worker" (or the legacy "external" spelling)A wait state, not something the engine dispatches itself: it parks the token and exposes a topic + payload through the status projection for an external worker to fetch, then complete, fail, or throw a BPMN error against, over a REST round-trip.
serviceTask + flowable:type="http" (Flowable’s HTTP task)Makes one outbound HTTP request, as a child job on a worker, and hands the response to the process — see HTTP tasks below. Authored in the editor as the HTTP Task shape.
serviceTask + flowable:type="dmn" (Flowable’s decision task)Compiles to the same kind as businessRuleTask and evaluates the same way — see below. The decision reference comes from a <flowable:field> (decisionTableReferenceKey or decisionServiceReference) instead of orvanta:decisionRef.
userTaskParks the token waiting for an external completion signal.
receiveTaskA wait state: the token parks until it is released by element id (POST /instances/{id}/triggers/{elementId}) or, when the element declares a messageRef, by that message name. It can never be the construct that starts an instance.
businessRuleTaskEvaluates its linked DMN decision table synchronously, inline, on the worker. A reference to a <decisionService> rather than a plain decision deploys with a warning, evaluating only the single decision it names — see Decision Services and DRDs.
task (untyped)Pure pass-through — no data transform, no child job, no wait. Standard BPMN semantics for an abstract task.
manualTaskPure pass-through — identical runtime behaviour to an untyped task. BPMN 2.0 gives it no engine semantics of its own (it models work done outside the system: approvals, phone calls, physical handling), so there’s nothing for the engine to do but advance past it. Kept as its own catalog entry rather than folded into task so the editor round-trips the author’s original tag instead of silently rewriting it.
subProcess (embedded, non-transaction, non-event)Activates a child scope. The parent token doesn’t get consumed — it parks as the anchor until every token and join inside the scope drains.
subProcess with triggeredByEvent="true" (event sub-process)Not entered over a sequence flow: it runs when its start event’s trigger (error, message, timer, escalation or conditional, interrupting or non-interrupting) fires inside the enclosing scope. A signalEventDefinition start is refused, and a timer start with a timeCycle is refused at deploy.
callActivityStarts a real child process instance of another deployed definition and parks until it finishes. A literal calledElement must name a definition you can see when you deploy. A ${...} expression is evaluated when the token arrives. Each time the call is made, literal or expression, it only starts a definition that the deployer of the calling process can see at that moment (for a preview run, the person running it). Any other target raises the same incident as a name that matches nothing, so the run learns nothing about definitions it cannot see. References that name a deployment rather than a definition (calledElementType="id") are refused.
exclusiveGatewayEvaluates each outgoing condition and routes down exactly one flow.
parallelGatewayStructured AND split/join.
inclusiveGatewayStructured OR split/join — activates every branch whose condition is truthy; the paired join fires on exactly that subset.
eventBasedGatewayA race between catch events. The token waits at the gateway, and the first of the timer, message or signal catch events it leads to to occur is taken; the others are abandoned (a timer stops counting, a signal subscription goes away, and a message left unconsumed stays in the instance). Each target must be an intermediateCatchEvent with exactly one incoming flow, and no two targets may wait for the same message or signal name. instantiate="true" and eventGatewayType="Parallel" are refused. See Event-based gateway.

multiInstanceLoopCharacteristics executes on tasks and embedded sub-processes, sequential and parallel. loopCardinality (an integer or a ${variable} lookup), flowable:collection / loopDataInputRef, flowable:elementVariable / inputDataItem and a JavaScript completionCondition are honoured; results aggregate into results.<activityId> ordered by loopCounter. A boundary event of any kind attached to a multi-instance activity is refused at deploy time.

Many of these constructs are only partly implemented — for example message correlation is by message name within an instance you already name, with no correlation-key matching, and userTask assignment and form fields are parsed but not applied. The authoritative per-construct record is element-support.json in the engine, as described above.

flowable:async, flowable:asyncLeave/asyncAfter, and flowable:exclusive apply across every element kind above — not a property of any one of them — and are covered separately in Execution semantics.

An HTTP task is Flowable’s <serviceTask flowable:type="http">, configured by <flowable:field> children, so a process written for Flowable imports and runs unchanged. In the editor it is the HTTP Task shape in the Tasks group, with a Request tab. A Service Task’s Implementation switch (Behaviour tab) changes between an Orvanta script and an HTTP request.

The request is made by a child job on a worker, exactly like a script task, so it has the same timeout, cancellation and automatic-retry behaviour, and the same network reach as a script. An HTTP task opens no door a script task does not already have.

FieldMeaning
requestMethodGET (default), POST, PUT, PATCH or DELETE.
requestUrlRequired — a task with no URL is refused at deploy.
requestHeadersOne Name: value per line.
requestBodySent with every method except GET.
requestBodyEncodingThe character set the body is sent in — see Body encoding. Unset sends UTF-8.
requestSecureHeadersHeaders whose value is a reference to a variable or secret — see Secure headers.
requestTimeoutMilliseconds. Defaults to 60 000.
disallowRedirectstrue returns a 3xx answer as it is instead of following it.
responseVariableNameThe process variable that receives the response. The response is also always available as results.<elementId>.
saveResponseVariableAsJsontrue parses the body; otherwise it stays text.
saveResponseParameterstrue keeps { statusCode, statusReason, headers, body } instead of the body alone.
handleStatusCodesStatus codes (404, ranges like 5XX) that raise a business error whose errorCode is the status, which an error boundary event can catch. Wins over failStatusCodes.
failStatusCodesStatus codes that fail the task like any technical failure — it retries or raises an incident.
ignoreExceptiontrue continues with { errorMessage } as the response when the request cannot be made at all (no connection, timeout).

Every value may be a ${...} expression, evaluated against the process variables when the token arrives — except responseVariableName, whose name must be known before the step runs, and requestSecureHeaders, which must be written literally (see Secure headers). A value that starts with $ is sent as that text: only a secure header ever reads a variable. requestHeaders may not set Host, Content-Length, Transfer-Encoding, Connection, Upgrade, TE, Keep-Alive, Trailer or any Proxy-* header, every header name must be a valid HTTP token, and a value may not contain a line break or NUL character once evaluated. A header that breaks one of these rules makes the task raise an incident naming the header (never its value), and no request is sent. When the task also sends secure headers, the rule is stricter: the evaluated requestHeaders may not have more lines than the field as deployed, so no value may contain a line break, whatever header it would add. A requestHeaders computed by a script or variable instead of written as text cannot be combined with secure headers. These rules stop a variable such as X-Tenant: ${tenant} from adding a header of its own.

Use a secure header for anything that must not sit in the diagram, such as an Authorization token. In the editor, each row is a header name plus a variable or secret that you pick from the workspace’s list; there is no value box, so a secret is never typed into the diagram. In the file, the field holds one Name: $var:<path> per line:

Authorization: $var:f/billing/api_token
X-Api-Key: $var:u/alice/partner_key

A path is u/<user>/<name> or f/<folder>/<name> (a folder path may nest further); each segment uses letters, digits, _ and - only.

  • Only the path is stored. The diagram, the request job’s arguments, logs, the task’s result and incident text carry the path, never the value. The value is read when the request job starts, and goes to the request as a header. If both a plain and a secure header share a name, the secure one wins.
  • The value is read with the rights of whoever deployed the process, not whoever starts it. The deployer is the person who chose the reference and the URL, so the request job that sends a secure header runs as the deployer of the process version the instance is on; the rest of the instance, and an HTTP task without secure headers, still run as the starter. Anyone allowed to start the process can therefore use a credential they cannot read themselves, without ever seeing it, and nobody can make a process send their secret by starting it. The starter still sees the run and its result (with any echoed value redacted).
  • Fixed at deploy, not at run time. Secure headers must be literal Name: $var:<path> lines; an expression (or any value computed when the process runs) deploys with the warning HttpSecureHeaderNotAReference and makes the task raise an incident without sending anything. A task with secure headers must also spell out its URL’s scheme and host literally. https://api.example.com/v1/${orderId} is fine, while ${baseUrl}/v1 and https://api.example.com${path} are refused at deploy. The data a process runs with therefore cannot choose which secret is sent or which origin (scheme, host and port) it goes to.
  • What the starter can still steer. Only the origin is pinned. Any part you write as an expression, such as the path after the host, the method or the body, still comes from the data the process runs with, and so from whoever starts it. The deployer’s credential can therefore be sent to any endpoint on that origin: dot segments in a templated path (../, or an encoded %2e%2e) are normalised away before the request is made, so /v1/${orderId} can reach /admin/... on the same host. The starter also reads the response. An endpoint that echoes the credential back in another form (base64, URL-encoded, hashed or partial) gets past redaction, which only catches exact copies. Write the path, method and body literally when the credential reaches more than the one operation the process needs, and give the process a credential scoped to that operation.
  • Checked when you deploy. If you cannot read a secure header’s variable or secret — or it does not exist yet — the deploy succeeds with the warning HttpSecureHeaderNotReadable, naming the task and the path. It is a warning rather than a refusal because the variable may simply be created later (for example by the same git sync). Until you can read it, every run of the task fails.
  • If the deployer loses access later. The check is repeated on every run, with the deployer’s rights at that moment. If the deployer can no longer read the variable, the task fails and names the path; if they have left the workspace or been disabled, the task raises an incident naming them. Either way nothing is sent and nobody else’s rights are used instead. To fix it, redeploy the process as someone who can read the variable: new instances use the new version’s deployer, while instances already running stay on the version (and the deployer) they started with until they are migrated to the new one.
  • The deployer can see the request jobs of runs other people start. The request job that sends a secure header runs as the deployer, so the deployer can read it, and under the job-visibility rules may be able to cancel it, for every instance of their process, including ones other people start. That job shows the URL, the headers (secure ones only as their $var: path), a body that may be built from the starter’s input, and the response. It never shows the resolved secret. If starters pass data the deployer should not see, do not put that data into an HTTP task with secure headers.
  • Anyone who can see the process can start it. Starting a process only requires being able to see it. Anyone who can see a process with secure headers can therefore start it with inputs of their choosing, and read the responses the deployer’s credential gets back, for any path, method or body the task builds from those inputs on the fixed origin. Share a process with secure headers only with people you would trust with what its credential can reach. Whether starting such a process should require more than read access is still an open decision.
  • Called processes are vetted the same way. A call activity, whether its calledElement is a literal name or an expression, only starts a process that the calling process’s deployer can see when the call is made (see callActivity). The same applies to a CMMN case’s processTask and caseTask, checked against the case’s deployer when the case is deployed and again each time it runs. A name cannot be used to borrow another person’s process, or the secure headers that process sends as its deployer. A target that is hidden from the deployer fails exactly like a name that matches nothing.
  • Redirects. Following a redirect to a different scheme, host or port sends the request on without the secure headers. With Do not follow redirects on, nothing is followed.
  • Redaction is a backstop. If the server echoes a secure value back verbatim in the body, a response header or an error message, it is replaced with [redacted]. Only an exact copy is caught; a base64, URL-encoded, partial or case-changed copy is not. Do not rely on it, and do not call servers that reflect credentials.

Imported processes with a plaintext secure header

Section titled “Imported processes with a plaintext secure header”

A process imported with a plaintext requestSecureHeaders value (or any line that is not a valid $var: reference) still deploys, with the warning HttpSecureHeaderNotAReference. The warning names the task and the header but never quotes the value. When the token reaches the task, the process raises an incident and sends nothing; no request job is created. This differs from earlier releases, where such a task ran without the header. To fix it, pick a variable or secret for the header in the editor, which also discards the stored plaintext.

requestBodyEncoding sets the character set the body is encoded in, and writes it as the charset of the Content-Type header (added as text/plain when you set none). Supported: UTF-8, UTF-16LE, UTF-16BE, US-ASCII, ISO-8859-1, ISO-8859-15 and windows-1252 (case-insensitive; UTF8 and ASCII are accepted spellings).

  • A literal value outside this list is refused when you deploy.
  • A value given as an expression cannot be checked until it is evaluated, so an unsupported result fails the task at run time.
  • A character the chosen set cannot represent fails the task, naming the character. The body is never altered with a substitute such as ?.

saveRequestVariables and resultVariablePrefix deploy and round-trip untouched but are declined: they exist to control how variables are named, whereas an Orvanta HTTP task returns one object under results.<elementId> (and the optional responseVariableName). Use Save response details to get the status code and headers.

An HTTP task runs in the same sandbox and has the same network access as a Script Task, because it is one: an inline script on a worker with Bun. It needs a worker that can run Bun, and any restriction on where processes may call belongs on the worker’s network, where it covers scripts and HTTP tasks alike.

callActivity (a real child process instance of the definition named by calledElement) and multiInstanceLoopCharacteristics (sequential and parallel, on tasks and embedded sub-processes) now execute; they are no longer in this list, though the supported-element table above has not been regenerated for them. The authoritative per-element list is element-support.json in orvanta-bpmn. The rest of this list has not been re-verified against it. These parse as recognised BPMN tags but compile to Unsupported and are rejected at deploy time — they block deploying a process, they don’t run with reduced fidelity:

  • transaction and compensation (isForCompensation="true", compensateEventDefinition)
  • standardLoopCharacteristics
  • sendTask
  • adHocSubProcess
  • complexGateway, and eventBasedGateway with instantiate="true" or eventGatewayType="Parallel"
  • Link and cancel event definitions, on any host (escalation and conditional events now execute, see the table above; a conditional or escalation startEvent outside an event sub-process is still refused)
  • Timer or other event definitions on end and throw events beyond those listed above, and a signal start event
  • property — recognised, then refused; its subtree is skipped rather than modelled

dataObjectReference, dataStore and dataStoreReference are not refused: the engine treats them as annotations, so drawing one does not block a deploy, and it has no effect on execution. Only the dataObject declaration itself carries meaning (see the table above).

Pools and lanes (laneSet/lane) are parsed but ignored — their subtree isn’t rejected, but it also isn’t modelled or executed. Treat them as diagram-only. The exception is a <collaboration> declaring more than one <participant>: it is refused at deploy, because only one process is compiled and every other pool would be dropped.

If you’re migrating an existing process definition, see Importing from Flowable/Camunda for what this means for real-world BPMN XML, and the measured conformance rate against a corpus of production Flowable processes.