| --- |
| title: Variables and Expressions |
| layout: website-normal |
| --- |
| |
| Steps can take input and return output, as do the containing workflows in most cases. |
| In workflow, Apache Brooklyn supports an interpolation expression syntax to access these values. |
| You can also use the Brooklyn DSL, but in most cases the interpolation syntax is more convenient. |
| |
| For example, you can write: |
| |
| ``` |
| - log Starting workflow ${workflow.name} |
| - let integer x = 1 |
| - id: log |
| step: log The value for x is now ${x} |
| - step: let x = ${x} + 1 |
| next: log |
| condition: |
| target: ${x} |
| less-than: 3 |
| ``` |
| |
| This will output a series of log messages such as: |
| |
| ``` |
| Starting workflow: my-workflow |
| The value for x is now 1 |
| The value for x is now 2 |
| The value for x is now 3 |
| ``` |
| |
| |
| ### Workflow Variables |
| |
| The above illustration showed how `let x = <VALUE>` can be used to set a workflow variable |
| and `${x}` or `"The value for x is now ${x}"` will resolve it. |
| This is the simplest example of an interpolation expression. |
| There is a large set of information available through these expressions, described below. |
| |
| Workflow variables, using `let`, have some additional behaviors described further below, |
| permitting for example the evaluation of `${x} + 1` and the specification that it should be an `integer`. |
| |
| |
| ### Workflow Contextual Information |
| |
| The interpolated reference `${workflow.<KEY>}` can be used to access workflow information, where `<KEY>` can be: |
| |
| * `name` - returns the name of the current workflow |
| * `task_id` - returns the ID of the current corresponding Brooklyn task which acts as a unique identifier for the instance of the workflow |
| * `link` - a link in the UI to this instance of workflow |
| * `input` - the map of input parameters |
| * `output` - the output object or map |
| * `error` - if there is an error in scope |
| * `current_step.<KEY>` - info on the current step, where `<KEY>` can be any of the above (and the returned data is specific to the current step) |
| * `previous_step.<KEY>` - info on the previously invoked step, as above |
| * `error_handler.<KEY>` - info on the current error handler, as above, if in an on-error step |
| * `step.<ID>.<KEY>` - info on the last invocation of the step with declared `id` matching `<ID>`, as above |
| * `var.<VAR>` - return the value of `<VAR>` which should be a workflow-scoped variable (set with `let`) |
| * `util.<UTIL>` - access a utility pseudo-variable, either `random` for a random between 0 and 1, |
| `now` for milliseconds since 1970, `now_iso` for ISO 8601 date string, `now_nice` or `now_stamp` for a human readable date format |
| |
| In the step contexts, the following is also supported after `workflow.`: |
| |
| * `step_id` -- the declared `id` of the step |
| * `step_index` -- the index of the current step in the workflow definition, starting at 0 |
| (note that for convenience in the UI, step task display names will include the index starting at 1) |
| |
| Where an item returns a map (such as `input` and usually `output`), a further `.<KEY>` can be used to |
| access the `<KEY>` entry within that map. |
| Similarly where an item returns a list, `[<INDEX>]` can be used to access the element at that index (starting at 0). |
| |
| |
| ### Entity Contextual Information |
| |
| The interpolated reference `${entity.<KEY>}` can be used to access information about the entity where the |
| workflow is running, where `<KEY>` can be: |
| |
| * `name` - returns the value of the config key `<KEY>` |
| * `id` - returns the value of the config key `<KEY>` |
| * `config.<KEY>` - returns the value of the config key `<KEY>` |
| * `sensor.<KEY>` - returns the value of the sensor key `<KEY>` |
| * `attributeWhenReady.<KEY>` - returns the value of the sensor key `<KEY>` once it is ready (available and truthy), for use with the `wait` step |
| * `parent.<KEY>` - returns the value of `<KEY>` per any of the above in the context of the application |
| * `application.<KEY>` - returns the value of `<KEY>` per any of the above in the context of the application |
| |
| |
| ### Output Access |
| |
| The token `${output}` refers to the nearest output in scope: |
| in a step's `output:` block, it refers to the default output from a step, thus |
| `output: ${output.stdout}` can be used on a `container` step to change the output from being the default map including `stdout` |
| to being just the `stdout` (alternatively just `${stdout}` can be used, per the next section). |
| With a nested workflow running over a list, e.g. of children, `output: ${output[0]}` |
| can be used to refer to the output from the first element in the list. |
| If used in a step _prior_ to the resolution of an `output` block, such as in the inputs, |
| it refers to the output from the previous step. |
| If used in the `output` block of a workflow, it refers to the default output of the workflow |
| which is the output of the last step. |
| |
| |
| ### Simple Expressions for Input, Output, and Variable |
| |
| Where `${<VAR>}` is supplied, assuming it doesn't match one of the models above, the following search order is used: |
| |
| * `${workflow.error_handler.output.<VAR>}` (only in an on-error block) |
| * `${workflow.error_handler.input.<VAR>}` (only in an on-error block) |
| * `${workflow.current_step.output.<VAR>}` (only set when evaluating `output` for a step, pointing at default output of the step) |
| * `${workflow.current_step.input.<VAR>}` |
| * `${workflow.previous_step.output.<VAR>}` |
| * `${workflow.var.<VAR>}` |
| * `${workflow.input.<VAR>}` |
| |
| Thus `${x}` will be matched against the current step first, then outputs from the previous step, |
| and then workflow vars and inputs. It will be an error if `x` is not defined in any of those scopes. |
| (The `output` of the `current_step` is only defined when processing an explicit `output` block defined on a step, |
| and the `error_handler` is only defined when running in an `on-error` step.) |
| |
| Note that the `workflow`, `entity`, and `output` models take priority over workflow variables, |
| so it is a bad idea to call a workflow variable `workflow`, as an attempt to evaluate |
| `${workflow}` refers to the model above which is not a valid result for an expression. |
| (You could still access such a variable, using `${workflow.var.workflow}`.) |
| |
| When populating a `<VAR>` for use in the scopes above, it might not make sense to include the previous scopes; |
| in these cases resolution starts at the appropriate scope. |
| For example when resolving a step's input, the step's output is not considered. |
| Furthermore when resolving a step's input, it is permitted to reference other input so long as there is no recursive reference, |
| and it is permitted to reference the variable being set, from a parent scope, but other local or recursive references are not permitted. |
| This only applies in very specific edge cases, and so can generally be ignored. |
| If resolution behavior is ever surprising, it is recommended to use the full syntax including scope (prefixed by `workflow.`). |
| |
| |
| |
| ### Arithmetic and Idempotency |
| |
| The `let` step allows mathematical operations, such as: |
| |
| ``` |
| - let x = ${x} * 3 + 1 |
| ``` |
| |
| The spaces around the operations are required, and this is the only place arithmetic is supported. |
| Any other usage, such as `set-sensor disallowed = ${x} + 1` or `let x = ${x}+1` will result in strings. |
| It is recommended to explicitly specify a mathematical type, `integer` or `double` to trigger an error |
| because the string `3+1` will not be coercible to an integer. |
| |
| The reason `let` is the only place operations is allowed is because Brooklyn is able to restore local variables |
| if a workflow is replayed from that step. |
| This ensures that most steps are individually idempotent, |
| so if interrupted at the step can be safely resumed from that step. |
| |
| For example, if the following were allowed: |
| |
| ``` |
| - set-sensor count = ${entity.sensor.count} + 1 # NOT supported |
| ``` |
| |
| if it were interrupted, Brooklyn would have no way of knowing whether |
| the sensor `count` contains the old value or the new value, |
| and a replay might cause it to be incremented twice. |
| The following sequence of steps (which is permitted) can always safely be replayed from any interrupted state: |
| |
| ``` |
| - let integer count_local = ${entity.sensor.count} ?? 0", |
| - let count_local = ${count_local} + 1 |
| - set-sensor count = ${count_local} |
| ``` |
| |
| Where workflows need to be resumed on interruption or might replay steps to recover from other errors, |
| idempotency is an important part of reliable workflow design. |
| External actions such as `http` and `container` are not guaranteed to be idempotent, |
| and neither are some `invoke-effector` calls, so care must be taken here for workflows to be replayable. |
| Good practice and the settings available for resilient workflows are covered in [Workflow Settings](settings.md). |
| |
| |
| ### Unavailable Variables and the `let` Nullish Check |
| |
| To guard against mistakes in variable names or state, workflow execution will typically throw an error if |
| a referenced variable is unavailable or null, including access to a sensor which has not yet been published. |
| There are three exceptions: |
| |
| * the `let` step supports the "nullish coalescing" operator `??` for this case, as described below below |
| * the `wait` step or `transform ... | wait` will block until a value becomes available, |
| such as using `${entity.attributeWhenReady.SENSOR_NAME}`. |
| * `condition` entries can reference a null or unavailable value in the `target`, |
| and check for absence using `when: absent_or_null` |
| |
| Where it is necessary to consider "nullish" values -- variables which are either null or not yet available -- |
| the "nullish coalescing" operator `??` can be used within `let` statements: |
| |
| ``` |
| - let x = ${entity.sensor.does_not_exist} ?? "unset" |
| ``` |
| |
| This will set `x = unset`, assuming there is no sensor `does_not_exist` (or if that sensor is `null`). |
| |
| A limited number of other operations is also permitted in `let`, |
| namely the basic mathematical operands `+`, `-`, `*`, and `/` for integers and doubles, |
| and the modulo operator `%` for integers giving the remainder. |
| These are evaluated in usual mathematical order. |
| Parentheses are not supported. |
| |
| The `transform` step can be used for more complicated transformations, such as whether to `wait` on values that are not yet ready, |
| conversion using `json` and `yaml`, and whether to `trim` strings or yaml documents. |
| This supports two types of trimming: if a `type` is specified, the value is scanned for `---` on a line by itself |
| and that token is used as a "document separator", and only the last document is considered; |
| if no `type` is specified, the value has leading and trailing whitespace removed. |
| The former is primarily intended for YAML processing from a script which might include unwanted output prior |
| to the outputting the YAML intended to set in the variable: the script can do `echo ---` before the |
| output which is wanted. |
| |
| |
| ### Interpolating Objects and Strings |
| |
| Shorthand form is designed primarily for simple strings as the data. To pass more complex objects or control |
| the quotes, longhand form (map) is recommended, and it may be helpful to convert complex objects to strings |
| in a previous step using e.g. `let string map_s = ${map}`. |
| |
| It is possible to embed strings with spaces, quotes, and complex types as shorthand, but care must be taken |
| and if doing this, it is helpful to understand the parsing process. |
| Shorthand will normally groups things using quotation marks, single or double, provided the quoted string is surrounded |
| by whitespace or an end-of-line, and will remove these outermost quotes and standardize whitespace not in the quotes. |
| Thus it is technically possible to set a workflow variable `a b` using |
| `let "a b" = 1`, although it is not recommended, and because the expression syntax doesn't allow spaces, |
| there is no way to access such a variable! |
| The one case where quotes are not stripped by the shorthand processor is when the step's |
| final argument accepts multiple words, such as after the `=` in `set-sensor` or `ssh <command>`; |
| if the final multi-word groups argument is one entire quoted string, it is unwrapped, |
| but otherwise its quotes are respected. This allows `ssh bash -c 'echo hi'` to pass the quotes, |
| and also allows it to be written `ssh 'bash -c "echo hi"'` or `ssh 'bash -c \'echo hi\''`, |
| with the outer quotes removed. |
| The syntax is optimized to be as intuitive as possible in common cases, |
| although it does get complicated at the margins; for example `log "hello world"` prints `hello world` (quotes unwrapped) but |
| `log "hello" "world"` prints `"hello" "world"` (quotes preserved). |
| If in doubt, you can always write `log "\"hello\" \"world\""` or `log '"hello" "world"'`. |
| It is suggested to follow the examples and do testing, use longhand, and review these notes only |
| if particularly interested or uncertain about quotes. |
| |
| Variable expansion occurs whenever `${var}` is used, expanding to the value of `var` as described above. |
| If matching a shorthand variable on its own, then the type of the value is preserved, |
| but if embedded in a larger word, simple values (numbers and booleans) will be converted to strings |
| but complex types will give an error, as will null or absent variables. |
| Thus if we run `let integer val = 1` then `set-sensor s1 = val is ${val}` or `set-sensor s1 = "val is ${val}"`, |
| the string `val is 1` will be set as the sensor `s1`; however `set-sensor s1 = ${val}` will emit the integer `1`. |
| If `val` is a map, then the last form will preserve the map, but the other two, including it in `val is ${val}`, |
| will throw an error. |
| |
| It can be helpful to use the `let` command to coerce data to the write format in a new variable |
| or to handle potentially unset values; for example `let json string val_json = ${val}` to create a string |
| `val_json` representing the JSON encoding of `val`, or even `"let map x = { a: 1 }"` for simple unambiguous map expressions, |
| where the string `{ a: 1 }` is converted to a map, but noting that YAML requires any string with `:` to be quoted in its entirety, |
| and the YAML parse will unwrap it before passing to the shorthand processor. |
| The longhand form, e.g. `{ step: "let x", value: { a: 1 } }`, can be used for potentially ambiguous values or for clarity. |
| |
| The `let` step has some special behavior. It can accept `yaml` and, when converting to complex types, |
| will strip everything before a `---` document separator. Thus a script can have any output, so long as it |
| ends with `---\n` followed the YAML to read in, then `let yaml fancy-bean = ${stdout}` will convert it to |
| a registered type `fancy-bean`. It will be an error if `stdout` is not coercible to `fancy-bean`. |
| For more conversion, see `transform`. |
| |
| Another special behavior of `let` is that its `value` is reprocessed, supporting arithmetic as described elsewhere, |
| and also unwrapping quoted words in the value (removing quotes) _without_ evaluating expressions within them. |
| This is the only way to embed `${...}` expressions in a value, and can simplify other places where quotation marks |
| and spaces are needed. Thus given the steps: |
| |
| ``` |
| - let msg = "${person}" is ${person} |
| - log ${msg} |
| ``` |
| |
| Brooklyn will log `${person} is { name: "Bob", age: 42 }`. |
| |
| It is also possible to use longhand syntax `{ type: set-sensor, sensor: x, value: value }` |
| or a hybrid syntax `{ step: set-sensor x, value: value }`, |
| which can be useful for complex types and bypassing the shorthand processor's unquoting strategy, |
| but in this case note that YAML processing will unwrap quotes. |
| |
| The `interpolation_mode` and `interpolation_errors` options can be used to specify other |
| behavior for interpolation, both on `let` and on `load` of data from a URL. |
| |
| |
| ### Advanced Details |
| |
| If unusual behaviour is encountered with encoding and resolving expressions, a few simple tips can often help: |
| |
| * Use the longhand (map) format for steps if using complex types or strings |
| * Use `let` with coercion, trim, and YAML/JSON options for fine-grained control and visibility of the output |
| * Remember thay YAML parsing will also remove strings; where quotes need to be included, the YAML `|` marker, |
| followed by the content on the following line, is a good pattern |
| |
| In rare cases it can be useful to understand some of the advanced nuances. These are described below. |
| |
| #### Quotes and Whitespaces |
| |
| Continuing from the above, because YAML and shorthand process quotes prior to passing the data to the step, |
| the treatment of quoted expressions can be subtle. For example, consider: |
| |
| ``` |
| - type: let |
| variable: x |
| value: "1 + 1" |
| ``` |
| |
| In YAML, this is identical to the above with `value: 1 + 1` passed, |
| so the `let` step does not know it was supplied in quotes, and so it will evaluate it as `2`. |
| You can pass instead `value: \"1 + 1\"` and `let` will get a quoted string which it will unquote |
| and set `x` to be `1 + 1`. |
| If using the shorthand, as noted above, quotes are preserved for the `value` to `let`, |
| so as one would expect, `let x = 1 + 1` sets `x` to `2` whereas `let x = "1 + 1"` and `let x = 1 "+" 1` set `1 + 1`. |
| |
| |
| #### Freemarker Templates |
| |
| Apache Brooklyn currently uses the Freemarker templating language to evaluate expressions. |
| |
| Freemarker has many advanced behaviors, but it is recommended not to rely on those, and instead again to use |
| the advanced functionality of `let`, in case the templating engine is changed in future. |
| |
| Some specific items to note about the templating language as used: |
| |
| * Entries of maps and lists can be accessed using a dot- or bracket- qualified index, e.g. `${map.key}` or `list[index]` |
| * Nested variables are not supported, e.g. `${list[${index}]}` or `${${varname}}` |
| * Spaces should not be used inside interpolated expressions `${...}` |
| |
| In rare cases the use of Freemarker may cause unexpected processing behavior of parameters, |
| normally where an intended literal expression is interpreted by Freemarker. |
| This can happen, for example, if sending an `ssh` or `container` containing a `${VAR}` expression intended to be |
| processed by the shell rather than by Brooklyn. |
| It is recommended in these situations to use the behavior of `let`, |
| where quoted expressions are not interpreted by Freemarker, |
| e.g. `let script = "echo var is ${VAR}"` or `let script = echo var is "${VAR}"`, followed by `ssh ${script}`. |