Skip to content

ScriptTrigger/CommandsTrigger: edge mode fires on every poll (in-memory lastMatched) #449

Description

@jymaire

Summary

In the Deno, Go, Node, Python, Ruby and Shell ScriptTrigger / CommandsTrigger, edge: true (the default) does nothing: the trigger starts an execution on every poll while the condition keeps matching, instead of only on the not-matching → matching transition. A script that keeps failing floods the flow with executions, one per interval.

Actual Behaviour

The previous result is kept in an in-memory field:

@Builder.Default
@Getter(AccessLevel.NONE)
private final AtomicBoolean lastMatched = new AtomicBoolean(false);

The scheduler rebuilds a polling trigger from the flow definition (and serializes it to a worker) on every evaluation, so lastMatched is always false when evaluate() runs. matched && !lastMatched is therefore always true when the condition matches.

The existing // Known limitation comment says edge mode "may re-fire once after a scheduler restart". What actually happens is much worse: it re-fires on every poll.

Observed with interval: PT10S, edge: true and a script that always exits 1:

Trigger Executions Window
ruby.ScriptTrigger (on main) 16 ~3 min
perl.ScriptTrigger (PR #446 before 0ba7de7, same code) 22 ~4.5 min
perl.CommandsTrigger (PR #446 before 0ba7de7), matching on vars 22 ~4.5 min

With edge mode working, each of these should have produced exactly 1 execution.

Expected Behaviour

With edge: true, the trigger emits once when the condition first matches. It stays silent while the condition keeps matching, and emits again only after the condition has stopped matching and then matches again. This state must survive both polls and scheduler restarts.

Reproducer

  1. Start Kestra OSS (kestra/kestra:v1.3.39) with plugin-scripts from main.
  2. Create this flow:
id: ruby_edge_repro
namespace: company.team

triggers:
  - id: ruby_failure
    type: io.kestra.plugin.scripts.ruby.ScriptTrigger
    interval: PT10S
    exitCondition: "exit 1"
    edge: true
    script: |
      exit 1

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "ruby exitCode={{ trigger.exitCode }}"
  1. Wait 60 seconds and open the flow's Executions tab.
  2. Actual: about 6 executions, one per poll. Expected: 1.

The same thing happens with the other affected triggers, for example a Shell ScriptTrigger running exit 1.

Logs / Stack Trace

No error is logged. Each poll just produces a new execution whose trigger variables look like this:

{'timestamp': '2026-09-23T14:15:15.32Z', 'condition': 'exit 1', 'exitCode': 1}
{'timestamp': '2026-09-23T14:15:27.26Z', 'condition': 'exit 1', 'exitCode': 1}
{'timestamp': '2026-09-23T14:15:40.60Z', 'condition': 'exit 1', 'exitCode': 1}

Environment

Additional Context

Affected, with AtomicBoolean lastMatched on main:

  • plugin-script-deno: ScriptTrigger, CommandsTrigger
  • plugin-script-go: ScriptTrigger, CommandsTrigger
  • plugin-script-node: ScriptTrigger, CommandsTrigger
  • plugin-script-python: AbstractPythonTrigger
  • plugin-script-ruby: ScriptTrigger, CommandsTrigger
  • plugin-script-shell: ScriptTrigger, CommandsTrigger
  • Open PR that copies the same pattern: R (feat/r-script-commands-triggers #438)

Already fixed, to use as the reference: plugin-script-bun, plugin-script-dotnet and plugin-script-powershell store the previous result in the flow's namespace KV store (shouldEmit(...) + edgeStateKey(...)), as the plugin guidelines require for polling-trigger watermarks.

Perl (#446) was fixed the same way in 0ba7de7 before merge: re-running the same two Perl flows on the same instance, each fired once in ~100 s (10 polls) instead of once per poll.

Suggested fix:

  • Move shouldEmit / edgeStateKey from Bun into a shared helper in plugin-script. This matches the existing TODO: extract shared trigger logic … AbstractScriptTrigger.
  • Use it from every trigger and remove the lastMatched fields and Known limitation comments.
  • Add an evaluation-level test per module that calls evaluate() twice on fresh trigger instances built from the same definition, and asserts one emission. Bun's EdgeStateTest is a model.

History: this was already spotted during the reviews of the Bun (#433), .NET (#435) and PowerShell (#436) trigger PRs. Those three moved edge state to the namespace KV store. Moving the other modules over was left as a follow-up that was never tracked; this issue now tracks it.


View as Artifact

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/pluginPlugin-related issue or feature request

    Type

    Fields

    Stage

    Done

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions