Skip to main content
The Bitfab Go SDK captures your AI function calls to automatically generate evaluations. Re-run your prompts with different models, parameters, and inputs to iterate faster.
Framework-native adapters (LangGraph, OpenAI Agents, BAML, Claude Agent SDK) are not yet available for Go. See Frameworks overview for current coverage. Instrument Go code manually via client.Span or client.Start.

Installation

Quick Start

Need an API key? Get one from the Bitfab dashboard or see the API Keys guide for detailed setup instructions.
Copy this prompt into your coding agent (tested with Cursor and Claude Code using Sonnet 4.5):

Basic Configuration

BITFAB_CAPTURE_ENABLED decides when your code does not. Set it to 0 to turn capture off or 1 to turn it on, and the switch applies to every client that was built without one. A value passed in code always wins over the variable.
Missing API key doesn’t crash. If the API key is missing, empty, or whitespace-only, the SDK automatically disables tracing and logs a warning. All instrumented functions still execute normally - no spans are sent, no errors are thrown. You don’t need any conditional logic around the API key.
When no key is passed (empty argument and no WithAPIKey), the SDK reads BITFAB_API_KEY from the environment. Unlike the JS and Python SDKs, Go resolves the key eagerly in NewClient: clients are constructed explicitly (normally in main, after env has loaded), so there is no import-time construction-before-env trap to defer around. For standalone programs where a run that emits no traces should be a hard failure, use WithStrict:

Tracing

Trace the whole workflow, not just its entrypoint. A single Start/End around the outer function records one input and one output for everything inside it, which leaves per-step diagnosis and prompt iteration with nothing to work on. Spans exist only where you create them: nesting is automatic, but only between spans that exist.
Give a step its own span when any of these is true:
  • It calls a model. Always. This is the span you iterate on, compare across experiments, and attach graders to.
  • It reads external mutable state (DB query, HTTP GET, object storage, vector search, cache). These are the spans you will want to mock on replay.
  • It writes external state (DB write, queue publish, email, charge, file write). Mark these to mock on replay so a replayed trace does not repeat the side effect.
  • It transforms the model output (parsing, validation, ranking, formatting), so a quality regression points at the model or at your post-processing.
  • It retries or loops, one span per attempt or iteration, so a trace shows how many attempts it really took.
Skip trivial in-memory helpers, per-item work inside a large loop (wrap the loop or the batch), and internals a framework integration already captures.
Go replay defaults to MockMarked: closure-style child spans tagged with WithMockOnReplay(true) reuse their recorded output. Untagged child code still runs, so mark or override unsafe side effects before replaying a workflow. Manual Start/End spans cannot skip caller-owned code; use closure-style Client.Span for mockable boundaries.
Worked examples, replay-mocking decisions, and common pitfalls: Instrumentation.

Replay Historical Traces

For experimental execution on your own GitHub Actions, see Cloud replay. Dispatch cloud flags with RunCloudReplayCLI before initializing your application’s registry. Client.Replay fetches historical root inputs, decodes them into your current Go function’s parameter types, runs the items concurrently, and creates an experiment in Bitfab. Pass the same top-level function production uses. Replay accepts typed functions with an optional leading context.Context and an optional final error return.
Replay wraps each invocation in a root span under the supplied trace function key. If the production function also uses Start/End, those spans nest beneath the replay root and retain the same key. The replay context is passed as the function’s leading context.Context, so continue passing the returned context through nested work. ReplayOptions supports:
  • Limit - recent traces to replay (default 5, maximum 5000). Omitted from the request when TraceIDs is set.
  • TraceIDs - explicit historical trace IDs (maximum 100). Passed alongside DatasetID or DatasetIDs they pin which members of that selection replay, and the server rejects any ID none of those datasets contains.
  • Name, ExperimentGroupID, and DatasetID / DatasetIDs - experiment organization and attribution. DatasetIDs runs against several datasets at once, replaying the union of their traces. ExperimentGroupName names the group from ExperimentGroupID when this replay creates it or the group has no name yet (a group that already has a name keeps it), and requires ExperimentGroupID.
  • GraderIDs - graders attached directly to this experiment.
  • MaxConcurrency - bounded worker count (default 10).
  • CodeChangeDescription and CodeChangeFiles - explicit code-change context. When files are omitted, Go checks BITFAB_CODE_CHANGE_PATH and then captures the rename-aware diff of uncommitted working-tree changes against HEAD, or, when the working tree is clean, against the branch it was cut from (the parent branch in a stack, otherwise the default branch) (set BITFAB_CODE_CHANGE_BASE to diff against the merge-base with another ref instead). DisableCodeChangeCapture opts one replay out.
  • Mock and MockOverrides - recorded-output strategy and selective substitutions for closure-style child spans.
  • DBBranch - non-nil enables a trace-time database branch; MinCU, MaxCU, and WarmupSQL tune it.
  • AdaptInputs - reshape historical positional inputs before type decoding.
  • OnItemStart and OnItemFinish - lifecycle callbacks with running totals. The finish callback normally includes the new server trace ID after a per-item flush; the final result is the fallback when that delivery cannot be confirmed. Callback panics are isolated from the replay.
When a signature changes, adapt the recorded values into the new positional shape:

Mock child spans

Mark an expensive or unsafe closure-style child span once in production code:
The default MockMarked strategy reuses that occurrence’s historical output during replay. Repeated same-name calls are matched in call order; the SDK uses strictly increasing microsecond timestamps so rapid sibling calls remain distinguishable. WithMockOutputType[T]() decodes recorded JSON into the concrete Go type the caller expects; it is unnecessary for primitive or deliberately dynamic any/map outputs. MockNone runs real child code unless an override matches it; MockAll substitutes every recorded child occurrence. A selected occurrence that is missing fails the item closed. Overrides run before the base strategy, with per-call overrides before client-registered overrides:
GetOriginalOutput fetches lazily and is memoized per item. Mocked uploads record whether the output came from recorded history or an override.

Replay against trace-time database state

Every Go root trace now carries a no-I/O wall-clock snapshot reference. To request a historical branch, pass a non-nil DBBranch and read it inside the replayed function:
Branch resolution runs inside the bounded replay workers, so MaxConcurrency also bounds live branches. A requested branch that cannot be resolved fails that item instead of silently using the live database. The SDK reports whether DatabaseURL() was obtained, includes provisioning timings on the trace and replay item, and releases every branch after the item; the connection string is excluded from branch JSON and formatting.

Bound replay keys

Plain Go function values carry no tracing metadata, so explicit-key replay remains valid. When code wants a declared-key guard, bind the callable:
Passing a bound target to Client.Replay under a different key fails before Bitfab selects any historical traces. An item failure does not stop the batch. ReplayItem.TraceError retains an error or panic from the replayed function, ReplayItem.ReplayError retains setup/adaptation failures, and ReplayItem.Error is the compatible message. A replay whose delivery Bitfab cannot confirm in time is logged as a warning, [bitfab] replay <original trace id>#<attempt> could not confirm all spans reached Bitfab within <N>s. The run continues; Bitfab will show this replay as capture incomplete., and is never an error: the run completes, the item keeps a nil Error, and --fail-on-error does not trip on it. A process child forwards the warning to the command’s stderr as a [replay] span delivery problem for ... line. A finalization failure, or an HTTP failure from the status check itself, returns *bitfab.ReplayError, whose Items, ExperimentID, ExperimentURL, and Cause preserve the partial run. Before finalizing, replay flushes the normal OpenTelemetry pipeline. Every carrier keeps a private delivery reference that never goes on the wire; successful ingestion acknowledges those references and returns each server TraceID. Replay finishes immediately when all carriers are acknowledged, and polls final trace status plus expected persisted span counts only when delivery is ambiguous. The final items include both the original trace lineage (OriginalTraceID, OriginalSpanID) and the new server TraceID, plus original duration/token/model measurements, replay duration, and replay token usage. When BITFAB_REPLAY_RESULT_PATH is set, the SDK writes the same structured result JSON there automatically. This is how the Bitfab plugin captures results without parsing stdout.

Replay registry command

Go has no installed bitfab-replay command. Your project compiles its registry into its own small command, conventionally cmd/registry/main.go. The command builds a registry of the functions production calls and passes its arguments to bitfab.RunReplayCLI. RunReplayCLI owns every replay flag, the progress events, the summary, and the result JSON. See the Go reference for the full flag list.
A plain Go function carries no trace function key, so every registration names one with TraceFunctionKey. Register the function production calls, with the same client the app uses. For cloud replay, route --cloud arguments to RunCloudReplayCLI before building the registry.

When the traced function needs the app’s dependencies

Most agents need tools, clients, and config that the app builds at startup. The registry command gets them the way production does, by calling the app’s own code.
  • Put the command where production builds the call. That is the module that builds the tools, clients, and config and then calls the traced function. Its go.mod already requires all of them. Run go run ./cmd/registry from that module’s directory. When the traced function lives in a lower-level library module, the command still goes in the app module that calls it. Never add the app’s requirements to the library.
  • Build the function the way production builds it. Call the constructor that production calls at startup, and register the method it returns. Replay runs that constructor, so give it the settings it requires, such as URLs and API keys, even for tools whose calls are mocked. Never write a smaller toolset or a simplified agent for replay. The replay would test that copy instead of your code.
  • Rebuild the arguments the recording cannot keep. A live value passed as an argument, such as a tool with its function, a client, or a callback, is recorded as data only. Rebuild these arguments in the registration’s Options.AdaptInputs with the same code production uses. Return each value as the exact type of the parameter it fills. A value of any other type is converted through JSON, which drops function fields. Never replace them with an empty list, a stub, or nil. When production builds them inline, move that code into an exported function that the app and the registry command both call.
  • Tag function fields json:"-". The replay result reports each item’s inputs as JSON, and Go cannot encode a function value. A struct field holding a function without that tag fails the whole command after the replay finishes. The tag belongs on the struct the traced function takes, which may live in the library.
  • Know which tool calls run for real. Under MockMarked, a call inside a span created with bitfab.WithMockOnReplay(true) returns its recorded output and never runs. Add bitfab.WithMockOutputType[T]() so the recorded value decodes into the tool’s return type. MockAll does the same for every recorded call. Every other tool call runs for real during replay. Put every call with an unsafe side-effect, such as sending an email or charging a card, behind a marked span before you replay. See Mark child spans.
  • Keep experiment settings out of the registry. Set a model swap or any other experiment setting through an environment variable the app already reads. Never add a second registration with the setting written into it.
  • Check the wiring before a real run. --dry-run resolves every item’s inputs without calling the function. Then replay one trace with --limit 1. With your code unchanged, the replay should call the same tools the original called.
A library runs the agent and the app passes in the tools. The traced method is Runner.Run in the agentcore library, which takes agentcore.RunOptions. The app builds the runner and the tools. The registry command lives in the app module and rebuilds the tools for each replayed call.
The adapter decodes the recorded options into the library’s own RunOptions, keeps the recorded fields, and puts the live tools in place. Returning the library’s type is what keeps the tools’ Execute functions intact.

Using Start/End to Instrument Existing Functions

The recommended way to add tracing to existing functions without restructuring them:
Calling processOrder records one trace with two spans:
Instrumenting only processOrder would record the same work as a single node. The nesting comes from passing the ctx that Start returns into the next step: hand a step the original ctx and its span becomes a separate root.
  • Start returns an updated context.Context (for nested span propagation) and an ActiveSpan
  • defer span.End() ensures the span is always completed and sent
  • SetInput / SetOutput / SetError record data on the span
  • End is idempotent - calling it multiple times is safe

Multi-File Projects

