Skip to Content

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 called invoke_agent followed 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.id groups 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.id is 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

  1. 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=[] to rius.init, or the anthropic integration records the same call and it is counted twice. Then wrap the pieces of your agent in the shapes below.
  2. 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)
  1. Open the prospect lookup with rius.start_as_current_span and kind=SpanKind.RETRIEVER plus a data_source_id, so it appears as a retrieval span under the root.
  2. Pass provider="anthropic" for every Claude call. Record token usage and never a cost value, because Rius works out cost itself.
  3. 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.
  4. Prompts, drafts and tool arguments count as content. Setting RIUS_CAPTURE_CONTENT to 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.

Still stuck? Get help

Last updated on