Repository navigation
feat(dotnet): add F# script execution support - #460
fdelbrayelle merged 8 commits into
Conversation
|
@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. |
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? |
|
Yes, your understanding is right. Please add F# as a separate For metadata, give F# its own Since
Example for the alias (same on 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 Thanks! |
jymaire
left a comment
There was a problem hiding this comment.
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#
Scriptsection. - [LOW] The F# task lacks the output-files example that the C#
Scripthas.
|
@fdelbrayelle @jymaire @Malaydewangan09, I have done all the required changes, Could you please review my pull request?? |
jymaire
left a comment
There was a problem hiding this comment.
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.mdno longer mentions F#, andio.kestra.plugin.scripts.fsharp.mddocuments Script, Commands and both triggers. The thread was already resolved. - ✅ [LOW] Missing F# output-files example:
fsharp/Script.javanow has thefsharp_generate_filesexample. 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).containerImagewith its default and@Schemais already declared inAbstractDotnetCommands; the C#Commandsstill redeclares it and shadows the parent field, while the F#Commandsdoes 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.
QA report for #460: F# script execution support (head
|
| # | 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_triggerbelow), or an existing flow onio.kestra.plugin.scripts.dotnet.ScriptTrigger(legacy_script_trigger). Neither creates an execution, and each poll logs the stack traceAbstractExecScript.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: AUTOset explicitly fires once, asedgeexpects (fsharp_trigger_targetos). ThemainJAR does not passtargetOS, and itslegacy_script_triggerandlegacy_commands_triggerboth fire on v1.3.42. - Why CI missed it: the new trigger tests set
.targetOS(Property.ofValue(TargetOS.LINUX))explicitly, for examplefsharp/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 leavestargetOSunset 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#
ScriptandCommands: hello world, NuGet references,dotnet fsion input files,dotnet --version,::{"outputs"}::vars, input rendering andoutputFiles. - 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.Scriptand.Commandsvalidate 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 separateio.kestra.plugin.scripts.csharpandio.kestra.plugin.scripts.fsharpsubgroups, separate icons and doc pages./plugins/io.kestra.plugin.scripts.dotnet.Scriptstill 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'smetadata/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 |
||
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!"| 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))| 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']) }}"| 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| 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| 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!");| 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));| 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!");| 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| 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| 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"| 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| 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"| 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| 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 }}"| 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']) }}"| 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#!")| 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) |
.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
left a comment
There was a problem hiding this comment.
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.
|
Hi @jymaire, thanks for the detailed review! I've pushed the follow-up fixes addressing the feedback:
The plugin test suite passes all 72 tests, and documentation lint passes all 25 rules. I've also retained the existing Groovy Thanks for your guidance. Could you please take another look when you get a chance? |
QA report for #460: F# script execution support (head
|
| 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#
ScriptandCommandsend to end: hello world, NuGet references,dotnet fsion input files,dotnet --version, inputs rendering,::{"outputs"}::vars,outputFilesread 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#
ScriptandCommandsexamples, includingdotnet_commandsafter thecontainerImageoverride was removed fromcsharp/Commands: it still runs in the default .NET SDK image. - Legacy task aliases
io.kestra.plugin.scripts.dotnet.Script/.Commandsrun, 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!"| 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))| 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."| 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| 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| 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!");| 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));| 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!");| 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']) }}"| 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| 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| 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"| 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| 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"| 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| 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 }}"| 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']) }}"| 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_triggerand the legacydotnet.ScriptTriggerfired at 53s and 45s, because each C# poll installsdotnet-scriptin 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-scriptinstall on every run). This is the existing C# behaviour onmain.
Caveats
- Only
fsharp_hello_world(1.3, editor) andfsharp_generate_files(2.0, Execute button) were created or run through the UI. The other flows were created withPOST /api/v1/main/flowsand 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.
* 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>




