What should I trace in a sales email agent?
Rius traces a sales email agent best as one root span per run, a generation for each model call, a span for each tool and step, and a session and user on every span. Each run then reads as one tree in the trace list. You see which prospect lookup, draft or send step was slow, failed or cost the most. Rius computes the cost from the model and token counts you record.
How it works
A trace is one run of your agent, and it has exactly one root span. For a sales email agent, one run means one unit of work, such as drafting and sending a single outreach email, or one reply in a thread. The root’s status and duration become the run’s status and latency, so a failed send fails the run.
Under the root, nest spans by kind:
- Root, kind
AGENT: the whole run. Unnamed, it is calledinvoke_agentfollowed by the agent’s name. - Retrieval, kind
RETRIEVER: looking up the prospect, company notes or past threads. Pass the data source id, such as your CRM or knowledge base, so the span is named after what it searched. - Generations, kind
LLM: every model call, for example the first draft, a rewrite for tone and a subject line pass. A generation records the model, provider and token usage, and it is what Rius counts as an LLM call. - Tools, kind
TOOL: actions such as sending the email, logging to the CRM or checking a suppression list. A tool span carries the tool’s name, and a tool call id joins it back to the generation that asked for it. - Steps, kind
CHAIN: the default kind, for anything else, such as personalizing a template or running a compliance check on a draft.
Two identifiers sit on every span in scope:
- Session:
session.idgroups the traces of one conversation or job. Use your outreach thread id or campaign job id, so each reply in a thread is its own trace and the thread is one session. Without a session id, each trace is its own single-trace session. - User:
user.idis the person the request served. For an internal sales agent that is usually the rep who started the run, so use an opaque id rather than an email. A span with no user is anonymous.
If you also set service.name, Rius groups the traces under that agent name.
Set it up
- Install and initialize the SDK as shown on Tracing your code and the quickstart. Because the draft below traces its Claude call as a manual generation, pass
instruments=[]torius.init, or theanthropicintegration records the same call and it is counted twice. Then wrap the pieces of your agent in the shapes below. - Mark the run as the root, each tool as a tool span, and each model call as a generation. Scope the session and user around the run:
import rius
from rius import SpanKind
@rius.observe(kind=SpanKind.AGENT)
def run_outreach(prospect_id: str) -> str:
notes = lookup_prospect(prospect_id)
draft = write_draft(notes)
check_compliance(draft)
send_email(prospect_id, draft)
return draft
@rius.observe(kind=SpanKind.TOOL)
def send_email(prospect_id: str, body: str) -> str:
...
@rius.observe # a CHAIN step, named after the function
def check_compliance(draft: str) -> bool:
...
def write_draft(notes: str) -> str:
messages = [{"role": "user", "content": f"Write a short outreach email. Notes: {notes}"}]
with rius.start_as_current_generation(
model="claude-haiku-4-5-20251001",
provider="anthropic",
) as gen:
gen.set_input(messages)
response = claude.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=400,
messages=messages,
)
text = response.content[0].text
gen.set_output(text)
gen.set_usage(
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
)
return text
with rius.user(rep_id):
with rius.session(thread_id):
run_outreach(prospect_id)- Open the prospect lookup with
rius.start_as_current_spanandkind=SpanKind.RETRIEVERplus adata_source_id, so it appears as a retrieval span under the root. - Pass
provider="anthropic"for every Claude call. Record token usage and never a cost value, because Rius works out cost itself. - Scope the session and user around each run, not once for the whole process. A server that handles many reps or threads would otherwise merge them. There is no process-wide default for the user, by design.
- Prompts, drafts and tool arguments count as content. Setting
RIUS_CAPTURE_CONTENTto false strips content at export, while names, timings, kinds, models and token counts still flow.
What you get
- One tree per run. The waterfall shows the lookup, each model call, the compliance check and the send in order, and highlights the path from the root to a failing span.
- Cost per call. Each generation gets a list-price estimate in US dollars from its model and token counts, so you can see whether drafting or rewriting costs more.
- Clear failures. An exception in the send tool gives that span an error status and the exception class as
error.type, and the run reads as failed. - Threads and reps. Sessions show every reply in an outreach thread together, and the Users page shows traces, failures and cost for each rep.
For how these traces fit into a wider monitoring routine, see Monitor AI agents in production.
When something else fits
- If your agent is a single model call with no tools, a generation under a root span is all you need, so skip the extra step spans.
- If one process serves many reps at once, scope the user and session per request instead of setting anything process-wide.