Skip to content

C# WASM Nodes

C# brings the .NET ecosystem to Flow-Like WASM nodes. The template uses the experimental WASI workload for .NET 10, with WIT bindings handled automatically by the MSBuild integration. A high-level FlowLikeWasmSdk NuGet package provides ergonomic Context, NodeDefinition, and ExecutionResult types.

Terminal window
dotnet workload install wasi-experimental

The WIT bindings are processed at build time by the SDK — no separate wit-bindgen or wasm-tools install required.

wasm-node-csharp/
├── FlowLikeWasmNode.csproj # Project file (wasi-wasm target)
├── Node.cs # Node definition & run logic
├── Program.cs # WASM entry point / CLI dispatcher
├── flow-like.toml # Flow-Like package manifest
└── mise.toml # Build tasks

The WIT file is copied from the monorepo at build time (mise run build runs wit-copy first).

FlowLikeWasmNode.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FlowLikeWasmSdk" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<Wit Include="wit/flow-like-node.wit" World="flow-like-node" />
</ItemGroup>
</Project>

The <Wit> item tells the WASI SDK to process the WIT file and generate bindings automatically.

Define your node’s metadata, pins, and permissions in Node.cs:

Node.cs
using FlowLike.Wasm.Sdk;
namespace FlowLike.Wasm.Node;
public static class CustomNode
{
public static NodeDefinition GetDefinition()
{
var nd = new NodeDefinition(
name: "my_custom_node_csharp",
friendlyName: "My Custom Node",
description: "A template WASM node",
category: "Custom/WASM"
);
nd.AddPermission("streaming");
nd.AddPin(PinDefinition.InputExec("exec"));
nd.AddPin(PinDefinition.InputPin("input_text", PinType.String, defaultValue: ""));
nd.AddPin(PinDefinition.InputPin("multiplier", PinType.I64, defaultValue: 1));
nd.AddPin(PinDefinition.OutputExec("exec_out"));
nd.AddPin(PinDefinition.OutputPin("output_text", PinType.String));
nd.AddPin(PinDefinition.OutputPin("char_count", PinType.I64));
return nd;
}
public static ExecutionResult Run(Context ctx)
{
var inputText = ctx.GetString("input_text", "") ?? "";
var multiplier = ctx.GetI64("multiplier", 1) ?? 1;
ctx.Debug($"Processing: '{inputText}' x {multiplier}");
var repeated = multiplier > 0
? string.Concat(Enumerable.Repeat(inputText, (int)multiplier))
: "";
ctx.StreamText($"Generated {repeated.Length} characters");
ctx.SetOutput("output_text", repeated);
ctx.SetOutput("char_count", repeated.Length);
return ctx.Success();
}
}

Program.cs dispatches WIT export calls. It also supports CLI invocation for local testing:

Program.cs
using FlowLike.Wasm.Sdk;
using FlowLike.Wasm.Node;
var cliArgs = Environment.GetCommandLineArgs();
if (cliArgs.Length >= 2)
{
var command = cliArgs[1];
if (string.Equals(command, "get-node", StringComparison.OrdinalIgnoreCase))
{
Console.Write(WitExports.GetNode());
return;
}
if (string.Equals(command, "run", StringComparison.OrdinalIgnoreCase))
{
var inputJson = cliArgs.Length >= 3 ? cliArgs[2] : Console.In.ReadToEnd();
Console.Write(WitExports.Run(inputJson ?? "{}"));
return;
}
}
public static class WitExports
{
public static string GetNode()
{
var definition = CustomNode.GetDefinition();
return Json.Serialize(new[] { definition.ToDictionary() });
}
public static string GetNodes() => GetNode();
public static string Run(string inputJson)
{
var ctx = Context.FromJson(inputJson);
var result = CustomNode.Run(ctx);
return result.ToJson();
}
public static int GetAbiVersion() => 1;
}
// Read inputs
ctx.GetString("pin_name", defaultValue) // -> string?
ctx.GetI64("pin_name", defaultValue) // -> long?
ctx.GetF64("pin_name", defaultValue) // -> double?
ctx.GetBool("pin_name", defaultValue) // -> bool?
// Write outputs
ctx.SetOutput("pin_name", value)
// Logging
ctx.Debug("message")
ctx.Info("message")
ctx.Warn("message")
ctx.Error("message")
// Streaming
ctx.StreamText("partial output")
// Execution control
ctx.Success() // -> ExecutionResult (activates "exec_out")
ctx.Fail("reason") // -> ExecutionResult with error

The template uses mise for task orchestration:

Terminal window
# One-time setup: install WASI workload
mise run setup
# Build (copies WIT, then publishes as single-file WASM bundle)
mise run build

The build command runs:

Terminal window
dotnet publish -c Release \
/p:WasmSingleFileBundle=true \
/p:WasiClangLinkOptimizationFlag=-O0 \
/p:WasiClangCompileOptimizationFlag=-O0 \
/p:WasiBitcodeCompileOptimizationFlag=-O0

Output: bin/Release/net10.0/wasi-wasm/AppBundle/FlowLikeWasmNode.wasm

Terminal window
# Copy WIT file
mkdir -p wit
cp ../../packages/wasm/wit/flow-like-node.wit wit/
# Install workload + restore
dotnet workload install wasi-experimental
dotnet restore
# Publish
dotnet publish -c Release /p:WasmSingleFileBundle=true
Terminal window
mise run test # runs dotnet test
mise run clean # removes bin/, obj/, wit/