Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

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 Markdown

A 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

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 test

After 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 greeter

axiom 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 carries option csharp_namespace = "Gen";, so every message compiles into the Gen namespace.
  • nodes/ — node implementations and their tests.
  • service.csproj — the deployable service project (generated; do not edit). It wires Grpc.Tools so dotnet build compiles the .proto files; there is no separate codegen step.
  • tests/Tests.csproj — the test project axiom test runs with dotnet test. It compiles the node sources, the Axiom/ context interface, and the message protos directly, and references xUnit.
  • .gitignore — excludes .axiom/, gen/, bin/, and obj/.

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_case in the .proto file; the generated C# properties are PascalCase (name becomes input.Name).
  • The generated types land in the Gen namespace — node files reference them with using 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 unary

This 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.
  • --input and --output must name messages defined in messages/messages.proto or available from an imported package — see import package types. Imported messages get a using imports.<proto-package>; line instead of using 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 --input and --output are required and --type defaults to unary.
  • --type is unary (one input in, one output out) or pipeline (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:

  • Loggingax.Log() returns a structured logger with Debug, Info, Warn, and Error, each taking a message and an optional IDictionary<string, string> of attributes. The logger type is nested as IAxiomContext.ILogger, so it never collides with Microsoft.Extensions.Logging.ILogger.
  • Secretsax.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 memoryax.Agent().Memory() exposes Search(query, limit) and Write(content, importance), plus Session(sessionId) for session-scoped memory with conversation History() (Last(n), Append(role, content)) and End(). See memory.
  • Identityax.ExecutionId(), ax.FlowId(), and ax.TenantId() return the current execution context.
  • Reflection and mutationax.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 test

Arguments 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 build

When you are ready to deploy:

axiom push

axiom 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