---
title: "Go SDK reference"
description: "Every capability on axiom.Context — the Go form of AxiomContext: structured logging, secrets, agent memory, execution identity, flow reflection, and flow mutation, with full signatures."
category: reference
surfaces: [sdk]
languages: [go]
related: [guides/create-a-node-go, concepts/memory, concepts/sandboxing-and-tenancy, concepts/execution-model, guides/manage-secrets, guides/inspect-agent-memory, reference/sdk/python, reference/sdk/typescript, reference/axiom-yaml]
last_reviewed: 2026-06-06
---

# Go SDK reference

In Go, AxiomContext is the `axiom.Context` interface: the single parameter
(conventionally named `ax`) through which a node handler reaches every
platform capability — logging, secrets, agent memory, execution identity,
flow reflection, and flow mutation. There is no external SDK module to
install: the `axiom` package is generated into your package directory as
`axiom/context.go`, and your business logic imports only that local package
and the generated `gen` message types.

**Prerequisites.** Examples on this page assume a Go package scaffolded with
the Axiom CLI — see [create a node in Go](/docs/guides/create-a-node-go).
They use that guide's `demo/text-tools` package (module path
`demo/text-tools`, messages `TextRequest{text}` and `WordCountResult{count}`);
sections that need other messages state their own `axiom create message`
commands.

## The handler signature

A unary node is one exported function in package `nodes` with this fixed
shape — `axiom validate` checks it:

```go
// nodes/count_words.go
package nodes

import (
	"context"
	"strings"

	"demo/text-tools/axiom"
	gen "demo/text-tools/gen"
)

// CountWords splits the input text on whitespace and returns the word count.
func CountWords(ctx context.Context, ax axiom.Context, input *gen.TextRequest) (*gen.WordCountResult, error) {
	words := strings.Fields(input.GetText())
	ax.Log().Info("counted words", "count", len(words))
	return &gen.WordCountResult{Count: int32(len(words))}, nil
}
```

- `ctx context.Context` — standard Go context for cancellation; pass it to
  every `ax` call that takes one.
- `ax axiom.Context` — the platform capability injection point, documented
  below.
- `input` / return value — pointers to the generated types of the node's
  input and output messages.
- Returning a non-nil `error` fails the invocation; the error text is
  reported to the platform as the node's error message.

A pipeline node (declared with `--type pipeline`) receives input as a
channel and emits any number of output frames through a callback:

```go
// nodes/stream_counts.go
func StreamCounts(ctx context.Context, ax axiom.Context, in <-chan *gen.TextRequest, emit func(*gen.WordCountResult) error) error {
	for input := range in {
		words := strings.Fields(input.GetText())
		if err := emit(&gen.WordCountResult{Count: int32(len(words))}); err != nil {
			return err
		}
	}
	return nil
}
```

For the entry node of a flow running in pipeline mode, the channel yields
exactly one item. See [execution model](/docs/concepts/execution-model).

## Context methods at a glance

`axiom.Context` has eight methods. Adding new platform capabilities never
changes node function signatures — capabilities are always added as methods
here.

| Method | Returns | Purpose |
|---|---|---|
| `ax.Log()` | `Logger` | Structured logging for this invocation |
| `ax.Secrets()` | `Secrets` | Read-only access to tenant secrets |
| `ax.Agent()` | `Agent` | Agent capabilities — today, memory |
| `ax.ExecutionID()` | `string` | ID of the current execution |
| `ax.TenantID()` | `string` | ID of the tenant that owns this invocation |
| `ax.Reflection()` | `Reflection` | Read-only view of the running flow |
| `ax.Mutation()` | `Mutation` | Append-only mutation of the running flow |

## Logging

`ax.Log()` returns a structured logger pre-configured for the current
invocation. Use it instead of `fmt.Println` for anything you want visible
in production.

```go
// axiom/context.go (generated)
type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}
```

`args` follows Go's `log/slog` convention — alternating key, value pairs:

```go
// inside any node handler
ax.Log().Info("order processed", "order_id", "abc123", "total", 99.99)
```

Under `axiom dev` the logger writes concise plain-text lines to the
terminal. In the deployed service each log line automatically carries the
node name and `execution_id`, plus the active trace context, so production
logs are searchable by execution. Concurrent invocations each receive their
own `Logger` instance — never share one across calls, and never construct
one yourself.

## Secrets

`ax.Secrets()` returns read-only access to the tenant secrets the platform
resolved for this invocation. Values are plaintext strings; encryption and
decryption are the platform's job.

```go
// axiom/context.go (generated)
type Secrets interface {
	// Get returns the value for the named secret and true when present.
	// Returns ("", false) when the secret was not resolved for this invocation.
	Get(name string) (string, bool)
}
```

```go
// inside any node handler
apiKey, ok := ax.Secrets().Get("ANTHROPIC_API_KEY")
if !ok {
	return nil, fmt.Errorf("secret ANTHROPIC_API_KEY not configured")
}
```

Declare every secret name the node reads under the node's
`required_secrets` list in `axiom.yaml`, so users know what to register in
the console before invoking — see
[manage secrets in a flow](/docs/guides/manage-secrets). Secrets reach the
node only through this interface; they never appear in package source.

## Agent memory

`ax.Agent().Memory()` is the entry point to the memory layer
(see [memory](/docs/concepts/memory) for the model). Memory is namespaced
under `Agent()` because it is an agent capability, separate from
general-purpose utilities like logging.

This example needs messages with a session ID field — run these in the
package directory first:

```bash
axiom create message ChatRequest --fields "session_id:string;text:string"
axiom create message ChatReply --fields "text:string"
axiom create node Remember --input ChatRequest --output ChatReply --type unary
```

```go
// nodes/remember.go
package nodes

import (
	"context"
	"fmt"

	"demo/text-tools/axiom"
	gen "demo/text-tools/gen"
)

// Remember appends the user turn to session history and recalls related memories.
func Remember(ctx context.Context, ax axiom.Context, input *gen.ChatRequest) (*gen.ChatReply, error) {
	session := ax.Agent().Memory().Session(input.GetSessionId())

	if err := session.History().Append(ctx, "user", input.GetText()); err != nil {
		return nil, err
	}
	turns, err := session.History().Last(ctx, 20)
	if err != nil {
		return nil, err
	}
	memories, err := session.Search(ctx, input.GetText(), 5)
	if err != nil {
		return nil, err
	}
	return &gen.ChatReply{
		Text: fmt.Sprintf("%d turns in session, %d related memories", len(turns), len(memories)),
	}, nil
}
```

The session ID always comes from the typed input message — the platform
never infers it.

### Memory interfaces

```go
// axiom/context.go (generated)
type Agent interface {
	Memory() AgentMemory
}

type AgentMemory interface {
	// Session returns memory scoped to a specific session ID.
	Session(sessionID string) SessionMemory
	// Search queries semantic memory across all sessions for this flow.
	Search(ctx context.Context, query string, limit int) ([]MemoryEntry, error)
	// Write stores a durable memory at the flow scope (persists across sessions).
	Write(ctx context.Context, content string, importance float32) (string, error)
}

type SessionMemory interface {
	// Search finds the most relevant memories for this session and its flow.
	Search(ctx context.Context, query string, limit int) ([]MemoryEntry, error)
	// Write stores a semantic memory scoped to this session.
	Write(ctx context.Context, content string, importance float32) (string, error)
	// History returns the episodic history accessor for this session.
	History() SessionHistory
	// End formally closes the session and triggers consolidation.
	End(ctx context.Context) error
}

type SessionHistory interface {
	// Last returns the most recent n turns, oldest-first (for LLM context injection).
	Last(ctx context.Context, n int) ([]ConversationTurn, error)
	// Append adds a new turn to this session's history.
	// role is "user" | "assistant" | "tool" | "system"
	Append(ctx context.Context, role, content string) error
}
```

`Write` returns the ID of the stored memory. `End` triggers consolidation —
the promotion of episodic records into durable semantic facts.

### Memory data types

```go
// axiom/context.go (generated)
type ConversationTurn struct {
	ID         string
	SessionID  string
	Role       string // "user" | "assistant" | "tool" | "system"
	Content    string
	ToolName   string
	ToolCallID string
	CreatedAt  int64 // unix millis
}

type MemoryEntry struct {
	ID         string
	ScopeLevel string
	MemoryType string
	Content    string
	Importance float32
	Confidence float32
	Score      float32 // relevance score, populated on retrieval
	CreatedAt  int64
}
```

## Execution identity

Three string accessors are declared for execution identity. They are
read-only facts injected by the platform — node code cannot change them,
and tenant isolation is enforced by the sidecar regardless of what the
node does (see
[sandboxing and tenancy](/docs/concepts/sandboxing-and-tenancy)).

| Method | Meaning |
|---|---|
| `ax.ExecutionID()` | ID of the current execution — the same ID the HTTP API, traces, and debug events reference |
| `ax.TenantID()` | ID of the tenant that owns this invocation |

## Flow reflection

`ax.Reflection().Flow()` returns a read-only view of the running flow's
topology and the current invocation's position in it. Field-level message
introspection is not exposed — `InputMessageName`/`OutputMessageName` are
fully-qualified Protocol Buffers message names.

```go
// nodes/describe_flow.go
package nodes

import (
	"context"
	"fmt"
	"strings"

	"demo/text-tools/axiom"
	gen "demo/text-tools/gen"
)

// DescribeFlow returns a text summary of the running flow's topology.
func DescribeFlow(ctx context.Context, ax axiom.Context, input *gen.ChatRequest) (*gen.ChatReply, error) {
	flow := ax.Reflection().Flow()
	var b strings.Builder
	fmt.Fprintf(&b, "graph %s: %d nodes, %d edges, at instance %d\n",
		flow.GraphID(), len(flow.Nodes()), len(flow.Edges()),
		flow.Position().CurrentInstance)
	for _, n := range flow.Nodes() {
		fmt.Fprintf(&b, "[%d] %s (%s@%s) %s -> %s\n",
			n.InstanceID, n.Name, n.PackageName, n.PackageVersion,
			n.InputMessageName, n.OutputMessageName)
	}
	return &gen.ChatReply{Text: b.String()}, nil
}
```

### Reflection interfaces and types

```go
// axiom/context.go (generated)
type Reflection interface {
	Flow() FlowReflection
}

type FlowReflection interface {
	Nodes() []ReflectionNode
	Edges() []ReflectionEdge
	LoopEdges() []ReflectionEdge
	Position() FlowPosition
	GraphID() string
}

type ReflectionNode struct {
	InstanceID        uint32
	NodeULID          string
	Name              string
	PackageName       string
	PackageVersion    string
	NodeType          string // "node" | "subflow" | "pipeline"
	InputMessageName  string
	OutputMessageName string
	CanvasNodeID      string
}

type ReflectionEdge struct {
	SrcInstance      uint32
	DstInstance      uint32
	CanvasEdgeID     string
	HasCondition     bool
	HasAdapter       bool
	MaxIterations    uint32
	ConditionSummary *ConditionSummary
}

type ConditionSummary struct {
	Field    string   // dotted field path, e.g. "tools"
	Op       string   // short op name (EQ | NEQ | CONTAINS | ...)
	Operands []string // comparison operand(s)
}

type FlowPosition struct {
	CurrentInstance      uint32
	Depth                uint32            // 0 at the root flow
	LoopIterations       map[uint32]uint32 // keyed by loop-head dst_instance
	SubflowStackGraphIDs []string          // root → immediate parent
}
```

