TypeScript SDK reference
Complete reference for AxiomContext in TypeScript — logging, secrets, agent memory, flow reflection, flow mutation, execution identifiers, and handler signatures.
View as MarkdownAxiomContext 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:
| 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:
// 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; returnsMemoryEntry[].limitdefaults to 10.write(content, importance?)— write a flow-scope memory entry; returns the memory ID.importancedefaults to 0.5.session(sessionId)— returns anAxiomSessionMemoryhandle 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 consolidationhistory().last(n)— the lastnconversation turns for this session.history().append(role, content)— append a turn;roleis 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 hasinstanceId,nodeUlid,name,packageName,packageVersion,nodeType('node' | 'subflow' | 'pipeline'),inputMessageName/outputMessageName(fully-qualified Protocol Buffers message names), andcanvasNodeId.edges: ReflectionEdge[]andloopEdges: ReflectionEdge[]— forward and loop edges. Each hassrcInstance,dstInstance,canvasEdgeId,hasCondition,hasAdapter,maxIterations(meaningful only onloopEdgesentries), and an optionalconditionSummary.conditionSummary?: ConditionSummary— when an edge is conditional, a readable digest of its dispatch predicate:field,op, andoperands. For examplefield: 'tools',op: 'EQ',operands: ['ToolX']means the edge fires when'ToolX'is in the repeatedtoolsfield. 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'sdstInstance), andsubflowStackGraphIds(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 optionalcanvasPosition({ x, y }) is a canvas placement hint. Use the returned ID inaddEdgecalls within the same handler.addEdge(srcInstance, dstInstance, condition?)— buffer a new edge. Omitconditionfor an unconditional edge. Pass anEdgeCondition—op('EQ' | 'NEQ' | 'LT' | 'LTE' | 'GT' | 'GTE' | 'CONTAINS'; empty string meansEQ),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.