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

# 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`,
`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 await other
Promise-based platform APIs:

```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.executionId` | `string` | ID of the current execution |
| `ax.flowId` | `string` | Stable ID of the compiled artifact (constant across executions) |
| `ax.tenantId` | `string` | ID 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).

`get()` alone can't tell "revoked" from "never configured" — both return
`['', false]`. Use the `secretStatusOf(ax.secrets, name)` helper (imported from
the generated `axiomContext`) for that: it returns a `SecretStatus`
(`Available`, `Revoked`, or `Unset`). See
[Manage secrets in a flow](../../guides/manage-secrets.md#distinguish-revoked-from-never-configured)
for the full accessor table across languages.

> **Agent memory removed for beta.** There is no `ax.agent` / `ax.agent.memory`
> in the beta SDK — agentic memory is out of scope for beta and has been
> removed at compile time.

## 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.

A thrown error here reports as `NODE_ERROR_USER`, which the platform **never**
retries at any layer — retrying a flaky call inside your own logic (a
downstream API blip) is your handler's job, using a normal `try`/`catch` retry
loop, not a platform mechanism.

## At-least-once invocation: design for idempotency

Axiom guarantees at-least-once invocation of every node by default (see
[the `axiom.yaml` reference](/docs/reference/axiom-yaml) for the retry
knobs). A transient invocation failure (`NODE_ERROR_TRANSPORT` /
`NODE_ERROR_TIMEOUT`) can cause the platform to call your handler again for
the same execution, with the same input. Write handlers so a repeat call is
safe:

- Prefer naturally idempotent operations — an upsert on a stable key, a
  conditional write, a `PUT` instead of a bare `POST`.
- For a non-idempotent side effect (charging a customer, sending an email),
  derive a stable idempotency key from `ax.executionId` and de-duplicate
  against it downstream, or use the target system's own idempotency-key API
  if it has one.

This is a design responsibility, not something the platform enforces.

## 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, 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).
