Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quent Tutorial

This tutorial builds a Quent model from basic events through finite-state machines and resources. Every lesson shows the complete YAML model beside the generated instrumentation API that uses it.

Use the Quent Schema Explorer to inspect an Application Event Schema interactively.

How Quent fits together

  1. Model the application. An Application Event Schema defines the entities, events, and attributes that describe the application’s behavior. Semantic modules add reusable constraints and meaning for analysis and user interfaces.
  2. Generate typed libraries. Code generation produces application-specific instrumentation and analysis libraries from the same schema.
  3. Capture events. The application emits events through the generated instrumentation API. The instrumentation library writes those events to the event store. Other event sources can contribute to the same data.
  4. Analyze behavior. An analysis service uses the generated analysis library to interpret the stored events and provide application-specific results.
Tutorial scope

Each example introduces one concept and shows the corresponding generated API.

Use the arrow on the right or the sidebar to begin.

Instrumentation

To use Quent, first instrument your code with an application-specific instrumentation library. Quent generates this library entirely from an Application Event Schema. From here on, this tutorial refers to it simply as a schema. You can define a schema in several ways, including with a YAML-based DSL or programmatically. This tutorial focuses on the YAML-based approach.

Schema

An Application Event Schema describes the event data that an application can emit. Quent uses it to generate an application-specific, typed instrumentation API.

Entities

An entity is anything you want to emit events about. It typically represents a control-flow or data-flow object in your code, such as a task, request, buffer, or worker. It can also represent a function call, a metric source, or another event-producing concept.

Each entity instance has a universally unique identity represented by a UUID, which keeps its events separate from events emitted by other instances. Processes can assign these identities independently without coordinating through a central allocator, shared counter, or global process state.

Entities exist to group related events around the thing they describe.

Events

An event is something that happens to an entity at a particular point in time, such as a task starting or ending.

For each event, the generated instrumentation API provides a named call that application code uses to emit it. Depending on the target language, this call may be exposed as a method or function.

In statically typed target languages, these generated calls are fully type-safe. The compiler checks that each event receives the expected number and types of attributes. This makes it harder for instrumentation changes to accidentally alter event semantics or break downstream analysis.

In this respect, Quent resembles structured logging: each event has a known name and typed data. Quent can also export events through an end-to-end statically typed pipeline. Exporters do not necessarily need to attach runtime type information to each value, which can reduce runtime work and improve performance.

Events exist to record how an entity behaves over time.

Attributes

An attribute is a typed value captured when an event occurs, such as a task name or result code. Each attribute becomes a typed argument to the generated event call.

Attributes exist to record the details needed to interpret an event.

Records

A field is a named, typed value inside a record. A record is a named, reusable group of fields. The generated API represents a record as a struct in Rust and C++. In Python, records are dictionaries with generated TypedDict type hints.

Records exist to keep related values together and avoid repeating the same field definitions.

Event cardinality

Event cardinality defines how often an event can occur for one entity instance. A once event occurs at most once. A multi event may occur repeatedly.

Cardinality exists to distinguish unique events from repeatable events.

Minimal model

Every model declares the YAML format version and a model name. This model has one entity type, Task, with two events. Events are emitted at most once per entity instance unless the model says otherwise.

YAML model

quent: alpha
model: minimal

entities:
  Task:
    events:
      started: {}
      ended: {}

Instrumentation API

The context provides an observer for each entity type. Calling .handle() on an observer creates a handle for a new entity instance and assigns it a fresh UUID. The handle exposes one method per event.

Every context is created with an exporter, which determines where emitted events go. This example uses Noop, an exporter that discards every event. It keeps the example focused on the generated API without creating files or starting another service. Applications replace Noop with an exporter that stores or sends their events.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/minimal.rs"));
}

use instrumentation::{Context, Minimal, Noop, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<Minimal>::try_new(Noop)?;
    let mut task = context.observer::<Task>().handle();

    task.started()?;
    task.ended()?;

    Ok(())
}
Key point

The two events have no declared ordering constraint, so either event may be emitted first.

Check yourself

How often can started be emitted for one Task handle?

What does task represent in the Rust program?

Event data

An event’s attributes describe the data captured when that event occurs. The generated event method receives one typed argument for each attribute, in declaration order.

Data types

The scalar types are:

YAML typeValue
booltrue or false
u8, u16, u32, u64Unsigned integers of the indicated width
i8, i16, i32, i64Signed integers of the indicated width
f32, f64Floating-point numbers of the indicated width
stringText
uuidA universally unique identifier

Types can also be composed or refer to generated types:

YAML typeValue
{ option: T }A value of type T that may be absent
{ list: T }An ordered collection of values of type T
A record nameAn instance of that record
dynamicString-keyed values whose names and types are chosen at runtime
refA reference to any entity instance

Semantic modules add more specific reference forms. These are introduced with targeted references, scoped references, and resources.

YAML model

quent: alpha
model: event_data

entities:
  Task:
    events:
      started:
        attributes:
          enabled: bool
          byte: u8
          short_count: u16
          attempt: u32
          item_count: u64
          small_offset: i8
          short_offset: i16
          offset: i32
          large_offset: i64
      ended:
        attributes:
          ratio: f32
          score: f64
          message: string
          run_id: uuid
          retry_after: { option: u64 }
          tags: { list: string }
          extra: dynamic

Instrumentation API

The generated API maps each YAML type to the corresponding type in the selected programming language. Options, lists, records, and references remain typed. dynamic is the exception: it deliberately accepts values whose names and types are determined at runtime.

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

#[allow(unused, clippy::too_many_arguments)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/event_data.rs"));
}

use instrumentation::{Context, DynamicAttributes, EventData, Noop, Task, Uuid};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<EventData>::try_new(Noop)?;
    let mut task = context.observer::<Task>().handle();

    task.started(true, 1, 2, 3, 4, -1, -2, -3, -4)?;

    let mut extra = DynamicAttributes::new();
    extra.add("worker", "alpha");
    extra.add("queue_depth", 3_u64);

    task.ended(
        0.5,
        0.95,
        "complete".to_owned(),
        Uuid::now_v7(),
        None,
        vec!["batch".to_owned(), "priority".to_owned()],
        extra,
    )?;

    Ok(())
}
Key point

Event attributes produce typed parameters in the generated instrumentation API.

Check yourself

Which declaration permits an attribute value to be absent?

Which declaration represents several ordered values of the same type?

Dynamic attributes

Most event attributes have names and types fixed by the schema. This lets the generated instrumentation API check their use before the application runs.

The dynamic type provides a typed container whose keys and value types are chosen when the event is emitted. It is useful when the available details cannot be known while writing the schema. The generated event method still requires a dynamic attribute container, but the schema does not check the keys or types placed inside it. Prefer regular attributes for stable event data.

YAML model

quent: alpha
model: dynamic_data

entities:
  Task:
    events:
      started:
        attributes:
          details: dynamic
      ended:
        attributes:
          details: dynamic

The schema declares one dynamic container on each event. It does not declare the individual keys that the containers will hold.

Instrumentation API

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/dynamic_data.rs"));
}

use instrumentation::{Context, DynamicAttributes, DynamicData, Noop, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<DynamicData>::try_new(Noop)?;
    let mut task = context.observer::<Task>().handle();

    let mut started_details = DynamicAttributes::new();
    started_details.add("queue", "priority");
    started_details.add("attempt", 2_u64);
    task.started(started_details)?;

    let mut ended_details = DynamicAttributes::new();
    ended_details.add("cached", false);
    ended_details.add("items_processed", 128_u64);
    task.ended(ended_details)?;

    Ok(())
}

The generic add method converts each supported Rust value into its dynamic representation. The application adds a string and an integer to the started event, then a boolean and an integer to the ended event. Each value retains its runtime type; integer suffixes such as _u64 select the intended type. Use DynamicNull for a key with no value: attributes.add("key", instrumentation::DynamicNull). Null values do not retain an intended value type.

Key point

Use dynamic attributes for event details that cannot be defined in advance.

Check yourself

Where are keys such as queue and cached defined?

What is the main tradeoff of using dynamic?

Repeated events

Events are once by default. Set multi: true when one entity instance may emit the same event repeatedly.

YAML model

quent: alpha
model: repeated_events

entities:
  Task:
    events:
      started:
        attributes:
          command: string
      progress:
        multi: true
        attributes:
          items_processed: u64
      ended:
        attributes:
          success: bool

Instrumentation API

