UVM Agent: Active vs Passive, is_active, Build & Connect

Every UVM testbench you will ever build is assembled from small, reusable blocks that snap into a verification environment. The most important of these blocks is the UVM agent. Get the agent right and the rest of UVM — environments, scoreboards, virtual sequences — falls into place, because the agent is the exact pattern UVM repeats at every interface. This tutorial builds a complete mental model of the uvm_agent class: what it groups, why it exists, how active and passive modes differ, and how you wire its parts inside the build and connect phases.

A quick honesty note before any code: the UVM examples below are written for a UVM-capable simulator such as industry-standard simulation tools, VCS or industry-standard simulators, or the free EDA Playground web tool. They depend on the UVM base-class library, so they will not compile on Icarus Verilog or Verilator. Read them as canonical UVM structure you can paste and adapt. The single runnable block later on is plain SystemVerilog — it carries a live simulation log so you can see the idea execute.

What is a UVM agent?

A UVM agent is a uvm_component that bundles together everything needed to talk to one DUT interface. For an ALU with a single input bus, you build one agent; for a design with a master port and a slave port, you build two agents, one per interface. The agent is a container with a job: own the components that drive and observe that one protocol, and present a clean boundary to the environment above it.

By convention an agent groups three sub-components, which the base class uvm_agent expects you to instantiate:

  • Sequencer — routes transaction-level items (sequence items) to the driver on request.
  • Driver — pulls items from the sequencer and wiggles the pins of the virtual interface, turning transactions into clocked signal activity.
  • Monitor — passively watches the same pins and reconstructs transactions, broadcasting them through an analysis port to scoreboards and coverage collectors.

Why group them together?

The driver, sequencer and monitor are not independent — they are three views of one protocol. The driver and monitor must agree on the signal timing. The sequencer and driver share a transaction type. Packaging them in one agent means you reuse, configure and relocate the trio as a unit. Want the same AXI interface in a block-level bench and later in a chip-level bench? You instantiate the same agent twice instead of re-plumbing six components by hand. This reuse is the whole reason UVM insists on the agent abstraction rather than letting you scatter drivers and monitors across the environment.

Active vs passive: the central decision

An agent runs in one of two modes, and choosing the mode is the first design decision you make for every interface.

  • Active (UVM_ACTIVE) — the agent drives the DUT. It builds all three components: sequencer, driver and monitor. Use this on interfaces where your test generates stimulus.
  • Passive (UVM_PASSIVE) — the agent only observes. It builds only the monitor; the sequencer and driver are never created. Use this on outputs, on interfaces driven by the DUT itself, or when another agent already drives the bus and you just need to watch.

The monitor is present in both modes — you always want visibility. What the mode really gates is the driving half of the agent. A common real-world setup is two agents on a point-to-point link: agent A active to drive requests, agent B passive to observe responses.

The is_active field

The base uvm_agent class already carries a field named is_active of type uvm_active_passive_enum, and a helper get_is_active() that returns it. You do not declare is_active yourself — you read it in build_phase to decide which children to construct, and you set it from the environment (usually through the configuration database) before the agent builds. Because UVM runs build_phase top-down, the parent sets the value first; by the time the agent’s own build runs, the flag is ready.

Here is the concept executing in plain SystemVerilog — no UVM library, so it runs live. It mirrors exactly what the agent’s build and connect do, using a flag instead of the real enum:

module tb;
  // Illustration (no UVM library): how an agent's is_active flag gates
  // the driver + sequencer, while the monitor is always present.
  localparam bit IS_ACTIVE = 1'b1;  // 1 = UVM_ACTIVE, 0 = UVM_PASSIVE

  task automatic build_phase(input bit active);
    $display("[build]   monitor   : created (always)");
    if (active) begin
      $display("[build]   sequencer : created");
      $display("[build]   driver    : created");
    end else begin
      $display("[build]   sequencer : skipped (passive)");
      $display("[build]   driver    : skipped (passive)");
    end
  endtask

  task automatic connect_phase(input bit active);
    $display("[connect] monitor.ap -> subscriber (always)");
    if (active)
      $display("[connect] driver.seq_item_port <-> sequencer.seq_item_export");
    else
      $display("[connect] no driver/sequencer link (passive)");
  endtask

  initial begin
    $display("=== agent build_phase  (is_active=%0d) ===", IS_ACTIVE);
    build_phase(IS_ACTIVE);
    $display("=== agent connect_phase (is_active=%0d) ===", IS_ACTIVE);
    connect_phase(IS_ACTIVE);
    $finish;
  end
endmodule