For projects with instrumented functions spread across multiple files, create a dedicated package that initializes the client and exposes a function handle. Import it wherever you need to instrument.
Spans from different files are automatically linked as parent-child when you pass ctx between instrumented functions.

Using GetFunction (Closure Style)

Make the trace function once, then wrap as many closures as you need off it without repeating the key. Output is captured automatically from the return value. Use WithInput to record inputs:

Using client.Span with an Inline Key

The same call with the key passed per span. Equally supported, and the shorter choice when a package has exactly one span:

Automatic Nesting

Spans nest automatically when you pass ctx from the outer span callback:
For reusable helpers that should appear only inside an existing trace, pass bitfab.WithCaptureWhen(bitfab.CaptureWhenNested):
With no parent span in ctx, helper runs normally without creating a trace. Pass the child context from Span or Start to capture it as a nested span.

Span Options

Parameters:
  • traceFunctionKey (required): Groups spans under a function key in Bitfab
  • WithName(name) (optional): Display name. Defaults to the trace function key
  • WithType(spanType) (optional): Span type. Defaults to "custom". A label only, used to organize and filter spans in the dashboard; it does not change how the span is traced, replayed, or evaluated
  • WithFunctionName(name) (optional): Override the function name in span data
  • WithInput(args...) (optional, closure style only): Record input data. A single arg is stored directly; multiple args as a slice
  • WithCaptureWhen(CaptureWhenNested) (optional): Capture only with an active parent span; otherwise run untraced. Defaults to CaptureWhenAlways; unknown values warn once and use that default
Span Types:
Examples:

Span Context

Use span.AddContext() on an ActiveSpan (Start/End style) to attach contextual key-value pairs at runtime - useful when context depends on computed values:
Each AddContext call pushes the entire map as one entry. Multiple calls accumulate entries:

Span Prompt

Use span.SetPrompt() on an ActiveSpan (Start/End style) to set the prompt string on the current span. This is stored in span_data.prompt and is useful for capturing the exact prompt text sent to an LLM:
The prompt is metadata only. It records the prompt text for display and reference in the dashboard; it does not send the prompt to any model or change what the span executes. The last SetPrompt call wins — it overwrites any previously set prompt on the span. Calling SetPrompt outside a span context is a no-op (it never crashes).

Trace Context

Use bitfab.GetCurrentTrace(ctx) to set context that applies to the entire trace (all spans within a single execution). This is useful for grouping traces by session or attaching trace-level metadata:
  • SetSessionID(id) - Groups traces by user session. Stored as a database column for efficient filtering.
  • SetName(name) - The trace’s title in Bitfab, and a field you can search and filter on. Use it for the case, ticket, or record the run is about. Unset, the trace is titled by its trace function key.
  • SetMetadata(map) - Searchable exact-match metadata on the trace. Merges with existing metadata.
  • AddContext(map) - Key-value context entries. Accumulates across multiple calls.
  • TraceID() - Returns the canonical Bitfab trace ID used for persisted lookups.
Caller metadata accepts up to 50 keys. Keys are 1-40 characters using letters, numbers, _, ., :, or -; values are strings, numbers, or booleans up to 500 characters after conversion to text. Metadata filters match exact key/value pairs case-insensitively. Search it from the SDK with client.Traces.Search. Up to 10 supplied pairs must all match; pass result.NextCursor back as Cursor for another page.
The same method searches by the name SetName recorded. Name matches the name’s words in order, so Name set to "Order ABC" also finds "Order ABC 123". NameContains matches any part of the name, including inside a word, and needs at least 3 characters. Both are case-insensitive, and every filter you supply has to match.
Set ExperimentID to list only the replay traces one experiment produced. Every entry also carries ExperimentID (nil for a captured trace), DurationMs (the trace’s wall-clock time, nil when it has no recorded end time), and Tokens (a *TokenUsage with Input, Output, Cached, and Total summed across the trace’s spans, nil when none were recorded). That is enough to compare two experiments on time and token spend without opening each trace:
At least one of CallerMetadata, Name, NameContains, or ExperimentID is required, so a search can never return the organization’s traces unfiltered. Reach for caller metadata when you are looking up a run by an identifier your own code owns, for the name when you are hunting for a run by the title a person reads, and for the experiment when you want what one replay run produced.

Read One Persisted Span

Fetch one span without loading the full trace. Repeated name matches default to the last span.
Both IDs are canonical Bitfab IDs; ingestion source IDs are not accepted. Use bitfab.SpanOccurrenceAt(index) for a zero-based occurrence. A missing trace or span returns nil, nil. Look a span up by type instead of name, or by both, to reach a position such as the first model call. Set ID alone, or Name, Type, or both; any other combination returns bitfab: provide id, or name and/or type.
To read many spans of one trace at once, in the order they ran, use client.Spans.List. It needs no filter; SpanType and SpanName narrow it, and Fields picks which span fields each one carries:
A field you did not list in Fields, or one with no content, is nil on each span. When you only have a span ID, use GetSpan. It takes Bitfab’s span ID or the span ID the SDK sent, and the span it returns carries its TraceID. A span outside your API key’s organization returns nil, nil.

Dropping a Trace

Use bitfab.GetCurrentTrace(ctx).Drop() to discard the in-flight trace. Once flagged, spans that complete afterward are not uploaded at all, and the flag rides out on the completion payload, so when the trace completes the server scrubs any payloads that already raced out (the trace, its external trace, and sibling spans), deletes the archived S3 objects, and marks it dropped instead of completed, keeping only a skeleton audit row. Use it to discard runs you never want stored (health checks, test traffic) or a run you know carries sensitive data.
  • Safe to call outside a span (GetCurrentTrace returns nil, and Drop() is a no-op on a nil receiver), and never panics into your application.

Error Handling

Errors are captured in the span and returned to the caller:
Each error is classified by source. Errors returned by your function are recorded with error_source: "code". SDK-internal errors are recorded with source: "sdk". Both appear in the span’s errors array in the Bitfab dashboard.

Flushing Traces

FlushTraces returns false when an export failed or the deadline expired. Go does not have an automatic atexit hook. You must call FlushTraces before your program exits to ensure all pending spans are sent.

Closing a Client

Close flushes and permanently shuts down the client’s background delivery worker. It is idempotent, and returns false when an export failed or the deadline expired. A closed client no longer records spans, so prefer it over FlushTraces for the final flush in main.

OpenTelemetry Transport

Span delivery runs on OpenTelemetry. This is an internal transport detail: Span, Start/End, and GetFunction still produce Bitfab spans with Bitfab trace IDs, and no OTel type appears in the Bitfab API. The SDK owns a private tracer provider per client and never touches your application’s global OTel provider. Each client lazily starts one bounded batching worker on its first span, so a client that never traces starts no worker. Replay carriers use this same worker. A private carrier reference follows each queued OTel span through batching and trimming but is excluded from the request body. A successful ingestion response returns the server trace IDs and acknowledges the references in that request. Replay polls the status endpoint only as a fallback when the flush leaves delivery unconfirmed. A single span may use up to 7,800,000 carrier bytes when its dedicated request gzips below the 3,000,000-byte wire target and remains below the 8,000,000-byte decompressed ingress limit. The carrier is the payload re-escaped into the OTLP attribute. If it does not compress enough, compression is unavailable, or it exceeds the raw ceiling, the SDK replaces its largest fields with <unserializable: too_large_N_bytes> placeholders until it fits the 2,800,000-byte fallback budget. The trim is recorded on the span’s errors so the trace is flagged as incomplete. Adopting OpenTelemetry raises the SDK’s minimum Go version to 1.25. The SDK links only the OpenTelemetry trace SDK, so protobuf and gRPC stay out of your build. See OpenTelemetry Transport Architecture for the full design.

Datasets

client.Datasets creates, reads, and modifies datasets programmatically, with the same operations your coding agent reaches through the Bitfab MCP tools. A dataset is a named bucket of traces under one trace function. Experiments replay against it and its graders score its members.
Save is an upsert on the dataset name within its trace function, so re-running a program does not accumulate duplicates. ClearDescription: true explicitly clears its description. An omitted description preserves the existing one. Membership and grader calls accept up to 100 ids and report ids they skipped rather than failing the whole call. RemoveTraces only drops membership. Traces are never deleted. RerunGraders blocks until the run finishes. An errored run returns a *GraderRerunError. When Timeout (90 seconds by default) passes first, it returns a *GraderRerunTimeoutError, and the run keeps going on Bitfab, so read it later with GetGraderRerun. Call it in a goroutine to do other work meanwhile, and cancel it through ctx. See the reference for every method and result type.

Assertions and verdicts

Read a trace’s assertions before judging its replay. client.Traces saves what should happen. client.Verdicts records whether it happened.
Use Categories.Get, List, and Delete to manage categories. A category groups assertions and scenarios. Deleting a category detaches assignments while preserving assertions and verdicts. Two justifications cite the spans supporting the assertion and its category assignment. Assertion reads expose one approval decision covering both; only a person in Bitfab can set it. Categories themselves are not approved. Use Verdicts.Skip when a check did not run, or Verdicts.Save with Skip: true and an Annotation when you can say why the verdict was withheld, and Verdicts.Archive to clear a previous verdict. Verdicts.Abstain, or Save with Verdict: bitfab.VerdictAbstained, records that the evidence did not show what the assertion checks. Every agent verdict scores one approved assertion, so Save, SaveAll, and Skip return an error without sending anything when AssertionID is empty. Archive may leave it empty to clear an old whole-trace agent verdict. SaveHuman writes a human-authored verdict, which needs an AssertionID too. Verdicts have no approval step, so agent and human verdicts both count as soon as they are written. SaveAssertionsAll, SaveAll, and SaveHumanAll support batches. client.Graders.GetVerdicts reads the reason and failure diagnostic recorded by automated graders. Graders.SaveVerdict and Graders.SaveVerdictAll post verdicts for external graders, which you run yourself and Bitfab never runs. The earlier client.Labels and Graders.*Label* names still work as deprecated aliases for one minor release. See the reference for assertions and targets, verdict methods and result types, and grader verdicts.

Experiments

client.Experiments reads runs back after the fact, for a dashboard or a report outside the replay process. List pages newest first with every filter optional and combinable, Get reads one with its provenance, and GetRollup / GetRollupAll return the pass/fail rollup the dashboard shows, with one row per assertion category. GetRollupAll takes up to 100 ids in one request, so read a month of runs in one call rather than one per run.
Set Metadata on ReplayOptions, or --metadata key=value on the replay command, to tag a run with your own strings. They come back on every read and List matches them by containment. See the reference for every method and type.

Capture a subtree automatically

Run your application with bitfab-instrument and enter a workflow through client.Trace. Calls to first-party functions beneath that root are recorded automatically. Configure individual functions with client.Node. See Go subtree tracing for a complete example.

BAML execution and collectors

The optional github.com/Project-White-Rabbit/bitfab-go/baml module supplies native BAML execution without adding cgo dependencies to ordinary SDK imports.
Call fetches the current prompt, coerces string inputs according to the BAML signature, and executes it locally. Dynamic BAML classes return maps, enums return strings, and unions return their selected value. Results and collectors use the client’s shared tracing transport. Configure WithEnvVars to supply provider credentials. The canonical credential filter forwards only OPENAI_API_KEY. For generated clients, bamladapter.WrapBAML takes the generated NewCollector function, a typed invocation receiving the collector, and an optional collector callback. Pass that collector through the generated WithCollector option. The wrapper exposes Call(ctx, input) and Collector(). Inside an active Bitfab span or node it records rendered prompt, provider, model, input/output token counts, and duration. Collector callbacks cannot change the caller’s result. The native adapter uses BAML 0.226.2 and requires cgo plus a supported native platform.