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
- 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.
- Generate typed libraries. Code generation produces application-specific instrumentation and analysis libraries from the same schema.
- 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.
- Analyze behavior. An analysis service uses the generated analysis library to interpret the stored events and provide application-specific results.
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(())
}
The two events have no declared ordering constraint, so either event may be emitted first.
Check yourself
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 type | Value |
|---|---|
bool | true or false |
u8, u16, u32, u64 | Unsigned integers of the indicated width |
i8, i16, i32, i64 | Signed integers of the indicated width |
f32, f64 | Floating-point numbers of the indicated width |
string | Text |
uuid | A universally unique identifier |
Types can also be composed or refer to generated types:
| YAML type | Value |
|---|---|
{ option: T } | A value of type T that may be absent |
{ list: T } | An ordered collection of values of type T |
| A record name | An instance of that record |
dynamic | String-keyed values whose names and types are chosen at runtime |
ref | A 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(())
}
Event attributes produce typed parameters in the generated instrumentation API.
Check yourself
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.
Use dynamic attributes for event details that cannot be defined in advance.
Check yourself
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(())
}
multi: true permits an event to be emitted more than once for an entity.
Check yourself
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(())
}
A named record provides one reusable structure for event attributes.
Check yourself
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.
A type-erased reference preserves an entity's identity without restricting its entity type.
Check yourself
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(())
}
An entity reference identifies a specific entity and preserves its type.
Check yourself
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(())
}
A scoped reference defines a parent relationship in a validated entity tree.
Check yourself
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(())
}
The generated handle exposes only the transitions allowed from its current state.
Check yourself
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(())
}
Every state on a direct or indirect cycle has a repeatable state-entry event.
Check yourself
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(())
}
A unit resource represents one indivisible resource instance.
Check yourself
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(())
}
A capacity resource records the quantity of a resource used by an entity.
Check yourself
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(())
}
A known bound records the available capacity separately from resource usage.
Check yourself
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(())
}
The job records its requested thread count and its usage of a bounded worker resource.
Check yourself
Analysis
No analysis API lessons are included in this tutorial yet. This is Work-In-Progress.