Frequently Asked Questions
Setup & Configuration
Do I need to call init() before importing LLM SDKs?
Yes. waxell.init() monkey-patches LLM libraries to add auto-instrumentation. If you import openai or anthropic before calling init(), those imports get the un-patched versions and LLM calls won't be captured automatically.
# Correct
import waxell_observe as waxell
waxell.init()
from openai import OpenAI # patched
# Wrong -- OpenAI is imported before init()
from openai import OpenAI # un-patched
import waxell_observe as waxell
waxell.init() # too late for OpenAI
If you can't control import order (e.g., framework code imports LLM SDKs at module level), use the drop-in imports instead:
from waxell_observe.openai import openai # always patched
from waxell_observe.anthropic import anthropic # always patched
Can I call init() multiple times?
Yes. init() is idempotent -- only the first call takes effect. Subsequent calls are no-ops.
How do I disable the SDK in tests or CI?
Set the WAXELL_OBSERVE environment variable to false, 0, or no:
WAXELL_OBSERVE=false pytest
This makes init() skip all initialization. Decorators and convenience functions become no-ops. Your code runs normally with zero overhead.
What's the difference between api_key and api_url?
api_url: Your Waxell control plane URL (e.g.,https://acme.waxell.dev). This is where your dashboard lives.api_key: A secret key starting withwax_sk_that authenticates requests to your control plane.
Both can be set via constructor arguments, environment variables (WAXELL_API_URL, WAXELL_API_KEY), or the CLI config file (~/.waxell/config).
What happens if the SDK can't reach the control plane?
The SDK is designed to never break your agent. If the control plane is unreachable:
init()succeeds (tracing setup failure is logged as a warning)@observeruns your function normally and logs a warning about the failed run- Auto-instrumentation continues to work locally (spans are captured but not exported)
- No exceptions are raised from telemetry failures
Instrumentation
Should I use decorators or the context manager?
Start with decorators. They cover 90% of use cases with less code:
| Situation | Use |
|---|---|
| Single function = one agent run | @observe |
| Tool, retrieval, decision, step functions | @tool, @retrieval, @decision, @step_dec |
| Inline enrichment (scores, tags) | waxell.score(), waxell.tag() |
| Multi-step orchestration spanning many functions | WaxellContext |
| Batch processing with many runs per loop | WaxellContext |
| Need mid-execution policy checks between steps | WaxellContext |
See the Context Manager page for advanced scenarios.
Why are some decorators named reasoning_dec, retry_dec, step_dec instead of reasoning, retry, step?
The names reasoning, retry, and step collide with the module-level convenience functions (waxell.reason(), waxell.retry(), waxell.step()). To avoid shadowing, the decorators are exported with a _dec suffix:
| Decorator | Convenience function |
|---|---|
@waxell.reasoning_dec() | waxell.reason() |
@waxell.retry_dec() | waxell.retry() |
@waxell.step_dec() | waxell.step() |
The other decorators (@tool, @decision, @retrieval) don't collide because their convenience function equivalents use different names (waxell.retrieve(), waxell.decide()).
Do decorators work on sync functions?
Yes. All decorators (@observe, @tool, @decision, @retrieval, @reasoning_dec, @retry_dec, @step_dec) work with both sync and async functions. The SDK detects the function type and wraps accordingly.
What happens if I use behavior decorators outside an @observe scope?
They're no-ops. Your function executes normally with zero overhead -- no context lookup, no recording, no errors. This means you can decorate functions once and call them from both instrumented and non-instrumented code paths.
Does auto-instrumentation capture everything?
waxell.init() auto-instruments 200+ libraries for LLM calls, vector DB queries, HTTP requests, database calls, and more. It captures:
- Model name, token counts, cost, latency
- Prompt/response previews (if
capture_content=True) - Streaming responses (tokens are aggregated)
It does not capture:
- Business logic decisions (use
@decisionorwaxell.decide()) - Custom tool calls (use
@tool) - Quality scores (use
waxell.score()) - Tags and metadata (use
waxell.tag(),waxell.metadata())
How do I see what libraries were auto-instrumented?
Use waxell.diagnose():
import waxell_observe as waxell
waxell.init()
info = waxell.diagnose()
print(info["active_instrumentors"]) # ["openai", "anthropic", ...]
print(info["detected_libraries"]) # {"openai": "1.52.0", ...}
print(info["sdk_version"]) # "0.0.40"
Can I exclude specific libraries from auto-instrumentation?
Yes, use the exclude parameter on init():
waxell.init(exclude=["litellm", "mcp"])
Or via environment variable:
WAXELL_EXCLUDE=litellm,mcp python my_agent.py
For infrastructure libraries (HTTP, DB, caches), use infra_exclude:
waxell.init(infra_exclude=["celery", "grpc"])
Governance & Policies
What's the difference between enforce_policy and mid_execution_governance?
enforce_policy=True(default): Checks policies once when the run starts. If blocked, raisesPolicyViolationErrorbefore your function executes.mid_execution_governance=True: In addition to the entry check, everyrecord_step()call flushes data to the control plane and re-checks policies. If a policy blocks mid-execution, raisesPolicyViolationErrorfrom inside your function.
Use mid_execution_governance for long-running agents where you want to catch budget overruns or policy violations before the agent finishes.
What is on_policy_block?
A callback that fires when a policy blocks execution. Instead of immediately raising PolicyViolationError, the SDK calls your handler and lets you decide what to do:
@waxell.observe(
agent_name="my-agent",
on_policy_block=waxell.prompt_approval, # Terminal Y/N prompt
)
async def my_agent(query: str):
...
Built-in handlers:
waxell.prompt_approval-- Interactive terminal promptwaxell.auto_approve-- Always proceed (for testing)waxell.auto_deny-- Always block (for testing)
You can write custom handlers for Slack approvals, webhook-based workflows, etc.
Can I check policies manually without using @observe?
Yes, through the client:
from waxell_observe import WaxellObserveClient
client = WaxellObserveClient()
result = await client.check_policy(agent_name="my-agent")
if result.blocked:
print(f"Blocked: {result.reason}")
Or inside a WaxellContext:
async with WaxellContext(agent_name="my-agent") as ctx:
# ... do some work ...
policy = await ctx.check_policy()
if policy.blocked:
return "Agent was blocked by policy"
Data & Privacy
Does the SDK send my prompts and responses to Waxell?
By default, no. Prompt and response content is not included in telemetry. Set capture_content=True on init() to opt in:
waxell.init(capture_content=True)
Without this flag, only metadata is captured: model name, token counts, cost, latency, and timing.
What is Prompt Guard?
Prompt Guard scans LLM inputs for PII (emails, phone numbers, SSNs), leaked credentials (API keys, tokens), and injection attempts before they reach the LLM provider. Enable it with:
waxell.init(
prompt_guard=True, # Client-side regex detection
prompt_guard_server=True, # Server-side ML detection (more accurate)
prompt_guard_action="redact", # "block", "warn", or "redact"
)
Can I use Waxell with on-premises LLM providers?
Yes. The SDK instruments any OpenAI-compatible API, local models (Ollama, vLLM, SGLang), and HuggingFace models. Cost estimation returns 0.0 for local/free providers.
Sessions & Users
How do sessions work?
A session groups related runs together. Pass the same session_id to multiple runs:
@waxell.observe(agent_name="chatbot", session_id="sess_abc123")
async def handle_message(msg: str):
...
# Or dynamically at call time:
await handle_message("Hello", session_id="sess_abc123")
await handle_message("Follow up", session_id="sess_abc123")
Use generate_session_id() to create unique session IDs.
How does user tracking work?
Pass user_id to attribute runs, costs, and usage to specific end users:
@waxell.observe(agent_name="chatbot", user_id="user_456")
async def handle_message(msg: str):
...
This enables per-user analytics, cost attribution, and user-scoped policies in the dashboard.
Performance
What's the overhead of the SDK?
Minimal. The SDK:
- Buffers all telemetry in memory and flushes asynchronously on context exit
- Uses non-blocking HTTP calls for the control plane API
- Auto-instrumentation adds microsecond-level overhead per LLM call
- Convenience functions (
waxell.score(),waxell.tag(), etc.) are simple dict appends
If the control plane is unreachable, telemetry is dropped silently -- no retry loops or blocking waits.
Does the SDK add latency to my LLM calls?
No. Auto-instrumentation wraps LLM SDK methods with timing and metadata capture, but does not add any network calls in the LLM request path. All telemetry is flushed asynchronously after the LLM call completes.
Can I flush telemetry manually for long-running agents?
Yes. Use waxell.flush() (async) or waxell.flush_sync() to flush buffered data without waiting for the context to exit:
@waxell.observe(agent_name="long-runner")
async def long_running_agent():
for batch in data:
process(batch)
await waxell.flush() # Send data to control plane now
Framework Integration
Does Waxell work with LangChain?
Yes. Use the callback handler:
from waxell_observe.integrations.langchain import WaxellLangChainHandler
handler = WaxellLangChainHandler(agent_name="my-chain")
result = chain.invoke(input, config={"callbacks": [handler]})
handler.flush_sync(result={"output": result})
Does Waxell work with CrewAI / LlamaIndex / AutoGen?
Yes. These frameworks are auto-instrumented by waxell.init(). LLM calls, tool invocations, and agent delegations are captured automatically.
Can I use Waxell with multiple LLM providers in the same agent?
Yes. Auto-instrumentation patches all installed provider SDKs. A single @observe run can contain calls to OpenAI, Anthropic, Groq, and others -- all captured in the same trace.