Spans Deep Dive
You’ll learn about the four span types, their lifecycle, how nesting works, and how to attach rich data to each span.
Span Types
Every span has a kind that describes what type of operation it represents.
llm_call — LLM API Calls
Captures calls to language model APIs with provider, model, tokens, and cost data.
with opswald.span('generate-reply', kind='llm_call', provider='openai', model='gpt-4o') as s: s.set_input({'messages': [{'role': 'user', 'content': 'Hello'}]}) result = openai.chat.completions.create( model='gpt-4o', messages=[{'role': 'user', 'content': 'Hello'}] ) s.set_output({'response': result.choices[0].message.content}) s.set_tokens(input_tokens=10, output_tokens=25)await span('generate-reply', { kind: 'llm_call', provider: 'openai', model: 'gpt-4o' }, async (s) => { s.setInput({ messages: [{ role: 'user', content: 'Hello' }] }); const result = await openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Hello' }] }); s.setOutput({ response: result.choices[0].message.content }); s.setTokens(10, 25);});tool_call — Tool Invocations
Captures when your agent calls external tools or functions.
with opswald.span('search-knowledge-base', kind='tool_call') as s: s.set_input({'query': 'refund policy', 'tool': 'vector_search'}) results = vector_db.search('refund policy') s.set_output({'results': results, 'count': len(results)})await span('search-knowledge-base', { kind: 'tool_call' }, async (s) => { s.setInput({ query: 'refund policy', tool: 'vector_search' }); const results = await vectorDb.search('refund policy'); s.setOutput({ results, count: results.length });});error — Error Events
Captures exceptions and error states. Useful for debugging failures.
with opswald.span('risky-operation', kind='error') as s: try: result = perform_operation() s.set_output({'result': result}) except Exception as e: s.set_output({'error': str(e), 'type': type(e).__name__}) raiseawait span('risky-operation', { kind: 'error' }, async (s) => { try { const result = await performOperation(); s.setOutput({ result }); } catch (e) { s.setOutput({ error: e.message, type: e.constructor.name }); throw e; }});custom — Custom Operations
Track any operation that doesn’t fit the other categories.
with opswald.span('format-response', kind='custom') as s: s.set_input({'raw_response': raw}) formatted = format_for_user(raw) s.set_output({'formatted': formatted})Span Lifecycle
Every span follows this lifecycle:
- Created — Span is opened with a name and kind
- Active — Input is set, your code runs, output is recorded
- Closed — Span ends, duration is calculated, data is sent to Opswald
When using context managers or callbacks, the lifecycle is automatic. The span closes when you exit the block, even if an exception occurs.
Nesting Spans
Spans nest naturally inside each other:
with opswald.trace('agent-run') as t: with opswald.span('process-request', kind='custom') as parent: with opswald.span('call-llm', kind='llm_call', provider='openai', model='gpt-4o') as child: # child is nested inside parent ... with opswald.span('call-tool', kind='tool_call') as sibling: # sibling is at the same level as child ...This produces the tree:
process-request (custom)├── call-llm (llm_call)└── call-tool (tool_call)Attaching Data
Every span supports:
| Method | Purpose |
|---|---|
set_input(data) | What went into this step |
set_output(data) | What came out of this step |
set_tokens(input, output) | Token counts for LLM calls |
All data is JSON serializable and fully searchable in the dashboard.
Next Steps
- Decision Graph — See how spans form decision trees
- Replay Sessions — Step through spans one by one
- Python SDK — Full API reference