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

// 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.executionIdstringID of the current execution
ax.flowIdstringStable ID of the compiled artifact (constant across executions)
ax.tenantIdstringID 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.

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

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

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

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