> This page is for NeMo Relay, version v0.10.0.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.0) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.10.0: https://docs.nvidia.com/nemo/relay/v0.10.0/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt
> - v0.6.0: https://docs.nvidia.com/nemo/relay/v0.6.0/llms.txt
> - v0.5.0: https://docs.nvidia.com/nemo/relay/v0.5.0/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# Instrument a Tool Call

> Instrument managed tool calls with NeMo Relay scopes, middleware, events, and subscribers.

Use this guide when you have an application tool callback and want NeMo Relay to emit lifecycle events, apply middleware, and preserve the active agent scope around the call.

## What You Build

You will wrap one existing tool callback with the managed tool execution API. The result is a tool call that:

* Receives JSON-compatible arguments.
* Runs request intercepts, guardrails, execution intercepts, and response guardrails.
* Emits tool start and tool end events.
* Keeps the tool span attached to the current agent or request scope.
* Returns a canonical `ToolExecutionResult` containing the application-owned
  result and an optional opaque annotation.

## Before You Start

Complete one binding Quick Start guide first:

* [Python Quick Start](/getting-started/quick-start/python)
* [Node.js Quick Start](/getting-started/quick-start/nodejs)
* [Rust Quick Start](/getting-started/quick-start/rust)

Create a scope for the active request or agent run before adding tool instrumentation. If you have not done that yet, start with [Adding Scopes and Marks](/instrument-applications/adding-scopes-and-marks).

The tool arguments, result, and optional annotation must be JSON-compatible. If
your framework passes clients, sockets, streams, callbacks, or other opaque
objects, use [Handle Non-Serializable Data](/integrate-into-frameworks/non-serializable-data)
before you instrument the callback.

## Integration Pattern

Follow this sequence to keep framework work attached to the expected runtime context.

1. Identify the stable tool boundary in your application.
2. Create or inherit a scope for the current agent run, request, or workflow.
3. Register a temporary subscriber while validating the integration.
4. Replace the direct callback invocation with the managed tool execute helper.
5. Pass the active scope handle when the binding supports it.
6. Check that the application result is unchanged and lifecycle events are emitted.

## Minimal Example

The examples below wrap a `search` callback and print emitted events.

#### Python

```python
import asyncio
from typing import TypedDict

import nemo_relay

class SearchArgs(TypedDict):
    query: str

def log_event(event: nemo_relay.Event) -> None:
    print(f"{event.kind} {event.name}")

async def search(args: SearchArgs) -> nemo_relay.ToolExecutionResult[dict[str, object]]:
    return nemo_relay.ToolExecutionResult(
        {
            "query": args["query"],
            "hits": [{"title": "NeMo Relay"}],
        }
    )

async def main() -> None:
    nemo_relay.subscribers.register("instrumentation-check", log_event)

    try:
        with nemo_relay.scope.scope("agent-run", nemo_relay.ScopeType.Agent) as handle:
            result = await nemo_relay.tools.execute(
                "search",
                {"query": "runtime instrumentation"},
                search,
                handle=handle,
            )
            print(result.result)
    finally:
        nemo_relay.subscribers.flush()
        nemo_relay.subscribers.deregister("instrumentation-check")

asyncio.run(main())
```

#### Node.js

