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

TypeScript SDK reference

Complete reference for AxiomContext in TypeScript — logging, secrets, agent memory, flow reflection, flow mutation, execution identifiers, and handler signatures.

View as Markdown

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.

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:

// 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:

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

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

AxiomContext fields

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

FieldTypeWhat it is
ax.logAxiomLoggerStructured logger for this invocation
ax.secretsAxiomSecretsRead-only tenant secrets
ax.agentAxiomAgentAgent capabilities; today exposes ax.agent.memory
ax.executionIdstringUUID of the current execution
ax.flowIdstringStable ID of the compiled artifact (constant across executions)
ax.tenantIdstringUUID of the tenant that owns this invocation
ax.reflectionAxiomReflectionRead-only view of the running flow (ax.reflection.flow)
ax.mutationAxiomMutationMutation 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:

// 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:

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

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

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 for the model (sessions, scopes, consolidation).

Flow-scope methods

// 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

// 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:

// 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: FlowPositioncurrentInstance, 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:

// 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 EdgeConditionop ('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.

// 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 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:

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