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
Go's semantic import versioning requires the module path to include the major version suffix for v2+. The current release line is v9.x, imported as:
import "github.com/getaxonflow/axonflow-sdk-go/v9"go get github.com/getaxonflow/axonflow-sdk-go/v9
go get github.com/getaxonflow/axonflow-sdk-go@latest(without/v9) resolves to v1.17.0 (a 2026-01 relic from before the v2 split) and is eight major release lines behind current. See the Migration Guide below.
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 hello@getaxonflow.com for private feedback.
Enterprise-grade Go SDK for AxonFlow AI governance platform. Add invisible AI governance to your applications with production-ready features including retry logic, caching, fail-open strategy, and debug mode.
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.
Videos covering different angles of the platform:
- Product demos: Platform + Fraud & Risk - runtime enforcement, HITL approvals, audit evidence, cost visibility, and agentic payment controls
- Community Quickstart walkthrough (2 min) - governed calls, PII blocking, Gateway Mode with LangChain/CrewAI, and MAP from YAML
- Architecture deep dive (12 min) - how the control plane works, policy enforcement flow, and multi-agent planning
go get github.com/getaxonflow/axonflow-sdk-go/v9Need 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
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-secretNo Docker, no license, no installation. Rate-limited to 20 req/min. Learn more.
package main
import (
"fmt"
"log"
"os"
"github.com/getaxonflow/axonflow-sdk-go/v9"
)
func main() {
// Simple initialization with OAuth2 credentials
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: os.Getenv("AXONFLOW_CLIENT_ID"),
ClientSecret: os.Getenv("AXONFLOW_CLIENT_SECRET"),
})
// Execute a governed query
resp, err := client.ProxyLLMCall(
"user-token",
"What is the capital of France?",
"chat",
map[string]interface{}{},
)
if err != nil {
log.Fatalf("Query failed: %v", err)
}
if resp.Blocked {
log.Printf("Request blocked: %s", resp.BlockReason)
return
}
fmt.Printf("Result: %s\n", resp.Data)
}import (
"time"
"os"
"github.com/getaxonflow/axonflow-sdk-go/v9"
)
// Full configuration with all features
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: os.Getenv("AXONFLOW_CLIENT_ID"),
ClientSecret: os.Getenv("AXONFLOW_CLIENT_SECRET"),
Mode: "production", // or "sandbox"
Debug: true, // Enable debug logging
Timeout: 60 * time.Second,
// Per-user identity for the READ path (see "Reading decisions" below).
// ClientID/ClientSecret say which ORGANIZATION is asking; this says WHO.
UserToken: os.Getenv("AXONFLOW_USER_TOKEN"),
// Retry configuration (exponential backoff)
Retry: axonflow.RetryConfig{
Enabled: true,
MaxAttempts: 3,
InitialDelay: 1 * time.Second,
},
// Cache configuration (in-memory with TTL)
Cache: axonflow.CacheConfig{
Enabled: true,
TTL: 60 * time.Second,
},
})Connect to a self-hosted AxonFlow instance running via docker-compose:
package main
import (
"fmt"
"log"
"github.com/getaxonflow/axonflow-sdk-go/v9"
)
func main() {
// Self-hosted (localhost) - no license key needed!
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8081",
// That's it - no authentication required for localhost
})
// Use normally - same features as production
resp, err := client.ProxyLLMCall(
"user-token",
"Test with self-hosted AxonFlow",
"chat",
map[string]interface{}{},
)
if err != nil {
log.Fatalf("Query failed: %v", err)
}
fmt.Printf("Result: %s\n", resp.Data)
}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
# Go SDK connects to http://localhost:8081 - 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
// Quick sandbox client for local testing - defaults to http://localhost:8080.
client := axonflow.Sandbox("demo-client", "demo-secret")
resp, err := client.ProxyLLMCall(
"",
"Test query with sensitive data: SSN 123-45-6789",
"chat",
map[string]interface{}{},
)
if resp.Blocked {
fmt.Printf("Blocked: %s\n", resp.BlockReason)
}Sandbox-mode clients fire telemetry like every other client - anonymous SDK heartbeat, classification-only payload, opt-out via
AXONFLOW_TELEMETRY=off. Pings are taggedstream="sandbox"server-side so dev/test usage is distinguishable from production heartbeat.
The AuthZEN-native surface: ask whether a subject may perform an action on a resource, and get back a decision your enforcement point can act on.
dec, err := client.Evaluate(ctx, axonflow.AuthZENRequest{
Subject: &axonflow.AuthZENSubject{Type: "gateway", ID: "llm-gateway-01"},
Action: &axonflow.AuthZENAction{Name: "llm.completion"},
Resource: &axonflow.AuthZENResource{Type: "llm", ID: "llm"},
Context: map[string]any{
"args": map[string]any{"query": userPrompt},
},
})
if err != nil {
return err // fail closed: an error is never a permit
}
if !dec.Allowed() {
return fmt.Errorf("blocked: %s", dec.State())
}Write new integrations against this surface. The existing decision surface
stays wire-stable through all of v11 and is not deprecated. At v11 the engine
behind Evaluate changes to the ADR-065 Policy Decision Point with no wire
change, so an integration written against it migrates once rather than twice.
The server refuses what it cannot evaluate rather than evaluating around it. Send a subject property, an unrecognised context member, or an argument beside the query, and you get a typed refusal naming the exact member - not a decision computed without it:
dec, err := client.Evaluate(ctx, req)
if azErr, ok := axonflow.AsAuthZENError(err); ok {
// azErr.Pointer: "/evaluation/subject/properties"
// azErr.Code: "unevaluable_attribute"
log.Printf("fix %s: %s", azErr.Pointer, azErr.Message)
if azErr.Code.Retryable() {
// only evaluation_unavailable is worth retrying; every other code
// names something about the request that a retry will not change.
}
}This is deliberate. A decision that silently ignored an attribute would report that the attribute was weighed when it was not, and every audit of that decision would inherit the claim.
EvaluateAll asks about several resources at once and returns one decision,
not one per entry. The entries are preconditions of a single operation, so they
combine to the least permissive outcome: one denied entry denies the operation.
Anything an entry omits is inherited from the shared base.
dec, err := client.EvaluateAll(ctx, axonflow.AuthZENBulk{
Subject: &axonflow.AuthZENSubject{Type: "gateway", ID: "llm-gateway-01"},
Action: &axonflow.AuthZENAction{Name: "tool.call"},
Context: map[string]any{"args": map[string]any{"query": userPrompt}},
Evaluations: []axonflow.AuthZENRequest{
{Resource: &axonflow.AuthZENResource{Type: "tool", ID: "jira/move_issue"}},
{Resource: &axonflow.AuthZENResource{Type: "tool", ID: "jira/update_project"}},
},
})An allow can carry instructions the enforcement point must discharge. A
mandatory obligation that cannot be discharged means the operation must not
proceed, even though Allowed() reported true.
for _, o := range dec.Obligations() {
if o.Mandatory && !canDischarge(o.Type) {
return fmt.Errorf("cannot discharge %s; refusing to proceed", o.Type)
}
}Every attribute bag (Context, and the Properties of a subject, action or
resource) holds facts you resolved from somewhere: an identity provider, a
trace propagator, a session store. Resolving a fact has three outcomes, and a
map[string]any can only express two of them. AuthZENAttribute carries the
third:
dec, err := client.Evaluate(ctx, axonflow.AuthZENRequest{
// ...
Context: map[string]any{
"args": map[string]any{"query": userPrompt},
"correlation": map[string]any{
"trace_id": axonflow.AuthZENKnown(traceID), // sent, as its value
"session_id": axonflow.AuthZENAbsent(), // omitted: there IS no session
"origin_id": axonflow.AuthZENUnknown( // REFUSES the request locally
axonflow.AuthZENUnknownResolutionFailed),
},
},
})
if unres, ok := axonflow.AsAuthZENUnresolvedError(err); ok {
// unres.Pointer: "/evaluation/context/correlation/origin_id"
// unres.Retryable(): false - the refusal is frozen inside the request;
// re-resolve the attribute and build a new one.
log.Printf("re-resolve %s: %s", unres.Pointer, unres.Reason)
}absent and unknown are not the same thing: absence is a fact (the source
answered: there is no value), so the member is omitted and the request is sent.
unknown means the source could not answer - and sending the request anyway
would have the gateway evaluate as though the attribute were missing, recording
an attribute as weighed that nobody ever read. The SDK refuses that request
before it exists on the wire, with a typed *AuthZENUnresolvedError that is
distinguishable from a server refusal (*AuthZENError) and from a transport
error, and is never retryable. The rule holds at every depth of every bag,
including inside slices and the plural envelope's shared base and entries. The
one exception is an attribute nested inside your own struct, which neither you
nor the SDK can rebuild with a member left out: there known still encodes as
its value and unknown still refuses, but absent degrades to a refusal (the
encode fails with an error naming the limitation, and nothing is sent) instead
of an omission - put the attribute in a map[string]any bag if it must be
omissible.
| Field | Accepted |
|---|---|
Subject.Type |
gateway |
Action.Name |
llm.completion, tool.call, agent.invoke |
Resource.Type / ID |
llm + provider/model; tool + server/tool; agent + agent |
Context |
args.query (the content to evaluate), correlation (string values) |
Anything else is refused by name. An end-user subject (Subject.Type other
than gateway) is not evaluable yet: it would have to be trusted from
caller-supplied JSON, which is an impersonation surface. It arrives with the
identity plane at v11.
authzen_types_gen.go is generated from testdata/authzen-surface.json, which
the platform publishes from its canonical decision contract. All five AxonFlow
SDKs generate from that one artifact, so no two of them can disagree about which
fields are optional. Regenerate with:
go run ./scripts/gen_authzen_typesCI regenerates and diffs, so editing either file without the other fails.
See examples/authzen for a runnable walkthrough of the
happy path and every refusal.
From v10.4.0 the platform 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:
declared, err := axonflow.NewPEPHandshake(
"checkout-gateway", // names this enforcement point within your credential
"https://pep.example.com", // what a decision proof is bound to
[]axonflow.PEPCapability{{Type: axonflow.AuthZENObligationTypeFieldRedact, Version: 1}},
)
if err != nil {
return err // a *axonflow.PEPHandshakeError naming the member at fault
}
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "...", ClientID: "...", ClientSecret: "...",
PEPHandshake: declared,
})
decision, err := client.Decide(ctx, request) // carries X-Axonflow-PEP-HandshakeThe client sends it on every call to a plane that reads it: Decide, the engine
round-trip of FulfillRequest and DecideAndFulfill, Evaluate and
EvaluateAll, MCPCheckInput and MCPCheckOutput (and their CheckTool*
aliases), and the gateway pre-check (PreCheck, GetPolicyApprovedContext,
PreCheckWithContext). 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. axonflow.ContextWithPEPHandshake(ctx, h)
declares h for the calls made with that context, in place of the client's;
PreCheckWithContext is the pre-check's form that takes a context.
- There is no default. A client given no declaration sends no header, and the platform takes the path it took before the handshake existed, except under an organization's redact override from v11.0.0 (below). An empty, non-nil capability slice declares that the enforcement point discharges nothing; a nil one is refused.
- Which edition refuses what. From v11.0.0, on every edition, the engine
refuses with
unsupported_obligationa mandatory obligation the caller's declaration cannot discharge, and a caller that presents no declaration can discharge none:Decideunder an organization's redact override refuses a caller that does not declare redaction (field_redactat version 1), where v10 allowed it with aredact_piiobligation, so on Community too a caller that declaresfield_maskbut notfield_redactis 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.
NewPEPHandshakeapplies the platform's own rules and returns a*PEPHandshakeErrornaming the member at fault (Pointeris/pep_id,/audienceor/capabilities), instead of the first governed call coming back400. APEPHandshakebuilt by hand is checked the same way on every call, which then fails before anything is sent.
Against a v11.0.0 platform this SDK surfaces the new decision plane. Against v10 the calls that existed before work as before, and each v11 field reads empty.
- Decision provenance. Governed responses (
ProxyLLMCall,Decide, the gateway pre-check, the MCP check-output and connector responses) carryEngine,SubjectTypeandPolicyBundle, and a decision addsPolicyIdentities,PolicyPacksandDocumentVersion. A v11.0.0 platform fills them.LegacyValidatorsis filled only where a checksum validator acted, so it is empty onProxyLLMCallby design. - Frozen legacy policy writes. A v11.0.0 platform answers a write to its
static- or dynamic-policy routes with 409
LEGACY_POLICY_WRITE_FROZEN, returned as a*LegacyPolicyWriteFrozenErrorthat names the typed policy route. - Route deprecation.
AxonFlowConfig.OnRouteDeprecationreports, once per route, each route a v11.0.0 platform marks deprecated (see v11.0.0 deprecations). - The PEP capability handshake. A platform reads the declaration from
v10.4.0. From v11.0.0,
Decideunder an organization's redact override refuses a caller that does not declare redaction (see PEP capability handshake). - Typed policy authoring. The routes exist from v11.0.0. An older platform
does not serve them, so each call is refused rather than answered, and
ActiveTypedPolicydoes not read that as nothing active (see Typed policy authoring).
Runnable programs, in this order: examples/pep_handshake,
then examples/typed_policies.
A v11 platform authors policy as a typed document: validated, published as a
signed artifact pinned by its digest, and promoted to active. Six methods reach
the routes the agent proxies under /api/v1/typed-policies:
edition, err := client.TypedPolicyEdition(ctx) // what this deployment may author
validation, err := client.ValidateTypedPolicy(ctx, document, fixtures) // every finding
published, err := client.PublishTypedPolicy(ctx, document, fixtures) // signed, pinned by digest
_, err = client.ActivateTypedPolicy(ctx, published.Digest, "") // promote to active
active, err := client.ActiveTypedPolicy(ctx) // the signed source in force, or nil
system, err := client.TypedPolicySystem(ctx) // the platform's own controls- 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.
PublishTypedPolicyandActivateTypedPolicyreport which ones a document omits asTemplateOmissions, 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; a user token on the context
(
ContextWithUserToken) is the caller, as on every other route. The organization is the one your credentials resolve to; on Community it is the deployment's (ORG_ID). - Refusals are typed. Every refusal is a
*TypedPolicyRefusalwith the HTTPStatus, the platform'sReason(such aspublication_refused,activation_refusedortier_limit), anyFindings, thePolicya tier refusal names, andRetryAfterwhen the refusal is retryable; a 401 is the client's usual error. On an edition with separation of duties, publishing refuses with the finding codeAPPROVER_IS_AUTHOR: the route names no approver, and such a deployment approves in the customer portal. - The document is the authoring model itself, a
map[string]anyrather than Go types, so a field the policy vocabulary gains is authorable without an SDK release.ValidateTypedPolicyanswers identically on every edition; the edition's boundary is applied when you publish. ActiveTypedPolicyreturns(nil, nil)only for the platform'snothing_active. Any other 404 is a*TypedPolicyRefusalwith status 404. A v11.0.0 platform answers a document store it cannot read with 503storage_unavailable, which is a*TypedPolicyRefusaltoo (getaxonflow/axonflow-enterprise#4255).
Automatic retry on transient failures with exponential backoff:
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: "your-client-id",
ClientSecret: "your-secret",
Retry: axonflow.RetryConfig{
Enabled: true,
MaxAttempts: 3, // Retry up to 3 times
InitialDelay: 1 * time.Second, // 1s, 2s, 4s backoff
},
})
// Automatically retries on 5xx errors or network failures
resp, err := client.ProxyLLMCall(...)Reduce latency and load with intelligent caching:
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: "your-client-id",
ClientSecret: "your-secret",
Cache: axonflow.CacheConfig{
Enabled: true,
TTL: 60 * time.Second, // Cache for 60 seconds
},
})
// First call: hits AxonFlow
resp1, _ := client.ProxyLLMCall("token", "query", "chat", nil)
// Second call (within 60s): served from cache
resp2, _ := client.ProxyLLMCall("token", "query", "chat", nil)Never block your users if AxonFlow is unavailable:
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: "your-client-id",
ClientSecret: "your-secret",
Mode: "production", // Fail-open in production
Debug: true,
})
// If AxonFlow is unavailable, request proceeds with warning
resp, err := client.ProxyLLMCall(...)
// err == nil, resp.Success == true, resp.Error contains warningWrap your LLM clients with automatic AxonFlow governance using the interceptors package:
import (
"context"
"github.com/sashabaranov/go-openai"
"github.com/getaxonflow/axonflow-sdk-go/v9"
"github.com/getaxonflow/axonflow-sdk-go/v9/interceptors"
)
// Initialize AxonFlow client
axonflowClient := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: os.Getenv("AXONFLOW_CLIENT_ID"),
ClientSecret: os.Getenv("AXONFLOW_CLIENT_SECRET"),
})
// Create an adapter for the OpenAI client
openaiClient := openai.NewClient(os.Getenv("OPENAI_API_KEY"))
// Use the function wrapper for direct usage
wrappedFn := interceptors.WrapOpenAIFunc(
func(ctx context.Context, req interceptors.ChatCompletionRequest) (interceptors.ChatCompletionResponse, error) {
// Convert to go-openai types and call
goReq := openai.ChatCompletionRequest{
Model: req.Model,
Messages: convertMessages(req.Messages),
}
resp, err := openaiClient.CreateChatCompletion(ctx, goReq)
if err != nil {
return interceptors.ChatCompletionResponse{}, err
}
return convertResponse(resp), nil
},
axonflowClient,
"user-token",
)
// Use wrapped function - governance happens automatically
resp, err := wrappedFn(ctx, interceptors.ChatCompletionRequest{
Model: "gpt-4",
Messages: []interceptors.ChatMessage{
{Role: "user", Content: "Hello, world!"},
},
})
if err != nil {
if interceptors.IsPolicyViolationError(err) {
pve, _ := interceptors.GetPolicyViolation(err)
log.Printf("Blocked: %s (policies: %v)", pve.BlockReason, pve.Policies)
}
}import (
"context"
"github.com/getaxonflow/axonflow-sdk-go/v9"
"github.com/getaxonflow/axonflow-sdk-go/v9/interceptors"
)
// Create Anthropic interceptor
wrappedFn := interceptors.WrapAnthropicFunc(
yourAnthropicCreateFn,
axonflowClient,
"user-token",
)
// Use wrapped function
resp, err := wrappedFn(ctx, interceptors.AnthropicMessageRequest{
Model: "claude-3-sonnet-20240229",
MaxTokens: 1024,
Messages: []interceptors.AnthropicMessage{
interceptors.CreateUserMessage("Hello, Claude!"),
},
})For more flexibility, implement the OpenAIChatCompleter or AnthropicMessageCreator interfaces:
// Implement the interface
type MyOpenAIClient struct {
// your fields
}
func (c *MyOpenAIClient) CreateChatCompletion(ctx context.Context, req interceptors.ChatCompletionRequest) (interceptors.ChatCompletionResponse, error) {
// your implementation
}
// Wrap the client
wrapped := interceptors.WrapOpenAIClient(&MyOpenAIClient{}, axonflowClient, "user-token")
// Use wrapped client
resp, err := wrapped.CreateChatCompletion(ctx, req)Integrate with external data sources using AxonFlow's MCP (Model Context Protocol) connectors:
connectors, err := client.ListConnectors()
if err != nil {
log.Fatalf("Failed to list connectors: %v", err)
}
for _, conn := range connectors {
fmt.Printf("Connector: %s (%s)\n", conn.Name, conn.Type)
fmt.Printf(" Description: %s\n", conn.Description)
fmt.Printf(" Installed: %v\n", conn.Installed)
}err := client.InstallConnector(axonflow.ConnectorInstallRequest{
ConnectorID: "redis-cache",
Name: "redis-cache",
TenantID: "your-tenant-id",
Options: map[string]interface{}{
// Host/port as seen from the platform (orchestrator), not from
// this process - "redis" on the docker-compose stack.
"host": "redis",
"port": 6379,
},
})
if err != nil {
log.Fatalf("Failed to install connector: %v", err)
}
fmt.Println("Connector installed successfully!")// Query the Redis connector. Redis connector queries are command
// statements (GET / EXISTS / TTL / KEYS) with the key in params.
resp, err := client.QueryConnector(
"user-session-token", // User token for authentication and audit trail
"redis-cache",
"GET",
map[string]interface{}{
"key": "user:123:preferences",
},
)
if err != nil {
log.Fatalf("Connector query failed: %v", err)
}
if resp.Success {
fmt.Printf("Redis data: %v\n", resp.Data)
} else {
fmt.Printf("Query failed: %s\n", resp.Error)
}AxonFlow now supports 7 production-ready connectors:
Query Salesforce data using SOQL:
// Query Salesforce contacts
resp, err := client.QueryConnector(
"user-session-token", // User token for authentication and audit trail
"salesforce-crm",
"Find all contacts for account Acme Corp",
map[string]interface{}{
"soql": "SELECT Id, Name, Email, Phone FROM Contact WHERE AccountId = '001xx000003DHP0'",
},
)
if err != nil {
log.Fatalf("Salesforce query failed: %v", err)
}
fmt.Printf("Found %d contacts\n", len(resp.Data.([]interface{})))Authentication: OAuth 2.0 password grant (configured in AxonFlow dashboard)
Execute analytics queries on Snowflake:
// Query Snowflake for sales analytics
resp, err := client.QueryConnector(
"user-session-token", // User token for authentication and audit trail
"snowflake-warehouse",
"Get monthly revenue for last 12 months",
map[string]interface{}{
"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`,
},
)
if err != nil {
log.Fatalf("Snowflake query failed: %v", err)
}
fmt.Printf("Revenue data: %v\n", resp.Data)Authentication: Key-pair JWT authentication (configured in AxonFlow dashboard)
Send notifications and alerts to Slack channels:
// Send Slack notification
resp, err := client.QueryConnector(
"user-session-token", // User token for authentication and audit trail
"slack-workspace",
"Send deployment notification to #engineering channel",
map[string]interface{}{
"channel": "#engineering",
"text": "🚀 Deployment complete! All systems operational.",
"blocks": []map[string]interface{}{
{
"type": "section",
"text": map[string]string{
"type": "mrkdwn",
"text": "*Deployment Status*\n✅ All systems operational",
},
},
},
},
)
if err != nil {
log.Fatalf("Slack notification failed: %v", err)
}
fmt.Printf("Message sent: %v\n", resp.Success)Authentication: OAuth 2.0 bot token (configured in AxonFlow dashboard)
| 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
Prevent large-scale data extraction with automatic row and byte limits:
// Query with exfiltration limits (default: 10K rows, 10MB)
response, err := client.QueryConnector("postgres", "SELECT * FROM customers", nil)
if err != nil {
log.Fatal(err)
}
// Check exfiltration info
if response.PolicyInfo.ExfiltrationCheck.Exceeded {
log.Printf("Data limit exceeded: %s", response.PolicyInfo.ExfiltrationCheck.LimitType)
// LimitType: "rows" or "bytes"
}
// Configure limits via environment:
// MCP_MAX_ROWS_PER_QUERY=1000
// MCP_MAX_BYTES_PER_QUERY=5242880Enable Orchestrator-based policy evaluation for rate limiting, budget controls, and more:
// Response includes dynamic policy info when enabled
response, err := client.QueryConnector("postgres", "SELECT id FROM users", nil)
if err != nil {
log.Fatal(err)
}
// Check dynamic policy evaluation results
dynamicInfo := response.PolicyInfo.DynamicPolicyInfo
if dynamicInfo.OrchestratorReachable {
log.Printf("Policies evaluated: %d", dynamicInfo.PoliciesEvaluated)
for _, policy := range dynamicInfo.MatchedPolicies {
log.Printf(" %s: %s", policy.PolicyName, policy.Action)
}
}
// Enable via environment:
// MCP_DYNAMIC_POLICIES_ENABLED=trueGenerate and execute complex multi-step plans using AI agent orchestration:
// Generate a travel planning workflow
plan, err := client.GeneratePlan(
"Plan a 3-day trip to Paris with moderate budget",
"travel", // Domain hint (optional)
)
if err != nil {
log.Fatalf("Plan generation failed: %v", err)
}
fmt.Printf("Generated plan %s with %d steps\n", plan.PlanID, len(plan.Steps))
fmt.Printf("Complexity: %d, Parallel: %v\n", plan.Complexity, plan.Parallel)
for i, step := range plan.Steps {
fmt.Printf(" Step %d: %s (%s)\n", i+1, step.Name, step.Type)
fmt.Printf(" Description: %s\n", step.Description)
fmt.Printf(" Agent: %s\n", step.Agent)
}// Execute the generated plan
execResp, err := client.ExecutePlan(plan.PlanID)
if err != nil {
log.Fatalf("Plan execution failed: %v", err)
}
fmt.Printf("Plan Status: %s\n", execResp.Status)
fmt.Printf("Duration: %s\n", execResp.Duration)
if execResp.Status == "completed" {
fmt.Printf("Result:\n%s\n", execResp.Result)
} else if execResp.Status == "failed" {
fmt.Printf("Error: %s\n", execResp.Error)
}// For long-running plans, check status periodically
status, err := client.GetPlanStatus(plan.PlanID)
if err != nil {
log.Fatalf("Failed to get plan status: %v", err)
}
fmt.Printf("Plan Status: %s\n", status.Status)Check if AxonFlow Agent is available:
err := client.HealthCheck()
if err != nil {
log.Printf("AxonFlow Agent is unhealthy: %v", err)
} else {
log.Println("AxonFlow Agent is healthy")
}For applications running in AWS VPC, use the private endpoint for lower latency:
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "https://<your-vpc-endpoint>.example.com:8443", // VPC private endpoint (replace with your deployment URL)
ClientID: "your-client-id",
ClientSecret: "your-secret",
Mode: "production",
})Network Latency Characteristics:
- Public endpoint: Higher latency (internet routing overhead)
- VPC private endpoint: Lower latency (intra-VPC routing)
ExplainDecision and ListDecisions - and the audit and override 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.
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: os.Getenv("AXONFLOW_CLIENT_ID"),
ClientSecret: os.Getenv("AXONFLOW_CLIENT_SECRET"),
UserToken: os.Getenv("AXONFLOW_USER_TOKEN"), // ← the per-user identity
})
// Per call:
exp, err := client.ExplainDecision(ctx, id, axonflow.WithUserToken(usersToken))
// Or, for a process acting on behalf of several people, derive a client bound
// to one person. Unlike the per-call and context forms, this reaches EVERY
// method, including the ones that build their request without a context.
rows, err := client.AsUser(alicesToken).ListDecisions(ctx, axonflow.ListDecisionsOptions{})Setting
UserTokenaffects 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 turnsListConnectors,InstallConnector,GetPlanStatusand policy CRUD into401s rather than merely unscoping a read. That is the correct, fail-closed direction, but it puts this value in the same rotation story asClientSecret.
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, and never reaches telemetry.
"Not found", "not yours" and "you presented nothing" used to arrive as the same
404, and an unscoped list arrived as an ordinary empty page. Both now carry
a cause:
decisions, err := client.ListDecisions(ctx, axonflow.ListDecisionsOptions{})
if rse, ok := axonflow.AsReadScopeError(err); ok && rse.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.
exp, err := client.ExplainDecision(ctx, id)
if rse, ok := axonflow.AsReadScopeError(err); ok && !rse.IdentityMissing() {
// Not among the rows this identity can see. A tenant-wide role
// (admin, owner, policy_admin) reads the whole tenant.
}A valid token can still resolve to nobody. The platform reserves the whole of
@axonflow.localand@axonflow.internalfor shared identities and censuses them to nothing before scoping. A correctly-signed developer token minted atdemo-user@axonflow.local- which isgenerate-jwt.sh's own default - reads zero rows and reportsIdentityMissing, exactly like no token at all. Mint per-user identities at a real domain.
resp, err := client.ProxyLLMCall(...)
if err != nil {
// Network errors, timeouts, or AxonFlow unavailability
log.Printf("Request failed: %v", err)
return
}
if resp.Blocked {
// Policy violation - request blocked by governance rules
log.Printf("Request blocked: %s", resp.BlockReason)
log.Printf("Policies evaluated: %v", resp.PolicyInfo.PoliciesEvaluated)
return
}
if !resp.Success {
// Request succeeded but returned error from downstream
log.Printf("Query failed: %s", resp.Error)
return
}
// Success - use resp.Data or resp.Result
fmt.Printf("Result: %v\n", resp.Data)-
Environment Variables: Never hardcode credentials
import "os" client := axonflow.NewClient(axonflow.AxonFlowConfig{ Endpoint: os.Getenv("AXONFLOW_AGENT_URL"), ClientID: os.Getenv("AXONFLOW_CLIENT_ID"), ClientSecret: os.Getenv("AXONFLOW_CLIENT_SECRET"), })
-
Fail-Open in Production: Use
Mode: "production"to fail-open if AxonFlow is unavailable -
Enable Caching: Reduce latency for repeated queries
-
Enable Retry: Handle transient failures automatically
-
Debug in Development: Use
Debug: trueduring development, disable in production -
Health Checks: Monitor AxonFlow availability with periodic health checks
-
Secure Storage: Store credentials in environment variables or secrets management systems (AWS Secrets Manager, HashiCorp Vault, etc.)
| Field | Type | Default | Description |
|---|---|---|---|
Endpoint |
string |
Required | AxonFlow Agent endpoint URL |
ClientID |
string |
Required | OAuth2 client ID for authentication |
ClientSecret |
string |
Required | OAuth2 client secret for authentication |
Mode |
string |
"production" |
"production" or "sandbox" |
Debug |
bool |
false |
Enable debug logging |
Timeout |
time.Duration |
60s |
Request timeout |
Retry.Enabled |
bool |
true |
Enable retry logic |
Retry.MaxAttempts |
int |
3 |
Maximum retry attempts |
Retry.InitialDelay |
time.Duration |
1s |
Initial retry delay (exponential backoff) |
Cache.Enabled |
bool |
true |
Enable caching |
Cache.TTL |
time.Duration |
60s |
Cache time-to-live |
PEPHandshake |
*PEPHandshake |
nil (no header) |
The PEP capability declaration sent on the planes that read it; see PEP capability handshake |
Note: For self-hosted (localhost) deployments, ClientID and ClientSecret are optional.
A v11.0.0 platform deprecates its legacy policy routes and removes them in
v11.1. Every response from them carries X-AxonFlow-Removed-In: v11.1 and a
successor Link naming /api/v1/typed-policies, plus an RFC 9745
Deprecation header once v11.0.0 is tagged, and the client reports each such
route once through
AxonFlowConfig.OnRouteDeprecation (or logs it once when that is unset). A route
that carries an id is reported once, by its template (for example
GET /api/v1/static-policies/{id}), not once per id.
- Policy simulation.
SimulatePolicies,GetPolicyImpactReportandDetectPolicyConflictsare deprecated. Each keeps answering until v11.1; 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. Policy is authored and tested through the typed policy methods (see Typed policy authoring). - Per-policy overrides. A v11.0.0 platform retires them:
CreatePolicyOverrideandDeletePolicyOverridereturn a*LegacyPolicyWriteFrozenErrorwhose message names the typed policy route.
If go get github.com/getaxonflow/axonflow-sdk-go@latest resolved to v1.17.0, you are on a 2026-01 relic because you used the bare module path. Go's semantic import versioning requires the /v5 suffix for v2+ releases.
# In go.mod, remove the old entry and replace with the v5 path:
# github.com/getaxonflow/axonflow-sdk-go → github.com/getaxonflow/axonflow-sdk-go/v9
go get github.com/getaxonflow/axonflow-sdk-go/v9Update all imports in your .go files to include /v5:
// Before:
import "github.com/getaxonflow/axonflow-sdk-go"
// After:
import "github.com/getaxonflow/axonflow-sdk-go/v9"The API surface between v1 and v5 is substantially different. Check the release notes for v2, v3, v4, and v5 for the breaking changes you'll need to adopt. If you're coming from v1.x directly, the fastest path is usually to re-read the Quick Start section rather than trying to incrementally migrate.
1. Update module path:
# In go.mod, change:
# github.com/getaxonflow/axonflow-sdk-go/v4 → github.com/getaxonflow/axonflow-sdk-go/v9
go get github.com/getaxonflow/axonflow-sdk-go/v9Update all imports in your .go files from /v4 to /v5. No API-surface changes are required for the v4 → v5 bump itself - the major version increment reflects a policy break in how plan-scoped HITL responses are returned. See the v5.0.0 release notes for the specifics.
1. Update module path:
# In go.mod, change:
# github.com/getaxonflow/axonflow-sdk-go/v3 → github.com/getaxonflow/axonflow-sdk-go/v4
go get github.com/getaxonflow/axonflow-sdk-go/v4@v4.0.0Update all imports in your .go files from v3 to v4.
2. Remove TotalSteps from CreateWorkflowRequest:
// Before (v3):
req := axonflow.CreateWorkflowRequest{
WorkflowName: "my-workflow",
TotalSteps: 5, // Remove this
}
// After (v4):
req := axonflow.CreateWorkflowRequest{
WorkflowName: "my-workflow",
}
// Total steps are auto-computed at terminal state (Platform v4.5.0+)3. Specify Operation for MCPCheckInput if you relied on the "query" default:
// Before (v3): defaulted to "query"
resp, _ := client.MCPCheckInput(ctx, req)
// After (v4): defaults to "execute" - pass explicitly if needed
req.Operation = "query"
resp, _ := client.MCPCheckInput(ctx, req)If you're using older authentication methods (LicenseKey or API keys), migrate to OAuth2 client credentials:
Before (v2.x):
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
LicenseKey: os.Getenv("AXONFLOW_LICENSE_KEY"),
})After (v3.x):
client := axonflow.NewClient(axonflow.AxonFlowConfig{
Endpoint: "http://localhost:8080",
ClientID: os.Getenv("AXONFLOW_CLIENT_ID"),
ClientSecret: os.Getenv("AXONFLOW_CLIENT_SECRET"),
})How to get credentials:
- Contact AxonFlow support at hello@getaxonflow.com
- Credentials are provided as part of your AxonFlow subscription
- Store credentials securely in environment variables or secrets management systems
Self-hosted users: No credentials required for localhost endpoints.
Complete working examples for all features are available in the examples folder.
// PII Detection - Automatically detect sensitive data
result, _ := client.GetPolicyApprovedContext("user", "SSN: 123-45-6789", nil, nil)
// result.RequiresRedaction = true (SSN detected)
// SQL Injection Detection - Block malicious queries
result, _ := client.GetPolicyApprovedContext("user", "SELECT * FROM users; DROP TABLE users;", nil, nil)
// result.Approved = false, result.BlockReason = "SQL injection detected"
// Static Policies - List and manage built-in policies
policies, _ := client.ListPolicies()
// Returns: [{Name: "pii-detection", Enabled: true}, {Name: "sql-injection", Enabled: true}, ...]
// Dynamic Policies - Create runtime policies
err := client.CreateDynamicPolicy(axonflow.DynamicPolicyRequest{
Name: "block-competitor-queries",
Conditions: `{"contains": ["competitor", "pricing"]}`,
Action: "block",
})
// MCP Connectors - Query external data sources
resp, _ := client.QueryConnector("user-token", "postgres-db", "SELECT name, email FROM customers", nil)
// resp.Data contains query results with PII automatically redacted
// Multi-Agent Planning - Orchestrate complex workflows
plan, _ := client.GeneratePlan("Research AI governance regulations and summarize", "legal")
result, _ := client.ExecutePlan(plan.PlanID)
// Audit Logging - Track all LLM interactions
logs, _ := client.ListAuditLogs(axonflow.AuditLogFilter{UserID: "user-123", Limit: 100})
// Execution Replay - Debug past executions
executions, _ := client.ListExecutions(axonflow.ExecutionFilter{Status: "failed"})These features require an AxonFlow Enterprise license:
// Code Governance - Automated PR reviews with AI
prResult, _ := client.ReviewPullRequest(axonflow.PRReviewRequest{
RepoOwner: "your-org",
RepoName: "your-repo",
PRNumber: 123,
CheckTypes: []string{"security", "style", "performance"},
})
// Cost Controls - Budget management for LLM usage
budget, _ := client.GetBudget("team-engineering")
// Returns: {Limit: 1000.00, Used: 234.56, Remaining: 765.44}
// MCP Policy Enforcement - Automatic PII redaction in connector responses
resp, _ := client.QueryConnector("user", "postgres", "SELECT * FROM customers", nil)
// resp.PolicyInfo.Redacted = true
// resp.PolicyInfo.RedactedFields = ["ssn", "credit_card"]For enterprise features, contact sales@getaxonflow.com.
- Documentation: https://docs.getaxonflow.com
- Issues: https://github.com/getaxonflow/axonflow-sdk-go/issues
- Email: hello@getaxonflow.com
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.
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 TelemetryEnabled 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).
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.
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.
Each heartbeat also reports two things about the AxonFlow platform the SDK is configured to talk to: which build it is running (edition: community or enterprise) and the platform's own deployment mode (platform_deployment_mode: for example community, in-vpc-enterprise, community-saas). Together with the licence tier these say what kind of deployment an adoption figure describes; none of the three is derivable from the others, because the Community SaaS fleet runs the enterprise build against the community-saas schema.
What is and is not collected:
- Collected: the two coarse strings only, exactly as the platform reported them.
- Not collected: your hostname, your endpoint URL, your organisation's name, your environment variables, or any other configuration. The SDK reads nothing from your machine to produce either value.
Both are read from the platform's own /health response - the same response the heartbeat already fetches for the platform version and licence tier, and an endpoint that returns these fields to any caller without authentication. No additional network request is made.
These are adoption-analytics signals, not entitlement ones, on exactly the same terms as the licence tier above: whoever operates your configured endpoint controls both values, the SDK relays them unchanged and verifies nothing, and they must never gate entitlement, unlock a feature, or enter any authorization or billing decision.
Both fields are omitted entirely whenever the platform did not report them - it is unreachable, returns an error or an unparseable body, or is a version older than 10.4.0 that does not serve these members at all. That last case will be the common one for some time. An absent field means "not known"; it is never defaulted, so it can never be read as community.
One naming caution for anyone reading raw payloads: the heartbeat's own
deployment_modefield is a different dimension - a coarse topology (self_hosted/community_saas/unknown) that this SDK derives from the endpoint URL you configured. The platform's own mode travels asplatform_deployment_mode.
AXONFLOW_TELEMETRY=off suppresses both fields along with the rest of the heartbeat.
If you are building a framework integration on top of this SDK - a LangChain or LangGraph wrapper, a LiteLLM callback, your own in-house adapter - 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 axonflow "github.com/getaxonflow/axonflow-sdk-go/v9"
func init() {
axonflow.RegisterAdapter("langchain")
}Call it before your first API call. 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. The SDK's own NewLangGraphAdapter registers adapter:langgraph from its constructor, which means simply using the adapter is enough — no telemetry code in your application:
client := axonflow.NewClient(cfg)
adapter := axonflow.NewLangGraphAdapter(client, "my-workflow") // declares itself
_, err := adapter.StartWorkflow(ctx, nil, "") // first request: ping carries adapter:langgraphA name registered after the first request rides the next heartbeat rather than that one.
The name is added to the features array of the heartbeat that already fires, as adapter:langchain. It adds no network request - there is no second ping, no second endpoint and no new configuration surface, and calling RegisterAdapter does not itself send anything. It is safe to call from any goroutine and from an init(); repeat registrations of the same name collapse to one entry.
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, and it would be recorded as if it were real. A name that is empty after trimming is ignored.
- The
featuresarray carries at most 32 entries, none longer than 128 bytes. These mirror the receiver's own bounds so an oversized array is shaped here rather than silently at ingest.
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 - so an adapter this SDK build predates still shows up as "something we do not recognise is in use" instead of vanishing at the client.
AXONFLOW_TELEMETRY=off suppresses this 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.
MIT