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
traceIdandparentEventId.
The package is ESM-only and runs on Node 20+ and Bun.
Install
npm install @axonpush/sdkOnly 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 exporterQuickstart
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
| Option | Env var | Default | Notes |
|---|---|---|---|
apiKey | AXONPUSH_API_KEY | Required at request time. | |
tenantId | AXONPUSH_TENANT_ID | Org UUID. Falls back to AXONPUSH_ORG_ID. | |
orgId | AXONPUSH_ORG_ID | mirrors tenantId | Alias; wins over tenantId when set. |
appId | AXONPUSH_APP_ID | Default app for resources that need one. | |
baseUrl | AXONPUSH_BASE_URL | https://api.axonpush.xyz | Point at your own deployment when self-hosting. |
environment | AXONPUSH_ENVIRONMENT | Logical env slug stamped on every event. | |
timeout | AXONPUSH_TIMEOUT | 30_000 ms | See the unit note below. |
maxRetries | AXONPUSH_MAX_RETRIES | 3 | Retry attempts for retryable errors. |
failOpen | AXONPUSH_FAIL_OPEN | true | Swallow APIConnectionError and resolve null. |
contentCaptureMode | AXONPUSH_CONTENT_CAPTURE | metadata_only | metadata_only, redacted, or full. |
redactKeys | AXONPUSH_REDACT_KEYS | [] | Extra key names to redact, comma-separated in the env var. |
maxContentLength | AXONPUSH_MAX_CONTENT_LENGTH | 4096 | Strings 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.
| Field | Type | Notes |
|---|---|---|
identifier | string | Required. Stable, caller-supplied; used for dedupe. |
payload | Record<string, unknown> | Required. Free-form JSON body. |
channelId | string | Required. Channel UUID. |
agentId | string | Logical agent that produced the event. |
traceId | string | Auto-generated from the trace context when omitted. |
spanId | string | Auto-generated from the trace context when omitted. |
parentSpanId | string | Parent W3C span id, for hierarchy reconstruction. |
parentEventId | string | Parent event id, models hand-offs between agents. |
eventType | EventType | Defaults to "custom". |
metadata | Record<string, unknown> | Free-form metadata. |
promptId | string | Emitted as gen_ai.prompt.id in metadata. |
promptVersionId | string | Emitted as gen_ai.prompt.version in metadata. |
environment | string | Only honoured when the API key has allowEnvironmentOverride. |
sync | boolean | Wait 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
}
}| Class | Status | Retryable |
|---|---|---|
APIConnectionError | , (network) | yes |
AuthenticationError | 401 | no |
ForbiddenError | 403 | no |
NotFoundError | 404 | no |
ValidationError | 400, 422 | no |
RateLimitError | 429 | yes, honours retryAfter |
ServerError | 5xx | yes |
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";LangChain
Callback handler for chain, LLM, and tool lifecycles.
LangGraph
LangChain handler plus per-node graph events.
OpenAI Agents
Run hooks for agent lifecycle, tools, and handoffs.
Anthropic
Wraps messages.create and messages.stream with token usage.
Vercel AI SDK
Language model middleware for generateText and streamText.
LlamaIndex
Hooks for LLM, embedding, retriever, and query stages.
Mastra
Native Mastra 1.x observability exporter.
Google ADK
before/after callbacks for agents, models, and tools.
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.
pino
Stream that turns pino records into app.log events.
winston
Transport with OTel severity mapping.
Console capture
Mirror console.* calls without changing your output.
OpenTelemetry
SpanExporter for any OTel tracer provider.
Sentry
Point any Sentry SDK at axonpush's envelope ingest.
BullMQ
Durable Redis-backed publish queue for any integration.
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:
mode | Behaviour |
|---|---|
"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. |
| Option | Type | Default | Applies to |
|---|---|---|---|
client | AxonPush | required | every integration |
channelId | string | required | every integration |
agentId | string | per-integration | every integration |
traceId | string | new trace | every integration |
mode | "background" | "sync" | "bullmq" | "background" | pino, winston, console, otel |
queueSize | number | 1000 | background mode |
overflowPolicy | "drop-oldest" | "drop-newest" | "block" | "drop-oldest" | background mode |
shutdownTimeoutMs | number | 2000 | background mode |
concurrency | number | 1 | background mode |
bullmqOptions | BullMQPublisherOptions | bullmq 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.