Skip to main content
The Simforge Go SDK captures your AI function calls to automatically generate evaluations. Re-run your prompts with different models, parameters, and inputs to iterate faster.

Installation

go get github.com/Project-White-Rabbit/simforge-go

Quick Start

package main

import (
	"context"
	"os"
	"time"

	simforge "github.com/Project-White-Rabbit/simforge-go"
)

func main() {
	client := simforge.NewClient(os.Getenv("SIMFORGE_API_KEY"))
	ctx := context.Background()

	client.Span(ctx, "my-service", func(ctx context.Context) (any, error) {
		return map[string]any{"result": "..."}, nil
	})

	client.FlushTraces(5 * time.Second)
}
Need an API key? Get one from the Simforge 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):
Modify existing Go code to add Simforge tracing.
Do NOT browse or web search. Use ONLY the API described below.

Simforge Go SDK (authoritative excerpt):
- Install: `go get github.com/Project-White-Rabbit/simforge-go`
- Init:
  import simforge "github.com/Project-White-Rabbit/simforge-go"
  client := simforge.NewClient(os.Getenv("SIMFORGE_API_KEY"))
- Start/End style (PREFERRED for existing functions):
  func myFunc(ctx context.Context, arg1 string) (Result, error) {
      ctx, span := client.Start(ctx, "<trace_function_key>", "<SpanName>", simforge.WithType("function"))
      defer span.End()
      span.SetInput(arg1)
      result, err := doWork(ctx, arg1)
      if err != nil {
          span.SetError(err)
          return Result{}, err
      }
      span.SetOutput(result)
      return result, nil
  }
- Closure style (for inline code):
  result, err := client.Span(ctx, "<trace_function_key>", func(ctx context.Context) (any, error) {
      return doWork(ctx), nil
  }, simforge.WithName("<SpanName>"), simforge.WithType("function"), simforge.WithInput(args...))
- Fluent API:
  fn := client.GetFunction("<trace_function_key>")
  ctx, span := fn.Start(ctx, "<SpanName>", simforge.WithType("function"))
  defer span.End()
- Span types: "llm", "agent", "function", "guardrail", "handoff", "custom"
- Always pass ctx from Start or Span callback into nested calls for parent-child linking.
- Always call client.FlushTraces(5 * time.Second) before program exit.

Task:
1) Ensure the simforge-go module is added (`go get github.com/Project-White-Rabbit/simforge-go`).
2) Ensure a client is initialized with the API key.
3) Read the codebase and identify ALL AI workflows (LLM calls, agent runs, AI-driven decisions).
4) Present me with a numbered list of workflows you found. For each, describe:
   - What it does
   - Why it's worth instrumenting — what visibility tracing gives you into each step
5) After I choose which workflow(s) to instrument:
   - Add Start/End at the top of each function (preferred) or wrap in Span closure
   - Use SetInput/SetOutput/SetError to capture data
   - Instrument intermediate steps (not just the final output) so each trace has enough context to diagnose issues
   - Pass `ctx` through for nested span support
6) Do not change function signature, behavior, or return value. Minimal diff.

Output:
- First: your numbered list of workflows with why each is worth instrumenting
- After my selection: minimal diffs for go.mod and the instrumented functions

Basic Configuration

// Default (production URL)
client := simforge.NewClient(apiKey)

// Custom service URL
client := simforge.NewClient(apiKey, simforge.WithServiceURL("http://localhost:4000"))

// Disable tracing (functions still execute, but no spans are sent)
client := simforge.NewClient(apiKey, simforge.WithEnabled(false))
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.

Tracing

Using Start/End to Instrument Existing Functions

The recommended way to add tracing to existing functions without restructuring them:
func processOrder(ctx context.Context, orderID string, amount float64) (Order, error) {
	ctx, span := client.Start(ctx, "order-processing", "ProcessOrder",
		simforge.WithType("function"))
	defer span.End()
	span.SetInput(orderID, amount)

	order, err := doWork(ctx, orderID, amount)
	if err != nil {
		span.SetError(err)
		return Order{}, err
	}
	span.SetOutput(order)
	return order, nil
}
  • 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.
// pkg/tracing/tracing.go — single source of truth
package tracing

import (
	"os"
	simforge "github.com/Project-White-Rabbit/simforge-go"
)

var Client = simforge.NewClient(os.Getenv("SIMFORGE_API_KEY"))
var OrderService = Client.GetFunction("order-processing")
// services/process_order.go
package services

import (
	"context"
	"myapp/pkg/tracing"
	simforge "github.com/Project-White-Rabbit/simforge-go"
)

func ProcessOrder(ctx context.Context, orderID string) (Order, error) {
	ctx, span := tracing.OrderService.Start(ctx, "ProcessOrder", simforge.WithType("function"))
	defer span.End()
	span.SetInput(orderID)

	order, err := doWork(ctx, orderID)
	if err != nil {
		span.SetError(err)
		return Order{}, err
	}
	span.SetOutput(order)
	return order, nil
}
// main.go
package main

import (
	"context"
	"myapp/pkg/tracing"
	"myapp/services"
	"time"
)

func main() {
	defer tracing.Client.FlushTraces(5 * time.Second)
	ctx := context.Background()
	services.ProcessOrder(ctx, "order-123")
}
Spans from different files are automatically linked as parent-child when you pass ctx between instrumented functions.

Using client.Span (Closure Style)

Wrap inline code in a closure. Output is captured automatically from the return value. Use WithInput to record inputs:
result, err := client.Span(ctx, "order-processing", func(ctx context.Context) (any, error) {
	return map[string]any{"order_id": "123", "total": 100}, nil
}, simforge.WithName("ProcessOrder"), simforge.WithType("function"),
   simforge.WithInput("order-123", 100))

Using GetFunction for a Static Trace Key

Bind a trace function key once, then create multiple spans without repeating it:
orderService := client.GetFunction("order-processing")

result, err := orderService.Span(ctx, func(ctx context.Context) (any, error) {
	return map[string]any{"order_id": "123"}, nil
}, simforge.WithName("ProcessOrder"), simforge.WithType("function"))

result, err = orderService.Span(ctx, func(ctx context.Context) (any, error) {
	return map[string]any{"valid": true}, nil
}, simforge.WithName("ValidateOrder"), simforge.WithType("guardrail"))

Automatic Nesting

Spans nest automatically when you pass ctx from the outer span callback:
client.Span(ctx, "pipeline", func(ctx context.Context) (any, error) {
	// This span becomes a child of the outer span
	client.Span(ctx, "pipeline", func(ctx context.Context) (any, error) {
		// This span becomes a grandchild
		return client.Span(ctx, "pipeline", func(ctx context.Context) (any, error) {
			return map[string]any{"safe": true}, nil
		}, simforge.WithName("CheckFraud"), simforge.WithType("guardrail"))
	}, simforge.WithName("Validate"), simforge.WithType("guardrail"))

	return map[string]any{"status": "done"}, nil
}, simforge.WithName("Process"), simforge.WithType("agent"))

Span Options

Parameters:
  • traceFunctionKey (required): Groups spans under a function key in Simforge
  • WithName(name) (optional): Display name. Defaults to the trace function key
  • WithType(spanType) (optional): Span type. Defaults to "custom"
  • 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
Span Types:
// Valid span types
"llm"       // LLM calls
"agent"     // Agent workflows
"function"  // Function calls
"guardrail" // Safety checks
"handoff"   // Human handoffs
"custom"    // Default
Examples:
// LLM call
client.Span(ctx, "chat-service", func(ctx context.Context) (any, error) {
	return callOpenAI(prompt), nil
}, simforge.WithName("ChatCompletion"), simforge.WithType("llm"))

// Safety check
client.Span(ctx, "safety-service", func(ctx context.Context) (any, error) {
	return map[string]any{"safe": true}, nil
}, simforge.WithName("ContentFilter"), simforge.WithType("guardrail"))

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:
ctx, span := client.Start(ctx, "order-processing", "ProcessOrder",
	simforge.WithType("function"))
defer span.End()

requestID := generateRequestID()
span.AddContext(map[string]any{"request_id": requestID, "user_id": "u-123"})
Each AddContext call pushes the entire map as one entry. Multiple calls accumulate entries:
span.AddContext(map[string]any{"user_id": "u-123"})
span.AddContext(map[string]any{"request_id": "req-789"})
// Result: contexts: [{"user_id": "u-123"}, {"request_id": "req-789"}]

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:
func classifyText(ctx context.Context, text string) (string, error) {
	ctx, span := client.Start(ctx, "classification", "ClassifyText",
		simforge.WithType("llm"))
	defer span.End()
	span.SetInput(text)

	prompt := fmt.Sprintf("Classify the following text: %s", text)
	span.SetPrompt(prompt)
	result, err := llm.Complete(prompt)
	if err != nil {
		span.SetError(err)
		return "", err
	}

	span.SetOutput(result)
	return result, nil
}
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 simforge.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:
result, err := client.Span(ctx, "order-processing", func(ctx context.Context) (any, error) {
	trace := simforge.GetCurrentTrace(ctx)

	// Set session ID (stored as database column, filterable in dashboard)
	trace.SetSessionID("session-123")

	// Set trace metadata (stored in raw trace data)
	trace.SetMetadata(map[string]any{"region": "us-west-2", "environment": "production"})

	// Add context entries (stored as key-value pairs, accumulates across calls)
	trace.AddContext(map[string]any{"workflow": "checkout-flow", "batch_id": "batch-2024-01"})

	return map[string]any{"status": "completed"}, nil
}, simforge.WithName("ProcessOrder"), simforge.WithType("function"))
  • SetSessionID(id) — Groups traces by user session. Stored as a database column for efficient filtering.
  • SetMetadata(map) — Arbitrary key-value metadata on the trace. Merges with existing metadata.
  • AddContext(map) — Key-value context entries. Accumulates across multiple calls.

Error Handling

Errors are captured in the span and returned to the caller:
result, err := client.Span(ctx, "risky-service", func(ctx context.Context) (any, error) {
	return nil, errors.New("something went wrong")
}, simforge.WithName("RiskyOperation"), simforge.WithType("function"))

// err contains "something went wrong"
// The span records the error message and timing

Flushing Traces

client.FlushTraces(5 * time.Second) // Wait up to 5s for pending spans
Go does not have an automatic atexit hook. You must call FlushTraces before your program exits to ensure all pending spans are sent.

Native Functions

Coming soon.

Sim Runners

Coming soon.