```js
const {
  ScopeType,
  deregisterSubscriber,
  flushSubscribers,
  registerSubscriber,
  toolCallExecute,
  withScope,
} = require("nemo-relay-node");

async function main() {
  registerSubscriber("instrumentation-check", (event) => {
    console.log(`${event.kind} ${event.name}`);
  });

  try {
    await withScope("agent-run", ScopeType.Agent, async (handle) => {
      const result = await toolCallExecute(
        "search",
        { query: "runtime instrumentation" },
        async (args) => ({
          result: {
            query: args.query,
            hits: [{ title: "NeMo Relay" }],
          },
        }),
        handle,
        null,
        null,
        null,
      );

      console.log(result.result);
    });
  } finally {
    await flushSubscribers();
    await new Promise((resolve) => setImmediate(resolve));
    deregisterSubscriber("instrumentation-check");
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

#### Rust

```rust
use nemo_relay::api::scope::{
    self, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeType,
};
use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber};
use nemo_relay::api::tool::{tool_call_execute, ToolCallExecuteParams, ToolExecutionResult};
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    register_subscriber(
        "instrumentation-check",
        Arc::new(|event| {
            println!("{} {}", event.kind(), event.name());
        }),
    )?;

    let handle = scope::push_scope(
        PushScopeParams::builder()
            .name("agent-run")
            .scope_type(ScopeType::Agent)
            .attributes(ScopeAttributes::empty())
            .data(json!({"example": "instrument-tool"}))
            .build(),
    )?;

    let result = tool_call_execute(
        ToolCallExecuteParams::builder()
            .name("search")
            .args(json!({"query": "runtime instrumentation"}))
            .func(Arc::new(|args| {
                Box::pin(async move {
                    Ok(ToolExecutionResult::new(json!({
                        "query": args["query"],
                        "hits": [{"title": "NeMo Relay"}]
                    })))
                })
            }))
            .parent(handle.clone())
            .build(),
    )
    .await?;

    println!("{}", result.result);

    scope::pop_scope(PopScopeParams::builder().handle_uuid(&handle.uuid).build())?;
    flush_subscribers()?;
    let _ = deregister_subscriber("instrumentation-check")?;
    Ok(())
}
```

## Python: Return Tool Results to the Model

In a Python manual chat loop, send only the managed tool result's business
payload to the model:

```python
import json

tool_result = await nemo_relay.tools.execute(name, args, tool_callback)
messages.append(
    {
        "role": "tool",
        "content": json.dumps(tool_result.result),
        "tool_call_id": tool_call["id"],
    }
)
```

Keep `tool_result.annotation` in the Python application for its own metadata
and observability use. Do not use `str(tool_result)` for the tool message: it
serializes the Python wrapper and can expose annotation data instead of the
tool's business result.

## Validate the Integration

Check both behavior and instrumentation:

* The `ToolExecutionResult.result` field matches what the application returned
  before the wrapper was added.
* The subscriber prints an agent or request scope event.
* The subscriber prints tool start and tool end events for `search`.

Native subscriber delivery is asynchronous. Await `flushSubscribers()` and
then one event-loop turn before validating printed output.

* Tool start input contains the request arguments after request intercepts and
  sanitize-request guardrails.
* Tool end output contains the tool result after response guardrails.

Managed callbacks must return `ToolExecutionResult`; raw JSON callbacks are
rejected. Use the optional `annotation` field for opaque adjacent metadata that
must travel with the result without changing the business payload. Refer to
[Tool Execution Intercept Outcomes](/reference/tool-execution-intercept-outcomes)
for annotation and sanitizer semantics.

If only the business result appears, the callback ran but instrumentation did not run. Confirm that the call goes through `tools.execute`, `toolCallExecute`, or `tool_call_execute`.

## Production Checklist

Use this checklist before running the pattern in production traffic.

* Keep tool names stable. Subscribers and downstream exporters use names for filtering and dashboards.
* Keep tool arguments, results, and annotations JSON-compatible.
* Register temporary debugging subscribers only in development or test environments.
* Pass the parent scope handle when the tool is part of a larger agent, request, or workflow.
* Pass the provider or harness tool-call ID when the application already has a
  stable identifier for the invocation.
* Use middleware names that describe ownership, such as `search.redact_args` or `retrieval.timeout`.

## Common Issues

Check these symptoms first when the workflow does not behave as expected.

* **No events appear**: The application is still calling the tool directly.
* **The tool appears outside the agent scope**: Pass the current scope handle into the managed execute helper.
* **The call fails before execution**: A conditional-execution guardrail rejected the request.
* **Subscribers see different data than the tool receives**: Sanitize guardrails change event payloads, while request intercepts change the real arguments.
* **Python: The model receives the result wrapper or annotation metadata**:
  Send `json.dumps(tool_result.result)` in the tool message, not
  `str(tool_result)`.

## Next Steps

Use these links to continue from this workflow into the next related task.

* Add model-provider instrumentation with [Instrument an LLM Call](/instrument-applications/instrument-llm-call).
* Add policy or transformation with [Add Middleware](/instrument-applications/advanced-guide).
* Export events with [Observability](/configure-plugins/observability/about).
* Use [Code Examples](/instrument-applications/code-examples) for manual lifecycle, streaming, scope, and partial middleware API examples.