Skip to content

Add .NET (C#) Script Sub-Plugin to Scripts Plugin #375

@fdelbrayelle

Description

@fdelbrayelle

Summary

Kestra's script plugin ecosystem covers more than a dozen languages — Python, Node.js, Ruby, Go, Deno, and more — but has no support for running C# scripts. This gap is especially felt by .NET-heavy teams (financial services, enterprise, Windows-centric shops) who orchestrate C# Windows Services, console apps, and analytical scripts.

Adding a plugin-script-dotnet sub-module with Script and Commands tasks would let these teams keep their C# code as first-class orchestration steps inside Kestra flows, eliminating the need for brittle wrapper scripts or separate runner services. C# scripting is widely cited as an underserved area in orchestration tooling — a first-mover Kestra integration would be a real differentiator.

Motivation

Without native C# script support, .NET-centric teams face common workarounds:

  • Wrapping dotnet run calls inside shell tasks — no syntax support, poor DX, error messages swallowed by the shell layer
  • Rewriting analytics in Python just to fit the orchestrator
  • Maintaining a separate runner service that Kestra calls over HTTP, adding operational overhead and an extra failure point

The target audience spans financial services teams (C# Windows Services for portfolio and trading workflows), enterprise platform engineers, and data analysts who work alongside Java/Python colleagues but prefer C# for business logic. The integration slots naturally alongside the existing PowerShell sub-plugin — both serve .NET-platform users, with C# covering the programmatic layer where PowerShell covers the operational/admin layer.

Context

No prior .NET/C# scripting issue found across kestra-io. The existing plugin-script-powershell sub-plugin is the nearest reference implementation — the architecture (Docker runner, Commands + Script tasks, RunnerType, AbstractLogConsumer) translates directly.

Implementation should use dotnet-script to run .csx (C# Script) files. This gives users a familiar scripting experience with inline NuGet package references (#r "nuget:PackageName,version"), full C# language features without a project file, and rapid iteration without a build step.

API Reference

  • Official docs: https://github.com/dotnet-script/dotnet-script
  • Authentication: N/A — local subprocess runtime, no auth required
  • Base URL pattern: N/A — subprocess-based execution
  • Runtime tool: dotnet-script CLI (dotnet tool install -g dotnet-script)
  • Docker image: mcr.microsoft.com/dotnet/sdk:10.0 (LTS)
  • Script format: .csx — C# Script files with inline NuGet directives

Gradle Dependencies

Add to plugin-script-dotnet/build.gradle:

// No additional JVM-side runtime dependencies needed.
// The .NET SDK is provided via the Docker/task-runner image.
// Kestra's internal HTTP client and Jackson are provided by the framework.
// Follow the same dependency block as plugin-script-powershell.

dependencies {
    implementation project(':plugin-script')
}

dotnet-script runs as a subprocess via the task runner infrastructure — no JVM-side SDK is required.

Kestra framework inclusions (do not add as dependencies): Kestra's internal HTTP client (io.kestra.core.http.client) and Jackson serializers are provided by the framework.

Plugin Structure

  • Repository: kestra-io/plugin-scripts (new sub-module plugin-script-dotnet)
  • Namespace: io.kestra.plugin.scripts.dotnet
  • Sub-plugins: None — flat structure
  • Categories: DATA, INFRASTRUCTURE

Task class naming: Use Script and Commands — consistent with every other sub-plugin in this repository. Do not prefix with DotNet, CSharp, or similar; the fully-qualified type already carries namespace context.

Suggested Tasks

  1. Add plugin-script-dotnet sub-module: register in settings.gradle, create build.gradle, scaffold package structure
  2. Implement Script task — executes an inline .csx C# script via dotnet-script
  3. Implement Commands task — runs arbitrary shell commands inside a .NET SDK container
  4. Add package-info.java with @PluginSubGroup(category = PluginSubGroup.PluginCategory.DATA)
  5. Add metadata/index.yaml and plugin icon SVG (io.kestra.plugin.scripts.dotnet.svg)
  6. Write unit + integration tests — happy path, failure scenarios, NuGet package reference support
  7. Add sanity-check YAML flow under src/test/resources/sanity-checks/
  8. Add how-to doc at src/main/resources/doc/io.kestra.plugin.scripts.dotnet.md

YAML Examples

Example 1 — Inline C# script with NuGet package

id: dotnet_inline_script
namespace: company.team
description: Run an inline C# script with a NuGet dependency

tasks:
  - id: hello_csharp
    type: io.kestra.plugin.scripts.dotnet.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));

Example 2 — Run dotnet commands against a script file

id: dotnet_commands
namespace: company.team
description: Run a dotnet-script file with a dynamic input

inputs:
  - id: data_file
    type: STRING

tasks:
  - id: run_dotnet
    type: io.kestra.plugin.scripts.dotnet.Commands
    commands:
      - dotnet-script analyze.csx --input {{ inputs.data_file }}

Example 3 — Scheduled portfolio analysis

id: portfolio_daily
namespace: company.team
description: Run portfolio analysis on weekday mornings

triggers:
  - id: daily
    type: io.kestra.plugin.core.trigger.Schedule
    cron: "0 6 * * MON-FRI"

tasks:
  - id: run_portfolio_analysis
    type: io.kestra.plugin.scripts.dotnet.Script
    script: |
      #r "nuget:CsvHelper,33.0.1"
      // load positions, compute P&L, emit CSV
      Console.WriteLine("Portfolio analysis complete: " + DateTime.UtcNow.ToString("o"));

Acceptance Criteria

Functional

  • Script task runs inline .csx C# scripts via dotnet-script
  • Commands task runs arbitrary shell commands in a .NET SDK container
  • NuGet package references (#r "nuget:...") work in the Script task
  • Unit + integration tests pass (./gradlew test)
  • Build passes with ./gradlew build

Kestra Plugin Coding Standards

  • All new properties use Property<T> — no legacy @PluginProperty(dynamic = true) on new code
  • Secret/credential properties annotated with @PluginProperty(secret = true)
  • Every property and output has a @Schema annotation
  • Task classes carry the five mandatory Lombok annotations (@SuperBuilder, @ToString, @EqualsAndHashCode, @Getter, @NoArgsConstructor)
  • Logging via runContext.logger() only
  • JSON serialization uses Jackson mappers from io.kestra.core.serializers
  • All Property<T> fields support Kestra expression language (template rendering)

Documentation & Structure

  • @Plugin(examples = ...) entries each set full = true with a complete runnable flow (id + namespace + tasks/triggers)
  • Sensitive values in examples use {{ secret('SECRET_NAME') }}
  • package-info.java with @PluginSubGroup(category = PluginSubGroup.PluginCategory.DATA)
  • metadata/index.yaml and plugin icon SVG present
  • How-to doc at src/main/resources/doc/io.kestra.plugin.scripts.dotnet.md

Repository Setup Checklist

This is a new sub-module in an existing repository — no new repository scaffolding or Terraform apply is required.

Add to Sanity Check page

Add plugin-script-dotnet to the Sanity check Notion page.


View as Artifact

Metadata

Metadata

Assignees

Labels

area/pluginPlugin-related issue or feature requestkind/customer-requestRequested by one or more customers
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions