Telemetry
AxonFlow SDKs, plugins, and the platform binaries themselves can emit a small anonymous heartbeat so the team can understand adoption, runtime compatibility, and broad deployment patterns. Telemetry is designed to be low-risk, infrequent, and easy to disable.
This page is mainly for platform owners and senior engineers who need to answer two practical questions: what leaves the environment by default, and how should we standardize telemetry behavior across teams?
The v1 schema in one paragraph
Every ping carries a telemetry_type discriminator that classifies the emitter class — sdk, plugin, platform, or synthetic. The class controls which other fields are required or rejected (a platform ping must carry a component; a synthetic ping is only accepted on the SigV4-authenticated internal endpoint). One orthogonal dimension sits alongside the class: a stream describes which ingestion sub-stream produced the row (heartbeat and sandbox are accepted on the public route; operationally-derived stream values are server-side only). The full wire-level field reference is below; the short story is that you can map every shape on the wire to one of four classes, and the class is the right thing to filter on for almost every analytics question.
Default Behavior
For the sdk and plugin emitter classes, AxonFlow emits at most one anonymous heartbeat per environment every 7 days, only when the SDK or plugin is in active use. The cadence is uniform across all four stable SDKs (Go, Python, TypeScript, Java), the preview Rust SDK, and all four plugins (OpenClaw, Claude Code, Cursor, Codex); no activity, no ping.
- SDK telemetry is off by default in sandbox mode
- SDK telemetry is on by default in non-sandbox modes, including localhost and self-hosted trial deployments
- Plugin telemetry (OpenClaw, Claude Code, Cursor, Codex) is on by default for local, self-hosted, and remote deployments
The trigger surface differs by integration even though the cadence is the same:
- SDKs evaluate the gate at every client construction (
new AxonFlow(...)/axonflow.NewClient(...)/AxonFlow.create(...)) and at every public HTTP request site (the gate is consulted from the SDK's shared HTTP middleware so a long-running service stays visible without redeploying — when the 7-day boundary crosses mid-process the next request fires the heartbeat asynchronously, without delaying the user's call). - The OpenClaw plugin evaluates the gate once per plugin registration (typically on each IDE launch).
- The Claude Code, Cursor, and Codex plugins evaluate the gate at every hook invocation. The 1-hour in-memory short-circuit and the stamp file together mean a hot session still results in at most one ping per 7 days.
That means local development on localhost emits the same anonymous heartbeat as other non-sandbox/self-hosted trial environments unless you explicitly opt out.
For the platform emitter class (the agent and orchestrator binaries themselves), each binary emits a single anonymous startup ping on first start in a given environment — no ongoing heartbeat after that. AXONFLOW_TELEMETRY=off short-circuits the platform emit path the same way it short-circuits SDK and plugin pings.
For the synthetic emitter class, no user binary phones home — synthetic pings are server-side aggregates emitted by AxonFlow-operated bridges against the SigV4-authenticated internal endpoint. They are not affected by AXONFLOW_TELEMETRY=off.
How the 7-day heartbeat works (sdk and plugin classes)
Each SDK and plugin keeps a small stamp file under the OS-native cache directory. When the gate is evaluated:
- If the stamp's modification time is less than 7 days old, no ping is sent.
- If the stamp is older than 7 days (or missing), a single anonymous ping is sent. The stamp is only updated after the server returns 2xx — failed POSTs leave it unchanged so the next attempt retries on schedule.
A transient network failure does not cause a 7-day silence period, and a single successful delivery does not cause a duplicate ping for another 7 days.
Emitter classes
Every ping carries a telemetry_type field — the v1 schema discriminator that identifies which kind of binary or service produced the ping. There are four values, only the first three of which originate from a user-controlled binary.
telemetry_type | Who emits it | When it fires | sdk field carries | Controlled by AXONFLOW_TELEMETRY=off? |
|---|---|---|---|---|
sdk | A language SDK (Go, Python, TypeScript, Java, Rust) | At every public HTTP request site, gated to one ping per machine per 7 days | The language name (go / python / typescript / java / rust) | YES |
plugin | An IDE / agent plugin (OpenClaw, Claude Code, Cursor, Codex) | At plugin registration / hook invocation, gated to one ping per machine per 7 days | The plugin identifier in <plugin>-plugin form (openclaw-plugin, claude-code-plugin, cursor-plugin, codex-plugin) | YES |
platform | An AxonFlow agent or orchestrator binary on first start | Once on each binary's first start in a given environment | Empty — the component field carries which binary (agent or orchestrator) | YES |
synthetic | An AxonFlow-operated server-side bridge derived from operational data already inherent to running the hosted service | On a server-side schedule (e.g. weekly aggregates) | n/a — synthetic pings are server-derived, not from a user-controlled binary | NO — synthetic events are operational, not heartbeat |
The first three classes are heartbeat: anonymous, infrequent, and disabled by AXONFLOW_TELEMETRY=off. The fourth class is server-derived (no user binary phones home) and therefore not controlled by the env var. Synthetic events are stored in a separate ingestion stream from heartbeat rows — see Stream and the privacy boundary below.
Class-specific required fields
If telemetry_type is | Then the request… |
|---|---|
sdk | Must have sdk ∈ {go, python, typescript, java, rust}. Plugin identifiers are rejected. |
plugin | Must have sdk ∈ {openclaw-plugin, claude-code-plugin, cursor-plugin, codex-plugin}. Language SDK identifiers are rejected. |
platform | Must have sdk empty AND component ∈ {agent, orchestrator}. Setting component on a non-platform ping is rejected. |
synthetic | Is rejected by the public /v1/ping endpoint. Only accepted on the SigV4-authenticated internal endpoint, and only by AxonFlow-operated server-side bridges. |
v1 migration window
During the v1 rollout window, the server accepts an empty telemetry_type for back-compat with pre-v1 SDK and plugin builds. The class is inferred server-side from the sdk field's membership: a value matching the plugin allowlist is classified as plugin, the language-SDK allowlist as sdk, anything else as unknown. The migration window will close once the v8.1 SDK minor releases roll out across the four stable SDKs; until then, both the v1-explicit and the v0-implicit shapes are valid on the wire.
This means doc-lag is safe: a v8.0 SDK reading this page will continue to send a valid ping even though the page describes the v1 schema. Doc-ahead-of-code (telling readers the new schema is required when the migration window is still open) is what's avoided here.
What Is Collected
The payload is intended to describe runtime usage shape, not business data.
| Field | Example | Emitter class(es) | Purpose |
|---|---|---|---|
telemetry_type | sdk / plugin / platform / synthetic | All — discriminator | Identify the emitter class. Required on all v1 pings; empty accepted during the migration window. |
sdk | typescript, claude-code-plugin | sdk, plugin | Language or plugin identifier. Empty on platform pings. |
sdk_version | semver string (e.g. 7.1.0 for SDKs, 1.3.0 / 2.3.0 for plugins) | sdk, plugin, platform | Track upgrade patterns. Carries the binary's version on platform pings. |
component | agent / orchestrator | platform only | Which platform binary emitted the ping. Required on platform, rejected elsewhere. |
deployment_mode | self_hosted / community_saas / unknown | sdk, plugin, platform | Deployment topology. SDKs and plugins set community_saas if endpoint host matches *.try.getaxonflow.com or AXONFLOW_TRY=1; else self_hosted; unknown if endpoint empty/unparseable. |
endpoint_type | localhost / private_network / remote / community-saas / unknown | sdk, plugin | SDK-derived classification of the configured AxonFlow URL. The raw URL is never sent or hashed. |
stream | heartbeat (default) / sandbox | sdk, plugin, platform | Self-classifies the heartbeat sub-stream. Empty defaults to heartbeat server-side. The reserved value community_saas_operational is rejected at the wire boundary — see Stream and the privacy boundary. |
license_tier | Community / Evaluation / Professional / Enterprise / EnterprisePlus / unknown | Some emitters set this (subject to operational needs) | The AxonFlow-issued license tier the binary is operating under (the same value AxonFlow itself assigned at issuance). Server-side normalization: unrecognized values bucket as unknown; never rejected. |
environment_class | lambda / ecs_fargate / ecs_ec2 / kubernetes / container / bare_metal / unknown | Some emitters set this (subject to operational needs) | Coarse runtime-environment classification. Server-side normalization: unrecognized values bucket as unknown; never rejected. |
os / arch | linux / arm64 | All except synthetic | Prioritize platform support. |
runtime_version | node 20.11.0, python 3.12.1, go1.22, JVM info, bash version | All except synthetic | Compatibility tracking. |
platform_version | semver string (e.g. 7.8.0) | plugin (best-effort) | Detected from AxonFlow /health endpoint (best-effort, 2s timeout). |
features | ["hooks:2"] | plugin | Hook count and configuration summary. |
instance_id | UUID-like value | All except synthetic | Random per-machine identifier. De-duplicates anonymous pings across heartbeats from the same machine. |
occurred_at | RFC3339 timestamp | synthetic only (internal endpoint) | Original event time for backdated server-side bridge events. Rejected with HTTP 400 on the public /v1/ping endpoint to prevent timestamp spoofing of analytics rows by unauthenticated callers. |
endpoint_type classification rules
localhost—localhost,127.0.0.1,::1,0.0.0.0,*.localhost, anything in the127.0.0.0/8loopback range, the IPv6 unspecified address::, and any fully-expanded equivalent of::1(e.g.0:0:0:0:0:0:0:1)private_network— RFC1918 IPv4 ranges (10/8,172.16-31,192.168/16), IPv4 link-local (169.254/16), IPv6 ULA (fc00::/7, RFC 4193), IPv6 link-local (fe80::/10), and the hostname suffixes.local,.internal,.lan,.intranetremote— everything else (public hostnames, public IPv4 and IPv6 addresses)unknown— any URL that can't be parsed
Deprecated site-local IPv6 addresses (fec0::/10, withdrawn by RFC 3879) are classified as remote, not private_network.
This field lets us distinguish localhost trial traffic from real production deployments on the checkpoint dashboard, without ever seeing the URL itself.
What Is Not Collected
- the configured AxonFlow URL itself — only the SDK-derived
endpoint_typeclassification - prompts, responses, or tool outputs
- MCP statements or connector parameters
- API keys, client secrets, tokens, or credentials
- tenant names, company names, or user identities
- file paths, environment-variable values, or request payload contents
- hashes of any of the above
How to Disable Telemetry
Environment Variables
AXONFLOW_TELEMETRY=off is the canonical AxonFlow-specific opt-out. It takes precedence over any in-code configuration.
export AXONFLOW_TELEMETRY=off
Scope of AXONFLOW_TELEMETRY=off
AXONFLOW_TELEMETRY=off disables the anonymous heartbeat classes (telemetry_type ∈ {sdk, plugin, platform}) described in this page. It is honored mechanically on every deployment shape and at every emitter site. Synthetic events are not controlled by this env var because they don't originate from a user-controlled binary — see the Stream and the privacy boundary section below for how the analytics path keeps the two streams separate.
What =off actually buys you depends on which deployment you're on:
- Self-hosted and in-VPC deployments: the heartbeat is the only data the SDK, plugin, or platform binary sends to AxonFlow. Setting
=offmeans we receive nothing from your environment. - 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 byAXONFLOW_TELEMETRY.
If you need no-data-leaves-network guarantees, self-host AxonFlow Community Edition.
DO_NOT_TRACK is not honored as an opt-out for AxonFlow telemetry. It is commonly inherited from host tools and developer environments — and host CLIs like Codex and Claude Code inject it unconditionally for hook subprocesses — which makes it an unreliable expression of user intent. AxonFlow telemetry is controlled exclusively by AXONFLOW_TELEMETRY=off.
Stream and the privacy boundary
Every heartbeat row carries a stream value that names which sub-stream produced it. There are two acceptable wire values for client-emitted pings (heartbeat for production-mode clients and sandbox for sandbox-mode clients) plus a third value reserved server-side for the synthetic class:
stream | Origin | Wire-acceptable from a client? | Used for |
|---|---|---|---|
heartbeat | sdk / plugin / platform ping in production-mode | Yes (default when empty) | The heartbeat analytics dataset |
sandbox | sdk ping from a sandbox-mode client | Yes | A separate sub-stream of heartbeat — analytically distinguishable, still heartbeat-derived |
community_saas_operational | synthetic ping derived from operational data | No — rejected with HTTP 400 if a client tries to send it | A separate ingestion stream; never co-mingled with heartbeat rows |
The privacy commitment that the SDK/plugin/platform heartbeat analytics dataset contains heartbeat-derived rows only is preserved on two independent paths:
- Write path — the public
/v1/pinghandler hardcodesstream=heartbeat(orsandboxwhen self-classified) on every record it writes. The handler cannot write a row taggedcommunity_saas_operationalregardless of payload. - Wire boundary — the validator rejects any ping that tries to set
stream=community_saas_operational. A misbehaving client cannot inject acommunity_saas_operational-tagged row into the heartbeat analytics dataset because the request never reaches the write path.
The combined effect is that operators reading the heartbeat analytics path can filter on stream ∈ {heartbeat, sandbox} and trust the result is heartbeat-derived end-to-end. See the privacy policy for the legal-language version of the same commitment.
Programmatic configuration was removed in v8.0
Earlier SDK versions accepted a programmatic disable flag (TelemetryEnabled / telemetry: false / .telemetry(false)). The v8.0 SDK majors removed those flags so every deployment has a single mental model for opt-out: set AXONFLOW_TELEMETRY=off in the process environment. Code that previously passed the flag should be updated to set the env var before constructing the client.
| Migration | Before (v7.x) | After (v8.0+) |
|---|---|---|
| Disable from code | new AxonFlow({ telemetry: false }) (or per-language equivalent) | Set AXONFLOW_TELEMETRY=off in the environment; pass no flag |
| Enable explicitly | new AxonFlow({ telemetry: true }) | Default — pass no flag, ensure AXONFLOW_TELEMETRY is unset or non-off |
The v8.0 majors moved every SDK to the same shape; v8.1 minors will additionally emit the v1 telemetry_type discriminator. Pre-v8.1 SDKs continue to send a valid v1-implicit ping during the migration window described above.
Endpoint
Telemetry pings are sent to one of two endpoints. Almost every reader of this page only ever interacts with the first.
| Endpoint | Auth | Accepts | Used by |
|---|---|---|---|
POST https://checkpoint.getaxonflow.com/v1/ping | None (public) | telemetry_type ∈ {sdk, plugin, platform}, or empty during the migration window. Rejects any synthetic ping or any caller-supplied occurred_at with HTTP 400. | All SDK / plugin / platform clients in the wild. |
POST https://checkpoint.getaxonflow.com/v1/ping/internal | SigV4 (AWS IAM) | telemetry_type=synthetic only. Honors caller-supplied occurred_at (RFC3339) so server-side bridges can backdate aggregates. | AxonFlow-operated server-side bridges. Not a public API; the IAM-grant model means the route is "internal but not secret" — security-by-IAM, not security-by-obscurity. |
For private or air-gapped deployments, override the public endpoint with the AXONFLOW_CHECKPOINT_URL environment variable. The internal endpoint is not reachable in air-gapped operation by design (no synthetic-bridge stream there).
For the full validation matrix, parameter rejection rules, and throttling behavior, see Checkpoint Service API.
Worked example payloads
The four emitter classes produce different payloads. The shapes below match what the server validator accepts on the wire under the v1 schema. Each example is the request body — the server stamps Timestamp, CorrelationID, APIGWRequestID, and (on synthetic) the inferred Stream.
sdk ping (Python SDK at startup)
{
"telemetry_type": "sdk",
"sdk": "python",
"sdk_version": "7.1.0",
"platform_version": "7.8.0",
"os": "linux",
"arch": "arm64",
"runtime_version": "python 3.12.1",
"deployment_mode": "self_hosted",
"endpoint_type": "private_network",
"stream": "heartbeat",
"instance_id": "f3a81c12-4b2e-4d31-a8f3-12c45d6e7f89"
}
plugin ping (Claude Code plugin hook invocation)
{
"telemetry_type": "plugin",
"sdk": "claude-code-plugin",
"sdk_version": "1.3.0",
"platform_version": "7.8.0",
"os": "darwin",
"arch": "arm64",
"runtime_version": "bash 5.2.21",
"deployment_mode": "community_saas",
"endpoint_type": "remote",
"stream": "heartbeat",
"features": ["hooks:2"],
"instance_id": "9c4e7b88-3a1d-4e8c-92f1-7a8b3c4d5e6f"
}
platform ping (orchestrator binary on first start)
{
"telemetry_type": "platform",
"component": "orchestrator",
"sdk_version": "7.8.0",
"os": "linux",
"arch": "amd64",
"runtime_version": "go1.22",
"deployment_mode": "self_hosted",
"stream": "heartbeat",
"instance_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}
Note the sdk field is empty on platform pings — component is the discriminator within the platform class.
synthetic ping (server-side bridge, internal endpoint only)
{
"telemetry_type": "synthetic",
"occurred_at": "2026-05-08T00:00:00Z",
"stream": "heartbeat",
"instance_id": "axonflow-csaas-bridge"
}
Synthetic pings don't represent a binary phoning home — they're aggregates derived from operational data already inherent to running the hosted service. The payload above would be sent by an AxonFlow-operated bridge using SigV4 against the internal endpoint, with occurred_at carrying the original event time so that backdated weekly aggregates land at the correct timestamp.
Operational Characteristics
- SDK and plugin telemetry land at most one ping per environment per 7 days during activity (see "How the 7-day heartbeat works" above)
- The stamp file's modification time is the source of truth for "last successful delivery"; the gate also keeps a short in-memory cache (1 hour) so a hot service or active hook session does not stat the stamp on every call
- Failures are silent and never break SDK or plugin usage. A failed POST leaves the stamp unchanged so the next attempt retries on schedule
- The call runs in the background with a short bounded timeout
- Explicit SDK config (
telemetry: false) andAXONFLOW_TELEMETRY=offboth bypass the gate entirely
Stamp file locations
The stamp file lives under the OS-native cache directory. Filenames are deterministic per surface so an operator can locate them on a managed fleet.
SDKs (Go, Python, TypeScript, Java) and the OpenClaw plugin support all three OS families:
| OS | Cache directory base |
|---|---|
| macOS | ~/Library/Caches/axonflow/ |
| Linux | $XDG_CACHE_HOME/axonflow/ when set, otherwise ~/.cache/axonflow/ |
| Windows | %LOCALAPPDATA%\axonflow\ |
The Claude Code, Cursor, and Codex plugins are bash-script integrations and run only where their host CLIs run (Linux/macOS); the stamp lives at $HOME/.cache/axonflow/.
Filenames inside that directory:
| Surface | Stamp file |
|---|---|
| Go SDK | go-telemetry-last-sent |
| Python SDK | python-telemetry-last-sent |
| TypeScript SDK | typescript-telemetry-last-sent |
| Java SDK | java-telemetry-last-sent |
| OpenClaw plugin | openclaw-plugin-telemetry-sent |
| Claude Code plugin | claude-code-plugin-telemetry-sent |
| Cursor plugin | cursor-plugin-telemetry-sent |
| Codex plugin | codex-plugin-telemetry-sent |
The SDK and plugin filenames differ for a deliberate reason. Pre-7-day bash plugin versions (Codex, Claude Code, Cursor) stored a per-machine instance_id UUID inside *-telemetry-sent at install time. The 7-day plugin code re-uses that exact filename so existing installs preserve their instance_id across the upgrade — without it, a single machine would appear as two distinct anonymous installs in the telemetry data. The OpenClaw plugin had no stamp file pre-heartbeat, but its filename follows the same *-telemetry-sent convention so all four plugin surfaces stay consistent. SDKs ship the heartbeat from the start, so they use the more descriptive *-telemetry-last-sent naming.
If you delete the stamp file, the next SDK construction or plugin invocation will fire one fresh ping (and re-create the stamp on success).
Where the cache directory is not user-writable (for example AWS Lambda with HOME unset, or LOCALAPPDATA absent on Windows containers), the SDK keeps the gate in memory only: one ping per process on cold start, then bounded by the 1-hour in-memory cache for the rest of that process. Nothing is persisted to disk and no stamp is created. This matches the pre-heartbeat default for those runtimes.
Recommended Team Policy
Most organizations land on one of these operating patterns:
- leave telemetry enabled in shared non-sandbox environments, including localhost or self-hosted trials, so adoption and compatibility signals reach the AxonFlow team
- disable telemetry in tightly regulated or locked-down production estates with
AXONFLOW_TELEMETRY=off(canonical) - bake the choice into base images, deployment templates, or internal SDK wrappers so application teams get a predictable default
Why This Exists
Telemetry helps the team prioritize the SDKs, operating systems, and runtime combinations that real engineers are using. That is especially important when AxonFlow is used across multiple languages and deployment modes.
If your organization prefers not to send anonymous usage data, the opt-out controls above are the supported way to disable it.
