Skip to content

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

Open
GauriKhedekar wants to merge 4 commits into
kestra-io:mainfrom
GauriKhedekar:feat/dotnet-fsharp-script-support
Open

GauriKhedekar wants to merge 4 commits into
kestra-io:mainfrom
GauriKhedekar:feat/dotnet-fsharp-script-support

Conversation

@GauriKhedekar

@GauriKhedekar GauriKhedekar commented Oct 3, 2026 •

Copy link
Copy Markdown

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
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
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
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: To review

Development

Successfully merging this pull request may close these issues.

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

3 participants