What changes are being made and why?
This PR adds native F# script support to the existing
plugin-script-dotnetmodule 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:
C# package
The existing C# implementation has been moved from
io.kestra.plugin.scripts.dotnettoio.kestra.plugin.scripts.csharp, with:io.kestra.plugin.scripts.csharp.Scriptio.kestra.plugin.scripts.csharp.Commandsio.kestra.plugin.scripts.csharp.ScriptTriggerio.kestra.plugin.scripts.csharp.CommandsTriggerA dedicated C#
package-info.javawas added with the package titleC#.F# package
A new dedicated
io.kestra.plugin.scripts.fsharppackage was added with:io.kestra.plugin.scripts.fsharp.Scriptio.kestra.plugin.scripts.fsharp.Commandsio.kestra.plugin.scripts.fsharp.ScriptTriggerio.kestra.plugin.scripts.fsharp.CommandsTriggerA dedicated F#
package-info.javawas added with the package titleF#.The temporary
io.kestra.plugin.scripts.dotnet.FSharptask has been removed and replaced by these four F# task types.F#
Scriptexecutes.fsxfiles usingdotnet fsiand supports inline scripts, NuGet#rreferences, input/output files,beforeCommands, custom container images, task runners, and workflow variables.Shared implementation
Shared .NET functionality was extracted into:
These common classes provide shared behavior for C# and F#, including:
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:
A
BackwardCompatibilityTestwas added to deserialize a flow using the oldio.kestra.plugin.scripts.dotnet.Scripttype 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 thecsharpandfsharppackages.Metadata and catalog
The existing
plugin-script-dotnetmodule metadata was updated from C#-only wording to.NET (C# and F#).Separate metadata entries were added for:
with package titles
C#andF#.Each package has its own
package-info.java, allowing separate catalog/documentation entries while remaining inside the existing.NETmodule.Documentation
The previous combined
.NETdocumentation was split into:The C# page remains focused on the existing C# implementation and uses the new
csharppackage names.The F# page documents:
ScriptCommandsScriptTriggerCommandsTrigger.fsxexecution withdotnet fsi#r "nuget:PackageName,Version"referencesThe general
.NETdocumentation page now explains the C# and F# package structure and links to the language-specific pages.The outdated
io.kestra.plugin.scripts.dotnet.mdpage 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:
The C# package has:
The existing C# artwork is retained for the C# package.
The F# package has:
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
.NETmodule 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
csharppackage and updated to use the new canonical task types.New F# tests mirror the C# coverage, including:
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.yamlsanity-check flow was updated to use the new canonical C# package:The Linux target OS is explicitly configured for the Docker-based sanity checks.
AGENTS.md
AGENTS.mdwas updated so the plugin inventory lists the new canonical public packages: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.4and the finalplugin-script-dotnetJAR built from this branch.Full plugin test suite
Result:
Documentation lint
Result:
Diff validation
No whitespace errors were reported.
Local Kestra UI verification
The final plugin JAR was built and loaded into a local Kestra
v2.0.4instance.The plugin catalog was verified to show:
with 8 tasks:
The final JAR was used for the functional UI checks below.
F# Hello World
Result: successful execution with
Hello from F# and Kestra!in the task logs.F# NuGet dependency
Result: successful execution using the restored NuGet dependency and expected JSON output.
F# output files
Result: successful execution; the log contained
Created hello.txt successfully.and the persisted task output contained thehello.txtkey.C# backward compatibility
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# :-
After Implementing F#:-
These 8 screenshots demonstrate the four C# and four F# task types in the final catalog.
1. C#
ScriptShows the C# inline
.csxscript task under the newcsharppackage.2. C#
CommandsShows the C# command execution task under the new
csharppackage.3. C#
ScriptTriggerShows the C# script trigger under the new
csharppackage.4. C#
CommandsTriggerShows the C# commands trigger under the new
csharppackage.5. F#
ScriptShows the new F# inline
.fsxscript task and itsdotnet fsidocumentation.6. F#
CommandsShows the F# command execution task under the new
fsharppackage.7. F#
ScriptTriggerShows the F# script trigger under the new
fsharppackage.8. F#
CommandsTriggerShows the F# commands trigger under the new
fsharppackage.Screenshots — functional QA
1. F# Hello World
The final JAR was tested in the local Kestra UI with:
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#
#rNuGet directive:The dependency was restored successfully and the script produced the expected JSON output.
3. F# output files
The final JAR was tested with:
The task completed successfully, the log contained
Created hello.txt successfully., and the task output contained the persistedhello.txtkey.4. C# backward compatibility
Backward compatibility was verified using the old public type:
The old type executed successfully and produced
Hello from old C# type!, confirming the alias resolves the old.dotnet.Scriptname to the newcsharp.Scriptimplementation.Final package layout
The resulting package structure is:
The existing module remains:
with the module/catalog title:
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:
v2.0.4instance..NET (C# and F#)catalog entry and all 8 language tasks.io.kestra.plugin.scripts.dotnet.Scriptcompatibility flow.Contributor Checklist ✅
io.kestra.plugin.scripts.fsharp.Script,Commands,ScriptTrigger, andCommandsTrigger.io.kestra.plugin.scripts.csharp..dotnet.*type names remain supported through aliases.package-info.javafiles.io.kestra.plugin.scripts.dotnet.mddocumentation page was removed.plugin-script-dotnettest suite passes: 69/69.git diff --cached --check.v2.0.4..NET (C# and F#)catalog entry and all 8 language tasks were verified in the Kestra UI.AGENTS.mdwas updated to list the new canonical C# and F# packages.Closes kestra-io/kestra#12742