Skip to content

feat(dotnet): add F# script execution support - #460

Merged
fdelbrayelle merged 8 commits into
kestra-io:mainfrom
GauriKhedekar:feat/dotnet-fsharp-script-support
Oct 12, 2026
Merged

fdelbrayelle merged 8 commits into
kestra-io:mainfrom
GauriKhedekar:feat/dotnet-fsharp-script-support

Conversation

@GauriKhedekar

@GauriKhedekar GauriKhedekar commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

What changes are being made and why?

This PR adds native F# script support to the existing plugin-script-dotnet module and restructures the public package layout so that C# and F# are exposed as separate packages.

The existing Gradle module remains plugin-script-dotnet, and the module/catalog title is now .NET (C# and F#).

The new public language packages are:

io.kestra.plugin.scripts.csharp
io.kestra.plugin.scripts.fsharp

C# package

The existing C# implementation has been moved from io.kestra.plugin.scripts.dotnet to io.kestra.plugin.scripts.csharp, with:

  • io.kestra.plugin.scripts.csharp.Script
  • io.kestra.plugin.scripts.csharp.Commands
  • io.kestra.plugin.scripts.csharp.ScriptTrigger
  • io.kestra.plugin.scripts.csharp.CommandsTrigger

A dedicated C# package-info.java was added with the package title C#.

F# package

A new dedicated io.kestra.plugin.scripts.fsharp package was added with:

  • io.kestra.plugin.scripts.fsharp.Script
  • io.kestra.plugin.scripts.fsharp.Commands
  • io.kestra.plugin.scripts.fsharp.ScriptTrigger
  • io.kestra.plugin.scripts.fsharp.CommandsTrigger

A dedicated F# package-info.java was added with the package title F#.

The temporary io.kestra.plugin.scripts.dotnet.FSharp task has been removed and replaced by these four F# task types.

F# Script executes .fsx files using dotnet fsi and supports inline scripts, NuGet #r references, input/output files, beforeCommands, custom container images, task runners, and workflow variables.


Shared implementation

Shared .NET functionality was extracted into:

AbstractDotnetScript
AbstractDotnetCommands
AbstractDotnetTrigger

These common classes provide shared behavior for C# and F#, including:

  • default .NET SDK container image handling
  • container image defaults
  • command defaults
  • trigger interval handling
  • exit-condition evaluation
  • structured output/vars handling
  • trigger edge-state handling
  • common trigger output handling
  • target operating system propagation

The language-specific implementations reuse these shared base classes instead of duplicating the common logic.


Backward compatibility

The new C# classes expose aliases for the previous C# task names so existing workflows continue to work:

io.kestra.plugin.scripts.dotnet.Script
    -> io.kestra.plugin.scripts.csharp.Script

io.kestra.plugin.scripts.dotnet.Commands
    -> io.kestra.plugin.scripts.csharp.Commands

io.kestra.plugin.scripts.dotnet.ScriptTrigger
    -> io.kestra.plugin.scripts.csharp.ScriptTrigger

io.kestra.plugin.scripts.dotnet.CommandsTrigger
    -> io.kestra.plugin.scripts.csharp.CommandsTrigger

A BackwardCompatibilityTest was added to deserialize a flow using the old io.kestra.plugin.scripts.dotnet.Script type and verify that it resolves to the new C# implementation.

The old .dotnet.* names are retained only for compatibility; the canonical public classes and documentation use the csharp and fsharp packages.


Metadata and catalog

The existing plugin-script-dotnet module metadata was updated from C#-only wording to .NET (C# and F#).

Separate metadata entries were added for:

io.kestra.plugin.scripts.csharp
io.kestra.plugin.scripts.fsharp

with package titles C# and F#.

Each package has its own package-info.java, allowing separate catalog/documentation entries while remaining inside the existing .NET module.


Documentation

The previous combined .NET documentation was split into:

io.kestra.plugin.scripts.csharp.md
io.kestra.plugin.scripts.fsharp.md

The C# page remains focused on the existing C# implementation and uses the new csharp package names.

The F# page documents:

  • Script
  • Commands
  • ScriptTrigger
  • CommandsTrigger
  • .fsx execution with dotnet fsi
  • NuGet #r "nuget:PackageName,Version" references
  • input/output files
  • triggers and exit conditions
  • Hello World, NuGet, and output-file examples

The general .NET documentation page now explains the C# and F# package structure and links to the language-specific pages.

The outdated io.kestra.plugin.scripts.dotnet.md page was removed.

The unrelated bullet-style change in the old C# documentation was reverted so the documentation diff stays focused.


Icons

The existing module-level icons remain unchanged:

io.kestra.plugin.scripts.dotnet.svg
plugin-icon.svg

The C# package has:

io.kestra.plugin.scripts.csharp.svg

The existing C# artwork is retained for the C# package.

The F# package has:

io.kestra.plugin.scripts.fsharp.svg

The F# icon was replaced with an actual F# logo so the new F# package is visually distinct from the C# package.

Thus the existing .NET module resources remain intact while the new language packages have package-specific icons.


Tests and test restructuring

The existing C# tests were moved to the new csharp package and updated to use the new canonical task types.

New F# tests mirror the C# coverage, including:

  • basic F# script execution
  • F# NuGet dependency execution
  • F# output-file handling
  • F# script failure handling
  • F# commands behavior
  • F# commands-trigger behavior
  • F# script-trigger evaluation
  • exit-code conditions
  • regex/structured-output matching
  • trigger edge behavior

The existing trigger/edge-state tests were updated to use the shared .NET trigger implementation.

A dedicated backward-compatibility test was added for the old C# task name.


Sanity checks

The all_dotnet.yaml sanity-check flow was updated to use the new canonical C# package:

io.kestra.plugin.scripts.csharp.Script
io.kestra.plugin.scripts.csharp.Commands

The Linux target OS is explicitly configured for the Docker-based sanity checks.


AGENTS.md

AGENTS.md was updated so the plugin inventory lists the new canonical public packages:

plugin-script-dotnet

C#:
io.kestra.plugin.scripts.csharp.Commands
io.kestra.plugin.scripts.csharp.CommandsTrigger
io.kestra.plugin.scripts.csharp.Script
io.kestra.plugin.scripts.csharp.ScriptTrigger

F#:
io.kestra.plugin.scripts.fsharp.Commands
io.kestra.plugin.scripts.fsharp.CommandsTrigger
io.kestra.plugin.scripts.fsharp.Script
io.kestra.plugin.scripts.fsharp.ScriptTrigger

The old .dotnet.* names are no longer listed there as the canonical classes. They remain only in the compatibility aliases and compatibility test.


How the changes have been QAed?

QA was performed locally using Kestra v2.0.4 and the final plugin-script-dotnet JAR built from this branch.

Full plugin test suite

.\gradlew.bat :plugin-script-dotnet:test --max-workers=1

Result:

69 total
69 passed
0 failed
0 skipped
BUILD SUCCESSFUL

Documentation lint

.\gradlew.bat :plugin-script-dotnet:lintPluginDocs

Result:

[plugin-doc-lint] All 25 rules passed.
BUILD SUCCESSFUL

Diff validation

git diff --cached --check

No whitespace errors were reported.

Local Kestra UI verification

The final plugin JAR was built and loaded into a local Kestra v2.0.4 instance.

The plugin catalog was verified to show:

.NET (C# and F#)

with 8 tasks:

C#:
- Commands
- CommandsTrigger
- Script
- ScriptTrigger

F#:
- Commands
- CommandsTrigger
- Script
- ScriptTrigger

The final JAR was used for the functional UI checks below.

F# Hello World

id: fsharp_hello_world
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      printfn "Hello from F# and Kestra!"

Result: successful execution with Hello from F# and Kestra! in the task logs.

F# NuGet dependency

id: fsharp_nuget
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      #r "nuget:Newtonsoft.Json,13.0.3"

      open Newtonsoft.Json

      let data = {| message = "Hello from Kestra" |}
      printfn "%s" (JsonConvert.SerializeObject(data))

Result: successful execution using the restored NuGet dependency and expected JSON output.

F# output files

id: fsharp_generate_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    outputFiles:
      - hello.txt
    script: |
      System.IO.File.WriteAllText("hello.txt", "Hello from F#!")
      printfn "Created hello.txt successfully."

Result: successful execution; the log contained Created hello.txt successfully. and the persisted task output contained the hello.txt key.

C# backward compatibility

id: old_csharp_compatibility
namespace: company.team

tasks:
  - id: hello
    type: io.kestra.plugin.scripts.dotnet.Script
    targetOS: LINUX
    script: |
      Console.WriteLine("Hello from old C# type!")

Result: successful execution with Hello from old C# type!, confirming the old type resolves to the new C# implementation.


Screenshots — package/task catalog verification

Before Implementing F# :-

Screenshot 2026-10-04 001119

After Implementing F#:-

These 8 screenshots demonstrate the four C# and four F# task types in the final catalog.

Screenshot 2026-10-05 195303

1. C# Script

io.kestra.plugin.scripts.csharp.Script
Screenshot 2026-10-05 212014

Shows the C# inline .csx script task under the new csharp package.


2. C# Commands

io.kestra.plugin.scripts.csharp.Commands
Screenshot 2026-10-05 211924

Shows the C# command execution task under the new csharp package.


3. C# ScriptTrigger

io.kestra.plugin.scripts.csharp.ScriptTrigger
Screenshot 2026-10-05 212024

Shows the C# script trigger under the new csharp package.


4. C# CommandsTrigger

io.kestra.plugin.scripts.csharp.CommandsTrigger
Screenshot 2026-10-05 212002

Shows the C# commands trigger under the new csharp package.


5. F# Script

io.kestra.plugin.scripts.fsharp.Script
Screenshot 2026-10-05 212101

Shows the new F# inline .fsx script task and its dotnet fsi documentation.


6. F# Commands

io.kestra.plugin.scripts.fsharp.Commands
Screenshot 2026-10-05 212043

Shows the F# command execution task under the new fsharp package.


7. F# ScriptTrigger

io.kestra.plugin.scripts.fsharp.ScriptTrigger
Screenshot 2026-10-05 212111

Shows the F# script trigger under the new fsharp package.


8. F# CommandsTrigger

io.kestra.plugin.scripts.fsharp.CommandsTrigger
Screenshot 2026-10-05 212050

Shows the F# commands trigger under the new fsharp package.


Screenshots — functional QA

1. F# Hello World

The final JAR was tested in the local Kestra UI with:

id: fsharp_hello_world
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      printfn "Hello from F# and Kestra!"
Screenshot 2026-10-05 195455 Screenshot 2026-10-05 195519

The task completed successfully and produced Hello from F# and Kestra! in the logs.


2. F# NuGet dependency

The final JAR was tested with an F# #r NuGet directive:

id: fsharp_nuget
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      #r "nuget:Newtonsoft.Json,13.0.3"

      open Newtonsoft.Json

      let data = {| message = "Hello from Kestra" |}
      printfn "%s" (JsonConvert.SerializeObject(data))
Screenshot 2026-10-05 195553 Screenshot 2026-10-05 195629

The dependency was restored successfully and the script produced the expected JSON output.


3. F# output files

The final JAR was tested with:

id: fsharp_generate_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    outputFiles:
      - hello.txt
    script: |
      System.IO.File.WriteAllText("hello.txt", "Hello from F#!")
      printfn "Created hello.txt successfully."
Screenshot 2026-10-05 195721 Screenshot 2026-10-05 195752

The task completed successfully, the log contained Created hello.txt successfully., and the task output contained the persisted hello.txt key.


4. C# backward compatibility

Backward compatibility was verified using the old public type:

id: old_csharp_compatibility
namespace: company.team

tasks:
  - id: hello
    type: io.kestra.plugin.scripts.dotnet.Script
    targetOS: LINUX
    script: |
      Console.WriteLine("Hello from old C# type!")
Screenshot 2026-10-05 195917 Screenshot 2026-10-05 200030

The old type executed successfully and produced Hello from old C# type!, confirming the alias resolves the old .dotnet.Script name to the new csharp.Script implementation.


Final package layout

The resulting package structure is:

io.kestra.plugin.scripts.csharp
├── package-info.java
├── Script
├── Commands
├── ScriptTrigger
└── CommandsTrigger

io.kestra.plugin.scripts.fsharp
├── package-info.java
├── Script
├── Commands
├── ScriptTrigger
└── CommandsTrigger

The existing module remains:

plugin-script-dotnet

with the module/catalog title:

.NET (C# and F#)

Existing C# flows using the old io.kestra.plugin.scripts.dotnet.* names remain supported through aliases.


Setup Instructions

No external API keys, accounts, or third-party services are required for the F# task itself.

For local QA:

  1. Build the final plugin JAR from this branch.
  2. Load the JAR into a local Kestra v2.0.4 instance.
  3. Restart Kestra so the final plugin metadata, package structure, documentation and icons are loaded.
  4. Verify the .NET (C# and F#) catalog entry and all 8 language tasks.
  5. Execute the F# Hello World, NuGet, and output-file flows.
  6. Execute the old io.kestra.plugin.scripts.dotnet.Script compatibility flow.

Contributor Checklist ✅

  • I have read and followed the plugin contribution guidelines.
  • F# is provided as io.kestra.plugin.scripts.fsharp.
  • F# provides Script, Commands, ScriptTrigger, and CommandsTrigger.
  • C# is provided as io.kestra.plugin.scripts.csharp.
  • Existing C# .dotnet.* type names remain supported through aliases.
  • A backward-compatibility deserialization test was added.
  • Shared .NET execution and trigger logic was extracted into common base classes.
  • C# and F# have separate package-info.java files.
  • C# and F# have separate metadata/catalog entries.
  • C# and F# have separate documentation pages.
  • The outdated io.kestra.plugin.scripts.dotnet.md documentation page was removed.
  • F# NuGet, output-file, and failure cases are covered by tests.
  • The complete plugin-script-dotnet test suite passes: 69/69.
  • Plugin documentation lint passes: 25/25 rules.
  • The staged diff passes git diff --cached --check.
  • The final plugin JAR was verified in local Kestra v2.0.4.
  • The .NET (C# and F#) catalog entry and all 8 language tasks were verified in the Kestra UI.
  • The F# package now has a dedicated F# icon.
  • AGENTS.md was updated to list the new canonical C# and F# packages.

Closes kestra-io/kestra#12742

@fdelbrayelle

Copy link
Copy Markdown
Member

@GauriKhedekar Could you take example on C# for the task names please? You're supposed to have Script, Commands, ScriptTrigger and CommandsTrigger, thanks.

@GauriKhedekar

Copy link
Copy Markdown
Contributor Author

@GauriKhedekar Could you take example on C# for the task names please? You're supposed to have Script, Commands, ScriptTrigger and CommandsTrigger, thanks.

Thanks! I currently added the dedicated FSharp task, but I understand that the F# support should follow the same task structure as the existing C# implementation with Script, Commands, ScriptTrigger, and CommandsTrigger. I'll update the F# implementation accordingly.

@GauriKhedekar

GauriKhedekar commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor Author

@GauriKhedekar Could you take example on C# for the task names please? You're supposed to have Script, Commands, ScriptTrigger and CommandsTrigger, thanks.

Just to confirm the expected structure before I make the changes: should F# be added as a separate fsharp package/module with the four task names Script, Commands, ScriptTrigger, and CommandsTrigger, while keeping the existing C# classes unchanged? Also, should the plugin metadata remain combined as .NET (C# and F#), or should F# have separate plugin metadata?

@fdelbrayelle

fdelbrayelle commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Yes, your understanding is right. Please add F# as a separate io.kestra.plugin.scripts.fsharp package, inside the existing plugin-script-dotnet module, with Script, Commands, ScriptTrigger and CommandsTrigger. Replace the current dotnet.FSharp task with these four, and mirror the C# tests (keeping the NuGet, output-file and failure cases).

For metadata, give F# its own package-info.java (title "F#") so it gets its own docs page and catalog entry. The module and group can stay ".NET (C# and F#)". Extract any logic shared with C# into a common base class rather than duplicating it.

Since Script, Commands, etc. are currently taken by C# in io.kestra.plugin.scripts.dotnet, we'd also like to make the layout symmetrical:

  • Move the existing C# classes from io.kestra.plugin.scripts.dotnet to a new io.kestra.plugin.scripts.csharp package (with its own package-info.java, title "C#").
  • Add aliases on the C# tasks (@Plugin(aliases = "io.kestra.plugin.scripts.dotnet.Script"), and the same for Commands, ScriptTrigger and CommandsTrigger) so existing flows using the old io.kestra.plugin.scripts.dotnet.* types keep working.
  • Update tests and docs to use the new csharp types.

Example for the alias (same on Commands, ScriptTrigger and CommandsTrigger):

package io.kestra.plugin.scripts.csharp;

@Plugin(
    examples = { /* ... */ },
    aliases = "io.kestra.plugin.scripts.dotnet.Script"
)
public class Script extends AbstractExecScript implements RunnableTask<ScriptOutput> {
    // ...
}

A flow using type: io.kestra.plugin.scripts.dotnet.Script will then resolve to io.kestra.plugin.scripts.csharp.Script. Please add a small test that deserializes a flow using the old type.

Thanks!

@fdelbrayelle fdelbrayelle added area/plugin Plugin-related issue or feature request kind/external Pull requests raised by community contributors labels Oct 4, 2026

@jymaire jymaire 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.

Thanks a lot for this contribution @GauriKhedekar, and for the thorough QA notes and tests (the four FSharpTest cases pass in CI).

The package restructuring that @fdelbrayelle described above (io.kestra.plugin.scripts.fsharp with Script, Commands, ScriptTrigger, CommandsTrigger, the C# move to io.kestra.plugin.scripts.csharp with aliases, and a shared base class) is the main change needed, so I won't repeat it here. Apart from that, the dotnet fsi invocation, .fsx temp file handling, default image and outputs match the existing C# Script task well.

Two smaller points to carry into the rework:

  • [MEDIUM] The plugin doc page currently mixes F# into the C# Script section.
  • [LOW] The F# task lacks the output-files example that the C# Script has.

Comment thread plugin-script-dotnet/src/main/resources/doc/io.kestra.plugin.scripts.dotnet.md Outdated
@GauriKhedekar
GauriKhedekar requested a review from jymaire October 6, 2026 04:04
@GauriKhedekar

GauriKhedekar commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

@fdelbrayelle @jymaire @Malaydewangan09, I have done all the required changes, Could you please review my pull request??

@jymaire jymaire 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.

Kestra Plugin Code Review

Business Requirements — met

The F# Script, Commands, ScriptTrigger and CommandsTrigger now live in io.kestra.plugin.scripts.fsharp with their own package-info, metadata and doc page. The C# classes moved to io.kestra.plugin.scripts.csharp with dotnet.* aliases, and shared logic sits in AbstractDotnetScript, AbstractDotnetCommands and AbstractDotnetTrigger. This matches the layout requested in the earlier discussion.

Previous Review Follow-up — 2 of 2 addressed

Baseline: 92b52407. The new head is d3637eb.

  • ✅ [MEDIUM] F# mixed into the C# doc section: io.kestra.plugin.scripts.csharp.md no longer mentions F#, and io.kestra.plugin.scripts.fsharp.md documents Script, Commands and both triggers. The thread was already resolved.
  • ✅ [LOW] Missing F# output-files example: fsharp/Script.java now has the fsharp_generate_files example. The thread was already resolved.

No threads needed resolving by this review.

Kestra Guidelines — 3 findings

  • 🟡 csharp/Commands.java:82 [Guidelines] containerImage redeclared (unchanged line, so not anchorable inline). containerImage with its default and @Schema is already declared in AbstractDotnetCommands; the C# Commands still redeclares it and shadows the parent field, while the F# Commands does not. Fix: keep it only in the base class.
  • 🟡 plugin-script-dotnet/src/test/java/io/kestra/plugin/scripts/dotnet/BackwardCompatibilityTest.java:14 [Guidelines] Alias test covers only Script
  • 🟢 plugin-script-dotnet/src/main/java/io/kestra/plugin/scripts/fsharp/Script.java:38 [Guidelines] Comments restate the example title

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

✅ No issues found. The PR has no .github, Gradle build or dependency changes. Commands are built from the same sources as before.

Performance — 0 findings

✅ No issues found

Compatibility note: kestraVersion is 1.3.39. The diff does not use FileSerde or any other core API changed in 2.0.x, so there is no Ion trap.

CI: check / main and check / OpenTelemetry Export Trace both pass on d3637eb. There are no pending fork workflow runs to approve.

Verdict: Recommend APPROVE. The three findings are non-blocking.

Comment thread plugin-script-dotnet/src/main/java/io/kestra/plugin/scripts/fsharp/Script.java Outdated
@fdelbrayelle
fdelbrayelle removed the request for review from Malaydewangan09 October 9, 2026 10:01
@jymaire

jymaire commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

QA report for #460: F# script execution support (head d3637eb)

Edition: OSS | Docker tags: v1.3.42 and v2.0.5. kestraVersion=1.3.39, so the same JAR ran on both, one instance at a time.
Instance: http://localhost:18460 on the reviewer machine (container qa-scripts460-kestra-v205, network qa-scripts460-net). It is left running with v2.0.5 and all flows below. Stop it with docker rm -f qa-scripts460-kestra-v205 && docker network rm qa-scripts460-net.
Screenshots: https://claude.ai/artifact/7MgTQcmwx4JEwqceaf4PYq
Setup: ./gradlew :plugin-script-dotnet:shadowJar at d3637eb, with only that JAR in /app/plugins. Tasks used the default Docker task runner and the pre-pulled image mcr.microsoft.com/dotnet/sdk:10.0. For comparison, I also built a JAR from main (8bde4e9f) and ran it on v1.3.42.

Verdict: ❌ QA_FAIL (1 HIGH, 1 MEDIUM)

# Severity Finding
1 HIGH None of the 4 trigger types fires when targetOS is not set. This covers the F# ScriptTrigger/CommandsTrigger, the C# ScriptTrigger/CommandsTrigger and the legacy io.kestra.plugin.scripts.dotnet.*Trigger aliases. Existing flows that use the old trigger types stop firing, which is a regression: they fire with the main JAR. Fails on both v1.3.42 and v2.0.5.
2 MEDIUM The new fsharp_generate_files @Example fails with Unable to find `outputDir` . The C# example has the same problem on main already.

1. HIGH: triggers fail on every poll. AbstractDotnetTrigger.java:68 declares protected Property<TargetOS> targetOS; with no default. Then fsharp/ScriptTrigger.java:96, fsharp/CommandsTrigger.java:86, csharp/ScriptTrigger.java:89 and csharp/CommandsTrigger.java:88 call .targetOS(this.targetOS) on the embedded task builder. When targetOS is null, that call replaces the task's @Builder.Default with null, and core AbstractExecScript.commands() (line 185, render(targetOS).as(TargetOS.class).orElseThrow()) throws java.util.NoSuchElementException: No value present. The worker swallows the exception and logs only WARN Trigger evaluation failed, returning empty result to avoid blocking the scheduler, so the flow never runs and nothing reaches the user beyond that WARN.

  • Repro: any trigger example from the PR, unchanged (flow fsharp_script_trigger below), or an existing flow on io.kestra.plugin.scripts.dotnet.ScriptTrigger (legacy_script_trigger). Neither creates an execution, and each poll logs the stack trace AbstractExecScript.commands(AbstractExecScript.java:185) → fsharp.Script.run(Script.java:109) → fsharp.ScriptTrigger.runTask(ScriptTrigger.java:100) → AbstractDotnetTrigger.runOnce(AbstractDotnetTrigger.java:170).
  • Root cause confirmed: the same F# trigger with targetOS: AUTO set explicitly fires once, as edge expects (fsharp_trigger_targetos). The main JAR does not pass targetOS, and its legacy_script_trigger and legacy_commands_trigger both fire on v1.3.42.
  • Why CI missed it: the new trigger tests set .targetOS(Property.ofValue(TargetOS.LINUX)) explicitly, for example fsharp/ScriptTriggerEvaluateTest.java:47.
  • Possible fix: give the field a default (Property.ofValue(TargetOS.AUTO)), or only call .targetOS(...) when it is non-null. A test that leaves targetOS unset should be added.

2. MEDIUM: the fsharp_generate_files example fails (fsharp/Script.java:76-88). {{ outputDir }} is only available when outputDirectory: true is set. Adding outputDirectory: true to the example makes it work (fsharp_outputdir_enabled), and so does the outputFiles: [hello.txt] pattern (fsharp_output_files). The C# dotnet_generate_files example (csharp/Script.java:76-88) fails the same way on main (LOW, not caused by this PR) and could get the same fix.

What works on both versions

  • F# Script and Commands: hello world, NuGet references, dotnet fsi on input files, dotnet --version, ::{"outputs"}:: vars, input rendering and outputFiles.
  • Error paths: an F# compile error (FS0001/FS0039) and a non-zero exit (code 3) both mark the task FAILED, with clear ERROR logs and the right exitCode.
  • C# tasks moved to io.kestra.plugin.scripts.csharp.*: all task examples except the pre-existing outputDir one.
  • Backward compatibility for tasks: flows using io.kestra.plugin.scripts.dotnet.Script and .Commands validate with an info message ("… is replaced by io.kestra.plugin.scripts.csharp.Script") and run. The triggers do not (finding 1).
  • Docs and UI on v1.3.42: one .NET (C# and F#) group with separate io.kestra.plugin.scripts.csharp and io.kestra.plugin.scripts.fsharp subgroups, separate icons and doc pages. /plugins/io.kestra.plugin.scripts.dotnet.Script still resolves.

Caveats

  • On v2.0.5, the plugin group page and breadcrumb show ".NET (C#)" with "Tasks that run .NET C# scripts using dotnet-script." and a flat list of 8 tasks. That text is not in this JAR. It matches main's metadata/index.yaml, so 2.0 appears to show the already-published metadata rather than the local one. I expect it to update after release, but this should be checked then. The per-task F# and C# doc pages and icons are correct on 2.0.5.
  • The memory limit I set through plugin defaults for the Docker task runner was not applied (HostConfig.Memory=0). The Kestra container itself was limited to 3 GB.
  • Containers spawned by the task runner were removed by Kestra after each run. The only trigger that fires (fsharp_trigger_targetos) is now disabled, so the instance left running does not spawn containers.

Results summary

Flow Source v1.3.42 v2.0.5
fsharp_hello_world @Example ✅ SUCCESS ✅ SUCCESS
fsharp_nuget @Example ✅ SUCCESS ✅ SUCCESS
fsharp_generate_files @Example ❌ FAILED ❌ FAILED
fsharp_commands @Example ✅ SUCCESS ✅ SUCCESS
dotnet_version @Example ✅ SUCCESS ✅ SUCCESS
dotnet_hello_world @Example ✅ SUCCESS ✅ SUCCESS
dotnet_inline_script @Example ✅ SUCCESS ✅ SUCCESS
dotnet_generate_files @Example ⚠️ FAILED (same as main) ⚠️ FAILED (same as main)
dotnet_commands @Example ✅ SUCCESS ✅ SUCCESS
dotnet_build_and_test @Example ✅ SUCCESS ✅ SUCCESS
legacy_dotnet_tasks custom ✅ SUCCESS ✅ SUCCESS
fsharp_compile_error custom ✅ FAILED (expected) ✅ FAILED (expected)
fsharp_nonzero_exit custom ✅ FAILED (expected) ✅ FAILED (expected)
fsharp_commands_fail custom ✅ FAILED (expected) ✅ FAILED (expected)
fsharp_vars_outputs custom ✅ SUCCESS ✅ SUCCESS
fsharp_output_files custom ✅ SUCCESS ✅ SUCCESS
fsharp_outputdir_enabled custom ✅ SUCCESS ✅ SUCCESS
fsharp_script_trigger @Example ❌ never fires ❌ never fires
fsharp_commands_trigger @Example ❌ never fires ❌ never fires
script_trigger @Example ❌ never fires ❌ never fires
commands_trigger @Example ❌ never fires ❌ never fires
legacy_script_trigger custom (legacy alias) ❌ never fires ❌ never fires
legacy_commands_trigger custom (legacy alias) ❌ never fires ❌ never fires
fsharp_trigger_targetos @Example + targetOS: AUTO ✅ fired once (SUCCESS) ✅ fired once (SUCCESS)

Flow: fsharp_hello_world (✅ SUCCESS)

Flow YAML
id: fsharp_hello_world
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      printfn "Hello from Kestra!"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_fsharp SUCCESS 3.1s SUCCESS 1.9s
Total SUCCESS 3.5s SUCCESS 1.9s

Logs synthesis (v1.3.42 · v2.0.5): Runs dotnet fsi <tmp>.fsx in mcr.microsoft.com/dotnet/sdk:10.0; logs Hello from Kestra!.

Outputs synthesis: exitCode: 0, vars: {}, outputFiles: {}.

Flow: fsharp_nuget (✅ SUCCESS)

Flow YAML
id: fsharp_nuget
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      #r "nuget: Newtonsoft.Json, 13.0.3"

      open Newtonsoft.Json

      let data = {| message = "Hello from Kestra" |}
      printfn "%s" (JsonConvert.SerializeObject(data))

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_fsharp SUCCESS 5.2s SUCCESS 5.1s
Total SUCCESS 5.3s SUCCESS 5.2s

Logs synthesis (v1.3.42 · v2.0.5): #r "nuget: Newtonsoft.Json, 13.0.3" resolved; logs {"message":"Hello from Kestra"}.

Outputs synthesis: exitCode: 0.

Flow: fsharp_generate_files (❌ FAILED)

Flow YAML
id: fsharp_generate_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      System.IO.File.WriteAllText("{{ outputDir }}/hello.txt", "Hello from F#!")

  - id: read_file
    type: io.kestra.plugin.core.log.Log
    message: "{{ read(outputs.write_file.outputFiles['hello.txt']) }}"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file FAILED 0.06s FAILED 0.05s
read_file not run not run
Total FAILED 0.14s FAILED 0.08s

Logs synthesis (v1.3.42 · v2.0.5): ERROR Unable to find `outputDir` used in the expression `System.IO.File.WriteAllText("{{ outputDir }}/hello.txt", "Hello from F#!")` at line 1. The @Example itself (without my read_file task) fails the same way. Adding outputDirectory: true fixes it (flow fsharp_outputdir_enabled below).

Outputs synthesis: None.

Flow: fsharp_commands (✅ SUCCESS)

Flow YAML
id: fsharp_commands
namespace: company.team

tasks:
  - id: run_fsharp
    type: io.kestra.plugin.scripts.fsharp.Commands
    inputFiles:
      analyze.fsx: |
        printfn "Analyzing data..."
        printfn "Hello from F# commands!"
    commands:
      - dotnet fsi analyze.fsx

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
run_fsharp SUCCESS 2.0s SUCCESS 1.9s
Total SUCCESS 2.1s SUCCESS 1.9s

Logs synthesis (v1.3.42 · v2.0.5): dotnet fsi analyze.fsx logs Analyzing data... and Hello from F# commands!.

Outputs synthesis: exitCode: 0.

Flow: dotnet_version (✅ SUCCESS)

Flow YAML
id: dotnet_version
namespace: company.team

tasks:
  - id: version
    type: io.kestra.plugin.scripts.fsharp.Commands
    commands:
      - dotnet --version

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
version SUCCESS 0.7s SUCCESS 0.8s
Total SUCCESS 0.7s SUCCESS 0.8s

Logs synthesis (v1.3.42 · v2.0.5): Logs 10.0.401.

Outputs synthesis: exitCode: 0.

Flow: dotnet_hello_world (✅ SUCCESS)

Flow YAML
id: dotnet_hello_world
namespace: company.team

tasks:
  - id: hello_csharp
    type: io.kestra.plugin.scripts.csharp.Script
    script: |
      Console.WriteLine("Hello from Kestra!");

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_csharp SUCCESS 16.2s SUCCESS 15.6s
Total SUCCESS 16.5s SUCCESS 15.8s

Logs synthesis (v1.3.42 · v2.0.5): Installs dotnet-script 2.0.1 and then logs Hello from Kestra!. Most of the time goes into the tool install, under the 30 s timeout.

Outputs synthesis: exitCode: 0.

Flow: dotnet_inline_script (✅ SUCCESS)

Flow YAML
id: dotnet_inline_script
namespace: company.team

tasks:
  - id: hello_csharp
    type: io.kestra.plugin.scripts.csharp.Script
    script: |
      #r "nuget:Newtonsoft.Json,13.0.3"
      using Newtonsoft.Json;
      var data = new { message = "Hello from Kestra", timestamp = DateTime.UtcNow };
      Console.WriteLine(JsonConvert.SerializeObject(data));

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_csharp SUCCESS 18.2s SUCCESS 17.5s
Total SUCCESS 18.4s SUCCESS 17.6s

Logs synthesis (v1.3.42 · v2.0.5): NuGet Newtonsoft.Json resolved; JSON with message and timestamp logged.

Outputs synthesis: exitCode: 0.

Flow: dotnet_generate_files (⚠️ FAILED (same as main))

Flow YAML
id: dotnet_generate_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.csharp.Script
    script: |
      File.WriteAllText("{{ outputDir }}/hello.txt", "Hello from dotnet-script!");

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file FAILED 0.07s FAILED 0.04s
Total FAILED 0.13s FAILED 0.08s

Logs synthesis (v1.3.42 · v2.0.5): Same Unable to find `outputDir` error. The main branch JAR fails the same way on v1.3.42 with the old dotnet.Script type, so this is not caused by this PR.

Outputs synthesis: None.

Flow: dotnet_commands (✅ SUCCESS)

Flow YAML
id: dotnet_commands
namespace: company.team

tasks:
  - id: run_dotnet
    type: io.kestra.plugin.scripts.csharp.Commands
    inputFiles:
      analyze.csx: |
        Console.WriteLine("Analyzing data...");
        Console.WriteLine($"Current time: {DateTime.UtcNow}");
    beforeCommands:
      - dotnet tool install -g dotnet-script --ignore-failed-sources || true
      - export PATH="$PATH:$HOME/.dotnet/tools"
    commands:
      - dotnet-script analyze.csx

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
run_dotnet SUCCESS 16.2s SUCCESS 16.4s
Total SUCCESS 16.5s SUCCESS 16.5s

Logs synthesis (v1.3.42 · v2.0.5): dotnet-script analyze.csx logs Analyzing data... and the current time.

Outputs synthesis: exitCode: 0.

Flow: dotnet_build_and_test (✅ SUCCESS)

Flow YAML
id: dotnet_build_and_test
namespace: company.team

tasks:
  - id: build
    type: io.kestra.plugin.scripts.csharp.Commands
    commands:
      - dotnet --version

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
build SUCCESS 0.8s SUCCESS 0.8s
Total SUCCESS 0.8s SUCCESS 0.8s

Logs synthesis (v1.3.42 · v2.0.5): Logs 10.0.401.

Outputs synthesis: exitCode: 0.

Flow: legacy_dotnet_tasks (✅ SUCCESS)

Flow YAML
id: legacy_dotnet_tasks
namespace: company.team

tasks:
  - id: legacy_script
    type: io.kestra.plugin.scripts.dotnet.Script
    script: |
      Console.WriteLine("Hello from the legacy dotnet.Script type!");

  - id: legacy_commands
    type: io.kestra.plugin.scripts.dotnet.Commands
    commands:
      - dotnet --version
      - echo "legacy dotnet.Commands OK"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
legacy_script SUCCESS 16.2s SUCCESS 16.7s
legacy_commands SUCCESS 0.8s SUCCESS 0.9s
Total SUCCESS 17.5s SUCCESS 17.7s

Logs synthesis (v1.3.42 · v2.0.5): Flow validation returns the infos io.kestra.plugin.scripts.dotnet.Script is replaced by io.kestra.plugin.scripts.csharp.Script (same for Commands). Both tasks run: Hello from the legacy dotnet.Script type!, 10.0.401, legacy dotnet.Commands OK. The UI shows the C# icon.

Outputs synthesis: exitCode: 0 for both tasks.

Flow: fsharp_compile_error (✅ FAILED (expected))

Flow YAML
id: fsharp_compile_error
namespace: company.team

tasks:
  - id: bad_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      let x : int = "not an int"
      printfn "%d" x

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
bad_fsharp FAILED 1.6s FAILED 1.7s
Total FAILED 1.7s FAILED 1.7s

Logs synthesis (v1.3.42 · v2.0.5): ERROR error FS0001: This expression was expected to have type 'int' but here has type 'string', then Command failed with exit code 1.

Outputs synthesis: exitCode: 1.

Flow: fsharp_nonzero_exit (✅ FAILED (expected))

Flow YAML
id: fsharp_nonzero_exit
namespace: company.team

tasks:
  - id: exit_script
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      eprintfn "about to fail"
      exit 3

  - id: never_runs
    type: io.kestra.plugin.core.log.Log
    message: "should not run"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
exit_script FAILED 1.8s FAILED 1.9s
never_runs not run not run
Total FAILED 1.9s FAILED 2.0s

Logs synthesis (v1.3.42 · v2.0.5): stderr about to fail is logged as ERROR, then Command failed with exit code 3. The downstream task does not run.

Outputs synthesis: exitCode: 3.

Flow: fsharp_commands_fail (✅ FAILED (expected))

Flow YAML
id: fsharp_commands_fail
namespace: company.team

tasks:
  - id: bad_commands
    type: io.kestra.plugin.scripts.fsharp.Commands
    inputFiles:
      broken.fsx: |
        printfn "%d" (undefinedValue + 1)
    commands:
      - dotnet fsi broken.fsx

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
bad_commands FAILED 1.5s FAILED 1.8s
Total FAILED 1.6s FAILED 1.8s

Logs synthesis (v1.3.42 · v2.0.5): ERROR broken.fsx(1,15): error FS0039: The value or constructor 'undefinedValue' is not defined.

Outputs synthesis: exitCode: 1.

Flow: fsharp_vars_outputs (✅ SUCCESS)

Flow YAML
id: fsharp_vars_outputs
namespace: company.team

inputs:
  - id: who
    type: STRING
    defaults: Kestra

tasks:
  - id: fsharp_outputs
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      printfn "Hello %s" "{{ inputs.who }}"
      printfn "::{\"outputs\":{\"answer\":42,\"lang\":\"fsharp\"}}::"

  - id: use_output
    type: io.kestra.plugin.core.log.Log
    message: "answer={{ outputs.fsharp_outputs.vars.answer }} lang={{ outputs.fsharp_outputs.vars.lang }}"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
fsharp_outputs SUCCESS 1.8s SUCCESS 1.9s
use_output SUCCESS SUCCESS
Total SUCCESS 1.9s SUCCESS 2.0s

Logs synthesis (v1.3.42 · v2.0.5): Logs Hello Kestra (input rendered into the script) and answer=42 lang=fsharp downstream.

Outputs synthesis (screenshot): vars: {answer: 42, lang: fsharp}.

Flow: fsharp_output_files (✅ SUCCESS)

Flow YAML
id: fsharp_output_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    outputFiles:
      - hello.txt
    script: |
      System.IO.File.WriteAllText("hello.txt", "Hello from F#!")

  - id: read_file
    type: io.kestra.plugin.core.log.Log
    message: "{{ read(outputs.write_file.outputFiles['hello.txt']) }}"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file SUCCESS 1.8s SUCCESS 1.9s
read_file SUCCESS SUCCESS
Total SUCCESS 2.0s SUCCESS 2.0s

Logs synthesis (v1.3.42 · v2.0.5): read_file logs Hello from F#!.

Outputs synthesis (screenshot): outputFiles.hello.txt: kestra:///company/team/fsharp-output-files/executions/…/hello.txt. The UI preview shows Hello from F#!.

Flow: fsharp_outputdir_enabled (✅ SUCCESS)

Flow YAML
id: fsharp_outputdir_enabled
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    outputDirectory: true
    script: |
      System.IO.File.WriteAllText("{{ outputDir }}/hello.txt", "Hello from F#!")

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file SUCCESS 1.9s SUCCESS 1.8s
Total SUCCESS 2.3s SUCCESS 1.8s

Logs synthesis (v1.3.42 · v2.0.5): The fsharp_generate_files example plus outputDirectory: true works.

Outputs synthesis: outputFiles.hello.txt: kestra:///…/write-file/…/hello.txt.

Triggers

All trigger flows were created on each instance and watched through at least two 60 s polls.

fsharp_script_trigger (❌ never fires)

Flow YAML
id: fsharp_script_trigger
namespace: company.team

triggers:
  - id: fsharp_script_failure
    type: io.kestra.plugin.scripts.fsharp.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    script: |
      System.Environment.Exit(1)

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

Flow logs: v1.3.42 · v2.0.5. Each poll logs WARN Trigger evaluation failed… with NoSuchElementException at AbstractExecScript.commands(AbstractExecScript.java:185). No execution is created (finding 1).

fsharp_commands_trigger (❌ never fires)

Flow YAML
id: fsharp_commands_trigger
namespace: company.team

triggers:
  - id: fsharp_commands_failure
    type: io.kestra.plugin.scripts.fsharp.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    commands:
      - dotnet fsi --exec "System.Environment.Exit(1)"

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

Each poll logs WARN Trigger evaluation failed… with NoSuchElementException at AbstractExecScript.commands(AbstractExecScript.java:185). No execution is created (finding 1).

script_trigger (❌ never fires)

Flow YAML
id: script_trigger
namespace: company.team

triggers:
  - id: script_failure
    type: io.kestra.plugin.scripts.csharp.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    script: |
      System.Environment.Exit(1);

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

Each poll logs WARN Trigger evaluation failed… with NoSuchElementException at AbstractExecScript.commands(AbstractExecScript.java:185). No execution is created (finding 1).

commands_trigger (❌ never fires)

Flow YAML
id: commands_trigger
namespace: company.team

triggers:
  - id: commands_failure
    type: io.kestra.plugin.scripts.csharp.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    commands:
      - dotnet run --project missing

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

Each poll logs WARN Trigger evaluation failed… with NoSuchElementException at AbstractExecScript.commands(AbstractExecScript.java:185). No execution is created (finding 1).

legacy_script_trigger (❌ never fires)

Flow YAML
id: legacy_script_trigger
namespace: company.team

triggers:
  - id: legacy_script_failure
    type: io.kestra.plugin.scripts.dotnet.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    script: |
      System.Environment.Exit(1);

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

Flow logs: v1.3.42 · v2.0.5. Each poll logs WARN Trigger evaluation failed… with NoSuchElementException at AbstractExecScript.commands(AbstractExecScript.java:185). No execution is created (finding 1).

legacy_commands_trigger (❌ never fires)

Flow YAML
id: legacy_commands_trigger
namespace: company.team

triggers:
  - id: legacy_commands_failure
    type: io.kestra.plugin.scripts.dotnet.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    commands:
      - dotnet run --project missing

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

Each poll logs WARN Trigger evaluation failed… with NoSuchElementException at AbstractExecScript.commands(AbstractExecScript.java:185). No execution is created (finding 1).

fsharp_trigger_targetos (✅ SUCCESS)

Flow YAML
id: fsharp_trigger_targetos
namespace: company.team

triggers:
  - id: fsharp_script_failure
    type: io.kestra.plugin.scripts.fsharp.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    targetOS: AUTO
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    script: |
      System.Environment.Exit(1)

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

Fired once per instance (edge), and the log task logged Triggered with exitCode=1 (condition=exit 1). Screenshots: v1.3.42 executions · v2.0.5 logs.

Docs and UI

Page v1.3.42 v2.0.5
Plugin group ✅ .NET (C# and F#), C# and F# subgroups (view) ⚠️ remote metadata .NET (C#), flat list of 8 (view)
fsharp.Script doc ✅ view ✅ view
fsharp.ScriptTrigger / csharp.ScriptTrigger doc ✅ view ✅ view
csharp.Script doc ✅ view n/a
Legacy dotnet.Script URL ✅ resolves (view) n/a

Timeout notes

No scenario hit the 30 s timeout. The C# Script tasks take 15 to 18 s because dotnet-script is installed on every run.

@jymaire jymaire 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.

Kestra Plugin Code Review

Business Requirements — partial

The F#/C# split is in place, but the legacy io.kestra.plugin.scripts.dotnet.*Trigger aliases no longer fire with default settings, which breaks backward compatibility for existing flows (QA report: #460 (comment)).

Previous Review Follow-up — 2 of 3 addressed

Baseline d3637eb; head is now 6313f6e (test-only commit). 🟡 Alias test covers only Script: ✅ Addressed (BackwardCompatibilityTest now covers the four aliases). 🟢 Comments restate the example title: ✅ Addressed (no // comments left in fsharp/Script.java). 🟡 Missing F# output-files example: 🟡 Partially addressed, the example was added but fails at runtime (see below).

Kestra Guidelines — 6 findings

  • 🔴 AbstractDotnetTrigger.java:68 [Guidelines] targetOS has no default, triggers never fire
  • 🔴 fsharp/ScriptTrigger.java:96, fsharp/CommandsTrigger.java:86, csharp/ScriptTrigger.java:89, csharp/CommandsTrigger.java:88 [Guidelines] Null targetOS overrides task default
  • 🟠 fsharp/ScriptTriggerEvaluateTest.java:47 [Guidelines] Tests mask the missing targetOS default

Also from QA (reply posted in the existing thread on fsharp/Script.java): the new fsharp_generate_files example fails with "Unable to find outputDir" unless outputDirectory: true or outputFiles is set. The identical C# example already fails on main (LOW, not caused by this PR).

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

✅ No issues found

Performance — 0 findings

✅ No issues found

Verdict: recommend REQUEST CHANGES (posted as COMMENT): fix the null targetOS regression and add a default-path trigger test before merge.

@GauriKhedekar

Copy link
Copy Markdown
Contributor Author

Fixed the example to declare outputFiles: - hello.txt and write directly to hello.txt, removing the dependency on {{ outputDir }}. I verified the corrected YAML in the local Kestra UI: the task executed successfully, logged the success message, and persisted hello.txt. I've attached screenshots of the successful execution and output in the PR conversation.

The attached screenshots show the successful execution and generated output file.

Screenshot 2026-10-09 194622 Screenshot 2026-10-09 194641 Screenshot 2026-10-09 194659 Screenshot 2026-10-09 194744

@GauriKhedekar

GauriKhedekar commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @jymaire, thanks for the detailed review!

I've pushed the follow-up fixes addressing the feedback:

  • Added a default for targetOS and updated trigger tests to cover the default behavior.
  • Fixed the F# output-files example using outputFiles and verified it locally.
  • Removed redundant comments from the F# examples.
  • Removed the duplicate containerImage declaration from C# Commands.
  • Added backward-compatibility test coverage for all four legacy .dotnet.* aliases.

The plugin test suite passes all 72 tests, and documentation lint passes all 25 rules.

I've also retained the existing Groovy ScriptTrigger entry while resolving the AGENTS.md conflict.

Thanks for your guidance. Could you please take another look when you get a chance?

@GauriKhedekar
GauriKhedekar requested a review from jymaire October 10, 2026 05:20
@jymaire

jymaire commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

QA report for #460: F# script execution support (head 39c5ff6)

Edition: OSS | Docker tags: v1.3.42 and v2.0.5. kestraVersion=1.3.39, so the same JAR ran on both, one instance at a time.
Instance: http://localhost:18460 on the reviewer machine (container qa-scripts460-kestra-v205, network qa-scripts460-net). It is left running with v2.0.5 and all flows below; the trigger flows are now disabled so it no longer spawns containers. Stop it with docker rm -f qa-scripts460-kestra-v205 && docker network rm qa-scripts460-net.
Screenshots: https://claude.ai/artifact/YKaF9kkEBDYwork6VFfhyT
Setup: ./gradlew :plugin-script-dotnet:shadowJar at 39c5ff6, with only that JAR in /app/plugins. Tasks used the default Docker task runner and mcr.microsoft.com/dotnet/sdk:10.0. This is a full re-run after the previous QA FAIL at d3637eb. The head matches the expected SHA; the new main-code changes since then are the targetOS default in AbstractDotnetTrigger, the outputFiles examples and the removed containerImage override in csharp/Commands.

Verdict: ✅ QA_PASS (no HIGH or MEDIUM findings)

Both findings from the previous QA are fixed on both versions:

Previous finding Status at 39c5ff6
HIGH: triggers never fire when targetOS is not set ✅ Fixed. All 4 trigger types (F# and C# ScriptTrigger/CommandsTrigger) and both legacy io.kestra.plugin.scripts.dotnet.*Trigger aliases fire without targetOS, on v1.3.42 and v2.0.5. The schema now shows targetOS default AUTO. No Trigger evaluation failed WARN or NoSuchElementException in the server logs on either version. edge: true holds: exactly one execution per trigger over 4+ polls (1.3) and about 9 polls (2.0).
MEDIUM: fsharp_generate_files example fails on outputDir ✅ Fixed. The F# and C# *_generate_files examples now use outputFiles and both produce hello.txt with the expected content on both versions. The C# one was a pre-existing failure on main and is fixed too.

What else works on both versions

  • F# Script and Commands end to end: hello world, NuGet references, dotnet fsi on input files, dotnet --version, inputs rendering, ::{"outputs"}:: vars, outputFiles read downstream.
  • F# error paths: compile errors (FS0001, FS0039) and a non-zero exit (code 3) mark the task FAILED, with clear ERROR logs and the right exitCode.
  • C# Script and Commands examples, including dotnet_commands after the containerImage override was removed from csharp/Commands: it still runs in the default .NET SDK image.
  • Legacy task aliases io.kestra.plugin.scripts.dotnet.Script / .Commands run, and validation returns the "… is replaced by io.kestra.plugin.scripts.csharp.…" info.

Results summary

Flow Source v1.3.42 v2.0.5
fsharp_hello_world @Example (created in the UI editor on 1.3) ✅ SUCCESS ✅ SUCCESS
fsharp_nuget @Example ✅ SUCCESS ✅ SUCCESS
fsharp_generate_files @Example (changed in this push; run from the UI Execute button on 2.0) ✅ SUCCESS ✅ SUCCESS
fsharp_commands @Example ✅ SUCCESS ✅ SUCCESS
dotnet_version @Example (F# Commands) ✅ SUCCESS ✅ SUCCESS
dotnet_hello_world @Example (C# Script) ✅ SUCCESS ✅ SUCCESS
dotnet_inline_script @Example (C# Script) ✅ SUCCESS ✅ SUCCESS
dotnet_generate_files @Example (C# Script, changed in this push) ✅ SUCCESS ✅ SUCCESS
dotnet_generate_files_read custom: C# example + downstream read() ✅ SUCCESS ✅ SUCCESS
dotnet_commands @Example (C# Commands; its containerImage override was removed in this push) ✅ SUCCESS ✅ SUCCESS
dotnet_build_and_test @Example (C# Commands) ✅ SUCCESS ✅ SUCCESS
legacy_dotnet_tasks custom: legacy dotnet.Script / dotnet.Commands aliases ✅ SUCCESS ✅ SUCCESS
fsharp_compile_error custom: error path ✅ FAILED (expected) ✅ FAILED (expected)
fsharp_nonzero_exit custom: error path ✅ FAILED (expected) ✅ FAILED (expected)
fsharp_commands_fail custom: error path ✅ FAILED (expected) ✅ FAILED (expected)
fsharp_vars_outputs custom: inputs + ::{"outputs"}:: ✅ SUCCESS ✅ SUCCESS
fsharp_output_files custom: outputFiles + downstream read() ✅ SUCCESS ✅ SUCCESS
fsharp_script_trigger @Example (F# ScriptTrigger, no targetOS) ✅ fired once (4.9s after save) ✅ fired once (31s after save)
fsharp_commands_trigger @Example (F# CommandsTrigger, no targetOS) ✅ fired once (4.2s after save) ✅ fired once (31s after save)
script_trigger @Example (C# ScriptTrigger, no targetOS) ✅ fired once (12s after save) ✅ fired once (53s after save)
commands_trigger @Example (C# CommandsTrigger, no targetOS) ✅ fired once (3.7s after save) ✅ fired once (30s after save)
legacy_script_trigger custom: legacy dotnet.ScriptTrigger alias, no targetOS ✅ fired once (15s after save) ✅ fired once (45s after save)
legacy_commands_trigger custom: legacy dotnet.CommandsTrigger alias, no targetOS ✅ fired once (3.7s after save) ✅ fired once (30s after save)
fsharp_trigger_targetos_linux custom: F# CommandsTrigger with explicit targetOS: LINUX ✅ fired once (4.2s after save) ✅ fired once (31s after save)

Flow: fsharp_hello_world (✅ SUCCESS)

Flow YAML
id: fsharp_hello_world
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      printfn "Hello from Kestra!"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_fsharp SUCCESS 2.5s SUCCESS 2.1s
Total SUCCESS 2.6s SUCCESS 2.1s

Logs synthesis (v1.3.42 · v2.0.5): Runs dotnet fsi on the generated .fsx in mcr.microsoft.com/dotnet/sdk:10.0; logs Hello from Kestra!.

Outputs synthesis: exitCode: 0, vars: {}, outputFiles: {}.

Flow: fsharp_nuget (✅ SUCCESS)

Flow YAML
id: fsharp_nuget
namespace: company.team

tasks:
  - id: hello_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      #r "nuget: Newtonsoft.Json, 13.0.3"

      open Newtonsoft.Json

      let data = {| message = "Hello from Kestra" |}
      printfn "%s" (JsonConvert.SerializeObject(data))

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_fsharp SUCCESS 6.3s SUCCESS 6.3s
Total SUCCESS 6.5s SUCCESS 6.4s

Logs synthesis (v1.3.42 · v2.0.5): #r "nuget: ..." resolves Newtonsoft.Json; logs {"message":"Hello from Kestra"}.

Outputs synthesis: exitCode: 0.

Flow: fsharp_generate_files (✅ SUCCESS)

Flow YAML
id: fsharp_generate_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    outputFiles:
      - hello.txt
    script: |
      System.IO.File.WriteAllText("hello.txt", "Hello from F#!")
      printfn "Created hello.txt successfully."

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file SUCCESS 2.6s SUCCESS 2.4s
Total SUCCESS 2.6s SUCCESS 2.5s

Logs synthesis (v1.3.42 · v2.0.5): Logs Created hello.txt successfully. and Captured 1 output file(s). No outputDir error any more.

Outputs synthesis (v1.3.42 · v2.0.5): outputFiles.hello.txt: kestra:///company/team/fsharp-generate-files/executions/<id>/tasks/write-file/.../hello.txt; the preview shows Hello from F#! on both versions.

Flow: fsharp_commands (✅ SUCCESS)

Flow YAML
id: fsharp_commands
namespace: company.team

tasks:
  - id: run_fsharp
    type: io.kestra.plugin.scripts.fsharp.Commands
    inputFiles:
      analyze.fsx: |
        printfn "Analyzing data..."
        printfn "Hello from F# commands!"
    commands:
      - dotnet fsi analyze.fsx

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
run_fsharp SUCCESS 2.5s SUCCESS 2.0s
Total SUCCESS 2.6s SUCCESS 2.1s

Logs synthesis (v1.3.42 · v2.0.5): dotnet fsi analyze.fsx on the input file; logs Analyzing data... and Hello from F# commands!.

Outputs synthesis: exitCode: 0.

Flow: dotnet_version (✅ SUCCESS)

Flow YAML
id: dotnet_version
namespace: company.team

tasks:
  - id: version
    type: io.kestra.plugin.scripts.fsharp.Commands
    commands:
      - dotnet --version

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
version SUCCESS 0.9s SUCCESS 0.8s
Total SUCCESS 1.0s SUCCESS 0.9s

Logs synthesis (v1.3.42 · v2.0.5): Logs 10.0.401.

Outputs synthesis: exitCode: 0.

Flow: dotnet_hello_world (✅ SUCCESS)

Flow YAML
id: dotnet_hello_world
namespace: company.team

tasks:
  - id: hello_csharp
    type: io.kestra.plugin.scripts.csharp.Script
    script: |
      Console.WriteLine("Hello from Kestra!");

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_csharp SUCCESS 9.6s SUCCESS 24.4s
Total SUCCESS 9.7s SUCCESS 24.5s

Logs synthesis (v1.3.42 · v2.0.5): Installs dotnet-script, then logs Hello from Kestra!. Most of the time is the dotnet-script install.

Outputs synthesis: exitCode: 0.

Flow: dotnet_inline_script (✅ SUCCESS)

Flow YAML
id: dotnet_inline_script
namespace: company.team

tasks:
  - id: hello_csharp
    type: io.kestra.plugin.scripts.csharp.Script
    script: |
      #r "nuget:Newtonsoft.Json,13.0.3"
      using Newtonsoft.Json;
      var data = new { message = "Hello from Kestra", timestamp = DateTime.UtcNow };
      Console.WriteLine(JsonConvert.SerializeObject(data));

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
hello_csharp SUCCESS 10.5s SUCCESS 18.6s
Total SUCCESS 10.5s SUCCESS 18.7s

Logs synthesis (v1.3.42 · v2.0.5): Logs {"message":"Hello from Kestra","timestamp":"..."}.

Outputs synthesis: exitCode: 0.

Flow: dotnet_generate_files (✅ SUCCESS)

Flow YAML
id: dotnet_generate_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.csharp.Script
    outputFiles:
      - hello.txt
    script: |
      File.WriteAllText("hello.txt", "Hello from dotnet-script!");

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file SUCCESS 11.1s SUCCESS 13.5s
Total SUCCESS 11.3s SUCCESS 13.6s

Logs synthesis (v1.3.42 · v2.0.5): Logs Captured 1 output file(s). No outputDir error any more.

Outputs synthesis (v1.3.42 · v2.0.5): outputFiles.hello.txt set; the preview shows Hello from dotnet-script! on both versions.

Flow: dotnet_generate_files_read (✅ SUCCESS)

Flow YAML
id: dotnet_generate_files_read
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.csharp.Script
    outputFiles:
      - hello.txt
    script: |
      File.WriteAllText("hello.txt", "Hello from dotnet-script!");

  - id: read_file
    type: io.kestra.plugin.core.log.Log
    message: "content={{ read(outputs.write_file.outputFiles['hello.txt']) }}"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file SUCCESS 11.4s SUCCESS 14.7s
read_file SUCCESS 0.03s SUCCESS 0.04s
Total SUCCESS 11.6s SUCCESS 14.8s

Logs synthesis (v1.3.42 · v2.0.5): read_file logs content=Hello from dotnet-script!, so the documented outputs.write_file.outputFiles['hello.txt'] expression works downstream.

Outputs synthesis: write_file.outputFiles.hello.txt set.

Flow: dotnet_commands (✅ SUCCESS)

Flow YAML
id: dotnet_commands
namespace: company.team

tasks:
  - id: run_dotnet
    type: io.kestra.plugin.scripts.csharp.Commands
    inputFiles:
      analyze.csx: |
        Console.WriteLine("Analyzing data...");
        Console.WriteLine($"Current time: {DateTime.UtcNow}");
    beforeCommands:
      - dotnet tool install -g dotnet-script --ignore-failed-sources || true
      - export PATH="$PATH:$HOME/.dotnet/tools"
    commands:
      - dotnet-script analyze.csx

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
run_dotnet SUCCESS 10.6s SUCCESS 15.7s
Total SUCCESS 10.8s SUCCESS 15.7s

Logs synthesis (v1.3.42 · v2.0.5): Still runs in the default .NET SDK image: installs dotnet-script, logs Analyzing data... and Current time: ....

Outputs synthesis: exitCode: 0.

Flow: dotnet_build_and_test (✅ SUCCESS)

Flow YAML
id: dotnet_build_and_test
namespace: company.team

tasks:
  - id: build
    type: io.kestra.plugin.scripts.csharp.Commands
    commands:
      - dotnet --version

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
build SUCCESS 0.9s SUCCESS 0.9s
Total SUCCESS 1.0s SUCCESS 1.0s

Logs synthesis (v1.3.42 · v2.0.5): Logs 10.0.401.

Outputs synthesis: exitCode: 0.

Flow: legacy_dotnet_tasks (✅ SUCCESS)

Flow YAML
id: legacy_dotnet_tasks
namespace: company.team

tasks:
  - id: legacy_script
    type: io.kestra.plugin.scripts.dotnet.Script
    script: |
      Console.WriteLine("Hello from the legacy dotnet.Script type!");

  - id: legacy_commands
    type: io.kestra.plugin.scripts.dotnet.Commands
    commands:
      - dotnet --version
      - echo "legacy dotnet.Commands OK"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
legacy_script SUCCESS 25.6s SUCCESS 14.1s
legacy_commands SUCCESS 1.0s SUCCESS 0.9s
Total SUCCESS 26.7s SUCCESS 15.0s

Logs synthesis (v1.3.42 · v2.0.5): Logs Hello from the legacy dotnet.Script type!, 10.0.401 and legacy dotnet.Commands OK. Validation returns the infos io.kestra.plugin.scripts.dotnet.Script is replaced by io.kestra.plugin.scripts.csharp.Script (same for Commands).

Outputs synthesis: exitCode: 0 for both.

Flow: fsharp_compile_error (✅ FAILED (expected))

Flow YAML
id: fsharp_compile_error
namespace: company.team

tasks:
  - id: bad_fsharp
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      let x : int = "not an int"
      printfn "%d" x

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
bad_fsharp FAILED 2.5s FAILED 1.9s
Total FAILED 2.6s FAILED 1.9s

Logs synthesis (v1.3.42 · v2.0.5): ERROR error FS0001: This expression was expected to have type 'int' but here has type 'string'.

Outputs synthesis: exitCode: 1.

Flow: fsharp_nonzero_exit (✅ FAILED (expected))

Flow YAML
id: fsharp_nonzero_exit
namespace: company.team

tasks:
  - id: exit_script
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      eprintfn "about to fail"
      exit 3

  - id: never_runs
    type: io.kestra.plugin.core.log.Log
    message: "should not run"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
exit_script FAILED 2.6s FAILED 2.0s
never_runs not run - not run -
Total FAILED 2.7s FAILED 2.0s

Logs synthesis (v1.3.42 · v2.0.5): ERROR about to fail, then Command failed with exit code 3.

Outputs synthesis: exitCode: 3.

Flow: fsharp_commands_fail (✅ FAILED (expected))

Flow YAML
id: fsharp_commands_fail
namespace: company.team

tasks:
  - id: bad_commands
    type: io.kestra.plugin.scripts.fsharp.Commands
    inputFiles:
      broken.fsx: |
        printfn "%d" (undefinedValue + 1)
    commands:
      - dotnet fsi broken.fsx

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
bad_commands FAILED 2.4s FAILED 1.9s
Total FAILED 2.4s FAILED 1.9s

Logs synthesis (v1.3.42 · v2.0.5): ERROR broken.fsx(1,15): error FS0039: The value or constructor 'undefinedValue' is not defined.

Outputs synthesis: exitCode: 1.

Flow: fsharp_vars_outputs (✅ SUCCESS)

Flow YAML
id: fsharp_vars_outputs
namespace: company.team

inputs:
  - id: who
    type: STRING
    defaults: Kestra

tasks:
  - id: fsharp_outputs
    type: io.kestra.plugin.scripts.fsharp.Script
    script: |
      printfn "Hello %s" "{{ inputs.who }}"
      printfn "::{\"outputs\":{\"answer\":42,\"lang\":\"fsharp\"}}::"

  - id: use_output
    type: io.kestra.plugin.core.log.Log
    message: "answer={{ outputs.fsharp_outputs.vars.answer }} lang={{ outputs.fsharp_outputs.vars.lang }}"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
fsharp_outputs SUCCESS 2.5s SUCCESS 2.0s
use_output SUCCESS 0.05s SUCCESS 0.05s
Total SUCCESS 2.7s SUCCESS 2.1s

Logs synthesis (v1.3.42 · v2.0.5): Logs Hello Kestra, then answer=42 lang=fsharp.

Outputs synthesis (v1.3.42 · v2.0.5): fsharp_outputs.vars: {answer: 42, lang: fsharp}.

Flow: fsharp_output_files (✅ SUCCESS)

Flow YAML
id: fsharp_output_files
namespace: company.team

tasks:
  - id: write_file
    type: io.kestra.plugin.scripts.fsharp.Script
    outputFiles:
      - hello.txt
    script: |
      System.IO.File.WriteAllText("hello.txt", "Hello from F#!")

  - id: read_file
    type: io.kestra.plugin.core.log.Log
    message: "{{ read(outputs.write_file.outputFiles['hello.txt']) }}"

Gantt (v1.3.42 · v2.0.5)

Task v1.3.42 v2.0.5
write_file SUCCESS 2.4s SUCCESS 2.0s
read_file SUCCESS 0.02s SUCCESS 0.04s
Total SUCCESS 2.5s SUCCESS 2.1s

Logs synthesis (v1.3.42 · v2.0.5): read_file logs Hello from F#!.

Outputs synthesis (v1.3.42 · v2.0.5): write_file.outputFiles.hello.txt set.

Trigger flows

Each trigger runs a script that exits with code 1 and has exitCondition: "exit 1" and edge: true, so it should fire once and then stay quiet. Every trigger fired exactly once on each version, and the downstream log task logged Triggered with exitCode=1 (condition=exit 1) (Legacy trigger fired … for the legacy flows). Triggers tab: v1.3.42 legacy alias · v2.0.5 F#.

fsharp_script_trigger (✅ fired once)

Flow YAML
id: fsharp_script_trigger
namespace: company.team

triggers:
  - id: fsharp_script_failure
    type: io.kestra.plugin.scripts.fsharp.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    script: |
      System.Environment.Exit(1)

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

fsharp_commands_trigger (✅ fired once)

Flow YAML
id: fsharp_commands_trigger
namespace: company.team

triggers:
  - id: fsharp_commands_failure
    type: io.kestra.plugin.scripts.fsharp.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    commands:
      - dotnet fsi --exec "System.Environment.Exit(1)"

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

script_trigger (✅ fired once)

Flow YAML
id: script_trigger
namespace: company.team

triggers:
  - id: script_failure
    type: io.kestra.plugin.scripts.csharp.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    script: |
      System.Environment.Exit(1);

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

commands_trigger (✅ fired once)

Flow YAML
id: commands_trigger
namespace: company.team

triggers:
  - id: commands_failure
    type: io.kestra.plugin.scripts.csharp.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    containerImage: mcr.microsoft.com/dotnet/sdk:10.0
    commands:
      - dotnet run --project missing

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

legacy_script_trigger (✅ fired once)

Flow YAML
id: legacy_script_trigger
namespace: company.team

triggers:
  - id: legacy_script_failure
    type: io.kestra.plugin.scripts.dotnet.ScriptTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    script: |
      System.Environment.Exit(1);

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

legacy_commands_trigger (✅ fired once)

Flow YAML
id: legacy_commands_trigger
namespace: company.team

triggers:
  - id: legacy_commands_failure
    type: io.kestra.plugin.scripts.dotnet.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    commands:
      - dotnet run --project missing

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

fsharp_trigger_targetos_linux (✅ fired once)

Flow YAML
id: fsharp_trigger_targetos_linux
namespace: company.team

triggers:
  - id: fsharp_commands_linux
    type: io.kestra.plugin.scripts.fsharp.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    edge: true
    targetOS: LINUX
    commands:
      - dotnet fsi --exec "System.Environment.Exit(1)"

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

Gantt (v1.3.42 · v2.0.5): one execution per version, log SUCCESS in under 0.1s. Logs (v1.3.42 · v2.0.5): exitCode=1 (condition=exit 1). Outputs: none (Log task).

Timeout notes

  • No scenario hit a timeout on v1.3.42: triggers fired 4 to 15s after the flow was saved.
  • On v2.0.5 the scheduler started the first trigger evaluation about 28s after the flows were saved, for all triggers at once. The Commands triggers and the F# ScriptTrigger then fired at 30-31s. The C# script_trigger and the legacy dotnet.ScriptTrigger fired at 53s and 45s, because each C# poll installs dotnet-script in a fresh container (about 25s here; the C# Script tasks show the same cost). I did not count these as KO: the evaluation started within the 30s budget, and the delay is the C# runtime install, not a trigger failure.
  • Some C# tasks take 10-26s for the same reason (dotnet-script install on every run). This is the existing C# behaviour on main.

Caveats

  • Only fsharp_hello_world (1.3, editor) and fsharp_generate_files (2.0, Execute button) were created or run through the UI. The other flows were created with POST /api/v1/main/flows and run through the API, then checked in the UI (Gantt, Logs, Outputs).
  • I did not re-check the v2.0.5 plugin group title (".NET (C#)") noted in the previous report. It comes from the already-published metadata and is expected to update after release.

@jymaire
jymaire requested review from fdelbrayelle and removed request for jymaire October 12, 2026 07:22
fdelbrayelle added a commit to GauriKhedekar/plugin-scripts that referenced this pull request Oct 12, 2026
* feat(dotnet): add F# script execution support

* feat(dotnet): add F# script support

* test(dotnet): cover all C# compatibility aliases

* fix(dotnet): address review feedback

---------

Co-authored-by: François Delbrayelle <fdelbrayelle@kestra.io>
@fdelbrayelle
fdelbrayelle merged commit 0009bd1 into kestra-io:main Oct 12, 2026
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.

Support C#/F# as one of the dedicated script plugin

3 participants