Vortex lets you run C# directly from your config file. You can write quick inline scripts, or set up a shared runtime so that multiple jobs can reuse the same packages, variables, and helper methods.
Under the hood, Vortex generates a .NET project and runs it with dotnet run. The first run takes a few seconds to compile, but subsequent runs reuse the cached build output and are near-instant.
The quickest way to run C# is with shell: csharp and an inline command:
name: dev
jobs:
- id: hello
shell: csharp
command: Console.WriteLine("hello from vortex");Your command uses top-level statements — no Main method or class wrapper needed. await works at the top level.
When you have multiple jobs that need the same setup — shared packages, variables, or helper methods — declare a csharp block at the top level and connect jobs to it with use:
name: dev
csharp:
usings:
- System.IO
vars:
apiBase: http://localhost:3000
functions:
LogBanner: |
public static void LogBanner(string text)
{
Console.WriteLine($"== {text} ==");
}
jobs:
- id: check-api
shell: csharp
use: csharp
command: |
LogBanner($"Checking {apiBase}");
using var client = new HttpClient();
var resp = await client.GetAsync(apiBase);
Console.WriteLine(resp.StatusCode);
- id: check-paths
shell: csharp
use: csharp
command: |
Console.WriteLine(Path.GetFileName("/tmp/demo.txt"));Both jobs can use LogBanner and apiBase without repeating them.
Two things are required for a job to use the shared runtime:
shell: csharp— tells Vortex to generate a .NET projectuse: csharp— connects the job to the sharedcsharpblock
How this works under the hood
When a job has use: csharp, Vortex generates a .NET project in ~/.cache/vortex/csharp-runtime/:
- project.csproj — targets the specified framework with NuGet package references
- Shared.cs — a static
Vortexclass containing vars aspublic static readonlyfields and functions as static methods - Program.cs — the job's command as top-level statements
The Shared.cs is included in all connected jobs, and using static Vortex; is auto-injected so vars and functions are available without a class prefix.
The project preserves bin/ and obj/ directories across runs, so only changed files trigger recompilation.
If you change the csharp block and reload the config, all connected jobs restart automatically.
The sources field lets you write your code in real .cs files on disk (with full IntelliSense) and make their types available in all connected jobs:
name: dev
csharp:
sources:
- ./lib/HttpHelpers.cs
vars:
apiBase: http://localhost:3000
jobs:
- id: smoke
shell: csharp
use: csharp
command: |
var data = await HttpHelpers.FetchJson(apiBase + "/health");
Console.WriteLine(data);Source files are copied into the generated project. Since everything compiles together, any public classes and methods are directly available in job commands.
// lib/HttpHelpers.cs
using System.Net.Http;
using System.Text.Json;
public static class HttpHelpers
{
private static readonly HttpClient _client = new();
public static async Task<JsonDocument> FetchJson(string url)
{
var response = await _client.GetAsync(url);
response.EnsureSuccessStatusCode();
var stream = await response.Content.ReadAsStreamAsync();
return await JsonDocument.ParseAsync(stream);
}
}Use source files when:
- Classes are complex with multiple methods
- You want full IDE support (IntelliSense, refactoring, debugging)
- Code is shared with other .NET projects
Use inline functions when:
- Helpers are short (1–10 lines)
- They're Vortex-specific glue code
- You want the config to be self-contained
You can combine both:
csharp:
sources:
- ./lib/ComplexLogic.cs
functions:
Wrap: |
public static string Wrap(string text)
{
return $"[{text}]";
}The usings field adds using directives to the shared source file:
csharp:
usings:
- System.IO
- System.Text.Json
- System.Net.HttpWhat's already available without adding usings
.NET's implicit usings are enabled by default, which includes:
SystemSystem.Collections.GenericSystem.LinqSystem.Threading.Tasks
You only need to add usings for namespaces not covered by implicit usings.
The packages field adds NuGet package references:
csharp:
packages:
- name: Newtonsoft.Json
version: "13.0.3"
- name: Dapper
version: "2.1.28"NuGet restore happens automatically on the first dotnet run. Packages are cached by the .NET SDK as usual.
Variables declared in vars are available directly in all connected jobs:
csharp:
vars:
apiBase: http://localhost:3000
port: 3000
debug: trueHow variable types are inferred
Types are mapped from YAML:
- Strings →
string - Integers →
int - Booleans →
bool - Floating point →
double
Vars become public static readonly fields on the generated Vortex class, and using static Vortex; makes them available without qualification.
Functions let you define reusable methods directly in the config:
csharp:
functions:
LogBanner: |
public static void LogBanner(string text)
{
Console.WriteLine($"\n== {text} ==\n");
}
MeasureTime: |
public static async Task<T> MeasureTime<T>(string label, Func<Task<T>> action)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = await action();
sw.Stop();
Console.WriteLine($"{label}: {sw.ElapsedMilliseconds}ms");
return result;
}Rules for functions:
- Must be valid C#
public staticmethod declarations - Are pasted into the generated
Vortexclass as-is - The YAML key should match the method name
Jobs with shell: csharp but without use: csharp also run via dotnet run — they just don't get the shared vars or functions:
name: dev
jobs:
- id: quick-check
shell: csharp
command: |
using var client = new HttpClient();
var resp = await client.GetStringAsync("http://localhost:3000/health");
Console.WriteLine(resp);This is useful for one-off scripts that don't need shared state.
The framework field controls the .NET target framework:
csharp:
framework: net9.0 # defaults to net8.0How this maps to the generated project
This sets the <TargetFramework> in the generated .csproj. You need the corresponding .NET SDK installed (e.g. .NET 9.0 SDK for net9.0).
- Fast rebuilds — the generated project keeps
bin/andobj/cached; only changed files trigger recompilation - Hot reload — changing the
csharpblock restarts all connected jobs automatically - Top-level statements — job commands are top-level C# code;
awaitworks directly - NuGet cache — packages are cached globally by the .NET SDK, so they're only downloaded once
- Cross-platform — works on macOS, Linux, and Windows wherever
dotnetis installed - No
use— jobs withshell: csharpbut nouse: csharprun on their own project with no shared runtime