The same Task handle emits progress more than once. Repeating started or ended on that handle would return an error.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/repeated_events.rs"));
}

use instrumentation::{Context, Noop, RepeatedEvents, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<RepeatedEvents>::try_new(Noop)?;
    let mut task = context.observer::<Task>().handle();

    task.started("compile".to_owned())?;
    // `started` is a once event, so a second call would return an error.
    // task.started("compile".to_owned())?;
    task.progress(64)?;
    task.progress(128)?;
    task.ended(true)?;

    Ok(())
}
Key point

multi: true permits an event to be emitted more than once for an entity.

Check yourself

Why can progress be called repeatedly?

What happens when started is emitted twice on one handle?

Records

A record groups related fields into a named, reusable type. Event attributes can use a record name wherever they can use a scalar type.

YAML model

quent: alpha
model: records

records:
  # Reused by events on both Task and Batch.
  WorkResult:
    fields:
      success: bool
      items_processed: u64

entities:
  Task:
    events:
      started: {}
      ended:
        attributes:
          result: WorkResult
  Batch:
    events:
      started: {}
      ended:
        attributes:
          result: WorkResult

Instrumentation API

The generated API represents WorkResult as a target-language record type. Both Task and Batch accept that type when emitting ended.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/records.rs"));
}

use instrumentation::{Batch, Context, Noop, Records, Task, WorkResult};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<Records>::try_new(Noop)?;
    let mut task = context.observer::<Task>().handle();
    let mut batch = context.observer::<Batch>().handle();

    task.started()?;
    task.ended(WorkResult {
        success: true,
        items_processed: 128,
    })?;

    batch.started()?;
    batch.ended(WorkResult {
        success: true,
        items_processed: 512,
    })?;

    Ok(())
}
Key point

A named record provides one reusable structure for event attributes.

Check yourself

What type do the Task and Batch ended events expect for result?

Why declare a record instead of repeating its fields?

Entity references

An attribute of type ref identifies another entity instance by its UUID. The reference is type-erased: the generated API knows that it is an entity reference, but does not restrict which entity type it targets. This is useful when any kind of entity is a valid target.

YAML model

quent: alpha
model: untyped_references

entities:
  Worker:
    events:
      started: {}
      ended: {}

  Task:
    events:
      started:
        attributes:
          source: ref
      ended: {}

Instrumentation API

Every entity handle can produce a type-erased reference. Here, as_any_entity_ref produces a reference to the Worker instance for the task’s started event.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/untyped_references.rs"));
}

use instrumentation::{Context, Noop, Task, UntypedReferences, Worker};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<UntypedReferences>::try_new(Noop)?;

    let mut worker = context.observer::<Worker>().handle();
    worker.started()?;

    let mut task = context.observer::<Task>().handle();
    task.started(worker.as_any_entity_ref())?;
    task.ended()?;

    worker.ended()?;

    Ok(())
}

When an attribute must target a particular entity type, the Reference Target semantic module adds that restriction to the generated API.

Key point

A type-erased reference preserves an entity's identity without restricting its entity type.

Check yourself

What does a type-erased entity reference preserve?

Does a bare ref require its target to be a Worker?

Semantic Modules

Semantic modules, or mods, are curated vertical slices of Quent’s stack. A mod adds reusable meaning and constraints to basic schema elements, then lets instrumentation, analysis, and user-interface tooling interpret those elements consistently.

For example, the Finite-State Machine mod describes the allowed order of an entity’s events, while the Resource mod describes capacity and usage. An Application Event Schema can combine mods to express the behavior relevant to that application.

Reference Target

The Reference Target semantic module constrains an entity reference to a specific entity type. A targeted ref links one entity to another entity of a declared type. Here, the task’s started event records which Worker runs it.

YAML model

quent: alpha
model: references

entities:
  Worker:
    events:
      registered: {}

  Task:
    events:
      started:
        attributes:
          worker: { ref: Worker }
      ended: {}

Instrumentation API

An entity handle produces a typed reference with as_entity_ref. The generated started method only accepts a reference targeting Worker.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/references.rs"));
}

use instrumentation::{Context, Noop, References, Task, Worker};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<References>::try_new(Noop)?;

    let mut worker = context.observer::<Worker>().handle();
    worker.registered()?;

    let mut task = context.observer::<Task>().handle();
    // A `Task` reference has the wrong target type, so this would not compile:
    // let other_task = context.observer::<Task>().handle();
    // task.started(other_task.as_entity_ref())?;
    task.started(worker.as_entity_ref())?;
    task.ended()?;

    Ok(())
}
Key point

