---
title: "TypeScript SDK reference"
description: "Complete reference for AxiomContext in TypeScript — logging, secrets, agent memory, flow reflection, flow mutation, execution identifiers, and handler signatures."
category: reference
surfaces: [sdk]
languages: [typescript]
related: [guides/create-a-node-typescript, concepts/memory, concepts/sandboxing-and-tenancy, guides/manage-secrets, reference/sdk/python, reference/sdk/go]
last_reviewed: 2026-06-06
---

# TypeScript SDK reference

`AxiomContext` is the single injection point for every platform capability a
TypeScript node can use. It is passed as the first parameter (`ax`) to every
node handler; node code never calls platform services directly — every call
goes through the [sidecar](../../concepts/sandboxing-and-tenancy.md).

## Where the SDK comes from

There is no npm package to install. `axiom generate` (run automatically by
`axiom create`, `axiom dev`, `axiom test`, and `axiom build`) writes the full
SDK into your package at
`gen/axiomContext.ts`, alongside the generated message bindings
(`gen/messages_pb.js`, `gen/messages_pb.d.ts`). The file carries a
`// Code generated by Axiom CLI; DO NOT EDIT.` banner — edits are overwritten
on the next generate. Import from it with a relative path:

```typescript
// nodes/greet.ts
import { AxiomContext } from '../gen/axiomContext';
```

All interfaces on this page (`AxiomContext`, `AxiomLogger`, `AxiomSecrets`,
`AxiomAgent`, `AxiomReflection`, `AxiomMutation`, and their supporting types)
are exported from `gen/axiomContext.ts`.

## Handler signatures

A unary node handler takes `AxiomContext` and one input message and returns
one output message. It may be synchronous or `async` — the platform awaits
the result either way. `async` is required when you use the Promise-based
memory API:

```typescript
// nodes/greet.ts
import { GreetRequest, GreetReply } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';

export async function greet(ax: AxiomContext, input: GreetRequest): Promise<GreetReply> {
  ax.log.info('greeting', { name: input.getName() });
  const out = new GreetReply();
  out.setGreeting(`Hello, ${input.getName()}!`);
  return out;
}
```

A pipeline node handler (scaffolded with `axiom create node ... --type
pipeline`) is an async generator: it consumes an `AsyncIterable` of input
frames and yields output frames. When a pipeline node is the entry node of a
flow, the iterable yields exactly one item.

```typescript
// nodes/tokenize.ts
import { GreetRequest, GreetReply } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';

export async function* tokenize(
  ax: AxiomContext,
  inputs: AsyncIterable<GreetRequest>,
): AsyncGenerator<GreetReply> {
  for await (const input of inputs) {
    const out = new GreetReply();
    out.setGreeting(`Hello, ${input.getName()}!`);
    yield out;
  }
}
```

Input and output messages are `google-protobuf` classes with getter/setter
accessors — see [The type system](../../concepts/type-system.md).

## AxiomContext fields

Every handler receives one `AxiomContext` per invocation, with these
read-only fields:

| Field | Type | What it is |
|---|---|---|
| `ax.log` | `AxiomLogger` | Structured logger for this invocation |
| `ax.secrets` | `AxiomSecrets` | Read-only tenant secrets |
| `ax.agent` | `AxiomAgent` | Agent capabilities; today exposes `ax.agent.memory` |
| `ax.executionId` | `string` | UUID of the current execution |
| `ax.flowId` | `string` | Stable ID of the compiled artifact (constant across executions) |
| `ax.tenantId` | `string` | UUID of the tenant that owns this invocation |
| `ax.reflection` | `AxiomReflection` | Read-only view of the running flow (`ax.reflection.flow`) |
| `ax.mutation` | `AxiomMutation` | Mutation surface for `mutation_capable` nodes (`ax.mutation.flow`) |

New platform capabilities are added to `AxiomContext`, never as extra handler
parameters — handler signatures stay stable across SDK versions.

## ax.log — structured logging

`AxiomLogger` has four levels, each taking a message and optional structured
attributes:

```typescript
// nodes/greet.ts — inside a handler
ax.log.debug('cache lookup', { key: 'user:42' });
ax.log.info('order processed', { order_id: 'abc123', total: 99.99 });
ax.log.warn('retrying upstream call', { attempt: 2 });
ax.log.error('upstream failed', { status: 502 });
```

The interface:

```typescript
// gen/axiomContext.ts (generated — shown for reference)
export interface AxiomLogger {
  debug(msg: string, attrs?: Record<string, unknown>): void;
  info(msg: string, attrs?: Record<string, unknown>): void;
  warn(msg: string, attrs?: Record<string, unknown>): void;
  error(msg: string, attrs?: Record<string, unknown>): void;
}
```

Use `ax.log` instead of `console.log()`. In local development (`axiom dev`)
it writes concise plain text to the terminal. In production it writes JSON
with `trace_id`, `span_id`, and `execution_id` baked into every line, making
logs searchable by execution and linkable to the distributed trace.

## ax.secrets — read secrets

`ax.secrets.get(name)` returns a `[value, ok]` tuple: `[value, true]` when
the named secret is present, `['', false]` otherwise. Values are plaintext
strings; the platform handles encryption and decryption.

```typescript
// nodes/greet.ts — inside a handler
const [apiKey, ok] = ax.secrets.get('OPENAI_API_KEY');
if (!ok) {
  ax.log.warn('OPENAI_API_KEY is not configured');
}
```

Secrets are tenant-scoped, stored in the console, and resolved by the
platform at invocation time — never hardcode credentials in node source.
Declare each secret a node reads under `required_secrets` in `axiom.yaml` so
it is validated at publish time; see
[Manage secrets in a flow](../../guides/manage-secrets.md).

## ax.agent.memory — agent memory

`ax.agent.memory` is the agent memory layer: session-scoped conversation
history and memory, plus cross-session (flow-scope) search and writes. All
memory methods return Promises, so handlers that use them must be `async`.
See [Agent memory](../../concepts/memory.md) for the model (sessions,
scopes, consolidation).

### Flow-scope methods

```typescript
// nodes/recall.ts — inside an async handler
const entries = await ax.agent.memory.search('prior decisions', 5);
const memoryId = await ax.agent.memory.write('User prefers TypeScript', 0.8);
const session = ax.agent.memory.session(input.getSessionId());
```

- `search(query, limit?)` — semantic search across all sessions at flow
  scope; returns `MemoryEntry[]`. `limit` defaults to 10.
- `write(content, importance?)` — write a flow-scope memory entry; returns
  the memory ID. `importance` defaults to 0.5.
- `session(sessionId)` — returns an `AxiomSessionMemory` handle scoped to
  one session. The session ID is a string your node code chooses — typically
  a field on your typed input message; the platform never infers it.

### Session-scope methods

```typescript
// nodes/chat.ts — inside an async handler
const session = ax.agent.memory.session(input.getSessionId());

const turns = await session.history().last(20);          // ConversationTurn[]
await session.history().append('user', input.getText()); // record a turn

const prefs = await session.search('user preferences');  // MemoryEntry[]
const id = await session.write('User prefers dark mode', 0.8);
await session.end();  // close the session and trigger consolidation
```

- `history().last(n)` — the last `n` conversation turns for this session.
- `history().append(role, content)` — append a turn; `role` is one of
  `'user' | 'assistant' | 'tool' | 'system'`.
- `search(query, limit?)` / `write(content, importance?)` — like the
  flow-scope versions, but scoped to this session.
- `end()` — formally close the session and trigger consolidation.

### Memory record types

`ConversationTurn`: `id`, `sessionId`, `role`, `content`, `createdAt` (Unix
milliseconds), plus optional `toolName`, `toolCallId`, and `metadata`.

`MemoryEntry`: `id`, `tenantId`, `flowId`, `sessionId`, `scopeLevel`
(`'tenant' | 'flow' | 'session'`), `memoryType`
(`'episodic' | 'semantic' | 'procedural'`), `content`, `importance`,
`confidence`, `createdAt`, `expiresAt`, plus optional `sourceId`, `score`
(set on search results), and `metadata`.

In environments without the memory service (unit tests, local runs), the
generated runtime substitutes no-op implementations: reads resolve to empty
arrays, writes resolve to an empty string — calls never throw for that
reason.

## ax.reflection.flow — flow reflection

`ax.reflection.flow` is a read-only view of the running flow's graph and the
current invocation's position in it:

```typescript
// nodes/router.ts — inside a handler: what runs after me?
const pos = ax.reflection.flow.position;
const downstream = ax.reflection.flow.edges
  .filter(e => e.srcInstance === pos.currentInstance);
```

- `nodes: ReflectionNode[]` — every node placement. Each has `instanceId`,
  `nodeUlid`, `name`, `packageName`, `packageVersion`, `nodeType`
  (`'node' | 'subflow' | 'pipeline'`), `inputMessageName` /
  `outputMessageName` (fully-qualified Protocol Buffers message names), and
  `canvasNodeId`.
- `edges: ReflectionEdge[]` and `loopEdges: ReflectionEdge[]` — forward and
  loop edges. Each has `srcInstance`, `dstInstance`, `canvasEdgeId`,
  `hasCondition`, `hasAdapter`, `maxIterations` (meaningful only on
  `loopEdges` entries), and an optional `conditionSummary`.
- `conditionSummary?: ConditionSummary` — when an edge is conditional, a
  readable digest of its dispatch predicate: `field`, `op`, and `operands`.
  For example `field: 'tools'`, `op: 'EQ'`, `operands: ['ToolX']` means the
  edge fires when `'ToolX'` is in the repeated `tools` field. Lets a node
  make idempotent decisions (such as skipping a tool that is already wired).
- `position: FlowPosition` — `currentInstance`, `depth` (0 at the root
  flow), `loopIterations` (keyed by the loop head's `dstInstance`), and
  `subflowStackGraphIds` (root flow first, immediate parent last).
- `graphId: string` — the running graph's ID.

Reflection is structural only: compiled edge adapters and compiled
conditions are not exposed, only the `hasAdapter` / `hasCondition` flags and
the condition summary.

## ax.mutation.flow — mutate the running flow

Nodes declared with `mutation_capable: true` on their entry in `axiom.yaml`
can append nodes and edges to the running flow:

```typescript
// nodes/addtool.ts — inside a mutation-capable handler
const toolInstance = ax.mutation.flow.addNode('my-org/tools', '1.2.0', { x: 400, y: 200 });
ax.mutation.flow.addEdge(
  ax.reflection.flow.position.currentInstance,
  toolInstance,
  { op: 'EQ', field: 'tools', value: 'ToolX' }, // optional condition
);
```

- `addNode(packageName, packageVersion, canvasPosition?)` — buffer a new
  node placement; returns the instance ID assigned to it, numbered after the
  nodes already in the running flow. The optional `canvasPosition`
  (`{ x, y }`) is a canvas placement hint. Use the returned ID in `addEdge`
  calls within the same handler.
- `addEdge(srcInstance, dstInstance, condition?)` — buffer a new edge. Omit
  `condition` for an unconditional edge. Pass an `EdgeCondition` —
  `op` (`'EQ' | 'NEQ' | 'LT' | 'LTE' | 'GT' | 'GTE' | 'CONTAINS'`; empty
  string means `EQ`), `field` (dotted path on the source node's output
  message), `value` (operand in string form) — for a conditional dispatch
  edge. A condition on a repeated field matches when any element matches.

Mutation is append-only and buffered: calls record locally during the
handler and are attached to the node's response when it returns — nothing
changes mid-handler. The platform validates buffered mutations after the
handler returns; a rejected mutation fails the execution with an error
message carrying the deterministic prefix `axiom: mutation rejected: `
followed by the human-readable reason. `AxiomMutationError` (an `Error`
subclass exported from `gen/axiomContext.ts`) is the SDK's type for that
rejection; the structured engine rejection code is not exposed to node
code.

## Error handling

Throw a plain `Error` (or any subclass) to fail the invocation: the
generated service wrapper catches anything thrown (or a rejected Promise)
and reports the error message to the platform as the node's failure reason.
There is no Axiom-specific error type to throw from handler code.

```typescript
// nodes/greet.ts — inside a handler
const [apiKey, ok] = ax.secrets.get('OPENAI_API_KEY');
if (!ok) {
  throw new Error('OPENAI_API_KEY is not registered for this tenant');
}
```

See [Debug a flow](../../guides/debug-a-flow.md) for how failed executions
surface in the canvas.

## Mock AxiomContext in tests

`axiom create node` scaffolds `nodes/<name>_test.ts` with a ready-made
`testContext: AxiomContext`: a silent logger, a secrets store that returns
`['', false]` for every name, no-op memory, empty reflection, and a
do-nothing mutation mock. Pass it straight to your handler — `await` the
result when the handler is `async`, as `greet` is above:

```typescript
// nodes/greet_test.ts — a jest test using the scaffolded testContext
it('greets by name', async () => {
  const input = new GreetRequest();
  input.setName('Ada');
  const result = await greet(testContext, input);
  expect(result).toBeInstanceOf(GreetReply);
  expect(result.getGreeting()).toBe('Hello, Ada!');
});
```

Because every field of `AxiomContext` is an interface, override exactly the
capability under test — for example, replace `secrets` with
`{ get: (name) => name === 'OPENAI_API_KEY' ? ['test-key', true] : ['', false] }`
or swap the mutation mock for a recorder that pushes `addNode` / `addEdge`
arguments into an array you assert on. Run the suite with `axiom test` — see
[Create a node in TypeScript](../../guides/create-a-node-typescript.md).
