Short-circuiting and null guards
&& and || evaluate both operands, so a null check in front of a comparison does not protect it:
A if condition else B is the only form that skips evaluation, so put the risky comparison inside it: ${{ ACTIONS.enrich.result.count > 0 if ACTIONS.enrich.result.count != None else False }}.
An && chain is safe only when every clause tolerates None on its own. Equality checks like != and == do, so ${{ TRIGGER.assignee != None && TRIGGER.severity == "high" }} returns false without error; ordering comparisons like > do not.
Unsupported Python syntax
Each Python form below fails withError parsing expression:
Nesting
${{ }} inside ${{ }} does not parse: ${{ FN.length(${{ TRIGGER.tags }}) }} fails with No terminal matches '$' in the current parser context. Write ${{ FN.length(TRIGGER.tags) }}.
Inside a JSONPath filter the casing inverts: use true and false. An uppercase literal matches nothing and returns an empty list instead of raising:
Missing fields
TRIGGER, ACTIONS, ENV, and var paths return None for anything that does not exist, including typos:
None becomes the literal text None. The template "Assignee: ${{ TRIGGER.assignee }}" renders as "Assignee: None", so guard string interpolation with a ternary: ${{ TRIGGER.assignee if TRIGGER.assignee != None else "unassigned" }}.
Bracket keys
Quoted keys work everywhere brackets do:TRIGGER["data"] and VARS.cfg["epss"] both resolve. Unquoted keys work in some contexts and fail in others:
SECRETS or VARS must use dot syntax: VARS["cfg"] fails with Expected one of: * ATTRIBUTE_PATH. Write VARS.cfg first, then brackets for anything deeper.
${{ TRIGGER["nope"] }} returns null, while the same lookup on VARS raises:
${{ TRIGGER["my-key"] }} works. A hyphenated secret or variable name is not reachable at all: SECRETS.my-secret.API_KEY fails to parse, and the only fix is renaming the secret or variable with underscores.
When the text after the hyphen starts with a context keyword, the expression parses as arithmetic: ${{ VARS.my-var.k }} evaluates as VARS.my minus var.k and fails with unsupported operand type(s) for -: 'NoneType' and 'NoneType'.
Comparison chains
Comparisons fold left instead of chaining:3 > 2 > 1 becomes (3 > 2) > 1, then True > 1:
${{ 3 > 2 && 2 > 1 }} returns true.
Indexing a filter result
[0] after a JSONPath filter is absorbed into the path and applied to each matched value, so with matched dates like "2015-03-20" you index into the string itself:
events[?(...)][0].eventDate raises Error trying to process rule "actions": 0. FN.at takes the first element:
Empty literals
Non-empty literals like${{ {"status": "ok"} }} and ${{ [1, 2] }} evaluate fine, but the empty forms raise. ${{ {} }} fails with cannot convert dictionary update sequence element #0 to a sequence and ${{ [] }} fails with 'NoneType' object is not iterable.
To pass an empty value to an action, write it in plain YAML outside the expression: value: {}. To test for emptiness, use FN.is_empty(x) or FN.length(x) == 0.
String escapes
Escape sequences stay literal:${{ "a\nb" }} is the four characters a, \, n, b, and FN.length("a\nb") returns 4. The exception is FN.join, which unescapes its separator:
YAML quoting
A: inside an unquoted expression breaks the YAML document:
# fails silently: title: ${{ FN.format("Alert #{0}", TRIGGER.alert_id) }} truncates to the literal text ${{ FN.format("Alert, which passes through unevaluated. Wrap the whole value in single quotes whenever the expression contains :, #, or quotes:
Field access after functions
Dotting into a function result does not parse:${{ FN.get_hour(FN.now()) }} returns the hour.
VARS key depth
AVARS path takes the variable name plus at most one key: ${{ VARS.cfg.network }} returns the stored object, but one level deeper raises:
FN.lookup: ${{ FN.lookup(VARS.cfg.network, "cidr") }} returns "10.0.0.0/8", or store the value one level flatter.
Naive and aware datetimes
FN.now() returns a local time without timezone info, while FN.utcnow() and any parsed timestamp that carries an offset are timezone-aware. Mixing the two in arithmetic raises:
${{ FN.to_datetime(TRIGGER.created_at) < FN.utcnow() }} returns true. To mix with FN.now(), strip the offset with FN.unset_timezone first.
Expressions in python_lambda
Expressions work insidepython_lambda parameters. Write the expression as the Python literal it should become:
None, lists, and objects substitute as valid literals without quotes. Strings need single quotes around the expression: '${{ VARS.cfg.severity }}' becomes 'High', while the unquoted form becomes the bare name High and fails at call time with NameError: name 'High' is not defined.
A string value containing an apostrophe breaks the lambda with Invalid syntax in expression: unterminated string literal. core.script.run_python accepts values that contain quotes through inputs.
Related pages
- See Expressions for expression syntax, contexts, and operators.
- See Transformations for recipes covering the tasks these mistakes come from.
- See JSONPath for filter syntax and return behavior.
- See Functions for the full
FNfunction reference. - See Python script for the
core.script.run_pythoncontract and examples.