<!--
SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Adding Scopes and Marks

Use this guide when you want NeMo Flow to identify one agent run, request, workflow, or operation before you instrument individual tool and LLM calls.

## What You Build

You will create an active scope, emit named mark events inside that scope, and validate that subscribers can observe the lifecycle. The result is a small trace boundary that later tool calls, LLM calls, middleware, and exporters can attach to.

Scopes give emitted work ownership. Marks record point-in-time checkpoints inside that ownership boundary.

## Before You Start

Complete one binding Quick Start guide first:

- [Python Quick Start](../getting-started/python/index.md)
- [Node.js Quick Start](../getting-started/nodejs.md)
- [Rust Quick Start](../getting-started/rust.md)

You should know which application boundary should own the trace. Common scope boundaries include:

- One HTTP request
- One agent run
- One workflow step
- One background job
- One tenant-isolated experiment

## Integration Pattern

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

1. Choose the scope boundary that owns the work.
2. Register a temporary subscriber while validating instrumentation.
3. Push or enter the scope before tool and LLM work begins.
4. Emit marks for important checkpoints that are not full nested calls.
5. Close the scope when the request, agent run, or workflow ends.
6. Confirm that the subscriber sees scope start, mark, and scope end events.

## Minimal Example

The examples below create one `agent-run` scope and emit two marks.

::::{tab-set}
:sync-group: language

:::{tab-item} Python
:sync: python

```python
import nemo_flow


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


nemo_flow.subscribers.register("scope-check", log_event)

try:
    with nemo_flow.scope.scope(
        "agent-run",
        nemo_flow.ScopeType.Agent,
        input={"request_id": "req-123"},
    ) as handle:
        nemo_flow.scope.event("planning-started", handle=handle, data={"step": 1})
        nemo_flow.scope.event("planning-finished", handle=handle, data={"step": 2})
finally:
    nemo_flow.subscribers.deregister("scope-check")
```
:::

:::{tab-item} Node.js
:sync: node

```js
const {
  ScopeType,
  deregisterSubscriber,
  event,
  registerSubscriber,
  withScope,
} = require("nemo-flow-node");

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

  try {
    await withScope(
      "agent-run",
      ScopeType.Agent,
      async (handle) => {
        event("planning-started", handle, { step: 1 }, null);
        event("planning-finished", handle, { step: 2 }, null);
      },
      null,
      null,
      null,
      null,
      { request_id: "req-123" },
    );
  } finally {
    deregisterSubscriber("scope-check");
  }
}

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

:::{tab-item} Rust
:sync: rust

```rust
use nemo_flow::api::scope::{
    self, EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeType,
};
use nemo_flow::api::subscriber::{deregister_subscriber, register_subscriber};
use serde_json::json;
use std::sync::Arc;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    register_subscriber(
        "scope-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())
            .input(json!({"request_id": "req-123"}))
            .build(),
    )?;

    scope::event(
        EmitMarkEventParams::builder()
            .name("planning-started")
            .parent(&handle)
            .data(json!({"step": 1}))
            .build(),
    )?;
    scope::event(
        EmitMarkEventParams::builder()
            .name("planning-finished")
            .parent(&handle)
            .data(json!({"step": 2}))
            .build(),
    )?;

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

::::

## When to Add Marks

Use marks for checkpoints that should appear in the event stream but do not wrap a callback:

- Request accepted
- Plan created
- Memory loaded
- Routing decision made
- Final answer assembled
- Retry scheduled

Use a nested scope instead of a mark when the work has a meaningful start and end boundary, child work, or a duration that matters.

## Validate the Integration

Check that the subscriber prints:

- One scope start event for `agent-run`
- One mark event for `planning-started`
- One mark event for `planning-finished`
- One scope end event for `agent-run`

If marks appear outside the intended trace, pass the active scope handle explicitly or make sure the mark is emitted while the scope is active.

## Production Checklist

Use this checklist before running the pattern in production traffic.

- Keep scope names stable enough for filtering and dashboards.
- Use marks for business checkpoints, not verbose debug logging.
- Include only JSON-compatible `data`, `metadata`, `input`, and `output` payloads.
- Do not attach sensitive payloads unless sanitize guardrails or exporter filters will remove them.
- Propagate the active scope stack when work crosses thread or worker boundaries.

## Next Steps

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

- Add tool instrumentation with [Instrument a Tool Call](instrument-tool-call.md).
- Add model-provider instrumentation with [Instrument an LLM Call](instrument-llm-call.md).
- Use [Code Examples](code-examples.md#scope-and-context-helpers) for explicit scope-stack propagation helpers.