`HasCondition` and `HasAdapter` are structural flags only — the compiled
condition and adapter recipes are not exposed by design. When an edge is
conditional, `ConditionSummary` digests its leaf predicate (a leaf on a
repeated field has ANY semantics: `Field="tools", Op="EQ",
Operands=["ToolX"]` means "ToolX" is in `tools`), so a node can make
idempotent decisions like skipping a tool that is already wired.
`MaxIterations` is meaningful only on entries returned by `LoopEdges()`.

## Flow mutation

`ax.Mutation().Flow()` lets a node add nodes and edges to the running flow
mid-execution. Only nodes declared with `mutation_capable: true` in
`axiom.yaml` may call it. Calls buffer locally during the handler and are
submitted to the platform when the handler returns.

```go
// nodes/add_tool.go — this node needs mutation_capable: true in axiom.yaml
package nodes

import (
	"context"

	"demo/text-tools/axiom"
	gen "demo/text-tools/gen"
)

// AddTool wires a calculator node into the running flow behind a conditional edge.
func AddTool(ctx context.Context, ax axiom.Context, input *gen.ChatRequest) (*gen.ChatReply, error) {
	flow := ax.Mutation().Flow()
	toolInstance := flow.AddNode("demo/calculator", "1.0.0", &axiom.CanvasPosition{X: 400, Y: 200})
	self := ax.Reflection().Flow().Position().CurrentInstance
	flow.AddEdge(self, toolInstance, &axiom.EdgeCondition{
		Op:    "EQ",
		Field: "tools",
		Value: "Calculator",
	})
	return &gen.ChatReply{Text: "calculator wired in"}, nil
}
```

### Mutation interfaces and types

```go
// axiom/context.go (generated)
type Mutation interface {
	Flow() FlowMutation
}

type FlowMutation interface {
	AddNode(packageName, packageVersion string, canvasPosition *CanvasPosition) uint32
	AddEdge(srcInstance, dstInstance uint32, condition *EdgeCondition)
}

type CanvasPosition struct {
	X float64
	Y float64
}

type EdgeCondition struct {
	Op    string // EQ | NEQ | LT | LTE | GT | GTE | CONTAINS | ... Empty -> EQ.
	Field string // dotted field path on the source node's output message
	Value string // comparison operand (string form)
}
```

- `AddNode` returns the batch-local instance ID assigned to the new node —
  use it as `dstInstance` in `AddEdge` calls within the same handler.
  `canvasPosition` is an optional layout hint; pass `nil` to omit it.
- `AddEdge` with a non-nil `condition` wires a conditional dispatch edge
  that fires only when the predicate holds on the source node's output
  message (a condition on a repeated field has ANY semantics). Pass `nil`
  for an unconditional edge. Conditions are structural only — no CEL
  expressions, no adapters.

### Mutation rejection

The platform validates buffered mutations after the handler returns. A
rejected mutation surfaces as an error message with the deterministic
prefix `axiom: mutation rejected: `; the generated `MutationError` type
carries the human-readable reason after that prefix:

```go
// axiom/context.go (generated)
type MutationError struct{ Message string }

func (e *MutationError) Error() string { return e.Message }
```

The structured engine rejection code is not exposed to node code. To stay
idempotent across invocations, check the existing topology via
`ax.Reflection().Flow()` (including each edge's `ConditionSummary`) before
re-adding nodes or edges.

## Where the interface comes from

`axiom/context.go` is generated by the Axiom CLI — `axiom create node`
writes it (and regenerates it idempotently on every run), and the file
carries the banner `Code generated by Axiom CLI; DO NOT EDIT`. Do not edit
it: your changes are overwritten on the next scaffold. The same interface
is implemented by the local `axiom dev` server and by the deployed service
that `axiom push` builds, so handler code behaves identically in both. The
generated test file for each node includes a `newTestContext(t)` helper for
unit-testing handlers — see the testing section of
[create a node in Go](/docs/guides/create-a-node-go).