An entity reference identifies a specific entity and preserves its type.

Check yourself

Which entity type may the worker attribute target?

Does ref: Worker place Task under Worker in a hierarchy?

Reference Tree

The Reference Tree semantic module marks entity references that form parent-child relationships. A scope-ref applies both the reference-target and reference-tree constraints. The parser validates all scoped references together as one tree.

Why is a target type required?

A type-erased ref cannot form part of the Reference Tree. A scope-ref must name its target entity type so Quent can validate the complete tree when it processes the schema.

Without a declared target type, different instances of the same child entity type could refer to different parent entity types at runtime. The generated instrumentation API could then no longer guarantee that the resulting relationships form the tree declared by the schema.

This gives analysis tools a preferred path from one root entity to every related entity and its events. In the model below, a task points to the pipeline that contains it. A user interface can open one pipeline and list its tasks, while analysis can associate each task’s events with that pipeline. Other tools can use the same hierarchy for their own purposes.

YAML model

quent: alpha
model: scoped_references

entities:
  Pipeline:
    events:
      created: {}

  Task:
    events:
      started:
        attributes:
          parent: { scope-ref: Pipeline }
      ended: {}

Instrumentation API

The parent entity’s handle provides the reference. The additional hierarchy meaning belongs to the model and its constraints.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/scoped_references.rs"));
}

use instrumentation::{Context, Noop, Pipeline, ScopedReferences, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<ScopedReferences>::try_new(Noop)?;

    let mut pipeline = context.observer::<Pipeline>().handle();
    pipeline.created()?;

    let mut task = context.observer::<Task>().handle();
    task.started(pipeline.as_entity_ref())?;
    task.ended()?;

    Ok(())
}
Key point

A scoped reference defines a parent relationship in a validated entity tree.

Check yourself

What does scope-ref add beyond a normal targeted ref?

Which value is passed as the task's parent?

Finite-State Machine

The Finite-State Machine semantic module defines an entity lifecycle as states and allowed transitions. The parser validates the topology and derives event cardinality from it. The generated Rust instrumentation API uses that topology to enforce transition order.

For example, a task might move from queued to running, then to either completed or failed. Making that lifecycle part of the schema gives analysis tools enough meaning to detect unexpected transitions, find work that never reached a final state, and measure how long entities remained in each state. A user interface can also present the declared lifecycle and the observed path through it.

The Resource lessons later show how an FSM state can declare the resources an entity uses while it remains in that state.

Basic lifecycle

An FSM puts lifecycle topology in the model. It declares an initial state, allowed transitions, and a reachable final state.

YAML model

quent: alpha
model: finite_state_machine

fsms:
  Job:
    states:
      queued:
        initial: true
        to: [loading_input, restoring_checkpoint]
      loading_input:
        to: [running]
      restoring_checkpoint:
        to: [running]
      running:
        to: [completed]
      completed: {}

The parser rejects missing initial states, unreachable states, invalid targets, and FSMs without a reachable final state. It also derives event cardinality from the topology.

Instrumentation API

Entering a state emits its generated event. The generated Rust API represents the current FSM state in the handle’s type. Each transition consumes that handle and returns a handle for the target state. Only transitions allowed from the current state are available as methods, so an invalid transition does not compile. This pattern is called typestate.

The comments after each call show how the handle’s type changes after every transition.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/finite_state_machine.rs"));
}

use instrumentation::{Context, FiniteStateMachine, Job, Noop};

#[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<FiniteStateMachine>::try_new(Noop)?;
    let _job = context
        .observer::<Job>()
        .handle()         // FsmHandle<Job>
        .queued()         // FsmHandle<Job, job_state::Queued>
        .loading_input()  // FsmHandle<Job, job_state::LoadingInput>
        .running()        // FsmHandle<Job, job_state::Running>
        .completed();     // FsmHandle<Job, job_state::Completed>

    Ok(())
}
Key point

The generated handle exposes only the transitions allowed from its current state.

Check yourself

Which property does the parser validate for this FSM?

Which states can directly precede running?

Self-loops

A direct self-loop transitions from a state back to itself, such as running → running. A state can also be part of an indirect cycle, such as running → paused → running. Both forms allow states on the cycle to be entered repeatedly, so their generated events have multi cardinality.

YAML model

quent: alpha
model: fsm_self_loop

fsms:
  Task:
    states:
      running:
        initial: true
        attributes:
          items_processed: u64
        to: [running, paused, completed]
      paused:
        to: [running]
      completed: {}

Instrumentation API

The task first repeats running through its direct self-loop. It then follows the indirect cycle through paused and back to running before entering completed once.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/fsm_self_loop.rs"));
}

use instrumentation::{Context, FsmSelfLoop, Noop, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<FsmSelfLoop>::try_new(Noop)?;
    let task = context.observer::<Task>().handle().running(0);

    // Direct self-loop: running -> running.
    let task = task.running(64);
    // Indirect cycle: running -> paused -> running.
    let task = task.paused().running(128);
    let _task = task.completed();

    Ok(())
}
Key point

Every state on a direct or indirect cycle has a repeatable state-entry event.

Check yourself

Which transition is a direct self-loop?

Why does paused have multi cardinality?

Resource

The Resource semantic module describes what an application provides and what its work consumes:

  • A resource is an entity that can be claimed, such as a thread, worker, or memory pool.
  • A capacity is a named quantity provided by a resource.
    • An occupancy is a quantity held throughout a usage. For example, a task might occupy 256 bytes of a memory pool until it leaves its current state.
    • A rate records a total quantity processed during a usage. For example, a transfer might process 1,024 bytes. Dividing that value by the usage duration gives the observed transfer rate.
    • A resource with no named capacities is a unit resource. Each usage claims the entire resource instance.
  • A usage is a claim on a specific resource instance. It identifies the resource and records how much of each capacity is claimed.
  • A bound is a reported upper limit for a capacity. Bounds belong to the resource and may be updated by its events.

A resource can be declared on an entity under either entities or fsms. An FSM can therefore provide a resource while also modeling its own lifecycle.

Only entities modeled as FSMs can use resources.

Why can only FSMs use resources?

Entering a state starts the usages declared by that state, and leaving it ends them. A final state cannot start a usage. The validated FSM topology therefore gives every usage a modeled way to end.

This is a schema-level guarantee, not a runtime guarantee. The instrumenting code remains responsible for emitting a valid transition out of a state that uses resources. If it does not, the event stream contains a logically unended usage.

Unit resources

resource: true declares an indivisible resource. This model places each Thread under a ThreadPool, then places a running Task under the specific thread it claims.

YAML model

quent: alpha
model: unit_resource

entities:
  ThreadPool:
    events:
      created: {}

  Thread:
    resource: true
    events:
      registered:
        attributes:
          pool: { scope-ref: ThreadPool }

fsms:
  Task:
    states:
      running:
        initial: true
        attributes:
          # ThreadUsage is generated from the Thread resource declaration.
          thread: { scope-ref: Thread, data: ThreadUsage }
        to: [completed]
      completed: {}

The ThreadUsage record is generated automatically. It has no fields because a unit resource is claimed as a whole.

Instrumentation API

as_entity_ref_with attaches the generated usage record to the scoped thread reference.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/unit_resource.rs"));
}

use instrumentation::{Context, Noop, Task, Thread, ThreadPool, ThreadUsage, UnitResource};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<UnitResource>::try_new(Noop)?;

    let mut pool = context.observer::<ThreadPool>().handle();
    pool.created()?;

    let mut thread = context.observer::<Thread>().handle();
    thread.registered(pool.as_entity_ref())?;

    let _task = context
        .observer::<Task>()
        .handle()
        .running(thread.as_entity_ref_with(ThreadUsage))
        .completed();

    Ok(())
}
Key point

A unit resource represents one indivisible resource instance.

Check yourself

What does resource: true mean for Thread?

Which hierarchy does the model define?

Resource capacities

A resource can expose measured capacities. occupancy describes a quantity held over the usage span, such as bytes of memory held while a task runs.

YAML model

quent: alpha
model: resource_capacity

entities:
  Memory:
    resource:
      bytes:
        kind: occupancy
    events:
      created: {}

fsms:
  Task:
    states:
      running:
        initial: true
        attributes:
          memory: { uses: Memory }
        to: [completed]
      completed: {}

Declaring the bytes capacity generates a MemoryUsage record with a bytes field.

Instrumentation API

The task’s reference to Memory carries the quantity it claims.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/resource_capacity.rs"));
}

use instrumentation::{Context, Memory, MemoryUsage, Noop, ResourceCapacity, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<ResourceCapacity>::try_new(Noop)?;

    let mut memory = context.observer::<Memory>().handle();
    memory.created()?;

    let _task = context
        .observer::<Task>()
        .handle()
        .running(memory.as_entity_ref_with(MemoryUsage { bytes: 512_000_000 }))
        .completed();

    Ok(())
}
Key point

A capacity resource records the quantity of a resource used by an entity.

Check yourself

What does an occupancy capacity measure?

Where is the task's claimed byte quantity declared?

Bounded resources

known-bounds: true states that a capacity has an explicit bound. An event or FSM state attribute marked with sets-resource-bounds: true carries the generated bounds record whenever that limit changes.

YAML model

quent: alpha
model: bounded_resource

entities:
  Memory:
    resource:
      bytes:
        kind: occupancy
        known-bounds: true
    events:
      resized:
        multi: true
        attributes:
          # MemoryBounds is generated because bytes has known bounds.
          limits: { sets-resource-bounds: true }

fsms:
  Task:
    states:
      running:
        initial: true
        attributes:
          memory: { uses: Memory }
        to: [completed]
      completed: {}

The resource declaration generates both MemoryUsage and MemoryBounds. Both records use u64 for the bytes field. Resource declarations do not currently support selecting another numeric width.

Instrumentation API

The memory entity publishes its current bound. The task separately records how much of that capacity it claims.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/bounded_resource.rs"));
}

use instrumentation::{BoundedResource, Context, Memory, MemoryBounds, MemoryUsage, Noop, Task};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<BoundedResource>::try_new(Noop)?;
    let memory = context.observer::<Memory>().handle();

    memory.resized(MemoryBounds {
        bytes: 8_000_000_000,
    })?;

    let _task = context
        .observer::<Task>()
        .handle()
        .running(memory.as_entity_ref_with(MemoryUsage { bytes: 512_000_000 }))
        .completed();

    Ok(())
}
Key point

A known bound records the available capacity separately from resource usage.

Check yourself

What additional generated record comes from known-bounds: true?

What does sets-resource-bounds: true identify?

Combining modules

Module constraints can be used together in one model. The job workload combines a finite-state lifecycle with bounded resource usage.

This example combines an FSM, event attributes, and measured resource usage. A worker publishes its thread limit. A job records how many threads it requests and how many it occupies while running.

YAML model

quent: alpha
model: job_workload

entities:
  Worker:
    # Generates WorkerUsage and WorkerBounds.
    resource:
      threads:
        kind: occupancy
        known-bounds: true
    events:
      ready:
        attributes:
          name: string
          limits: { sets-resource-bounds: true }

fsms:
  Job:
    states:
      queued:
        initial: true
        attributes:
          name: string
          requested_threads: u64
        to: [running]
      running:
        attributes:
          worker: { uses: Worker }
        to: [completed]
      completed: {}

Instrumentation API

The generated API distinguishes the worker’s WorkerBounds from the job’s WorkerUsage. No event names or payload keys are assembled at runtime.

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

#[allow(unused)]
mod instrumentation {
    include!(concat!(env!("OUT_DIR"), "/job_workload.rs"));
}

use instrumentation::{Context, Job, JobWorkload, Noop, Worker, WorkerBounds, WorkerUsage};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<JobWorkload>::try_new(Noop)?;

    let mut worker = context.observer::<Worker>().handle();
    worker.ready("worker-1".to_owned(), WorkerBounds { threads: 16 })?;

    let _job = context
        .observer::<Job>()
        .handle()
        .queued("compile".to_owned(), 4)
        .running(worker.as_entity_ref_with(WorkerUsage { threads: 4 }))
        .completed();

    Ok(())
}
Key point

The job records its requested thread count and its usage of a bounded worker resource.

Check yourself

What does WorkerBounds { threads: 16 } represent?

Which call records where the job runs and how many threads it occupies?

Analysis

No analysis API lessons are included in this tutorial yet. This is Work-In-Progress.