=== agent build_phase (is_active=1) ===
[build] monitor : created (always)
[build] sequencer : created
[build] driver : created
=== agent connect_phase (is_active=1) ===
[connect] monitor.ap -> subscriber (always)
[connect] driver.seq_item_port <-> sequencer.seq_item_export
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpzdjinj7q/tb.sv:30: $finish called at 0 (1s)

Flip IS_ACTIVE to 1'b0 and re-run to watch the driver and sequencer disappear while the monitor stays. That single branch is the heart of every real agent.

Building the agent: build_phase

Now the canonical UVM. In build_phase you create the monitor unconditionally and gate the driver and sequencer on get_is_active(). Always use the factory (type_id::create) rather than new() so tests can override component types later.

class alu_agent extends uvm_agent;
  `uvm_component_utils(alu_agent)

  alu_driver     drv;
  alu_sequencer  seqr;
  alu_monitor    mon;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // Monitor is always built - we always want to observe.
    mon = alu_monitor::type_id::create("mon", this);

    // Driver + sequencer only when this agent must drive the DUT.
    if (get_is_active() == UVM_ACTIVE) begin
      seqr = alu_sequencer::type_id::create("seqr", this);
      drv  = alu_driver   ::type_id::create("drv",  this);
    end
  endfunction
endclass

Wiring inside connect_phase

Construction and connection are deliberately split across two phases. build_phase creates objects top-down; connect_phase runs afterwards, bottom-up, once every component exists, so handles are guaranteed valid when you link them. The agent’s one mandatory connection is the driver-to-sequencer TLM link, and you guard it with the same active check — a passive agent has no driver or sequencer to connect.

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (get_is_active() == UVM_ACTIVE)
      drv.seq_item_port.connect(seqr.seq_item_export);
  endfunction

The monitor’s analysis port is typically connected one level up, in the environment, because its subscribers (scoreboard, coverage) live there — not inside the agent. Keep the agent’s connect_phase focused on its own internal driver-sequencer handshake.

Setting the mode from the environment

You rarely hard-code the mode inside the agent. Instead the parent environment writes is_active into the configuration database before creating each agent, so the same agent class serves both roles:

class alu_env extends uvm_env;
  `uvm_component_utils(alu_env)
  alu_agent agt_a;   // will drive stimulus
  alu_agent agt_b;   // will only observe

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // Set roles BEFORE the agents build - build_phase is top-down.
    uvm_config_db#(uvm_active_passive_enum)::set(this, "agt_a", "is_active", UVM_ACTIVE);
    uvm_config_db#(uvm_active_passive_enum)::set(this, "agt_b", "is_active", UVM_PASSIVE);

    agt_a = alu_agent::type_id::create("agt_a", this);
    agt_b = alu_agent::type_id::create("agt_b", this);
  endfunction
endclass

Active vs passive at a glance

AspectActive (UVM_ACTIVE)Passive (UVM_PASSIVE)
MonitorBuiltBuilt
DriverBuiltNot built
SequencerBuiltNot built
Drives the DUT?YesNo, observe only
connect_phase linkdriver.seq_item_port to sequencer.seq_item_exportNone inside the agent
Typical useInterfaces your test stimulatesOutputs, DUT-driven buses, shared buses

Common mistakes to avoid

  1. Dereferencing a null driver in a passive agent. If you call drv.something in connect_phase without the active guard, a passive agent crashes because drv was never created.
  2. Setting is_active after the agent builds. The config write must happen in the parent before create, or the agent reads a stale default.
  3. Connecting the monitor inside the agent. Expose the monitor’s analysis port and connect it where its subscribers live.
  4. Using new() instead of the factory. Direct construction blocks type overrides and defeats UVM reuse.

Master this one component and you have mastered the repeatable unit of every UVM bench. An agent is simply: a monitor that always watches, plus a driver and sequencer that appear only when the agent must drive, wired together in a strict build-then-connect order.

Quick check

Which component does a passive agent still build?

The monitor. A passive agent omits the driver and sequencer but always builds the monitor, because observation (for scoreboards and coverage) is wanted in both modes.

Why is the driver-to-sequencer connection made in connect_phase, not build_phase?

build_phase constructs components top-down, so handles may not all exist yet. connect_phase runs afterwards, bottom-up, once every component is created, guaranteeing valid handles before you link seq_item_port to seq_item_export.

Where should is_active be set, and when?

In the parent (usually the environment) via the configuration database, before the agent is created. Since build_phase is top-down, the parent sets the value first and the agent reads it during its own build.

You have a point-to-point link driven by another agent and you only need to check its traffic. Which mode?

Passive (UVM_PASSIVE). You build just the monitor to observe; no driver or sequencer is needed because something else already drives the bus.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD