npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@axonflow/sdk

v9.5.0

Published

AxonFlow SDK - Add invisible AI governance to your applications in 3 lines of code

Readme

AxonFlow SDK for TypeScript

npm version npm downloads License: MIT TypeScript

Upgrade strongly recommended. AxonFlow ships substantial monthly security and quality hardening; staying on the latest major is the security-supported release line. Latest release · Security advisories

Taking a sponsored workflow to production?

Choose the path that fits:

  • Self-serve: free 90-day Evaluation License
  • Paid production program: Design Partner or Confidential Pilot - one scoped workflow over 60 or 75 days, founder-led rollout support, upfront conversion pricing, and a fixed decision date; public track from $2,000 or confidential track from $4,000

The paid program requires a dated forcing event, written controls, an executive sponsor, and a technical owner. Prices are subject to eligibility and a signed agreement.

Questions or feedback?

Comment in GitHub Discussions or email [email protected] for private feedback.

Add invisible AI governance to your applications in 3 lines of code. No UI changes. No user training. Just drop-in enterprise protection.

How This SDK Fits with AxonFlow

This SDK is a client library for interacting with a running AxonFlow control plane. It is used from application or agent code to send execution context, policies, and requests at runtime.

A deployed AxonFlow platform (self-hosted or cloud) is required for end-to-end AI governance. SDKs alone are not sufficient—the platform and SDKs are designed to be used together.

See AxonFlow in Action

Videos covering different angles of the platform:

Installation

npm install @axonflow/sdk

Install from Source

git clone https://github.com/getaxonflow/axonflow-sdk-typescript.git
cd axonflow-sdk-typescript && npm install && npm run build && npm link
# In your project: npm link @axonflow/sdk

Evaluation Tier (Free License)

Need more capacity than Community without moving to Enterprise? Evaluation uses the same core features with higher limits:

| Limit | Community | Evaluation (Free) | Enterprise | |-------|-----------|-------------------|------------| | Tenant policies | 20 | 50 | Unlimited | | Org-wide policies | 0 | 5 | Unlimited | | Audit retention | 3 days | 14 days | 3650 days | | Concurrent executions | 5 | 25 | Unlimited | | Pending execution approvals | 5 | 25 | Unlimited | | Evidence export (CSV / JSON) | — | 5,000 records · 14d window · 3/day | Unlimited | | Policy simulation | — | 300 / day | Unlimited |

Concurrent executions applies to MAP and WCP executions per tenant. Pending execution approvals applies to MAP confirm/step mode and WCP approval queues.

Note: Evidence export and policy simulation are licensed AxonFlow platform capabilities available alongside the SDK on your deployed platform — not language-specific SDK helpers. Access them via the platform API or customer portal. The SDK row is included to show what your licensed deployment unlocks at each tier.

Get a free Evaluation license · Run a paid production program · Full feature matrix

Try Without Installing

Skip local setup entirely — try AxonFlow instantly at try.getaxonflow.com:

# 1. Register (30 seconds)
curl -X POST https://try.getaxonflow.com/api/v1/register \
  -H "Content-Type: application/json" -d '{"label":"my-trial"}'

# 2. Set credentials and auto-connect
export AXONFLOW_TRY=1
export AXONFLOW_CLIENT_ID=cs_your-tenant-id
export AXONFLOW_CLIENT_SECRET=your-secret

No Docker, no license, no installation. Rate-limited to 20 req/min. Learn more.

Quick Start

Gateway Mode (Recommended)

Gateway Mode provides the most reliable integration by explicitly separating policy checks, LLM calls, and audit logging:

import { AxonFlow } from '@axonflow/sdk';
import OpenAI from 'openai';

// Initialize clients
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const axonflow = new AxonFlow({
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
  endpoint: process.env.AXONFLOW_ENDPOINT || 'http://localhost:8080'
});

const prompt = 'What is the capital of France?';

// Step 1: Pre-check policies
const ctx = await axonflow.getPolicyApprovedContext({
  userToken: 'user-123',
  query: prompt
});

if (!ctx.approved) {
  throw new Error(`Blocked: ${ctx.blockReason}`);
}

// Step 2: Make your own LLM call
const startTime = Date.now();
const response = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [{ role: 'user', content: prompt }]
});
const latencyMs = Date.now() - startTime;

// Step 3: Audit the call
await axonflow.auditLLMCall({
  contextId: ctx.contextId,
  responseSummary: response.choices[0].message.content?.substring(0, 100) || '',
  provider: 'openai',
  model: 'gpt-4',
  tokenUsage: {
    promptTokens: response.usage?.prompt_tokens || 0,
    completionTokens: response.usage?.completion_tokens || 0,
    totalTokens: response.usage?.total_tokens || 0
  },
  latencyMs
});

console.log('Response:', response.choices[0].message.content);

Proxy Mode (Simpler Alternative)

For simpler integrations, Proxy Mode handles policy checking and auditing in a single call:

import { AxonFlow } from '@axonflow/sdk';

const axonflow = new AxonFlow({
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
  endpoint: 'http://localhost:8080'
});

// Single call - policies checked, query processed, audit logged
const response = await axonflow.executeQuery({
  userToken: 'user-123',
  query: 'What is the capital of France?',
  requestType: 'chat',
  context: {
    provider: 'openai',
    model: 'gpt-4'
  }
});

if (response.success) {
  console.log('Response:', response.data);
}

Self-Hosted Mode (No License Required)

Connect to a self-hosted AxonFlow instance running via docker-compose:

import { AxonFlow } from '@axonflow/sdk';
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// Self-hosted (localhost) - no license key needed!
const axonflow = new AxonFlow({
  endpoint: 'http://localhost:8080'
  // That's it - no authentication required for localhost
});

// Use Gateway Mode for self-hosted
const prompt = 'Test with self-hosted AxonFlow';

const ctx = await axonflow.getPolicyApprovedContext({
  userToken: 'user-123',
  query: prompt
});

if (!ctx.approved) {
  throw new Error(`Blocked: ${ctx.blockReason}`);
}

const startTime = Date.now();
const response = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [{ role: 'user', content: prompt }]
});

// Don't forget to audit!
await axonflow.auditLLMCall({
  contextId: ctx.contextId,
  responseSummary: response.choices[0].message.content?.substring(0, 100) || '',
  provider: 'openai',
  model: 'gpt-4',
  tokenUsage: {
    promptTokens: response.usage?.prompt_tokens || 0,
    completionTokens: response.usage?.completion_tokens || 0,
    totalTokens: response.usage?.total_tokens || 0
  },
  latencyMs: Date.now() - startTime
});

console.log(response.choices[0].message.content);

Self-hosted deployment:

# Clone and start AxonFlow
git clone https://github.com/getaxonflow/axonflow.git
cd axonflow
export OPENAI_API_KEY=sk-your-key-here
docker-compose up

# SDK connects to http://localhost:8080 - no license needed!

Features:

  • ✅ Full AxonFlow features without license
  • ✅ Perfect for local development and testing
  • ✅ Same API as production
  • ✅ Automatically detects localhost and skips authentication

Proxy Mode (executeQuery)

Proxy Mode routes all requests through AxonFlow's /api/request endpoint, providing a simpler integration pattern with automatic policy enforcement:

Basic Query Execution

import { AxonFlow, PolicyViolationError } from '@axonflow/sdk';

const axonflow = new AxonFlow({
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET
});

// Execute a chat query with policy enforcement
const response = await axonflow.executeQuery({
  userToken: 'user-123',
  query: 'Explain quantum computing in simple terms',
  requestType: 'chat',
  context: {
    provider: 'openai',
    model: 'gpt-4'
  }
});

if (response.success) {
  console.log('Response:', response.data);
  console.log('Policies evaluated:', response.policyInfo?.policiesEvaluated);
}

Handling Policy Violations

try {
  await axonflow.executeQuery({
    userToken: 'user-123',
    query: 'Process this SSN: 123-45-6789',
    requestType: 'chat'
  });
} catch (error) {
  if (error instanceof PolicyViolationError) {
    console.log('Request blocked:', error.blockReason);
    console.log('Violating policies:', error.policies);
  }
}

SQL Query Governance

// SQL queries get additional injection detection
const sqlResponse = await axonflow.executeQuery({
  userToken: 'analyst-user',
  query: 'SELECT name, email FROM customers WHERE status = active LIMIT 100',
  requestType: 'sql'
});

Health Check

// Check if AxonFlow agent is healthy
const health = await axonflow.healthCheck();

if (health.status === 'healthy') {
  console.log('Agent version:', health.version);
  console.log('Uptime:', health.uptime);
} else {
  console.warn('Agent status:', health.status);
}

Request Types

| Request Type | Description | |--------------|-------------| | chat | General chat/LLM queries | | sql | SQL queries (with injection detection) | | mcp-query | MCP connector queries | | multi-agent-plan | Generate multi-agent plans | | execute-plan | Execute a generated plan |

Gateway Mode (Direct LLM Calls)

Gateway Mode is for advanced users who want to make direct LLM calls while still getting policy enforcement:

// Step 1: Pre-check policies
const ctx = await axonflow.getPolicyApprovedContext({
  userToken: 'user-jwt',
  query: 'Analyze customer data',
  dataSources: ['postgres']
});

if (!ctx.approved) {
  throw new Error(`Blocked: ${ctx.blockReason}`);
}

// Step 2: Make direct LLM call with approved data
const llmResponse = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [{ role: 'user', content: JSON.stringify(ctx.approvedData) }]
});

// Step 3: Audit the call
await axonflow.auditLLMCall({
  contextId: ctx.contextId,
  responseSummary: llmResponse.choices[0].message.content.substring(0, 100),
  provider: 'openai',
  model: 'gpt-4',
  tokenUsage: {
    promptTokens: llmResponse.usage.prompt_tokens,
    completionTokens: llmResponse.usage.completion_tokens,
    totalTokens: llmResponse.usage.total_tokens
  },
  latencyMs: 250
});

React Example

import { AxonFlow } from '@axonflow/sdk';
import { useState } from 'react';

const axonflow = new AxonFlow({
  clientId: process.env.REACT_APP_AXONFLOW_CLIENT_ID,
  clientSecret: process.env.REACT_APP_AXONFLOW_CLIENT_SECRET,
  endpoint: process.env.REACT_APP_AXONFLOW_ENDPOINT || 'http://localhost:8080'
});

function ChatComponent() {
  const [response, setResponse] = useState('');
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (prompt: string) => {
    setError(null);
    try {
      // Use Proxy Mode for simple integrations
      // Note: In production, get userToken from your auth context
      const result = await axonflow.executeQuery({
        userToken: 'user-123', // Replace with actual user token
        query: prompt,
        requestType: 'chat'
      });

      if (result.success) {
        setResponse(result.data);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    }
  };

  return (
    // Your existing UI - no changes needed
    <div>...</div>
  );
}

Next.js API Route

// pages/api/chat.ts
import { AxonFlow, PolicyViolationError } from '@axonflow/sdk';
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const axonflow = new AxonFlow({
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
  endpoint: process.env.AXONFLOW_ENDPOINT || 'http://localhost:8080'
});

export default async function handler(req, res) {
  const { prompt, userToken } = req.body;

  try {
    // Step 1: Pre-check policies
    const ctx = await axonflow.getPolicyApprovedContext({
      userToken: userToken || 'anonymous',
      query: prompt
    });

    if (!ctx.approved) {
      return res.status(403).json({ error: ctx.blockReason });
    }

    // Step 2: Make the LLM call
    const startTime = Date.now();
    const completion = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [{ role: 'user', content: prompt }]
    });
    const latencyMs = Date.now() - startTime;

    // Step 3: Audit the call
    await axonflow.auditLLMCall({
      contextId: ctx.contextId,
      responseSummary: completion.choices[0].message.content?.substring(0, 100) || '',
      provider: 'openai',
      model: 'gpt-4',
      tokenUsage: {
        promptTokens: completion.usage?.prompt_tokens || 0,
        completionTokens: completion.usage?.completion_tokens || 0,
        totalTokens: completion.usage?.total_tokens || 0
      },
      latencyMs
    });

    res.json({ success: true, response: completion.choices[0].message.content });
  } catch (error) {
    if (error instanceof PolicyViolationError) {
      return res.status(403).json({ error: error.blockReason });
    }
    const message = error instanceof Error ? error.message : 'Unknown error';
    res.status(500).json({ error: message });
  }
}

AuthZEN-Native Authorization

axonflow.evaluate asks the gateway an AuthZEN question - may this subject perform this action on this resource? - over POST /api/v1/access/evaluation. It is the surface to write new integrations against: at v11 the engine behind it becomes AxonFlow's new Policy Decision Point with no wire change, so an integration written here migrates once rather than twice. Nothing is deprecated by it today; decide and the gateway/proxy methods are wire-stable through all of v11.

import { AuthZENRefusal, AxonFlow } from '@axonflow/sdk';

const decision = await axonflow.evaluate({
  subject: { type: 'gateway', id: 'llm-gateway-01' },
  action: { name: 'llm.completion' },
  resource: { type: 'llm', id: 'llm' },
  context: { args: { query: userPrompt } },
});

if (!decision.allowed) {
  throw new Error(`blocked: ${decision.state} (${decision.reason})`);
}
for (const obligation of decision.mandatoryObligations) {
  // an allow you cannot discharge is not an allow
}

Several preconditions of one operation go in a bulk envelope, which returns one decision - a denied entry denies the operation, so a caller cannot act on the entry it liked:

const decision = await axonflow.evaluateAll({
  subject: { type: 'gateway', id: 'llm-gateway-01' },
  action: { name: 'tool.call' },
  context: { args: { query: userPrompt } },
  evaluations: [
    { resource: { type: 'tool', id: 'jira/move_issue' } },
    { resource: { type: 'tool', id: 'jira/update_project' } },
  ],
});

Known gotchas

A refusal is not a denial. This surface refuses anything it cannot evaluate rather than evaluating around it - send a subject property or an unrecognised context member and you get an AuthZENRefusal naming the exact member, not a decision computed without it. Treating every error as a deny fails closed, which is safe, but blocks traffic that would be allowed once the request is corrected.

try {
  const decision = await axonflow.evaluate(request);
} catch (err) {
  if (err instanceof AuthZENRefusal) {
    err.code; // e.g. 'unevaluable_attribute' - a closed, generated set
    err.pointer; // '/evaluation/subject/properties' - the member to fix
    err.refusedBy; // 'client' (this SDK) or 'gateway'
    err.retryable; // only a gateway dependency failure is
  }
}

AuthZENProtocolError is separate and means something else: the gateway answered 200 with a body this build cannot safely act on - no profile context, a profile it cannot read, or a decision boolean that disagrees with its operational state. It is always fail-closed, and the fix is an upgrade or an operator, not a corrected request. A 401 surfaces as the SDK's ordinary AuthenticationError, because the gateway answers authentication before this route runs.

decision.allowed, never decision.decision. The bare boolean is AuthZEN 1.0's collapsed rendering; allowed additionally requires the operational state to be ALLOW, so a CHALLENGE or an ERROR can never be read as permission.

Three states, not two. undefined cannot express the difference between "the source established there is no value" and "the source could not be reached", and collapsing them is how an attribute nobody resolved gets recorded as one that was weighed. Attributes inside the context and properties bags may be explicit:

import { AUTHZEN_UNKNOWN_RESOLUTION_FAILED, AuthZENAttribute } from '@axonflow/sdk';

const context = {
  args: { query: userPrompt },
  correlation: {
    session_id: AuthZENAttribute.absent(), // a fact: omitted, request sent
    trace_id: AuthZENAttribute.unknown(AUTHZEN_UNKNOWN_RESOLUTION_FAILED), // refused locally
  },
};

The tri-state applies to attribute data, not to the structural members (subject.id, action.name, …): those are the identity of the question being asked, and an identity you cannot resolve is not an attribute whose absence a policy could evaluate - there is no request to make.

Wire member names are snake_case, not camelCase. These interfaces ARE the wire documents - they are serialised verbatim - so source_policy, decision_id and schema_version keep the names the server uses. That is deliberate: a server refusal's JSON Pointer names a member, and the pointer is this surface's whole diagnosis, so it has to name a member the caller can find in their own code.

Today's mapping is deliberately narrow. subject.type must be 'gateway' (an end-user subject needs the identity plane, which activates at v11); an llm or agent resource id must be the stage name itself, not a provider/model pair, because nothing on the serving path reads a provider or a model; a tool resource id is 'server/tool', because both halves ARE read. Everything else is refused by name.

The wire types AND their runtime validators are generated from the platform's canonical contract artifact (scripts/gen-authzen-types/generate.js); CI fails if the committed module is not what the artifact produces. A TypeScript interface is erased at runtime, so the validators are what actually refuses a body this build cannot interpret. Runnable example: examples/authzen/index.ts. Migration notes: docs/AUTHZEN_MIGRATION_DRAFT.md.

v11.0.0 platform

Against a v11.0.0 platform this SDK reaches the new decision plane. Against an older platform the calls that existed before work as before, and each v11 field is absent. What each part needs from the platform:

  • Decision provenance (v11.0.0). DecideResponse, MCPCheckOutputResponse and ConnectorResponse carry engine, subject_type, policy_bundle and legacy_validators, and the gateway pre-check result and proxyLLMCall's response carry them in camelCase. A decision adds policy_identities, policy_packs and document_version. legacy_validators is filled only where a checksum validator acted, so it is absent on proxyLLMCall by design.
  • Frozen legacy policy writes (v11.0.0). A write to the static- or dynamic-policy routes is refused with 409 LEGACY_POLICY_WRITE_FROZEN, thrown as LegacyPolicyWriteFrozenError, whose message names the typed policy route.
  • Route deprecation (v11.0.0). The platform stamps its legacy policy routes, and the client reports each stamped route once through PlatformRouteDeprecationWarning; a route that carries an id is reported once, by its template (GET /api/v1/static-policies/{id}), not once per id. simulatePolicies, getPolicyImpactReport and detectPolicyConflicts are marked @deprecated: each keeps answering until v11.1, and on a v11.0.0 platform its result comes from the legacy engine, which no longer decides, so it does not predict what the platform enforces.
  • The PEP capability handshake (v10.4.0). The platform reads the declaration from v10.4.0. From v11.0.0, an organization's redact override on decide refuses a caller that does not declare redaction (see PEP Capability Handshake).
  • Typed policy authoring (v11.0.0). An older platform does not serve these routes: edition(), validate(), publish() and activate() throw TypedPolicyRefusal with the platform's answer, and active() does not read that as nothing active: it returns null only for the platform's 404 nothing_active (see Typed Policy Authoring).

Runnable programs, in this order: examples/pep-handshake, then examples/typed-policies. Run the handshake example first: after a document with an organization-scope constraint is activated, a decide that does not supply the attribute the constraint conditions on is denied fail-closed with reasons ["unknown_constraint"]; supply the attribute or run this example on a fresh stack. From v11.0.0 the deny's first reason is that code, followed by one naming each constraint it could not evaluate and the attribute it needed (getaxonflow/axonflow-enterprise#4247). The typed-policies example publishes and activates only when asked, and before it activates it prints which of the organization template's controls the document omits.

PEP Capability Handshake

The platform, from v10.4.0, lets an enforcement point declare, on each call, the exact obligation types and schema versions it can discharge. Build the declaration once and give it to the client:

import { AxonFlow, PEPHandshake } from '@axonflow/sdk';

const pepHandshake = new PEPHandshake({
  pepId: 'checkout-gateway', // names this enforcement point within your credential
  audience: 'https://pep.example.com', // what a decision proof is bound to
  capabilities: [{ type: 'field_redact', version: 1 }],
});

const axonflow = new AxonFlow({ endpoint, clientId, clientSecret, pepHandshake });
const decision = await axonflow.decide(request); // carries X-Axonflow-PEP-Handshake

The client sends it on every call to a plane that reads it: decide, evaluate and evaluateAll, mcpCheckInput and mcpCheckOutput (and their checkTool* aliases), the engine round-trip of fulfillRequest and decideAndFulfill, and the gateway pre-check (getPolicyApprovedContext, preCheck). It does not send it to proxyLLMCall (/api/request), the OpenAI-compatible route or any other route, because none of them reads it.

One process can be two enforcement points: a request path and a response path that discharge different obligations. Pass pepHandshake to one of those methods (in its options object, or as { pepHandshake } after the request for decide, evaluate, evaluateAll, fulfillRequest and decideAndFulfill) to declare it for that call only, in place of the client's.

  • There is no default. A client given no declaration sends no header. From v11.0.0 that is not neutral: decide under an organization's redact override refuses a caller that does not declare redaction, in every edition. capabilities: [] declares that the enforcement point discharges nothing.
  • Which edition refuses what. From v11.0.0, on every edition, the engine refuses with unsupported_obligation a mandatory obligation the caller's declaration cannot discharge, and a caller that presents no declaration can discharge none: decide under an organization's redact override refuses a caller that does not declare redaction (field_redact at version 1), so on Community too a caller that declares field_mask but not field_redact is refused there. What only Enterprise adds happens at the handler, for an enforcement point that presented a declaration: an allow carrying a mandatory obligation outside the declared set becomes a deny, a refusal names the capability the declaration lacks, and on the MCP check-input round-trip a redaction the declaration cannot discharge is refused rather than handed back masked. So declare every obligation your enforcement point carries out, and only those. A Community deployment drops a declared capability in a family its edition does not issue, counts it, and lets the request proceed.
  • Refused before it is sent. new PEPHandshake(...) applies the platform's own rules and throws PEPHandshakeError naming the member at fault (.pointer is /pep_id, /audience or /capabilities), instead of the first governed call coming back 400. An entry is exactly { type, version }.

Typed Policy Authoring

A v11 platform authors policy as a typed document: validated, published as a signed artifact pinned by its digest, and promoted to active. client.typedPolicies reaches the six routes the agent proxies under /api/v1/typed-policies:

const edition = await axonflow.typedPolicies.edition(); // what this deployment may author
const validation = await axonflow.typedPolicies.validate(document, fixtures); // every finding
const published = await axonflow.typedPolicies.publish(document, fixtures); // signed, pinned by digest
await axonflow.typedPolicies.activate(published.digest); // promote to active
const active = await axonflow.typedPolicies.active(); // the signed source in force, or null
const system = await axonflow.typedPolicies.system(); // the platform's own controls, read-only
  • Activation replaces the organization template. Activating a document that omits the organization template's controls removes those controls for the organization. The template's controls carry the destructive-command blocks, DROP and TRUNCATE prevention and the blocking SQL-injection rows. publish() and activate() report which ones a document omits as template_omissions, and the example prints that report. The publish fixture the example uses is a minimal example, not a starting point for production: it omits all of them.
  • Activation promotes. A digest whose version does not advance past the active one is refused. Rolling back to an earlier document, and withdrawing the active one, are operations of the customer portal behind its session; the agent does not proxy them, so the SDK has no method for either.
  • The agent stamps the organization and the author. The platform signs the caller as author whatever the document names. The organization is the one your credentials resolve to; on Community it is the deployment's (ORG_ID).
  • Refusals are typed. Every refusal throws TypedPolicyRefusal, an APIError, with statusCode, the platform's reason (such as publication_refused, activation_refused or tier_limit), any findings, the policy a tier refusal names, and retryAfter when the refusal is retryable. On an edition with separation of duties, publishing refuses with the finding code APPROVER_IS_AUTHOR: the route names no approver, and such a deployment approves in the customer portal.
  • active() is null only for the platform's nothing_active. Any other 404, from a platform before v11.0.0 or an endpoint that is not an agent, throws TypedPolicyRefusal with status 404. A v11.0.0 platform answers a document store it cannot read with 503 storage_unavailable, which throws TypedPolicyRefusal too (getaxonflow/axonflow-enterprise#4255).
  • validate() answers identically on every edition; the edition's boundary is applied when you publish.

Configuration Options

const axonflow = new AxonFlow({
  // Authentication (OAuth2 client credentials)
  clientId: 'your-client-id',         // Required for cloud/enterprise
  clientSecret: 'your-client-secret', // Required for cloud/enterprise

  // Optional settings
  mode: 'production',                // or 'sandbox' for testing
  endpoint: 'https://staging-eu.getaxonflow.com', // Default public endpoint
  tenant: 'your-tenant-id',         // For multi-tenant setups
  debug: true,                       // Enable debug logging

  // Cache configuration
  cache: {
    enabled: true,
    ttl: 60000  // 1 minute
  }
});

A note on HTTP retries. The TypeScript SDK does not wrap its HTTP calls in an internal retry loop. Each auditToolCall, proxyLLMCall, mcpCheckInput, etc. issues exactly one outbound request and surfaces the response (or throws on error) to the caller.

In particular, 401 Unauthorized is terminal — the SDK throws AuthenticationError and does not retry. Retrying a 401 with the same credential just compounds load on the agent; refresh the token via the customer portal and reconfigure the SDK instead. If you need retries for transient infra errors (5xx, network), wrap the SDK call in your own retry helper but do not retry on AuthenticationError.

VPC Private Endpoint (Low-Latency)

For customers running within AWS VPC, use the private endpoint for lowest latency:

const axonflow = new AxonFlow({
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
  endpoint: 'https://vpc-private-endpoint.getaxonflow.com:8443',  // VPC private endpoint
  mode: 'production'
});

// VPC deployment provides lowest latency due to intra-VPC routing

Network Latency Characteristics:

  • Public endpoint: Higher latency (internet routing overhead)
  • VPC private endpoint: Lower latency (intra-VPC routing)

Note: VPC endpoints require AWS VPC peering setup with AxonFlow infrastructure.

Sandbox Mode (Local Testing)

// Quick sandbox client pointed at the local docker-compose default
// (http://localhost:8080). Use a custom endpoint if you need to point
// sandbox at a hosted environment.
const axonflow = AxonFlow.sandbox('demo-client', 'demo-secret');

// Test with PII detection (will be blocked)
try {
  const response = await axonflow.executeQuery({
    userToken: 'test-user',
    query: 'My SSN is 123-45-6789',
    requestType: 'chat'
  });
} catch (error) {
  // Expected: PolicyViolationError - PII detected
  console.log('Correctly blocked:', error.message);
}

Sandbox-mode clients fire telemetry like every other client — anonymous SDK heartbeat, classification-only payload, opt-out via AXONFLOW_TELEMETRY=off. Pings are tagged stream="sandbox" server-side so dev/test usage is distinguishable from production heartbeat. Pre-v8 sandbox mode silently suppressed pings; that suppression was removed in v8.0 to leave a single, ops-controlled opt-out lever.

What Gets Protected?

AxonFlow automatically:

  • Blocks prompts containing sensitive data (PII, credentials, etc.)
  • Redacts personal information from responses
  • Enforces rate limits and usage quotas
  • Prevents prompt injection attacks
  • Logs all requests for compliance audit trails
  • Monitors costs and usage patterns

Reading decisions: who is asking decides what comes back

explainDecision and listDecisions — and the audit reads — are scoped to the per-user identity you present, not to the tenant credential. Since platform #2922:

| What you present | What an enterprise stack returns | |---|---| | a tenant-wide role (admin, owner, policy_admin) | the whole tenant | | any other identity (developer, viewer) | only the rows attributed to it | | no identity | nothing at all — every list is empty, every explain is not-found |

clientId/clientSecret authenticate the organization. They do not say who is asking, so on their own they land in the third row. Community and Community-SaaS deployments are single-operator and read tenant-wide with no identity needed.

const client = new AxonFlow({
  endpoint: 'http://localhost:8080',
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
  userToken: process.env.AXONFLOW_USER_TOKEN, // <- the per-user identity
});

// Per call:
const exp = await client.explainDecision(decisionId, { userToken: usersToken });

// Or, for a process acting on behalf of several people, derive a client bound
// to one person. Unlike the per-call option, which only the read methods
// accept, this reaches EVERY method.
const rows = await client.asUser(alicesToken).listDecisions();

The token is a per-user JWT — minted by the customer portal's user-token API, or for local testing by scripts/generate-jwt.sh --kind user. It is not the tenant JWT and not clientSecret. It is sent as X-User-Token, is never logged, never reaches telemetry, and is never sent to any origin but the configured endpoint.

Telling the outcomes apart

"Not found", "not yours" and "no identity resolved" used to arrive as the same 404, and an unscoped list arrived as an ordinary empty page. Both now carry a cause:

import { ReadScopeError } from '@axonflow/sdk';

try {
  const decisions = await client.listDecisions();
} catch (err) {
  if (err instanceof ReadScopeError && err.identityMissing) {
    // The platform resolved no identity, so it returned zero rows by
    // construction. The empty answer was never evidence about your data.
  }
}

explainDecision is where the other scope shows up. Under own-rows the platform answers "not attributed to you" and "not there at all" with the same 404, deliberately, so that a miss cannot be used to probe for another user's rows — the error reports the scope the read ran under, never a claim about what exists.

A valid token can still resolve to nobody. The platform reserves the whole of @axonflow.local and @axonflow.internal for shared identities and censuses them to nothing before scoping. A correctly-signed developer token minted at [email protected] — which is generate-jwt.sh's own default — reads zero rows and reports identityMissing, exactly like no token at all. Mint per-user identities at a real domain.

Setting userToken affects more than reads. The header rides every request and the agent validates it on every route it proxies — not just the scoped reads. A stale or rotated token therefore turns listConnectors, installConnector and policy CRUD into 401s rather than merely unscoping a read. That is the correct, fail-closed direction, but it puts this value in the same rotation story as clientSecret.

Error Handling

import { AxonFlow, PolicyViolationError, AuthenticationError, APIError } from '@axonflow/sdk';

try {
  const response = await axonflow.executeQuery({
    userToken: 'user-123',
    query: prompt,
    requestType: 'chat'
  });
} catch (error) {
  if (error instanceof PolicyViolationError) {
    // Request violated a policy
    console.log('Policy violation:', error.blockReason);
    console.log('Policies:', error.policies);
  } else if (error instanceof AuthenticationError) {
    // Authentication failed
    console.error('Auth error:', error.message);
  } else if (error instanceof APIError) {
    // API error (status, statusText, body)
    console.error(`API error ${error.status}:`, error.body);
  } else {
    // Other errors
    console.error('Error:', error);
  }
}

Production Best Practices

  1. Environment Variables: Never hardcode credentials

    const axonflow = new AxonFlow({
      clientId: process.env.AXONFLOW_CLIENT_ID,
      clientSecret: process.env.AXONFLOW_CLIENT_SECRET
    });
  2. Fail Open: In production, AxonFlow fails open if unreachable

    // If AxonFlow is down, the original call proceeds
    // This ensures your app stays operational
  3. Tenant Isolation: Use tenant IDs for multi-tenant apps

    const axonflow = new AxonFlow({
      clientId: process.env.AXONFLOW_CLIENT_ID,
      clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
      tenant: getCurrentTenantId()
    });

Examples

Complete working examples for all features are available in the examples folder.

Community Features

// PII Detection - Automatically detect sensitive data
const result = await axonflow.getPolicyApprovedContext({
  userToken: 'user-123',
  query: 'My SSN is 123-45-6789'
});
// result.approved = true, result.requiresRedaction = true (SSN detected)

// SQL Injection Detection - Block prohibited queries
const result = await axonflow.getPolicyApprovedContext({
  userToken: 'user-123',
  query: "SELECT * FROM users WHERE role = 'admin'"
});
// result.approved = false, result.blockReason = "SQL query policy violation"

// Typed Policy Authoring (v11.0.0) - validate, publish and activate a policy document
const validation = await axonflow.typedPolicies.validate(document, fixtures);
const published = await axonflow.typedPolicies.publish(document, fixtures);
await axonflow.typedPolicies.activate(published.digest);

// MCP Connectors - Query external data sources
const resp = await axonflow.queryConnector('postgres-db', 'SELECT name FROM customers', {});
// resp.data contains query results

// Multi-Agent Planning - Orchestrate complex workflows
const plan = await axonflow.generatePlan('Research AI regulations', 'legal');
const result = await axonflow.executePlan(plan.planId);

// Audit Logging - Track all LLM interactions
await axonflow.auditLLMCall({
  contextId: ctx.contextId,
  responseSummary: 'AI response summary',
  provider: 'openai',
  model: 'gpt-4',
  tokenUsage: { promptTokens: 100, completionTokens: 200, totalTokens: 300 },
  latencyMs: 450
});

Enterprise Features

These features require an AxonFlow Enterprise license:

// Code Governance - Automated PR reviews with AI
const prResult = await axonflow.reviewPullRequest({
  repoOwner: 'your-org',
  repoName: 'your-repo',
  prNumber: 123,
  checkTypes: ['security', 'style', 'performance']
});

// Cost Controls - Budget management for LLM usage
const budget = await axonflow.getBudget('team-engineering');
// Returns: { limit: 1000.00, used: 234.56, remaining: 765.44 }

// MCP Policy Enforcement - Automatic PII redaction in connector responses
const resp = await axonflow.queryConnector('postgres', 'SELECT * FROM customers', {});
// resp.policyInfo.redacted = true
// resp.policyInfo.redactedFields = ['ssn', 'credit_card']

For enterprise features, contact [email protected].

Support

  • Documentation: https://docs.getaxonflow.com
  • Issues: https://github.com/getaxonflow/axonflow-sdk-typescript/issues
  • Email: [email protected]

If you are evaluating AxonFlow in a company setting and cannot open a public issue, you can share feedback or blockers confidentially here: Anonymous evaluation feedback form

No email required. Optional contact if you want a response.

MCP Connector Marketplace

Integrate with external data sources using AxonFlow's MCP (Model Context Protocol) connectors:

List Available Connectors

const connectors = await axonflow.listConnectors();

connectors.forEach(conn => {
  console.log(`Connector: ${conn.name} (${conn.type})`);
  console.log(`  Description: ${conn.description}`);
  console.log(`  Installed: ${conn.installed}`);
  console.log(`  Capabilities: ${conn.capabilities.join(', ')}`);
});

Install a Connector

await axonflow.installConnector({
  connector_id: 'redis-cache',
  name: 'redis-cache',
  tenant_id: 'your-tenant-id',
  options: {
    // Host/port as seen from the platform (orchestrator), not from this
    // process — 'redis' on the docker-compose stack.
    host: 'redis',
    port: 6379
  },
  credentials: {}
});

console.log('Connector installed successfully!');

Query a Connector

// Query the Redis connector. Redis connector queries are command
// statements (GET / EXISTS / TTL / KEYS) with the key in params.
const resp = await axonflow.queryConnector(
  'redis-cache',
  'GET',
  {
    key: 'user:123:preferences'
  }
);

if (resp.success) {
  console.log('Redis data:', resp.data);
} else {
  console.error('Query failed:', resp.error);
}

Production Connectors (November 2025)

AxonFlow now supports 7 production-ready connectors:

Salesforce CRM Connector

Query Salesforce data using SOQL:

// Query Salesforce contacts
const contacts = await axonflow.queryConnector(
  'salesforce-crm',
  'Find all contacts for account Acme Corp',
  {
    soql: "SELECT Id, Name, Email, Phone FROM Contact WHERE AccountId = '001xx000003DHP0'"
  }
);

console.log(`Found ${contacts.data.length} contacts`);

Authentication: OAuth 2.0 password grant (configured in AxonFlow dashboard)

Snowflake Data Warehouse Connector

Execute analytics queries on Snowflake:

// Query Snowflake for sales analytics
const analytics = await axonflow.queryConnector(
  'snowflake-warehouse',
  'Get monthly revenue for last 12 months',
  {
    sql: `SELECT DATE_TRUNC('month', order_date) as month,
          COUNT(*) as orders,
          SUM(amount) as revenue
          FROM orders
          WHERE order_date >= DATEADD(month, -12, CURRENT_DATE())
          GROUP BY month
          ORDER BY month`
  }
);

console.log('Revenue data:', analytics.data);

Authentication: Key-pair JWT authentication (configured in AxonFlow dashboard)

Slack Connector

Send notifications and alerts to Slack channels:

// Send Slack notification
const result = await axonflow.queryConnector(
  'slack-workspace',
  'Send deployment notification to #engineering channel',
  {
    channel: '#engineering',
    text: '🚀 Deployment complete! All systems operational.',
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: '*Deployment Status*\n✅ All systems operational'
        }
      }
    ]
  }
);

console.log('Message sent:', result.success);

Authentication: OAuth 2.0 bot token (configured in AxonFlow dashboard)

Available Connectors

| Connector | Type | Use Case | |-----------|------|----------| | PostgreSQL | Database | Relational data access | | Redis | Cache | Distributed rate limiting | | Slack | Communication | Team notifications | | Salesforce | CRM | Customer data, SOQL queries | | Snowflake | Data Warehouse | Analytics, reporting | | Amadeus GDS | Travel | Flight/hotel booking | | Cassandra | NoSQL | Distributed database |

For complete connector documentation, see https://docs.getaxonflow.com/docs/mcp/overview

MCP Policy Features (v3.3.0)

Exfiltration Detection

Prevent large-scale data extraction with automatic row and byte limits:

// Query with exfiltration limits (default: 10K rows, 10MB)
const response = await axonflow.queryConnector('postgres', 'SELECT * FROM customers', {});

// Check exfiltration info
if (response.policyInfo?.exfiltrationCheck?.exceeded) {
  console.log('Data limit exceeded:', response.policyInfo.exfiltrationCheck.limitType);
  // limitType: 'rows' | 'bytes'
}

// Configure limits via environment:
// MCP_MAX_ROWS_PER_QUERY=1000
// MCP_MAX_BYTES_PER_QUERY=5242880

Dynamic Policy Evaluation

Enable Orchestrator-based policy evaluation for rate limiting, budget controls, and more:

// Response includes dynamic policy info when enabled
const response = await axonflow.queryConnector('postgres', 'SELECT id FROM users', {});

// Check dynamic policy evaluation results
const dynamicInfo = response.policyInfo?.dynamicPolicyInfo;
if (dynamicInfo?.orchestratorReachable) {
  console.log('Policies evaluated:', dynamicInfo.policiesEvaluated);
  dynamicInfo.matchedPolicies?.forEach(policy => {
    console.log(`  ${policy.policyName}: ${policy.action}`);
  });
}

// Enable via environment:
// MCP_DYNAMIC_POLICIES_ENABLED=true

Multi-Agent Planning (MAP)

Generate and execute complex multi-step plans using AI agent orchestration:

Generate a Plan

// Generate a travel planning workflow
const plan = await axonflow.generatePlan(
  'Plan a 3-day trip to Paris with moderate budget',
  'travel'  // Domain hint (optional)
);

console.log(`Generated plan ${plan.planId} with ${plan.steps.length} steps`);
console.log(`Complexity: ${plan.complexity}, Parallel: ${plan.parallel}`);

plan.steps.forEach((step, i) => {
  console.log(`  Step ${i + 1}: ${step.name} (${step.type})`);
  console.log(`    Description: ${step.description}`);
  console.log(`    Agent: ${step.agent}`);
  if (step.dependsOn.length > 0) {
    console.log(`    Depends on: ${step.dependsOn.join(', ')}`);
  }
});

Execute a Plan

// Execute the generated plan
const execResp = await axonflow.executePlan(plan.planId);

console.log(`Plan Status: ${execResp.status}`);
console.log(`Duration: ${execResp.duration}`);

if (execResp.status === 'completed') {
  console.log(`Result:\n${execResp.result}`);

  // Access individual step results
  Object.entries(execResp.stepResults || {}).forEach(([stepId, result]) => {
    console.log(`  ${stepId}:`, result);
  });
} else if (execResp.status === 'failed') {
  console.error(`Error: ${execResp.error}`);
}

Check Plan Status

// For long-running plans, check status periodically
const status = await axonflow.getPlanStatus(plan.planId);

console.log(`Plan Status: ${status.status}`);
if (status.status === 'running') {
  console.log('Plan is still executing...');
}

Complete Example: Trip Planning with MAP

import { AxonFlow } from '@axonflow/sdk';

async function planTrip() {
  // Initialize client with OAuth2 credentials
  const axonflow = new AxonFlow({
    clientId: process.env.AXONFLOW_CLIENT_ID,
    clientSecret: process.env.AXONFLOW_CLIENT_SECRET,
    debug: true
  });

  // 1. Generate multi-agent plan
  const plan = await axonflow.generatePlan(
    'Plan a 3-day trip to Paris for 2 people with moderate budget',
    'travel'
  );

  console.log(`✅ Generated plan with ${plan.steps.length} steps (parallel: ${plan.parallel})`);

  // 2. Execute the plan
  console.log('\n🚀 Executing plan...');
  const execResp = await axonflow.executePlan(plan.planId);

  // 3. Display results
  if (execResp.status === 'completed') {
    console.log(`\n✅ Plan completed in ${execResp.duration}`);
    console.log(`\n📋 Complete Itinerary:\n${execResp.result}`);
  } else {
    console.error(`\n❌ Plan failed: ${execResp.error}`);
  }
}

planTrip().catch(console.error);

Migration Guide

Migrating to OAuth2 Client Credentials

If you're using older authentication methods (apiKey or licenseKey), migrate to OAuth2 client credentials:

Before (v2.x):

const axonflow = new AxonFlow({
  apiKey: process.env.AXONFLOW_API_KEY
});
// or
const axonflow = new AxonFlow({
  licenseKey: process.env.AXONFLOW_LICENSE_KEY
});

After (v3.x):

const axonflow = new AxonFlow({
  clientId: process.env.AXONFLOW_CLIENT_ID,
  clientSecret: process.env.AXONFLOW_CLIENT_SECRET
});

How to get credentials:

  1. Contact AxonFlow support at [email protected]
  2. Credentials are provided as part of your AxonFlow subscription
  3. Store credentials securely in environment variables or secrets management systems

Self-hosted users: No credentials required for localhost endpoints.

Telemetry

This SDK sends anonymous usage telemetry (SDK version, OS, enabled features) to help improve AxonFlow. No prompts, payloads, or PII are ever collected. Opt out: AXONFLOW_TELEMETRY=off.

AXONFLOW_TELEMETRY=off is the sole opt-out lever as of v8.0. The v7.x telemetry config field has been removed; the previous silent suppression of sandbox-mode pings has also been removed (sandbox-mode pings now fire and are tagged stream="sandbox" so they're distinguishable from production heartbeat).

Scope of AXONFLOW_TELEMETRY=off

AXONFLOW_TELEMETRY=off disables the anonymous SDK heartbeat (version, OS, architecture). On self-hosted and in-VPC deployments, that heartbeat is the only data the SDK sends to AxonFlow, so setting =off means we receive nothing. On Community SaaS (try.getaxonflow.com) the hosted service also processes operational data — registrations, audit logs, policy enforcement records, workflow state, plan data, and request-header metadata aggregated for usage analytics — as part of running the platform; that operational data flow is governed by the Privacy Policy, not by AXONFLOW_TELEMETRY.

Platform licence tier (license_tier)

Each heartbeat also reports the licence tier of the AxonFlow platform the SDK is configured to talk to — for example community, evaluation, Enterprise, or the transient starting while a platform is still booting. This lets us tell an enterprise-licensed deployment apart from an unlicensed community one in aggregate adoption figures, which the heartbeat previously could not distinguish.

What is and is not collected:

  • Collected: the coarse tier string only.
  • Not collected: your licence key, its expiry, its seat or node count, your organisation's name, and any other licence detail. The SDK never reads your licence key.

The value is read from the tier field of the platform's own /health response — the same response the heartbeat already fetches to report the platform version, and an endpoint that returns this field to any caller without authentication. No additional network request is made, and the SDK gains no access to anything /health does not already return.

This is an adoption-analytics signal, not an entitlement one. The value is whatever the platform at your configured endpoint reported about itself, relayed unchanged: the SDK derives nothing and verifies nothing, and the receiver cannot verify the relay either. Whoever operates that endpoint controls the value completely, so it must never gate entitlement, unlock a feature, or enter any authorization or billing decision. It is used only for aggregate adoption figures.

The field is omitted entirely whenever the tier could not be determined — the platform is unreachable, returns an error, returns an unparseable body, or returns no tier field. It is never defaulted to a guessed value, so an absent field means "not known", never "community".

AXONFLOW_TELEMETRY=off suppresses this field along with the rest of the heartbeat.

Platform build and deployment mode (edition, platform_deployment_mode)

Each heartbeat also reports which build the connected platform is running (edition: community or enterprise) and the platform's own deployment mode (platform_deployment_mode). Neither is derivable from the other or from the licence tier — the Community SaaS fleet runs the enterprise build against the community-saas schema.

Both are read from the platform's own /health response — the same response the heartbeat already fetches for the platform version and licence tier. No additional network request is made. Both are omitted entirely when the platform did not report them; an absent field means "not known" and is never defaulted.

These are adoption-analytics signals, not entitlement ones: whoever operates your configured endpoint controls both values and the SDK verifies neither.

One naming caution for anyone reading raw payloads: the heartbeat's own deployment_mode field is a different dimension — a coarse topology this SDK derives from the endpoint URL you configured. The platform's own mode travels as platform_deployment_mode.

Declaring a framework adapter (registerAdapter)

If you are building a framework integration on top of this SDK, you can declare it so aggregate adoption figures can tell adapter-driven usage apart from bare SDK usage. Without this they are indistinguishable: an adapter reports the same sdk, the same sdk_version and the same endpoint as any other client.

import { registerAdapter } from '@axonflow/sdk';

registerAdapter('my-framework');

The SDK's own AxonFlowLangGraphAdapter already does this, so simply using it is enough — no telemetry code in your application.

The name is added to the features array of the heartbeat that already fires, as adapter:my-framework. It adds no network request, and calling registerAdapter does not itself send anything. It is idempotent.

The heartbeat fires on the client's first outbound request, not at construction, so anything registered before that request is on the very first ping. A name registered afterwards rides the next heartbeat.

What is and is not collected:

  • Collected: the adapter name you pass, lowercased and trimmed.
  • Not collected: anything about what the adapter does — no prompts, no payloads, no tool names, no user identities, no configuration.

Bounds, so a malformed call cannot damage the ping it rides on:

  • A name longer than 64 bytes is dropped whole, never truncated — a truncated adapter name is a name nothing is running. A name that is empty after trimming, or not a string, is ignored.
  • The features array carries at most 32 entries, none longer than 128 bytes.

The name is not validated against a list of known frameworks. The canonical vocabulary lives on the receiving service, which folds an unrecognised name into an adapter:unknown bucket while keeping the raw name on the row.

heartbeatReady moved with the trigger. It resolves after the first request's heartbeat rather than after a constructor ping, and stays pending for a client that never sends a request. If you await client.heartbeatReady in a short-lived process, move that await to after your first API call.

AXONFLOW_TELEMETRY=off suppresses all of the above along with the rest of the heartbeat.

DO_NOT_TRACK is not honored as an opt-out for AxonFlow telemetry. It is commonly inherited from host tools and developer environments, which makes it an unreliable expression of user intent.

See Telemetry Documentation for full details.

License

MIT