Skip to content

[One Workflow] Fix the variable type detection within liquidjs for loops#270596

Merged
dej611 merged 16 commits into
elastic:mainfrom
dej611:fix/17490
Jun 15, 2026
Merged

[One Workflow] Fix the variable type detection within liquidjs for loops#270596
dej611 merged 16 commits into
elastic:mainfrom
dej611:fix/17490

Conversation

@dej611

@dej611 dej611 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Liquid {% for %} loops in workflow YAML were not validated or autocompleted reliably. Invalid collection paths could slip through, and loop variables inside block-folded (>-) message fields were often treated as unknown because offset mapping used the wrong source text.

This change validates for-loop collection paths against the step context schema (same rules as foreach steps) and reports dedicated diagnostics, separate from variable validation. Loop variables in invalid collections stay permissive in the variable pass so users get one clear error on the collection, not duplicate markers.

Block literal and folded scalars now map cursor offsets using the editor’s YAML source, so template-local context (assign, capture, for-loop scope) lines up with what the user actually typed. Liquid syntax and for-loop collection checks share a single YAML pass when the workflow graph is available, and template parsing skips work when a scalar has no {% tags.

Checklist

@dej611 dej611 added release_note:fix backport:version Backport to applied version labels Team:One Workflow Team label for One Workflow (Workflow automation) v9.5.0 v9.4.2 labels May 22, 2026
@dej611 dej611 marked this pull request as ready for review May 25, 2026 14:10
@dej611 dej611 requested a review from a team as a code owner May 25, 2026 14:10
@rosomri

rosomri commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

While running the branch locally, I identified a couple of additional false positives in the Liquid {% for %} validation -

{% assign rows = consts.items %}{% for row in rows %} should not report rows as invalid, because rows is a Liquid-local variable. Similarly, {% for i in (1..3) %} is valid Liquid but not a workflow context path.

Can we limit collection-path diagnostics to expressions that are actually workflow context paths, and leave Liquid-local/range/non-path expressions alone unless we can resolve them through template-local type inference?

In the meantime, to unblock users from saving workflows with false-positive Liquid validation errors, I’ll reduce the error severity to a warning.

--- In the meantime, to unblock users from saving workflows with false-positive Liquid validation errors, I’ll reduce the error severity to a warning. ---
^^ nvm, the errors are not Liquid-related but come from variable path validation, so they can’t generally be downgraded to warnings.

Workflows with errors as reference -

name: Local assigned collection
enabled: false
triggers:
  - type: manual
consts:
  items:
    - name: Alice
    - name: Bob
steps:
  - name: summarize
    type: console
    with:
      message: >-
        {% assign rows = consts.items %}
        {% for row in rows %}
        - {{ row.name }}
        {% endfor %}

Why it fails: the validator sees rows as the loop collection and checks it against the workflow context schema. rows is a Liquid-local variable, not a workflow context path, so it reports Collection rows is invalid.

name: Range loop
enabled: false
triggers:
  - type: manual
steps:
  - name: count
    type: console
    with:
      message: >-
        {% for i in (1..3) %}
        Page {{ i }}
        {% endfor %}

Why it fails: (1..3) is valid LiquidJS, but it is not a variable path, so parseVariablePath('(1..3)') rejects it and the validator reports Invalid collection path: (1..3).

name: Nested local collection
enabled: false
triggers:
  - type: manual
consts:
  groups:
    - name: admins
      users:
        - name: Alice
steps:
  - name: summarize
    type: console
    with:
      message: >-
        {% for group in consts.groups %}
          {% assign users = group.users %}
          {% for user in users %}
          {{ group.name }}: {{ user.name }}
          {% endfor %}
        {% endfor %}

Why it fails: users is valid inside Liquid, but the collection validator only validates against the workflow schema and does not account for locals introduced earlier in the template.

@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] Jest Tests #9 / StepAboutRuleComponent does not modify the provided risk score until the user changes the severity

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
workflowsManagement 2.6MB 2.6MB +3.7KB
Unknown metric groups

ESLint disabled line counts

id before after diff
workflowsManagement 129 130 +1

Total ESLint disabled count

id before after diff
workflowsManagement 152 153 +1

History

cc @dej611

@rosomri rosomri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look good overall. I left two comments on concrete edge cases.

Stepping back a bit, and thinking out loudly (let me know wdyt), but this feels like we’re entering a long-term maintenance race with LiquidJS.

The current direction already needs Kibana-side handling for Liquid scoping semantics: assign, reassignment order, for loop scopes, nested loops, range expressions, filters, forloop, block scalar offset mapping, etc.

My main concern is this becomes a never-ending race where each valid Liquid construct needs another local patch, and a future LiquidJS major version could break assumptions we made about the AST.

Longer term, I think the robust fix is to push this kind of logic into LiquidJS static analysis, rather than expanding custom Liquid semantics inside workflows.

What we need from the Liquid side is something like:

analyzeTemplate(template, {
  resolveGlobal(path),
  getProperty(type, key),
  getIterableItem(type),
  applyFilter(type, filter, args),
})

And it would return typed/resolved references plus locations, for example:

{
  references: [
    {
      sourcePath: ['row', 'name'],
      resolvedPath: ['consts', 'items', '*', 'name'],
      range,
      type,
    }
  ],
  locals,
  scopes,
  diagnostics
}

Liquid should own Liquid semantics: scoping, reassignment order, assign, capture, for, ranges, filters, forloop, increment/decrement, nesting, and future syntax changes.

Kibana should own only the host-specific parts: building the workflow context schema for the current step, resolving workflow paths like steps.foo.output, deciding step visibility, and mapping template-relative ranges back to YAML/Monaco positions.

For this PR, maybe we should keep the validation conservative: validate collection expressions only when we can confidently resolve them as workflow context paths, and avoid hard errors for dynamic/unknown Liquid expressions. Then we can separately explore contributing a schema-aware static analysis API upstream to LiquidJS.

Comment on lines +91 to +92
/** Liquid for-loop range literal, e.g. `(1..3)`. */
export const LIQUID_RANGE_LITERAL_REGEX = /^\(\s*\d+\s*\.\.\s*\d+\s*\)$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic Liquid ranges are still treated as invalid collection paths. LiquidJS accepts {% for i in (1..n) %} and {% for i in (start..finish) %}, but LIQUID_RANGE_LITERAL_REGEX only exempts numeric literals like (1..3). Those valid templates fall through to validateCollectionPath() and get Invalid collection path.

Consider detecting range expressions structurally via LiquidJS token/AST shape, or at least broadening the range exemption beyond numeric literals...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's expected for now. I've avoided to blow up the scope here. When a range is encountered it will just early exit and fallback to any validation


while (SINGLE_IDENTIFIER_REGEX.test(path) && !seen.has(path)) {
seen.add(path);
const assign = assignVars.find((a) => a.name === path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveAssignChain() uses the first assignment for a local name, but Liquid assignment semantics should use the latest in-scope assignment before the use. This can produce false errors or false passes after reassignment:

{% assign rows = steps.missing.output %}
{% assign rows = consts.items %}
{% for row in rows %}{{ row.name }}{% endfor %}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess this type of things may be incorrectly detected. It is a very bad pattern tho. Even TS engine would be fooled about it.

@dej611 dej611 merged commit 2b4f07f into elastic:main Jun 15, 2026
35 checks passed
@kibanamachine

Copy link
Copy Markdown
Contributor

Starting backport for target branches: 9.4

https://github.com/elastic/kibana/actions/runs/27546998035

@kibanamachine

Copy link
Copy Markdown
Contributor

💔 All backports failed

Status Branch Result
9.4 Backport failed because of merge conflicts

You might need to backport the following PRs to 9.4:
- [One Workflow] Support nested data map aliases in validation (#270008)

Manual backport

To create the backport manually run:

node scripts/backport --pr 270596

Questions ?

Please refer to the Backport tool documentation

delanni pushed a commit to delanni/kibana that referenced this pull request Jun 16, 2026
…ops (elastic#270596)

## Summary

Liquid `{% for %}` loops in workflow YAML were not validated or
autocompleted reliably. Invalid collection paths could slip through, and
loop variables inside block-folded (`>-`) message fields were often
treated as unknown because offset mapping used the wrong source text.

This change validates for-loop collection paths against the step context
schema (same rules as `foreach` steps) and reports dedicated
diagnostics, separate from variable validation. Loop variables in
invalid collections stay permissive in the variable pass so users get
one clear error on the collection, not duplicate markers.

Block literal and folded scalars now map cursor offsets using the
editor’s YAML source, so template-local context (assign, capture,
for-loop scope) lines up with what the user actually typed. Liquid
syntax and for-loop collection checks share a single YAML pass when the
workflow graph is available, and template parsing skips work when a
scalar has no `{%` tags.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
@kibanamachine kibanamachine added the backport missing Added to PRs automatically when the are determined to be missing a backport. label Jun 17, 2026
@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

VladimirFilonov pushed a commit to VladimirFilonov/kibana that referenced this pull request Jun 17, 2026
…ops (elastic#270596)

## Summary

Liquid `{% for %}` loops in workflow YAML were not validated or
autocompleted reliably. Invalid collection paths could slip through, and
loop variables inside block-folded (`>-`) message fields were often
treated as unknown because offset mapping used the wrong source text.

This change validates for-loop collection paths against the step context
schema (same rules as `foreach` steps) and reports dedicated
diagnostics, separate from variable validation. Loop variables in
invalid collections stay permissive in the variable pass so users get
one clear error on the collection, not duplicate markers.

Block literal and folded scalars now map cursor offsets using the
editor’s YAML source, so template-local context (assign, capture,
for-loop scope) lines up with what the user actually typed. Liquid
syntax and for-loop collection checks share a single YAML pass when the
workflow graph is available, and template parsing skips work when a
scalar has no `{%` tags.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

1 similar comment
@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

flash1293 pushed a commit to flash1293/kibana that referenced this pull request Jun 23, 2026
…ops (elastic#270596)

## Summary

Liquid `{% for %}` loops in workflow YAML were not validated or
autocompleted reliably. Invalid collection paths could slip through, and
loop variables inside block-folded (`>-`) message fields were often
treated as unknown because offset mapping used the wrong source text.

This change validates for-loop collection paths against the step context
schema (same rules as `foreach` steps) and reports dedicated
diagnostics, separate from variable validation. Loop variables in
invalid collections stay permissive in the variable pass so users get
one clear error on the collection, not duplicate markers.

Block literal and folded scalars now map cursor offsets using the
editor’s YAML source, so template-local context (assign, capture,
for-loop scope) lines up with what the user actually typed. Liquid
syntax and for-loop collection checks share a single YAML pass when the
workflow graph is available, and template parsing skips work when a
scalar has no `{%` tags.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

1 similar comment
@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

logeekal pushed a commit to logeekal/kibana that referenced this pull request Jun 25, 2026
…ops (elastic#270596)

## Summary

Liquid `{% for %}` loops in workflow YAML were not validated or
autocompleted reliably. Invalid collection paths could slip through, and
loop variables inside block-folded (`>-`) message fields were often
treated as unknown because offset mapping used the wrong source text.

This change validates for-loop collection paths against the step context
schema (same rules as `foreach` steps) and reports dedicated
diagnostics, separate from variable validation. Loop variables in
invalid collections stay permissive in the variable pass so users get
one clear error on the collection, not duplicate markers.

Block literal and folded scalars now map cursor offsets using the
editor’s YAML source, so template-local context (assign, capture,
for-loop scope) lines up with what the user actually typed. Liquid
syntax and for-loop collection checks share a single YAML pass when the
workflow graph is available, and template parsing skips work when a
scalar has no `{%` tags.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
@kibanamachine

Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 270596 locally
cc: @dej611

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport missing Added to PRs automatically when the are determined to be missing a backport. backport:version Backport to applied version labels release_note:fix Team:One Workflow Team label for One Workflow (Workflow automation) v9.4.2 v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants