Skip to content

feat/r-script-commands-triggers - #438

Merged
fdelbrayelle merged 3 commits into
kestra-io:mainfrom
Abhishek84313:feat/r-script-commands-triggers
Sep 25, 2026
Merged

fdelbrayelle merged 3 commits into
kestra-io:mainfrom
Abhishek84313:feat/r-script-commands-triggers

Conversation

@Abhishek84313

@Abhishek84313 Abhishek84313 commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

feat(r): add ScriptTrigger and CommandsTrigger to plugin-script-r

What changes are being made and why?

The R module only shipped Script and Commands tasks, so an R script could be run by a flow but could not start one. Teams that watch an external system with R (a CRAN job, a data-quality check, a health probe) had to wrap it in a Schedule plus a conditional task, or poll from outside Kestra.

This PR adds two polling triggers to plugin-script-r, bringing it in line with the Shell and Node modules:

  • io.kestra.plugin.scripts.r.ScriptTrigger — polls by running an inline R script in a container and starts the flow when the result matches a condition.
  • io.kestra.plugin.scripts.r.CommandsTrigger — same, driven by a list of R commands instead of an inline script.

Behaviour shared by both triggers:

Property Type Default Purpose
containerImage Property<String> r-base Image used by the underlying task.
script / commands Property<String> / Property<List<String>> — (required) What is executed on each poll.
exitCondition Property<String> — (required) Condition evaluated after each run.
interval Duration PT60S Time between polls.
edge Property<Boolean> true Emit only on a not matching → matching transition.

Condition matching (matchesCondition) supports two forms:

  1. exit N — case-insensitive, compares against the process exit code. A null exit code never matches.
  2. Anything else — compiled as a regex and matched against the task's emitted vars; if the pattern is invalid or takes longer than 5s to evaluate, it falls back to a plain substring check. The 5s cap exists so a user-supplied pattern cannot hang the scheduler thread through catastrophic backtracking.

Edge mode (edge: true, the default) is anti-spam: a condition that stays true across polls emits once, not on every tick. Set edge: false to emit on every matching poll.

Failure handling: a RunnableTaskException is unwrapped down the cause chain to find a TaskException and recover its exit code, so a failing script still produces a usable exitCode output instead of propagating. Any other exception during evaluation is logged at WARN and returns Optional.empty() — a broken poll never blocks the scheduler.

Outputs exposed to the flow ({{ trigger.* }}): timestamp, condition (the rendered exitCondition), exitCode, and vars.

Known limitation: the edge state (lastMatched) is an in-memory AtomicBoolean. It resets when the trigger is rehydrated, so edge mode may re-fire once after a scheduler restart. Both classes carry a TODO to extract the duplicated evaluate / matchesCondition / extractFailure / Output logic into a shared AbstractScriptTrigger in plugin-script — the duplication across Shell, Node, Ruby and R is deliberate for now and should be consolidated in a follow-up rather than inside this PR.

Files changed

File Change
plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/ScriptTrigger.java New — 294 lines.
plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/CommandsTrigger.java New — 290 lines.
plugin-script-r/src/test/java/io/kestra/plugin/scripts/r/ScriptTriggerTest.java New — edge mode + condition matching.
plugin-script-r/src/test/java/io/kestra/plugin/scripts/r/ScriptTriggerConditionTest.java New — parameterized condition matrix.
plugin-script-r/src/test/java/io/kestra/plugin/scripts/r/CommandsTriggerTest.java New — @KestraTest integration tests.
plugin-script-r/src/test/java/io/kestra/plugin/scripts/r/CommandsTriggerConditionTest.java New — parameterized condition matrix.
plugin-script-r/build.gradle Adds scheduler and worker test dependencies needed by @KestraTest.
AGENTS.md Lists the two new classes under plugin-script-r.

How the changes have been QAed?

CommandsTrigger — fires when a command exits non-zero:

id: r_commands_trigger
namespace: company.team

triggers:
  - id: on_fail
    type: io.kestra.plugin.scripts.r.CommandsTrigger
    interval: PT5S
    exitCondition: "exit 1"
    commands:
      - Rscript -e 'stop("boom")'

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"

ScriptTrigger — inline R script, edge mode on:

id: r_script_trigger
namespace: company.team

triggers:
  - id: script_failure
    type: io.kestra.plugin.scripts.r.ScriptTrigger
    interval: PT10S
    exitCondition: "exit 1"
    edge: true
    script: |
      stop("boom")

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"

ScriptTrigger — match on structured outputs rather than exit code:

id: r_script_trigger_vars
namespace: company.team

triggers:
  - id: on_ready
    type: io.kestra.plugin.scripts.r.ScriptTrigger
    interval: PT30S
    exitCondition: "status=\\w+"
    edge: true
    script: |
      cat('::{"outputs":{"status":"status=ready"}}::\n')

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "vars={{ trigger.vars }}"

Automated coverage

CommandsTriggerTest is a @KestraTest that runs the trigger end to end against a real r-base container and asserts on the generated execution:

  • a command exiting 1 against exitCondition: "exit 1" emits, with exitCode == 1, condition == "exit 1" and a non-null timestamp;
  • a command emitting ::{"outputs":{"listing":"toto"}}:: against exitCondition: "toto" emits, with exitCode == 0 and non-null vars;
  • a command exiting 0 against exitCondition: "exit 1" does not emit.

ScriptTriggerTest, ScriptTriggerConditionTest and CommandsTriggerConditionTest cover matchesCondition and edge-mode transitions as pure unit tests, driving the Output model directly. They deliberately avoid needing an R runtime, since it is not present on every CI machine — the runtime-backed coverage lives in CommandsTriggerTest.

Condition cases asserted: exit 0/exit 1/exit 42 matching and mismatching, uppercase EXIT 1 (case-insensitivity), null exit code, substring match in vars, absent substring, regex match in vars, empty haystack, empty condition, and null condition.

Run them with:

./gradlew :plugin-script-r:test

Setup Instructions

  • Docker must be available to the Kestra worker — both triggers execute through a container, defaulting to r-base.
  • The r-base image is pulled from Docker Hub on first use; no credentials needed.
  • For scripts that need CRAN packages, either point containerImage at an image that already includes them or install them inside the script/commands.
  • ./gradlew :plugin-script-r:test needs Docker running for CommandsTriggerTest; the three unit test classes run without it.

Contributor Checklist ✅

closes #432

@github-project-automation github-project-automation Bot moved this to To review in Pull Requests Sep 16, 2026
@MilosPaunovic MilosPaunovic added kind/external Pull requests raised by community contributors area/plugin Plugin-related issue or feature request labels Sep 18, 2026
@MilosPaunovic
MilosPaunovic requested review from a team and fdelbrayelle September 18, 2026 05:38

@fdelbrayelle fdelbrayelle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kestra Plugin Code Review

Business Requirements — met

No linked issue is referenced in the PR body. The stated goal (bring plugin-script-r trigger support to parity with the Shell/Node modules by adding ScriptTrigger/CommandsTrigger) is self-consistent and fully implemented, with tests and QA examples matching the description.

Kestra Guidelines — 4 findings

  • 🟠 TODO placeholders left in production code (both new files) — forbidden by the Code Comments guideline.
  • 🟠 lastMatched (AtomicBoolean) not excluded from Lombok @ToString/@EqualsAndHashCode (both files) — correctness bug, see inline comment.
  • 🟠 Plugin how-to doc plugin-script-r/src/main/resources/doc/io.kestra.plugin.scripts.r.md not updated to mention the two new triggers — mandatory per the 'Plugin How-To Doc' section whenever a trigger is added to an existing plugin. Node's equivalent doc has a one-line mention of its CommandsTrigger/ScriptTrigger; the R doc still only lists Script/Commands.
  • 🟡 Required exitCondition is rendered with .orElse("") (both files) instead of failing loudly on an unrenderable value — see inline comment.

Security (OWASP Top 10:2025 + KPS) — 0 blocking issues

No secret fields, no new HTTP calls, no unsafe deserialization, no shell string concatenation beyond what the existing Commands/Script tasks already do (out of scope of this diff). The regex-timeout resource-leak noted under Performance has a DoS angle (A10-adjacent) but is tracked there since its primary consequence is thread-pool exhaustion, not an authorization/injection gap.

Performance — 1 finding

  • 🟠 ReDoS guard in matchesCondition abandons the backtracking regex thread on the shared ForkJoinPool.commonPool() instead of cancelling it — see inline comments on both files.

Additional non-blocking notes

  • 🟡 Edge-mode tests (edgeMode_preventsConsecutiveEmit in CommandsTriggerTest, and the three edgeMode_* tests in ScriptTriggerTest) re-implement !lastMatched.getAndSet(x) && x inline rather than calling trigger.evaluate() twice on the same instance. They verify AtomicBoolean arithmetic, not the trigger's actual edge-mode wiring — a regression in evaluate()'s edge logic wouldn't be caught by these tests. Given edge mode is this PR's headline feature, at least one @KestraTest should call evaluate() twice on the same trigger instance and assert the second call returns Optional.empty().
  • 🟡 DRY: CommandsTrigger/ScriptTrigger duplicate ~90% of their logic (evaluate, matchesCondition, extractFailure, Output). This mirrors pre-existing duplication already merged for Shell/Node/Ruby, so it's not net-new debt introduced by this PR, and the TODO (flagged above for violating the no-TODO-comment rule) at least names the follow-up. Non-blocking, but worth tracking as a real issue rather than a comment.
  • 🟢 Nit: trigger/task ids in tests are fixed strings ("commands-trigger", …) rather than randomized, per the Flaky Test Prevention guideline — mirrors existing Node/Shell test precedent, so not introduced by this PR.

Verdict: REQUEST CHANGES

Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/CommandsTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/CommandsTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/CommandsTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/CommandsTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/ScriptTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/ScriptTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/ScriptTrigger.java Outdated
Comment thread plugin-script-r/src/main/java/io/kestra/plugin/scripts/r/ScriptTrigger.java Outdated
jymaire added a commit to Abhishek84313/plugin-scripts that referenced this pull request Sep 22, 2026
The R ScriptTrigger/CommandsTrigger in this branch are byte-for-byte
identical to PR kestra-io#438, which is dedicated to the R module. Remove them
here so kestra-io#446 is scoped to the Perl triggers and kestra-io#438 owns the R work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jymaire added a commit that referenced this pull request Sep 23, 2026
* feat/r-script-commands-triggers

* feat/perl-script-commands-triggers

* chore(r): drop R triggers, keep them in #438

The R ScriptTrigger/CommandsTrigger in this branch are byte-for-byte
identical to PR #438, which is dedicated to the R module. Remove them
here so #446 is scoped to the Perl triggers and #438 owns the R work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(perl): guard trigger exitCondition regex against ReDoS

Run the user-supplied exitCondition regex with a 5s timeout and fall
back to substring matching, as the Ruby/Shell/Node/Bun triggers do, so a
catastrophic-backtracking pattern can no longer hang the scheduler poll
thread. Document the in-memory edge-state limitation and add condition
tests for pathological and invalid regexes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(perl): make ReDoS test hit the timeout and fix CommandsTrigger example

(a+)+$ is memoized by the JDK 25 regex engine and fails instantly, so the
test never reached the 5s guard; use (.*a){20}$ and assert the elapsed
time. The CommandsTrigger example ran `perl missing.pl`, which exits 2 and
never matched `exit 1`; use `perl -e 'exit 1'` instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(perl): persist trigger edge state in the namespace KV store

The in-memory lastMatched flag was rebuilt with the trigger on every poll,
so edge mode fired on every matching poll. Keep the previous result in the
namespace KV store instead, as the Bun/.NET/PowerShell triggers do.

Replaces the tautological AtomicBoolean edge tests with EdgeStateTest and
an evaluate-level test that polls through a serialized copy.

Refs #449

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: jymaire <jmaire@kestra.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: jymaire <jymaire@users.noreply.github.com>
@fdelbrayelle

Copy link
Copy Markdown
Member

Hello @Abhishek84313 👋 Any news on the code review feedback? Thanks!

Aligns the R triggers with the pattern merged for Bun in kestra-io#433.

- Persist edge state in the namespace KV store instead of an in-memory
  AtomicBoolean. This drops the field the review flagged for breaking
  Lombok equals/hashCode (AtomicBoolean has identity equality), and also
  fixes kestra-io#449: a rebuilt or rehydrated trigger no longer re-fires.
- Replace the CompletableFuture regex timeout with a bounded LRU cache of
  compiled patterns. The abandoned future leaked a ForkJoinPool.commonPool()
  worker on timeout, since Matcher#find() is not interruptible.
- Fail loudly when the required exitCondition renders empty, instead of
  silently coercing it to "" and never matching.
- Remove the TODO comments from both trigger classes.
- Document both triggers in the plugin how-to doc.
- Replace the edge-mode tests that asserted AtomicBoolean arithmetic with
  tests that drive shouldEmit and evaluate() for real, and randomize
  trigger ids now that edge state persists between runs.
- Revert the scheduler/worker test dependencies, which are not needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Abhishek84313

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @fdelbrayelle, and sorry for the delay. All findings are addressed in 8fe149d.

While rebasing I noticed #433 (Bun triggers) had landed on main with a design that already solves most of what you flagged, so rather than patching each comment in isolation I aligned the R triggers to that merged precedent. The two modules now match.

@fdelbrayelle

Copy link
Copy Markdown
Member

Follow-up Review — Verification of Prior Feedback (commit 8fe149d)

Re-checked every item raised in review #5266634945 against the current head (8fe149d, per the author's note that R triggers were realigned to the merged Bun trigger precedent, #433).

Per-item status

# Item (file) Status Evidence
1 TODO placeholder — CommandsTrigger.java ADDRESSED No TODO strings remain anywhere in the file; grep -i todo on all changed Java files returns nothing.
2 lastMatched AtomicBoolean not excluded from @EqualsAndHashCode/@ToString — CommandsTrigger.java ADDRESSED The mutable field was removed entirely. Edge state is no longer held on the trigger instance; it's persisted via runContext.namespaceKv(...) in shouldEmit(...) (CommandsTrigger.java:165-182), so there is no non-config runtime field left for Lombok to touch.
3 Abandoned CompletableFuture leaks a thread on ForkJoinPool.commonPool() — CommandsTrigger.java ADDRESSED matchesCondition (CommandsTrigger.java:223-243) no longer uses CompletableFuture/timeouts/async dispatch at all — it's a plain synchronous Matcher.find() against a bounded (MAX_CACHED_CONDITIONS = 64, LRU-evicted) pattern cache. No future, no leak. See new finding below re: the tradeoff this introduces.
4 Required exitCondition silently defaults to "" via .orElse("") — CommandsTrigger.java ADDRESSED CommandsTrigger.java:198-201 now uses .filter(condition -> !condition.isBlank()).orElseThrow(() -> new IllegalArgumentException("exitCondition must render to a non-empty value")).
5 TODO placeholder — ScriptTrigger.java ADDRESSED Same as #1, verified on ScriptTrigger.java.
6 lastMatched AtomicBoolean not excluded — ScriptTrigger.java ADDRESSED Same fix as #2 (ScriptTrigger.java:161-178).
7 Abandoned future thread leak — ScriptTrigger.java ADDRESSED Same fix as #3 (ScriptTrigger.java:219-239).
8 exitCondition .orElse("") — ScriptTrigger.java ADDRESSED Same fix as #4 (ScriptTrigger.java:194-197).
9 Plugin how-to doc not updated (review body, non-inline) ADDRESSED plugin-script-r/src/main/resources/doc/io.kestra.plugin.scripts.r.md now has a ## Triggers section documenting both ScriptTrigger and CommandsTrigger, their required/optional properties, and output variables.
10 Edge-mode tests only exercise AtomicBoolean arithmetic, not real evaluate() wiring (review body, non-blocking suggestion) ADDRESSED (bonus) CommandsTriggerTest.edgeMode_preventsConsecutiveEmit and ScriptTriggerTest's equivalent now call trigger.evaluate(...) twice on the same instance against a real (Dockerized) run and assert the second call returns Optional.empty(). A new EdgeStateTest.java also covers shouldEmit cross-flow/cross-trigger scoping and KV-key collision safety directly.
11 DRY duplication between CommandsTrigger/ScriptTrigger (review body, non-blocking) NOT ADDRESSED (acknowledged, non-blocking) Still ~90% structural duplication (evaluate, shouldEmit, matchesCondition, extractFailure, Output). Author's stated rationale: this now matches the merged Bun precedent, so it's consistent with the codebase rather than net-new debt. Not re-flagged as blocking.
12 Fixed (non-randomized) trigger/task ids in tests (nit, review body) ADDRESSED All test builders now suffix ids with IdUtils.create() (e.g. "commands-trigger-" + IdUtils.create()), avoiding the KV-store collisions the nit warned about — also fixes a latent flakiness risk since the KV store persists between test runs.

Threads resolved (8/8)

All 8 previously unresolved inline review threads were verified against current code and resolved via GraphQL:

  • PRRT_kwDOE5QdFs6kWa7J — TODO, CommandsTrigger.java
  • PRRT_kwDOE5QdFs6kWa7O — AtomicBoolean equals/hashCode, CommandsTrigger.java
  • PRRT_kwDOE5QdFs6kWa7V — abandoned future, CommandsTrigger.java
  • PRRT_kwDOE5QdFs6kWa7c — orElse(""), CommandsTrigger.java
  • PRRT_kwDOE5QdFs6kWa7m — TODO, ScriptTrigger.java
  • PRRT_kwDOE5QdFs6kWa7v — AtomicBoolean equals/hashCode, ScriptTrigger.java
  • PRRT_kwDOE5QdFs6kWa74 — abandoned future, ScriptTrigger.java
  • PRRT_kwDOE5QdFs6kWa8E — orElse(""), ScriptTrigger.java

Item #11 (DRY) has no thread — it was a review-body-only note, left open as tracked-but-non-blocking per the author's comment.

New findings from this pass

  • 🟡 Suggestion — ReDoS mitigation removed, not replaced (CommandsTrigger.java:223-243, ScriptTrigger.java:219-239): the fix for the "abandoned future" leak was to drop the async/timeout path entirely rather than replace it with a cancellable bounded executor. matchesCondition now runs conditionPattern(cond).matcher(haystack).find() fully synchronously with no timeout guard. A pathological exitCondition regex with catastrophic backtracking (e.g. (a+)+$-style) against a large vars haystack will now hang the polling thread indefinitely instead of leaking a common-pool thread. This is a lower-severity issue than the original (single dedicated poll thread blocks vs. JVM-wide common-pool starvation) and exitCondition is flow-author-controlled config rather than external attacker input, so it's not a merge blocker — but worth a follow-up to wrap the match in a bounded, cancellable executor (not the shared common pool) if this pattern is reused in other script-language triggers going forward.
  • No other new blocking issues found in CommandsTrigger.java, ScriptTrigger.java, the four test files, the updated doc, or the AGENTS.md index-entry diff. Bounded LinkedHashMap LRU cache for compiled patterns, KV-store edge-state scoping (flow+trigger keyed, length-prefixed to avoid collisions), and the .orElseThrow render-failure handling all check out.

Verdict: Ready to merge

All 8 blocking/request-changes items from the prior review are fixed and verified against current code, tests were strengthened beyond what was asked, and the lone remaining note (DRY duplication) is an accepted, tracked, non-blocking design choice consistent with merged precedent (#433). The one new observation (ReDoS timeout removal) is a non-blocking suggestion for a future follow-up, not a regression severe enough to hold this PR.

Note: PR mergeable_state is currently blocked — this is because the CHANGES_REQUESTED review is still outstanding and CI (check / main) is pending, not because of any unresolved code issue. Re-approving (or otherwise dismissing the stale review) is a manual step outside this comment's scope.

@fdelbrayelle

fdelbrayelle commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

🧪 QA report: R ScriptTrigger / CommandsTrigger

Edition: OSS | Docker tag: develop (next: 2.1.0-SNAPSHOT)
Plugin: plugin-script-r 1.10.1-SNAPSHOT built from PR head 8fe149d
Instance: http://i4cqu.kestra.docker.localhost:1355/ui/ (left running for manual review, docker rm -f kestra-i4cqu to stop)
Screenshots: https://claude.ai/artifact/G39nzMaeDsaP1JA5ZiVMo8 (private internal Kestra gallery)

Summary

# Flow Type Scenario Result
1 r_commands_trigger NEW, source @Example CommandsTrigger, exit 1, default edge ✅ fired once, SUCCESS
2 r_script_trigger NEW, source @Example ScriptTrigger, exit 1, edge: true ✅ fired once over ~5 min of 10s polls
3 r_script_trigger_vars NEW ScriptTrigger, regex status=\w+ on vars ✅ fired once, vars exposed
4 r_script_no_edge NEW ScriptTrigger, exit 1, edge: false, PT5S ✅ fired every poll (47 executions in ~4.5 min)
5 r_commands_no_match NEW (negative) CommandsTrigger, cmd exits 0 vs exit 1 ✅ 0 executions
6 r_script_task Non-regression R Script + Commands tasks ✅ SUCCESS

First trigger executions appeared 7–12s after flow creation (well inside the 30s budget). No timeouts.

Flow 1: r_commands_trigger (✅ SUCCESS)

Flow YAML
id: r_commands_trigger
namespace: company.team

triggers:
  - id: on_fail
    type: io.kestra.plugin.scripts.r.CommandsTrigger
    interval: PT5S
    exitCondition: "exit 1"
    commands:
      - Rscript -e 'stop("boom")'

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"

Gantt (screenshot)

Task Status Duration
log SUCCESS 0.04s
Total SUCCESS 0.07s

Logs synthesis: Triggered with exitCode=1 (condition=exit 1). Only 1 execution over the whole run: edge defaults to true as documented.

Outputs synthesis (screenshot): trigger vars timestamp, condition: "exit 1", exitCode: 1. No vars key (script emitted none).

Flow 2: r_script_trigger (✅ SUCCESS)

Flow YAML
id: r_script_trigger
namespace: company.team

triggers:
  - id: script_failure
    type: io.kestra.plugin.scripts.r.ScriptTrigger
    interval: PT10S
    exitCondition: "exit 1"
    edge: true
    script: |
      stop("boom")

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"

Gantt (screenshot)

Task Status Duration
log SUCCESS 0.03s
Total SUCCESS 0.08s

Logs synthesis: Triggered with exitCode=1 (condition=exit 1). Condition stayed true on every later poll, but no second execution: edge state (now in KV store) works.

Outputs synthesis (screenshot): condition: "exit 1", exitCode: 1, timestamp.

Flow 3: r_script_trigger_vars (✅ SUCCESS)

Flow YAML
id: r_script_trigger_vars
namespace: company.team

triggers:
  - id: on_ready
    type: io.kestra.plugin.scripts.r.ScriptTrigger
    interval: PT10S
    exitCondition: "status=\\w+"
    edge: true
    script: |
      cat('::{"outputs":{"status":"status=ready"}}::\n')

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "vars={{ trigger.vars }} exitCode={{ trigger.exitCode }}"

Gantt (screenshot)

Task Status Duration
log SUCCESS 0.03s
Total SUCCESS 0.06s

Logs synthesis: vars={"status":"status=ready"} exitCode=0. Regex matched against emitted vars.

Outputs synthesis (screenshot): condition: "status=\w+", exitCode: 0, vars: {status: "status=ready"}.

Flow 4: r_script_no_edge (✅ SUCCESS)

Flow YAML
id: r_script_no_edge
namespace: company.team

triggers:
  - id: every_fail
    type: io.kestra.plugin.scripts.r.ScriptTrigger
    interval: PT5S
    exitCondition: "exit 1"
    edge: false
    script: |
      stop("boom")

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered at {{ trigger.timestamp }} exitCode={{ trigger.exitCode }}"

Gantt (screenshot), first of 47 executions

Task Status Duration
log SUCCESS 0.03s
Total SUCCESS 0.07s

Logs synthesis: Triggered at <timestamp> exitCode=1, one execution roughly every 5–7s. All 47 SUCCESS.

Outputs synthesis (screenshot): condition: "exit 1", exitCode: 1, distinct timestamp per execution.

Flow 5: r_commands_no_match (✅ no execution, as expected)

Flow YAML
id: r_commands_no_match
namespace: company.team

triggers:
  - id: never
    type: io.kestra.plugin.scripts.r.CommandsTrigger
    interval: PT5S
    exitCondition: "exit 1"
    commands:
      - Rscript -e 'quit(status = 0)'

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Should never run"

Result: 0 executions over ~5 min (executions list). No WARN in server logs.

Flow 6: r_script_task, non-regression (✅ SUCCESS)

Flow YAML
id: r_script_task
namespace: company.team

tasks:
  - id: script
    type: io.kestra.plugin.scripts.r.Script
    script: |
      x <- sum(1:10)
      cat(sprintf('::{"outputs":{"sum":%d}}::\n', x))

  - id: commands
    type: io.kestra.plugin.scripts.r.Commands
    commands:
      - Rscript -e 'cat(R.version.string, "\n")'

Gantt (screenshot)

Task Status Duration
script SUCCESS 1.63s
commands SUCCESS 0.86s
Total SUCCESS 2.61s

Logs synthesis: both containers created, ran with exit code 0, and were cleaned up. commands logged R version 4.6.1 (2026-06-24).

Outputs synthesis (screenshot): script returned vars: {sum: 55}, exitCode: 0, outputFiles: {}. commands returned exitCode: 0.

Notes

  • No topology checks: PR has no UI/artifact changes.
  • Not covered: edge behaviour across a scheduler restart (KV persistence). Unit test EdgeStateTest covers it.
  • Timeouts: none.

@fdelbrayelle
fdelbrayelle merged commit 9b3a6cc into kestra-io:main Sep 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/plugin Plugin-related issue or feature request kind/external Pull requests raised by community contributors

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Introduce ScriptTrigger & CommandsTrigger for the R plugin

3 participants