axonpush
TypeScript SDK

TypeScript SDK

Publish and trace agent events from Node.js and Bun with @axonpush/sdk, a typed REST client with framework integrations for LangChain, Vercel AI, Mastra, OpenAI Agents and more.

@axonpush/sdk is the TypeScript client for axonpush. It does two things:

  • Publish events over a typed REST client generated from the axonpush OpenAPI spec.
  • Trace multi-agent workflows through traceId and parentEventId.

The package is ESM-only and runs on Node 20+ and Bun.

Install

npm install @axonpush/sdk

Only consola (internal diagnostics) is a hard dependency. Every framework integration sits behind an optional peer dependency, the package loads fine without any of them. Install the host library for the ones you want:

npm install @langchain/core                                   # LangChain, LangGraph
npm install winston winston-transport                         # winston transport
npm install pino                                              # pino stream
npm install @opentelemetry/api @opentelemetry/sdk-trace-base   # span exporter
npm install @sentry/node                                      # Sentry installer
npm install bullmq                                            # durable publish queue
npm install @anthropic-ai/sdk                                 # Anthropic tracer
npm install @mastra/core                                      # Mastra exporter

Quickstart

import { AxonPush } from "@axonpush/sdk";

const client = new AxonPush();
const event = await client.events.publish({
  identifier: `quickstart-${Date.now()}`,
  channelId: process.env.AXONPUSH_CHANNEL_ID!,
  eventType: "custom",
  payload: { hello: "world", source: "examples/01-quickstart" },
});
console.log("published event:", event);
client.close();

new AxonPush() takes no arguments: every field is resolved from AXONPUSH_* environment variables. Pass an options bag to override any of them.

Channel IDs are string UUIDs

Every channelId on the public boundary is a string UUID. The integrations still accept a number for one release, coercing it with a single console.warn, code written against @axonpush/sdk@0.0.4 keeps working, but the numeric form is going away.

Configuration

OptionEnv varDefaultNotes
apiKeyAXONPUSH_API_KEYRequired at request time.
tenantIdAXONPUSH_TENANT_IDOrg UUID. Falls back to AXONPUSH_ORG_ID.
orgIdAXONPUSH_ORG_IDmirrors tenantIdAlias; wins over tenantId when set.
appIdAXONPUSH_APP_IDDefault app for resources that need one.
baseUrlAXONPUSH_BASE_URLhttps://api.axonpush.xyzPoint at your own deployment when self-hosting.
environmentAXONPUSH_ENVIRONMENTLogical env slug stamped on every event.
timeoutAXONPUSH_TIMEOUT30_000 msSee the unit note below.
maxRetriesAXONPUSH_MAX_RETRIES3Retry attempts for retryable errors.
failOpenAXONPUSH_FAIL_OPENtrueSwallow APIConnectionError and resolve null.
contentCaptureModeAXONPUSH_CONTENT_CAPTUREmetadata_onlymetadata_only, redacted, or full.
redactKeysAXONPUSH_REDACT_KEYS[]Extra key names to redact, comma-separated in the env var.
maxContentLengthAXONPUSH_MAX_CONTENT_LENGTH4096Strings longer than this are truncated.

Caller-supplied options always win when defined.

AXONPUSH_TIMEOUT is in seconds; the option is in milliseconds

AXONPUSH_TIMEOUT=30 and new AxonPush({ timeout: 30_000 }) mean the same thing. The environment variable matches the Python and .NET SDKs, which express it in seconds; the TypeScript option stays in milliseconds so it reads like every other timeout in Node.

const client = new AxonPush({
  apiKey: process.env.AXONPUSH_API_KEY,
  tenantId: process.env.AXONPUSH_TENANT_ID,
  baseUrl: "https://api.axonpush.xyz",
  environment: "production",
  failOpen: true,
});

Fail-open

With failOpen: true (the default) an APIConnectionError is swallowed and the call resolves null instead of throwing, so a telemetry outage cannot take your application down. Every resource method is therefore typed Promise<T | null>. Set failOpen: false when you want transport failures to surface, the examples do this to demonstrate the error hierarchy.

Publishing events

client.events.publish(params) is the single write path; the integrations all funnel through it.

FieldTypeNotes
identifierstringRequired. Stable, caller-supplied; used for dedupe.
payloadRecord<string, unknown>Required. Free-form JSON body.
channelIdstringRequired. Channel UUID.
agentIdstringLogical agent that produced the event.
traceIdstringAuto-generated from the trace context when omitted.
spanIdstringAuto-generated from the trace context when omitted.
parentSpanIdstringParent W3C span id, for hierarchy reconstruction.
parentEventIdstringParent event id, models hand-offs between agents.
eventTypeEventTypeDefaults to "custom".
metadataRecord<string, unknown>Free-form metadata.
promptIdstringEmitted as gen_ai.prompt.id in metadata.
promptVersionIdstringEmitted as gen_ai.prompt.version in metadata.
environmentstringOnly honoured when the API key has allowEnvironmentOverride.
syncbooleanWait for the DB write before returning. Audit-critical calls only.

The response is an Event carrying eventId (aliased as id), identifier, dedupKey, duplicate, queued, and createdAt.

Reading events back:

const page = await client.events.list(channelId, { limit: 50, traceId });
for (const event of page?.data ?? []) {
  console.log(event.identifier, event.eventType);
}

const hits = await client.events.search({ query: "timeout", since: "2026-01-01T00:00:00Z" });

list() and search() both return { data, meta }, read .data for the events, .meta for the pagination cursor.

Event types

eventType accepts the canonical backend enum plus any user-defined string. The canonical members, which the UI knows how to render:

agent.start · agent.end · agent.message · agent.tool_call.start · agent.tool_call.end · agent.error · agent.handoff · agent.llm.token · agent.log · app.log · app.span · custom

Import CanonicalEventType when you want the closed enum without the string widening.

Tracing

A TraceContext carries a trace id across an async flow, so events published from different services stitch into one run in the UI.

import { AxonPush, getOrCreateTrace } from "@axonpush/sdk";

const client = new AxonPush();
const trace = getOrCreateTrace();

const planner = await client.events.publish({
  identifier: `plan-${Date.now()}`,
  channelId,
  agentId: "planner",
  traceId: trace.traceId,
  eventType: "agent.start",
  payload: { goal: "answer the user's question about TypeScript generics" },
});

await client.events.publish({
  identifier: `tool-${Date.now()}`,
  channelId,
  agentId: "search-tool",
  traceId: trace.traceId,
  parentEventId: planner?.eventId,
  eventType: "agent.tool_call.start",
  payload: { tool: "web_search", query: "typescript generic constraints" },
});

traceId is propagated as the X-Axonpush-Trace-Id header and stored on every event. parentEventId models the hand-off. TraceContext also exposes nextSpanId(), w3cTraceId(), and traceparent() for interop with W3C trace context.

Errors

Every SDK error extends AxonPushError, which carries code, hint, requestId, and statusCode from the backend’s error envelope.

import {
  AxonPushError,
  AuthenticationError,
  NotFoundError,
  RateLimitError,
  RetryableError,
  ValidationError,
} from "@axonpush/sdk";

try {
  await client.apps.get(id);
} catch (err) {
  if (err instanceof RateLimitError) {
    await new Promise((r) => setTimeout(r, (err.retryAfter ?? 1) * 1000));
  } else if (err instanceof AuthenticationError) {
    rotateApiKey();
  } else if (err instanceof RetryableError) {
    // safe to retry with your own backoff
  } else if (err instanceof NotFoundError || err instanceof ValidationError) {
    throw err; // not retryable
  }
}
ClassStatusRetryable
APIConnectionError, (network)yes
AuthenticationError401no
ForbiddenError403no
NotFoundError404no
ValidationError400, 422no
RateLimitError429yes, honours retryAfter
ServerError5xxyes
RetryableError, (marker)yes

The transport already retries the retryable set with backoff 250, 500, 1000, 2000, 4000 ms, honouring Retry-After, up to maxRetries times. Catch them yourself only when you need a different policy.

Content capture and redaction

Payloads and metadata pass through a client-side redactor before they leave the process. Secret-looking keys (authorization, password, api_key, access_token, …) are always replaced with [REDACTED]. Under the default contentCaptureMode: "metadata_only", content-bearing keys (prompt, messages, input, output, response, tool_result, retrieval documents) are redacted too. Strings longer than maxContentLength are truncated.

Set contentCaptureMode: "redacted" to keep non-secret content, and add project-specific keys through redactKeys. Use "full" only after checking your organisation’s telemetry policy.

metadata_only hides prompt and response content

The capture mode decides what content ends up on the trace. Under the default metadata_only the trace carries no prompt or response text, only models, tools, timing and errors. Choose contentCaptureMode: "redacted" to keep content with secret-shaped keys masked (and redacted again server-side by the org telemetry policy), or "full" after checking your telemetry policy. See the privacy boundary.

Framework integrations

Each integration is exported from the package root and as a sub-path import for tree-shaking:

import { AxonPushCallbackHandler } from "@axonpush/sdk";
import { AxonPushCallbackHandler } from "@axonpush/sdk/integrations/langchain";

Logging and observability

Four integrations adapt an existing logging or tracing stack. All of them publish through the same bounded background queue, so the call site stays non-blocking.

Publisher modes

Every integration takes the same IntegrationConfig. The framework integrations above publish directly, one fire-and-forget call per lifecycle event, with failures logged and swallowed. The four logging integrations plus the span exporter route their writes through a publisher chosen by mode, because they run at a much higher rate:

modeBehaviour
"background" (default)Bounded in-memory queue drained by a background loop. Records are lost if the process dies.
"sync"Publish inline on the caller’s path. Simplest, slowest.
"bullmq"Enqueue to Redis via BullMQ. Survives restarts; needs a worker. Requires bullmqOptions.
OptionTypeDefaultApplies to
clientAxonPushrequiredevery integration
channelIdstringrequiredevery integration
agentIdstringper-integrationevery integration
traceIdstringnew traceevery integration
mode"background" | "sync" | "bullmq""background"pino, winston, console, otel
queueSizenumber1000background mode
overflowPolicy"drop-oldest" | "drop-newest" | "block""drop-oldest"background mode
shutdownTimeoutMsnumber2000background mode
concurrencynumber1background mode
bullmqOptionsBullMQPublisherOptionsbullmq mode

When the SDK detects a serverless runtime (AWS Lambda, Google Cloud Functions, Azure Functions) it logs a reminder to flush at the end of each invocation. flushAfterInvocation(handler, fn) wraps a handler to do exactly that; on a normal process exit, beforeExit / SIGTERM / SIGINT hooks drain every live publisher.

Resources

Beyond events, the client exposes channels, apps, environments, webhooks, organizations, alerts, analytics and tracesV2. They are plain properties on the instance, constructed eagerly, so client.channels.get(id) needs no await beyond the call itself.