From install to your first trace
Install the SDK, connect a project, wrap your agent. Automatic instrumentation handles the rest, capturing nested calls, arguments, results, and timing into a single trace that streams to the dashboard while the run is still in flight. The whole path is below, including the parts worth getting right on day one.
1. Install the SDK
NOMOO ships a Python client and a TypeScript client. They produce the same trace format, so a mixed-language agent network reads as one system rather than two.
pip install nomoo
npm install @nomoo/sdk
Both packages are dependency-light on purpose. Capture runs in-process, and there is no collector, queue, or database for you to stand up before the first trace.
2. Connect a project
A project is the scope that everything else hangs from: traces, scores, redaction rules, and retention. Call init() once, as early in your process as you can, before the first agent run.
import nomoo nomoo.init( project="support-triage", environment="production", release="v1.7.2", )
import { init } from "@nomoo/sdk"; init({ project: "support-triage", environment: "production", release: "v1.7.2", });
NOMOO_API_KEY in the environment. Pass it explicitly only if your deployment makes that impossible.Set the release, even on day one
The release value is what lets regression detection attribute a change in quality to the deploy that caused it. Without it, a drop in score is a mystery; with it, the alert names a version. Wire it to whatever your pipeline already knows, usually a commit SHA or a tag.
Keep environments separate
Use distinct environment values for production, staging, and local development. Scores and baselines are computed per environment, so mixing them produces a baseline that describes nothing in particular.
3. Instrument your agent
Pass your existing agent through instrument(). Your orchestration, prompts, and control flow stay exactly where they are. Automatic instrumentation follows the agent down into the planner it calls, the tools that planner selects, the HTTP or database work inside those tools, and the model call that composes the answer.
import nomoo from my_agents import support_agent agent = nomoo.instrument(support_agent) result = agent.run("Where is my refund?") # every nested call, argument and result is now traced
import { instrument } from "@nomoo/sdk"; import { supportAgent } from "./agents"; const agent = instrument(supportAgent); const result = await agent.run("Where is my refund?"); // every nested call, argument and result is now traced
Open the project in NOMOO and you will see the run arrive as a tree: the root call at the top, each nested step beneath its parent, with duration and status on every line. Select a span to read the arguments it received and the value it returned.
What you should see first
- A root span named for your agent entry point, with the total duration of the run.
- Nested model and tool spans at the right depth, tagged by kind.
- Arguments and return values on each span, structured rather than stringified.
- A quality score on the trace once the automated review has read it.
4. Add custom spans
Automatic instrumentation covers standard agent, model, tool, and retrieval calls. It does not know which of your own functions matter to your domain. Mark those with a decorator and they join the hierarchy in the right place, with their arguments and return values captured.
import nomoo @nomoo.span(kind="tool") def lookup_order(order_id: str) -> dict: return orders.get(order_id) @nomoo.span(kind="retrieval", name="kb.search") def search_kb(query: str, top_k: int = 5) -> list: return index.query(query, top_k=top_k) # or wrap a block instead of a function with nomoo.span("policy.check", kind="fn") as s: s.set(rule="refund_window") allowed = policy.evaluate(order)
import { span, withSpan } from "@nomoo/sdk"; export const lookupOrder = span( { kind: "tool" }, async (orderId: string) => orders.get(orderId) ); export const searchKb = span( { kind: "retrieval", name: "kb.search" }, async (query: string, topK = 5) => index.query(query, topK) ); // or wrap a block instead of a function await withSpan({ name: "policy.check", kind: "fn" }, async (s) => { s.set({ rule: "refund_window" }); return policy.evaluate(order); });
Choosing a kind
The kind argument is not decoration. It decides which review rules apply to the span and which charts it appears in, so it is worth setting accurately.
| Kind | Use for |
|---|---|
llm | Any call to a model, including planning and composition steps |
tool | Functions the agent chooses to invoke, especially ones with side effects |
retrieval | Lookups against a vector store or search index, so grounding checks apply |
fn | Plain application logic on the path of a run |
Do not instrument everything
A span per helper function produces a trace nobody reads. Mark the steps a reviewer would ask about: decisions, side effects, and anything whose result the agent acts on. The automatic capture already covers the rest.
5. Group runs into sessions
One trace covers one run. A session covers a conversation, which may be several runs in sequence. Grouping them is what lets you review the handoff between turns, which is where multi-turn agents most often lose the thread.
with nomoo.session(id=conversation.id, user=conversation.account_id): for message in conversation.messages: agent.run(message.text) # all runs above share one session in the dashboard
await withSession({ id: conversation.id, user: conversation.accountId }, async () => { for (const message of conversation.messages) { await agent.run(message.text); } }); // all runs above share one session in the dashboard
6. Attach metadata
Metadata is how you cut the data along the lines your business cares about. Anything you attach travels with every span in the run and becomes available as a filter and a chart dimension.
# set once for the process nomoo.init(project="support-triage", metadata={"region": "us-west"}) # or per run, where the values are request-specific with nomoo.context(tenant=account.id, locale="en-US", plan="enterprise"): agent.run(question)
// set once for the process init({ project: "support-triage", metadata: { region: "us-west" } }); // or per run, where the values are request-specific await withContext({ tenant: account.id, locale: "en-US", plan: "enterprise" }, async () => { await agent.run(question); });
Pattern analysis uses these dimensions too. A failure mode that only affects one locale shows up as exactly that, instead of being averaged into a number that looks acceptable.
7. Configure redaction
This is the one piece of configuration worth doing before your first production trace. Redaction rules are applied inside your process, before a span is buffered, so a redacted value is replaced at the source rather than stored and hidden later.
nomoo.init( project="support-triage", redact=[ "email", # any field with this name "customer.address.*", # a path and everything under it nomoo.pattern(r"\b\d{13,19}\b"), # card-like digit runs ], redact_placeholder="[redacted]", )
init({ project: "support-triage", redact: [ "email", // any field with this name "customer.address.*", // a path and everything under it /\b\d{13,19}\b/, // card-like digit runs ], redactPlaceholder: "[redacted]", });
Retention is the companion setting, configured per project in the dashboard. Payload retention controls how long full arguments and results are stored. Aggregate retention controls how long scores and trends live, and it can be longer, so a short payload window does not cost you trend history.
8. Configuration reference
Everything accepted by init(). Defaults are chosen so that a call with only a project name is a reasonable production setup.
| Option | Default | Purpose |
|---|---|---|
project | required | The scope traces, scores, and settings belong to. |
environment | "development" | Separates baselines so staging traffic does not move production scores. |
release | unset | Version tag used to attribute regressions to a deploy. |
api_key | from env | Read from NOMOO_API_KEY unless passed explicitly. |
endpoint | NOMOO cloud | Point at your own deployment when running self-managed. |
metadata | {} | Process-wide dimensions attached to every span. |
redact | [] | Field names, paths, and patterns replaced before buffering. |
sample_rate | 1.0 | Share of runs captured in full, for very high volume services. |
capture_prompts | True | Whether rendered prompts are stored with model spans. |
otel_endpoint | unset | Mirror captured spans to your own OpenTelemetry collector. |
flush_interval | 2.0 | Seconds between asynchronous span deliveries. |
enabled | True | Switch capture off entirely without removing the calls. |
9. Continuous checks
Once runs are scored, the score becomes something a pipeline can enforce. Run an evaluation against a suite of saved cases and fail the build when quality falls below the floor you set, or when it drops by more than you are willing to accept against the deployed release.
name: agent-eval on: [pull_request] jobs: eval: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pip install nomoo - run: nomoo eval --suite support-triage --min-score 8.0 env: NOMOO_API_KEY: ${{ secrets.NOMOO_API_KEY }} - run: nomoo compare --baseline production --fail-on-drop 0.3
--min-scoresets an absolute floor, useful as a release gate from the first week.--fail-on-dropcompares against a named baseline, which catches slow erosion that a fixed floor would miss.- Suites are built from real runs. Promote traces you have already reviewed into the suite rather than inventing test cases.
10. Troubleshooting
No traces are appearing
Check that init() runs before the first agent call and that NOMOO_API_KEY is present in the environment the process actually sees, which in containerized deployments is often not the one you set it in. The SDK logs a single warning when it cannot reach the endpoint rather than raising, so capture failures never take down a run.
Spans are flat with no hierarchy
This happens when agent steps run in separate processes or across a queue boundary. Pass the trace context explicitly at the boundary so the child work attaches to the right parent instead of starting a new root.
Some tool calls are missing
Automatic instrumentation discovers calls the agent makes through its framework. A tool invoked directly by your own code, outside that path, needs a decorator. Adding @nomoo.span(kind="tool") puts it back in the hierarchy.
Arguments are showing as redacted
A redaction rule is broader than intended. Field-name rules match at any depth, so a rule for email also covers customer.contact.email. Use a path when you mean a specific location.
Scores look lower than expected
Open the findings behind the score before changing anything. A low score with grounding findings on a retrieval-heavy agent usually reflects retrieval quality rather than the model, and prompt edits will not move it.
11. Where to go next
With traces arriving and a release tag set, the rest of the platform follows from the record you are now collecting.
Read a trace properly
What every field in a span means, how the inspector is laid out, and how to filter forty thousand runs down to the three that matter.
Turn scores into changes
How quality scoring works, how regressions are attributed to a release, and how prompt and parameter proposals are built from your own runs.
Wire it into your stack
OpenTelemetry export, CI gating, alerting webhooks, and what running inside your own network changes.
Get help with a real agent
Bring the run you cannot explain. We will instrument it with you and read the trace together.