Create a node in C#
Scaffold a C# package, implement a unary or pipeline node against IAxiomContext, and run xUnit tests with axiom test — codegen happens at dotnet build via Grpc.Tools.
View as MarkdownA node is the single unit of compute in Axiom: a plain function with a typed
input message and a typed output message. This guide creates a C# package,
implements a unary node and a pipeline node against IAxiomContext, and runs
the tests — everything you need before publishing with axiom push.
New to Axiom? Do Write your first node first — it walks the same loop step by step (in Python). This guide covers what is specific to C#.
Prerequisites
- The Axiom CLI installed and on your PATH — see Install the Axiom CLI.
- The .NET SDK 8.0 or newer (
dotneton your PATH) — the generated projects targetnet8.0. Install from https://dotnet.microsoft.com/download. - Docker, only if you run
axiom buildlocally.
No protoc and no protobuf plugins are needed: C# proto code generation runs
inside dotnet build via the Grpc.Tools MSBuild package, which the generated
project files already reference.
The fast path
axiom init my-org/greeter --language csharp
cd greeter
axiom create message GreetRequest --fields "name:string"
axiom create message GreetReply --fields "greeting:string"
axiom create node Greet --input GreetRequest --output GreetReply --type unary
axiom testAfter these commands you have a package with one node, a generated xUnit test file, and a passing test run. The sections below explain each artifact and how to implement the node body.
Create a C# package
axiom init my-org/greeter --language csharp
cd greeteraxiom init creates ./greeter/ (the part of the package name after the
last /) containing:
axiom.yaml— the package manifest. See axiom.yaml reference.messages/messages.proto— the single file where all of the package's message types are defined. For C# it carriesoption csharp_namespace = "Gen";, so every message compiles into theGennamespace.nodes/— node implementations and their tests.service.csproj— the deployable service project (generated; do not edit). It wires Grpc.Tools sodotnet buildcompiles the.protofiles; there is no separate codegen step.tests/Tests.csproj— the test projectaxiom testruns withdotnet test. It compiles the node sources, theAxiom/context interface, and the message protos directly, and references xUnit..gitignore— excludes.axiom/,gen/,bin/, andobj/.
Because codegen is deferred to dotnet build, axiom generate is a no-op
for C# — it prints C# compiles protobufs at build time via Grpc.Tools — nothing to generate. and exits successfully.
Define the input and output messages
Every node input and output is a message defined in
messages/messages.proto. axiom create message appends a message block to
that file:
# Run inside the package directory (where axiom.yaml is).
axiom create message GreetRequest --fields "name:string"
axiom create message GreetReply --fields "greeting:string"--fields is a semicolon-separated list and accepts canonical proto3
(string name = 1), proto3 without field numbers (string name), or the
colon shorthand above. Field numbers are auto-assigned when omitted. Without
--fields you get placeholder fields plus a HINTS comment block to edit by
hand.
Two C#-specific points:
- Proto field names are
snake_casein the.protofile; the generated C# properties are PascalCase (namebecomesinput.Name). - The generated types land in the
Gennamespace — node files reference them withusing Gen;.
Comments written directly above a message or field (no blank line in between) are extracted at publish time and shown in the Axiom registry as documentation. For how messages type-check across a flow, see the type system.
Scaffold the node
# Run inside the package directory (where axiom.yaml is).
axiom create node Greet --input GreetRequest --output GreetReply --type unaryThis creates nodes/greet.cs and nodes/greet_test.cs (file names are the
snake_case of the node name), appends the node entry to axiom.yaml, and
writes Axiom/Context.cs — the generated IAxiomContext interface
(marked DO NOT EDIT; it is regenerated idempotently).
Rules and behavior:
- The node name must be PascalCase (letters and digits only). For C# the function name is the node name itself.
--inputand--outputmust name messages defined inmessages/messages.protoor available from an imported package — see import package types. Imported messages get ausing imports.<proto-package>;line instead ofusing Gen;, where the namespace is the source package name with/becoming.and-becoming_(e.g.using imports.axiom_official.axiom_text_ops;).- At an interactive terminal, omitted flags trigger prompts: a numbered list
of available messages, and a node type prompt defaulting to
unary. In non-interactive runs--inputand--outputare required and--typedefaults tounary. --typeisunary(one input in, one output out) orpipeline(streaming) — see the execution model.
Implement the node
Replace the generated stub in nodes/greet.cs with a working body. A unary
node is a static method on a static class in the Nodes namespace, and its
signature must match exactly — axiom validate checks it:
// nodes/greet.cs
using Axiom;
using Gen;
using System.Collections.Generic;
namespace Nodes;
public static class GreetNode
{
/// <summary>
/// Returns a personalized greeting for the caller's name.
/// </summary>
public static GreetReply Greet(IAxiomContext ax, GreetRequest input)
{
ax.Log().Info("greeting requested",
new Dictionary<string, string> { ["name"] = input.Name });
return new GreetReply { Greeting = $"Hello, {input.Name}!" };
}
}The XML doc comment (///) directly above the method is extracted at
publish time and shown in the Axiom registry as the node's documentation —
write a real description before publishing.
Use platform capabilities through IAxiomContext
ax is the single injection point for every platform capability — node code
never calls platform services directly (the sidecar mediates everything; see
sandboxing and tenancy). The
interface lives in the generated Axiom/Context.cs:
- Logging —
ax.Log()returns a structured logger withDebug,Info,Warn, andError, each taking a message and an optionalIDictionary<string, string>of attributes. The logger type is nested asIAxiomContext.ILogger, so it never collides withMicrosoft.Extensions.Logging.ILogger. - Secrets —
ax.Secrets().Get("MY_API_KEY")returns a(string Value, bool Found)tuple:
// Inside a node method body. required_secrets is informational: an
// unregistered secret yields found=false here at read time, not an error.
var (apiKey, found) = ax.Secrets().Get("MY_API_KEY");
if (!found)
{
ax.Log().Warn("MY_API_KEY not configured");
}List each secret name under the node's required_secrets in axiom.yaml;
axiom validate warns when a Secrets().Get call references a name not
listed there. See manage secrets.
- Agent memory —
ax.Agent().Memory()exposesSearch(query, limit)andWrite(content, importance), plusSession(sessionId)for session-scoped memory with conversationHistory()(Last(n),Append(role, content)) andEnd(). See memory. - Identity —
ax.ExecutionId(),ax.FlowId(), andax.TenantId()return the current execution context. - Reflection and mutation —
ax.Reflection().Flow()is a read-only view of the running flow graph (Nodes(),Edges(),Position());ax.Mutation().Flow()buffers additive graph changes (AddNode,AddEdge).
Write a pipeline node
A pipeline node streams: it consumes an async stream of input frames and
yields output frames. Scaffold one with --type pipeline; the required
signature is an async iterator:
// nodes/stream_greetings.cs
using Axiom;
using Gen;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Nodes;
public static class StreamGreetingsNode
{
/// <summary>
/// Emits one greeting per incoming request frame.
/// </summary>
public static async IAsyncEnumerable<GreetReply> StreamGreetings(
IAxiomContext ax,
IAsyncEnumerable<GreetRequest> inputs,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var input in inputs.WithCancellation(cancellationToken))
{
yield return new GreetReply { Greeting = $"Hello, {input.Name}!" };
}
}
}For the entry node of a pipeline flow, the stream yields exactly one item.
axiom create node --type pipeline generates a pipeline-shaped test file
alongside the implementation. See
the execution model for when to choose
pipeline over unary.
Run the tests
axiom test validates the package, then runs
dotnet test tests/Tests.csproj in place, streaming output live; the exit
code mirrors the test runner's. (The generate step it runs first is a no-op
for C#.)
axiom create node generated nodes/greet_test.cs containing a
TestContext — a no-op IAxiomContext you can edit to drive a scenario —
and one xUnit [Fact]. Replace the generated test body so it asserts real
output values:
// nodes/greet_test.cs — replace the body of the generated [Fact] method
[Fact]
public void TestGreet()
{
IAxiomContext ax = new TestContext();
var input = new GreetRequest { Name = "Ada" };
var result = GreetNode.Greet(ax, input);
Assert.Equal("Hello, Ada!", result.Greeting);
}axiom testArguments after -- go straight to dotnet test, e.g.
axiom test -- --filter TestGreet.
axiom validate warns — without blocking — when a node has no test, and
axiom test runs the suite with dotnet test. The push build compiles only
the service project — tests never enter the deploy image — so a green
axiom test before pushing is the quality gate that matters. Assert output
fields meaningfully — not just null-checked.
Build and publish
axiom dev supports C# via a rebuild-on-save-restart loop: each saved change
runs dotnet build and restarts the service (compiled languages recompile
rather than hot-swap, so the first build is slower while the SDK warms up; a
failed build leaves the previous server running). The HTTP bridge (default
:8083) converts JSON to and from Protobuf so you can curl your nodes. Use
axiom build to verify the deployable artifact.
axiom build produces the same Docker image the publish pipeline builds:
it validates the package, generates the service artifacts, and runs
docker build. The resulting image runs the service on the ASP.NET 8 runtime;
the test project is never included, so no test framework ships in the image.
axiom buildWhen you are ready to deploy:
axiom pushaxiom push validates locally, then pushes the package to the Axiom
platform, visible only to your own tenant. It requires a prior axiom login,
and the current git HEAD must be pushed to the remote — the platform builds
from the repository, not your working tree. Pushing the same version again
overwrites the previous push. See
push the package and build your first flow.
Next steps
- Push the package and build your first flow.
- Invoke a flow via the API.
- Import package types — use messages from other packages as node inputs and outputs.
- Manage secrets in a flow.
- Writing in another language? See the guides for Python, Go, TypeScript, Rust, and Java.