Task

Script Task

Script Task

Evaluates an ordered pipeline of compute steps against the current process variables and writes the results back. No external worker or HTTP call is involved — the engine runs the pipeline synchronously before advancing the token.

Each step is one of three types — jq, feel, or rhai — and later steps see everything earlier steps wrote.

Step Types

TypeWhat it’s forEngine
jqJSON navigation and reshaping (extract a sub-object, rename keys, build a new shape)jq via jaq-interpret
feelBusiness logic, conditions, numeric and date computationFEEL via dsntk-feel-evaluator
rhaiGeneral scripting — string building, loops, multi-statement logicRhai (sandboxed)

Gateway conditions and DMN decisions remain FEEL-only. Rhai is available only inside a Script Task pipeline.

How Steps Write Back

Every step receives the current variable map as input and writes results back into it. The merge rule depends on the step type:

  • jq — fed the full variable map as a JSON object. The step’s output must be a JSON object; each of its keys is merged into the variable map. Non-object output is a step error.
  • feel — evaluated against the variable map.
    • If the result is an object (context literal), each key is merged.
    • If the result is a scalar (number, string, boolean, list), the step’s Result Variable is required and stores the value.
  • rhai — the Rhai scope is seeded with every current variable that is a valid identifier. After the script runs, every scope entry that was added or changed is merged back into the variable map.

If any step fails, the pipeline stops, the failing step’s error is recorded against the instance, and the instance is marked as error.

XML

<bpmn:scriptTask id="calc_fee" name="Calculate Fee">
  <bpmn:extensionElements>
    <conduit:pipeline xmlns:conduit="http://conduit.io/ext">
      <conduit:step type="jq" name="Extract header">
        { doc_no: .grn.header.docNo, vendor: .grn.header.vendor }
      </conduit:step>
      <conduit:step type="feel" name="Shortage %" resultVariable="shortage_pct">
        (ordered - received) / ordered * 100
      </conduit:step>
      <conduit:step type="rhai" name="Build reference">
        let grn_ref = "GRN-" + vendor + "-" + doc_no;
      </conduit:step>
    </conduit:pipeline>
  </bpmn:extensionElements>
  <bpmn:incoming>flow_to_calc</bpmn:incoming>
  <bpmn:outgoing>flow_to_next</bpmn:outgoing>
</bpmn:scriptTask>

After this pipeline runs, the instance has doc_no, vendor, shortage_pct, and grn_ref set.

Testing in the Modeller

A stateless endpoint, POST /api/v1/scripts/execute, runs a pipeline against caller-supplied sample variables — no instance is started, no DB writes happen. The BPMN editor uses it to power the Run Pipeline and per-step Test buttons so you can close the write → test → adjust loop without re-deploying.

curl -X POST http://localhost:8080/api/v1/scripts/execute \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "steps": [
      {"name": "Extract header", "type": "jq", "expression": "{ doc_no: .grn.header.docNo }"},
      {"name": "Reference", "type": "rhai", "expression": "let grn_ref = \"GRN-\" + doc_no;"}
    ],
    "variables": { "grn": { "header": { "docNo": "A123" } } }
  }'

The response includes per-step status, output, timing, and the final accumulated variable map. Use the optional run_until_step (0-based index) to stop after a specific step.

Rhai Sandbox

The Rhai engine is constructed per call with strict per-engine limits and no IO functions registered — Rhai steps cannot read files, open sockets, or shell out. All knobs come from environment variables (single source of truth — see Operations):

VariableDefaultDescription
CONDUIT_RHAI_MAX_OPERATIONS100000Hard cap on operations executed per script (kills while true {}-style loops)
CONDUIT_RHAI_MAX_CALL_LEVELS20Maximum function-call stack depth
CONDUIT_RHAI_MAX_EXPR_DEPTHS64Maximum expression and statement nesting depth
CONDUIT_RHAI_DISABLED_SYMBOLSeval,print,debugComma-separated. eval is a hard floor — it is unioned in even if you omit it

Legacy Single-Expression Form

The older form is still parsed for back-compat. A plain <bpmn:script>…</bpmn:script> (optionally with <conduit:resultVariable>) is lowered to a single FEEL step named Script. New work should use <conduit:pipeline> directly.

<bpmn:scriptTask id="sum_total" name="Sum Total"
  conduit:resultVariable="totalCost">
  <bpmn:script>amount + shipping</bpmn:script>
</bpmn:scriptTask>

Per-Step History

Each pipeline step produces its own execution_history row carrying step_name, step_index, and step_type alongside the outer scriptTask entry. The events stream also emits a script_step_completed or script_step_failed event per step, so post-mortem and timeline UIs can show exactly which step produced which variables.

Notes

Script tasks execute synchronously inside the engine — they are not suitable for I/O-bound work (HTTP calls, database queries). For those use cases, use a Service Task with a worker subscription.

Cookbook

The return shape decides what happens — for FEEL steps:

  • Context literal { a: …, b: … } — every key becomes a variable; the step’s Result Variable is ignored.
  • Scalar (number, string, boolean, list) — written to the step’s Result Variable; required.

Counter (FEEL)

{ counter: (if counter = null then 0 else counter) + 1 }

Accumulator (FEEL)

{ total: (if total = null then 0 else total) + amount }

Extract + compose (jq + Rhai)

A two-step pipeline that pulls fields out of a nested object then builds a reference string:

Step 1 (jq):   { doc_no: .grn.header.docNo, vendor: .grn.header.vendor }
Step 2 (rhai): let grn_ref = "GRN-" + vendor + "-" + doc_no;

After running, doc_no, vendor, and grn_ref are all set on the instance.

List append (FEEL)

{ items: append(if items = null then [] else items, newItem) }

For more patterns see the language-specific cookbooks:

And the syntax indexes: