Skip to content

Latest commit

 

History

241 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AxonFlow SDK for Go

Go Reference Go Report Card License: MIT

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

⚠️ Use the /v9 import path

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.

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

go get github.com/getaxonflow/axonflow-sdk-go/v9

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

Basic Usage (OAuth2 Client Credentials)

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)
}

Advanced Configuration

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,
    },
})

Self-Hosted Mode (No License Required)

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

Sandbox Mode (Local Testing)

// 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 tagged stream="sandbox" server-side so dev/test usage is distinguishable from production heartbeat.

AuthZEN Authorization (v10.3.0+)

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 one thing to know before you call it

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.

Several preconditions of one operation

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"}},
    },
})

Obligations

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)
    }
}

An attribute you could not resolve

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.

What is evaluable today

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.

Types are generated, not written

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_types

CI 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.

PEP capability handshake (v10.4.0+)

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-Handshake

The 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_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), where v10 allowed it with a redact_pii obligation, 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. NewPEPHandshake applies the platform's own rules and returns a *PEPHandshakeError naming the member at fault (Pointer is /pep_id, /audience or /capabilities), instead of the first governed call coming back 400. A PEPHandshake built by hand is checked the same way on every call, which then fails before anything is sent.

v11.0.0 platform

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) carry Engine, SubjectType and PolicyBundle, and a decision adds PolicyIdentities, PolicyPacks and DocumentVersion. A v11.0.0 platform fills them. LegacyValidators is filled only where a checksum validator acted, so it is empty on ProxyLLMCall by 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 *LegacyPolicyWriteFrozenError that names the typed policy route.
  • Route deprecation. AxonFlowConfig.OnRouteDeprecation reports, 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, Decide under 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 ActiveTypedPolicy does not read that as nothing active (see Typed policy authoring).

Runnable programs, in this order: examples/pep_handshake, then examples/typed_policies.

Typed policy authoring (v11.0.0+)

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. PublishTypedPolicy and ActivateTypedPolicy report which ones a document omits as TemplateOmissions, 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 *TypedPolicyRefusal with the HTTP Status, 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; a 401 is the client's usual error. 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.
  • The document is the authoring model itself, a map[string]any rather than Go types, so a field the policy vocabulary gains is authorable without an SDK release. ValidateTypedPolicy answers identically on every edition; the edition's boundary is applied when you publish.
  • ActiveTypedPolicy returns (nil, nil) only for the platform's nothing_active. Any other 404 is a *TypedPolicyRefusal with status 404. A v11.0.0 platform answers a document store it cannot read with 503 storage_unavailable, which is a *TypedPolicyRefusal too (getaxonflow/axonflow-enterprise#4255).

Features

✅ Retry Logic with Exponential Backoff

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(...)

✅ In-Memory Caching with TTL

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)

✅ Fail-Open Strategy (Production Mode)

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 warning

LLM Interceptors (OpenAI & Anthropic)

Wrap your LLM clients with automatic AxonFlow governance using the interceptors package:

OpenAI Interceptor

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)
    }
}

Anthropic Interceptor

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!"),
    },
})

Interface-Based Wrapping

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)

MCP Connector Marketplace

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

List Available 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)
}

Install a Connector

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 a Connector

// 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)
}

Production Connectors (November 2025)

AxonFlow now supports 7 production-ready connectors:

Salesforce CRM Connector

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)

Snowflake Data Warehouse Connector

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)

Slack Connector

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)

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.2.0)

Exfiltration Detection

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=5242880

Dynamic Policy Evaluation

Enable 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=true

Multi-Agent Planning (MAP)

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

Generate a Plan

// 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 a Plan

// 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)
}

Check Plan Status

// 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)

Health Check

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")
}

VPC Private Endpoint (Low-Latency)

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)

Reading decisions: who is asking decides what comes back

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 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, GetPlanStatus 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.

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.

Telling the three misses apart

"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.local and @axonflow.internal for shared identities and censuses them to nothing before scoping. A correctly-signed developer token minted at demo-user@axonflow.local - 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.

Error Handling

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)

Production Best Practices

  1. 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"),
    })
  2. Fail-Open in Production: Use Mode: "production" to fail-open if AxonFlow is unavailable

  3. Enable Caching: Reduce latency for repeated queries

  4. Enable Retry: Handle transient failures automatically

  5. Debug in Development: Use Debug: true during development, disable in production

  6. Health Checks: Monitor AxonFlow availability with periodic health checks

  7. Secure Storage: Store credentials in environment variables or secrets management systems (AWS Secrets Manager, HashiCorp Vault, etc.)

Configuration Reference

AxonFlowConfig

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.

Migration Guide

v11.0.0 deprecations

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, GetPolicyImpactReport and DetectPolicyConflicts are 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: CreatePolicyOverride and DeletePolicyOverride return a *LegacyPolicyWriteFrozenError whose message names the typed policy route.

Migrating from v1.x (bare import path) to v5

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/v9

Update 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.

Migrating from v4 to v5

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/v9

Update 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.

Migrating from v3 to v4

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.0

Update 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)

Migrating to OAuth2 Client Credentials

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:

  1. Contact AxonFlow support at hello@getaxonflow.com
  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.

Examples

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

Community Features

// 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"})

Enterprise Features

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.

Support

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.

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 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).

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 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_mode field 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 as platform_deployment_mode.

AXONFLOW_TELEMETRY=off suppresses both fields along with the rest of the heartbeat.

Declaring a framework adapter (RegisterAdapter)

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:langgraph

A 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 features array 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.

License

MIT

About

Official Go SDK for AxonFlow — runtime control, MCP policy enforcement, approvals, and audit trails for production AI

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages