UVM
Why Objections Exist
The problem of knowing when a test is finished — why a fixed timeout or waiting for tasks to return both fail, and how raising and dropping objections lets components collectively decide when a run-time phase may end, ending it only when no work is in flight.
Objections · Module 17 · Page 17.1
The Engineering Problem
A UVM test runs — the run_phase advances simulation time, driving stimulus, processing it through the DUT, observing and checking the results. But when should it stop? This is not a trivial question. The phase must keep going while there's work in flight — stimulus still being driven, transactions still in the DUT's pipeline, responses still arriving — and stop when the work is done. There is no fixed end time: how long a test takes depends on what it does (a short directed test vs a long random soak). And the work is distributed — many components run concurrently (drivers, monitors, sequences, scoreboards), so no single component knows when everything is finished. The naive answers all fail: a fixed timeout either cuts off work or wastes time; waiting for run_phase tasks to return doesn't work because many fork forever (monitors run perpetually). The problem this chapter solves is how UVM knows when to end a run-time phase — when all the distributed work is truly done.
The answer is the objection. An objection is a vote that says "work is still in progress — don't end the phase yet." A component raises an objection when it starts work and drops it when it finishes. The framework keeps a count of outstanding objections, and the phase stays alive while that count is greater than zero and ends when it returns to zero — i.e., when no component is objecting to ending. This lets components collectively decide when the phase may end: it ends when everyone agrees there is no more work. Typically the test (or a sequence) raises an objection before starting stimulus and drops it after — holding the phase open exactly as long as the work is in flight. This chapter is why objections exist: the problem of knowing when a test is done, why the naive approaches fail, and how the raise/drop/count-to-zero mechanism solves it.
How does UVM know when to end a run-time phase — when all the distributed, variable-duration work is truly done — and why do objections (raise while working, drop when finished, end when the count reaches zero) solve this where a fixed timeout or waiting for tasks to return both fail?
Motivation — why ending a phase is hard
Ending a run-time phase correctly is hard because the duration is unknown and the work is distributed. The reasons:
- The duration is test-dependent. A directed test might finish in microseconds; a random soak might run for milliseconds. There's no fixed number of cycles that's right for all tests — the phase must end based on whether work remains, not a preset time.
- A fixed timeout is wrong both ways. Set it too short and you cut off work mid-flight (a transaction still in the pipeline, a response not yet checked) — false results. Set it too long and you waste simulation time idling after the work is done. No fixed value is correct.
- Tasks that fork forever never return. You can't end the phase when
run_phasetasks return, because many don't — monitorsforeverloops, driversforeverloops; they run perpetually. Waiting for them is waiting forever. - The work is distributed across concurrent components. Drivers, monitors, sequences, scoreboards all run at once. No single component knows when everything is done — the driver finishing doesn't mean the DUT's pipeline is drained or the scoreboard has checked everything.
- Ending needs a collective decision. Since no one component knows, the decision must be collective — each component contributes "I still have work" / "I'm done," and the phase ends when all say done. This is a distributed consensus problem.
The motivation, in one line: a run-time phase has no fixed end time (duration depends on the test) and its work is distributed across concurrent components (no single one knows when all is done), so neither a fixed timeout (cuts off or wastes) nor waiting for tasks to return (they fork forever) can end it correctly — ending requires a collective, work-driven decision, which is exactly what objections provide.
Mental Model
Hold objections as a meeting that can't adjourn while anyone still has business:
Objections are like a meeting that can't adjourn while anyone still has business: each participant raises a hand to say 'I still have something to do,' and lowers it when done; the chair may adjourn only when every hand is down. No one participant knows when the whole meeting is finished — but collectively, when all hands drop, it is. Picture a meeting (the run-time phase) with many participants (the components — drivers, sequences, scoreboards). The meeting can't end while anyone still has business — a participant with work to do raises a hand (raises an objection: "I have business — don't adjourn") and lowers it when finished (drops the objection). The chair (the phase) watches the hands: while any hand is up, the meeting continues; only when every hand is down does the chair adjourn. The crucial property is that no single participant knows when the whole meeting is done — the sequence finishing its stimulus doesn't know whether the DUT's responses have all arrived, or the scoreboard has checked everything. But collectively, the hands encode it: while a participant has business, their hand is up; when all business is done, all hands are down — and that's the signal to adjourn. The chair doesn't guess (a fixed timeout — "adjourn after an hour regardless") and doesn't wait for everyone to leave (some participants — the monitors — never leave; they'd stay forever). The chair waits for the hands: raise while you have business, lower when done, adjourn when all are down. The count of raised hands is the single number that tells the chair whether to continue or adjourn — greater than zero: continue; zero: adjourn.
So objections are a meeting adjourning by consensus of raised hands: each component raises an objection while it has work and drops it when done; the phase (chair) continues while any objection is raised and ends when all are dropped (the count hits zero). No one component decides; the collective of raised/lowered hands encodes "are we done?", and the phase ends when the consensus is yes. Raise while you have work, drop when done, and the phase ends when no hand is up.
Visual Explanation — the objection count gates the phase
The defining picture is the gate: components raise and drop objections, the framework keeps a count, and the phase stays alive while the count > 0, ending at zero.
The figure shows the objection count as the gate on the phase. Components (A, B, C) raise objections when they start work and drop them when they finish — each contributing to the objection count (the total outstanding). The phase stays alive while the count is greater than zero and ends when the count returns to zero. The crucial reading is that the warning-colored count is the single signal that gates the phase: greater than zero means keep running (someone has work), zero means end (no one is objecting). The components don't coordinate with each other — each simply raises and drops its own objection, and the framework aggregates them into the count. This is the distributed nature made concrete: Component A doesn't know about B or C; it just raises while it's busy and drops when done. The framework sums all their objections, and the phase watches the sum. So the collective decision — "is all the work done?" — is encoded in the count: it's non-zero while any component has work, and zero only when every component is done. The success-colored phase ends at exactly the moment the count hits zero — the instant the last component drops its last objection. The diagram is the mechanism in one picture: raise/drop (components) → count (framework) → alive-while-positive/end-at-zero (phase) — a distributed, work-driven gate that ends the phase precisely when no work remains, without any component needing to know the global state.
RTL / Simulation Perspective — raise before work, drop after
In code, a component raises an objection before starting time-consuming work and drops it after — holding the phase open for exactly that work. The code shows the canonical pattern.
// === TEST: raise an objection before stimulus, drop it after — keeps run_phase alive ===
class my_test extends uvm_test;
task run_phase(uvm_phase phase);
my_seq seq = my_seq::type_id::create("seq");
phase.raise_objection(this); // "I have work — don't end the phase yet" (count: 0 → 1)
seq.start(env.agt.seqr); // the time-consuming work: drive stimulus, wait for it
phase.drop_objection(this); // "my work is done" (count: 1 → 0 → phase may end)
endtask
endclass
// the phase stays alive from raise to drop — exactly as long as the stimulus is in flight.
// === Without the objection, the phase ends IMMEDIATELY (the DebugLab) ===
class broken_test extends uvm_test;
task run_phase(uvm_phase phase);
fork seq.start(env.agt.seqr); join_none; // forked stimulus
// ✗ NO raise_objection → count is 0 → phase ends NOW → stimulus never runs
endtask
endclass
// === A monitor that forks forever does NOT keep the phase alive — it has no objection ===
class monitor ...;
task run_phase(uvm_phase phase);
forever begin @(posedge vif.clk); ... end // runs forever, but raises NO objection
endtask // so it does NOT prevent the phase from ending
endclassThe code shows the canonical objection pattern and why it's needed. The test raises an objection before starting stimulus (phase.raise_objection(this) — count 0 → 1, "I have work"), runs the time-consuming work (seq.start(...) — drives stimulus and waits for it), and drops the objection after (phase.drop_objection(this) — count 1 → 0, "done"). The phase stays alive from raise to drop — exactly as long as the stimulus is in flight. The broken test shows the failure (the DebugLab): with no raise_objection, the count is zero, so the phase ends immediately — the forked stimulus never runs. The monitor shows a subtle point: a forever loop runs perpetually but raises no objection — so it does not keep the phase alive (it doesn't prevent ending). This is deliberate: monitors should not hold the phase open (they'd run forever); the phase end is gated by the stimulus (the test/sequence's objection), and when the stimulus is done, the phase ends — killing the forever monitors (which is fine; their work is done when there's no more stimulus to observe). The shape to carry: raise an objection before time-consuming work that must hold the phase open, drop it after — the phase lives from raise to drop. The test/sequence raises (it defines the work); monitors and drivers (which forever) don't raise (they follow the stimulus). The objection makes the phase wait for the stimulus to complete — not a fixed time, not forever.
Verification Perspective — collective, work-driven termination
Objections give collective, work-driven termination — each component contributes, and the phase ends when all the real work is done. Contrast with the naive approaches shows why this is right.
The figure contrasts objections with the naive approaches, showing why objections are right. A fixed timeout ends at a preset time, regardless of whether work remains — so it cuts off work (too short) or wastes time (too long): wrong both ways. Waiting for tasks to return never ends, because monitors and drivers fork forever — there's nothing to wait for that ever completes. Objections end the phase exactly when the work is done: components raise while working and drop when finished, and the phase ends when the count hits zero — work-driven, self-adjusting, collective. The verification insight is that the right termination is the one tied to the work, not to time (the timeout) nor to tasks returning (which they don't). Objections measure the work directly: the count is non-zero iff some component has work in flight, so the phase ends iff no work remains — precisely the condition you want. It's self-adjusting: a short test drops its objection early (short run); a long test holds it longer (long run) — automatically, without a preset time. And it's collective: each component contributes its own "I have work," and the aggregate decides — so the phase respects all the distributed work (the driver's stimulus, a sequence's completion, a scoreboard's drain), ending only when all of it is done. The figure is the argument for objections: terminate on the work, not the clock — the only model that ends a phase when (and only when) the work is actually finished, across all the distributed, variable-duration components. Objections are work-driven consensus; the alternatives are blind guesses.
Runtime / Execution Flow — the phase lifecycle with objections
At run time, the phase lives from the first raise to the last drop: it starts, components raise objections as they begin work, the phase stays alive while any are outstanding, and ends when the count returns to zero. The flow shows the lifecycle.
The flow shows the phase lifetime as from the first raise to the last drop. Start (step 1): run_phase begins; components are about to do their work. Raise (step 2): a component raises an objection as it begins work — the count goes positive, and the phase is held open. Run (step 3): the phase keeps running as long as any objection is outstanding (the count positive) — work in flight. End (step 4): as components finish, they drop their objections, lowering the count; when the last objection is dropped and the count reaches zero, the phase ends. The runtime insight is that the phase's lifetime is exactly the duration of the work: it lives from the first raise (when work begins) to the last drop (when all work finishes) — no longer, no shorter. This is why objections are self-adjusting: the phase automatically lasts as long as the work does, because its end is defined by the work's end (the count returning to zero). The critical subtlety — and the source of the DebugLab and later bugs — is the boundary conditions: if no objection is ever raised, the count is zero from the start, so the phase ends immediately (Step 4 before any work — the never-raise bug); if an objection is raised but never dropped, the count never returns to zero, so the phase runs forever (the never-drop bug, Module 17.5). The correct lifecycle is raise-when-work-starts, drop-when-work-ends — so the count tracks the work exactly, and the phase ends precisely when work does. The flow is the temporal map of objections: the phase opens with the first raise, stays open while work is in flight, and closes with the last drop — its lifetime is the work's lifetime, which is exactly the behavior a correct termination requires.
Waveform Perspective — the count rises and falls, the phase ends at zero
The objection mechanism is visible on a timeline: the count rises as components raise, the phase stays alive while it's positive, and the phase ends when the count falls to zero. The waveform shows the count gating the phase.
The objection count rises and falls; the phase ends when it reaches zero
12 cyclesThe waveform shows the count gating the phase. The phase is active (phase_active high) while work is in flight. A component raises an objection (raise pulses) and the objection count (obj_count) goes from 0 to 1 — holding the phase open; another raises and it goes to 2 (two components have work). As components finish, they drop (drop pulses) and the count falls — 2 → 1, then 1 → 0. The crucial visual is the instant the count reaches 0: the phase ends — phase_active goes low and phase_end pulses. The picture to carry is that the phase's lifetime is exactly while the count is positive — from the first raise (count 0 → 1) to the last drop (count 1 → 0). The count encodes "how many components have work," and the phase lives while it's positive and dies the moment it's zero. Reading this waveform — does the count track the raises and drops? does the phase end exactly when the count hits zero? — is reading the objection mechanism directly. The count rising, plateauing, and falling to zero, with the phase ending at that instant is the signature of objection-based termination: the phase opens with the first raise, stays open while any objection is outstanding, and closes the instant the count returns to zero — the work's lifetime, made into the phase's lifetime. And the boundary lessons are visible: if obj_count were never raised above 0, phase_active would never go high (end immediately); if it never returned to 0, phase_active would never go low (run forever).
DebugLab — the test that ended before it ran
A test finishing in zero time because no objection was raised to hold the phase open
A test completed instantly — it passed, but in zero (or near-zero) simulation time: no stimulus was driven, no transactions appeared on the interface, the scoreboard checked nothing, and the log showed the run_phase starting and ending with no activity in between. The test's sequence was written and correct, but it never ran — the phase ended before the stimulus got a chance to execute.
The test never raised an objection, so when run_phase started, the objection count was zero — and UVM ended the phase immediately, because nothing was objecting to ending. The forked stimulus never ran:
✗ NO objection raised → count is 0 from the start → phase ends immediately:
task run_phase(uvm_phase phase);
fork seq.start(env.agt.seqr); join_none; // forked stimulus
// ✗ no raise_objection → objection count = 0 → UVM ends run_phase NOW
// → the forked seq never gets to run → zero-time test, no stimulus
endtask
✓ RAISE before the work, DROP after → phase held open while stimulus runs:
task run_phase(uvm_phase phase);
phase.raise_objection(this); // count 0 → 1 → phase held open
seq.start(env.agt.seqr); // stimulus runs to completion (phase stays alive)
phase.drop_objection(this); // count 1 → 0 → phase may now end
endtaskThis is the no-objection bug — the most fundamental objection mistake, and the one that demonstrates why objections exist: without an objection, the phase has nothing holding it open, so it ends immediately. UVM ends a run-time phase as soon as the objection count is zero. At the start of run_phase, before anyone raises, the count is zero — so if the test doesn't raise an objection, the phase sees count zero and ends right away, before the stimulus runs. The forked sequence (join_none) never gets to execute, because the phase ends in the same moment — a zero-time test with no activity. The DUT was never stimulated; the test "passed" vacuously (it checked nothing). The tell is exactly this: a test that finishes in zero time with no stimulus and no checking — the phase ended before the work ran. The fix is to raise an objection before the work and drop it after: phase.raise_objection(this) holds the phase open (count 0 → 1), the stimulus runs to completion (the phase stays alive while the objection is outstanding), and phase.drop_objection(this) lets the phase end (count 1 → 0) once the work is done. Now the phase lives exactly as long as the stimulus runs. The general lesson, and the chapter's thesis: a run-time phase ends as soon as the objection count is zero — and the count is zero at the start — so you must raise an objection to hold the phase open while there's work; without one, the phase ends immediately, before your stimulus runs. This is why objections exist: they are the mechanism that holds the phase open while work is in flight — no objection, no holding, instant end. Raise to keep the phase alive; the phase waits only for what objects.
The tell is a test ending in zero time with no stimulus. Diagnose missing objections:
- Check whether an objection was raised. A run_phase that starts time-consuming work but never calls raise_objection ends immediately.
- Look for zero-time completion. A test that finishes with no simulation time elapsed and no activity didn't hold the phase open.
- Confirm the work was forked or started. If the stimulus never ran at all, the phase ended before it got the chance — a missing objection.
- Remember the count starts at zero. With no raise, the phase has nothing objecting to ending, so it ends at once.
Raise an objection to hold the phase open while there's work:
- Raise before time-consuming work. Call phase.raise_objection(this) before starting stimulus that must keep the phase alive.
- Drop after the work completes. Call phase.drop_objection(this) once the work is done, so the phase can end.
- Bracket the work, not before or after the start. The raise must precede the work and the drop must follow it, so the phase is open for exactly the work's duration.
- Verify simulation time elapses. Confirm the test runs for non-zero time with stimulus — a zero-time test signals a missing objection.
The one-sentence lesson: a run-time phase ends as soon as the objection count is zero, and the count is zero at the start, so you must raise an objection (raise_objection before the work, drop_objection after) to hold the phase open while there's work; without one, the phase ends immediately, before your stimulus runs.
Common Mistakes
- Not raising an objection. With no objection, the count is zero and the phase ends immediately, before the work runs; raise to hold it open.
- Expecting forked tasks to keep the phase alive. A forever loop runs but raises no objection, so it doesn't prevent the phase from ending; the stimulus's objection gates the end.
- Relying on a fixed timeout to end the phase. A preset time cuts off work or wastes time; objections end the phase when the work is actually done.
- Raising after the work starts. The raise must precede the work; if the phase can end between start and raise, the work may be cut off.
- Thinking one component knows when everything is done. Termination is collective; each component raises and drops its own objection, and the framework aggregates.
- Forgetting the count starts at zero. The phase has nothing holding it open until something raises; never assume it waits on its own.
Senior Design Review Notes
Interview Insights
Objections exist to solve the problem of knowing when to end a run-time phase — when all the distributed, variable-duration work of a test is truly done. A run-time phase like run_phase advances simulation time, driving stimulus, processing it through the DUT, and checking results, but it must know when to stop. That's hard for two reasons. First, the duration is test-dependent: a directed test might finish in microseconds, a random soak in milliseconds, so there's no fixed number of cycles that's right for all tests. Second, the work is distributed across many concurrent components — drivers, monitors, sequences, scoreboards — so no single component knows when everything is done. The naive approaches both fail. A fixed timeout ends at a preset time regardless of whether work remains, so it either cuts off work mid-flight, giving false results, or wastes simulation time idling after the work is done — wrong both ways. Waiting for the run_phase tasks to return doesn't work either, because many fork forever — monitors and drivers run perpetual loops that never return. Objections solve this with a collective, work-driven mechanism. An objection is a vote that says work is still in progress, don't end the phase yet. A component raises an objection when it starts work and drops it when it finishes. The framework counts outstanding objections, and the phase stays alive while the count is greater than zero and ends when it returns to zero — when no component is objecting. So the components collectively decide when the phase may end: it ends when everyone agrees there's no more work. The mental model is a meeting that can't adjourn while anyone still has business — each participant raises a hand while they have work and lowers it when done, and the meeting adjourns only when all hands are down. That's why objections exist: to end a phase exactly when the work is done.
Because a fixed timeout isn't tied to the work and waiting for tasks doesn't terminate, while correct ending must be work-driven. A fixed timeout ends the phase at a preset time regardless of whether work remains. If you set it too short, you cut off work that's still in flight — a transaction still in the DUT's pipeline, a response not yet checked — producing false results. If you set it too long, you waste simulation time idling after the work is already done. There's no single fixed value that's correct, because the right duration varies by test — a directed test and a long random soak need very different times. So a timeout is wrong both ways: it can't adapt to the actual work. Waiting for the run_phase tasks to return fails for a different reason: many of them fork forever. Monitors run a forever loop sampling the interface; drivers run a forever loop pulling and driving transactions. These tasks are designed to run perpetually for the life of the phase, so they never return on their own. If you waited for them, you'd wait forever — the phase would never end. Objections fix both problems by measuring the work directly. The objection count is non-zero exactly when some component has work in flight, so the phase ends exactly when no work remains — tied to the work, not to a clock or to tasks returning. It's self-adjusting: a short test drops its objection early, a long test holds it longer, automatically, with no preset time. And it's collective: each component contributes its own indication of having work, and the aggregate decides, so the phase respects all the distributed work and ends only when all of it is done. The deeper point is that the right termination condition is work-driven — end when the work is finished — and only objections express that, while the timeout guesses based on time and waiting-for-tasks never resolves.
The framework keeps a running count of outstanding objections, and the phase stays alive while the count is greater than zero and ends the instant it returns to zero. Each component raises an objection when it begins work, which increments the count, and drops it when it finishes, which decrements the count. So at any moment, the count is the number of components that currently have work in flight and are objecting to the phase ending. The phase watches this single number. While it's positive, at least one component has work, so the phase keeps running. When it reaches zero, no component has work, so the phase ends. Concretely, the phase's lifetime is from the first raise to the last drop. At the start of run_phase, before anyone raises, the count is zero. A component raises — say the test before starting its stimulus — and the count goes to one, holding the phase open. Other components might raise too, pushing the count higher. As each finishes, it drops, lowering the count. When the last objection is dropped and the count hits zero, the phase ends immediately. The components don't coordinate with each other — each just raises and drops its own objection, and the framework aggregates them into the count. This is what makes the decision collective and distributed: no single component knows the global state, but the sum of all their objections encodes whether any work remains. Two boundary conditions follow directly: if no objection is ever raised, the count is zero from the start and the phase ends immediately; if an objection is raised but never dropped, the count never returns to zero and the phase runs forever. So the mechanism is simply: raise increments, drop decrements, phase alive while positive, phase ends at zero — a reference count of pending work that gates the phase's lifetime.
Because keeping the phase alive depends on raising an objection, not on having a running task, and a monitor's forever loop raises no objection. The phase's end is gated solely by the objection count, which is the number of outstanding raised objections. A monitor runs a forever loop sampling the interface, but unless it explicitly raises an objection, it contributes nothing to that count. So the monitor's perpetual task does not prevent the phase from ending. This is deliberate and correct. Monitors are meant to run for the entire life of the phase, observing whatever happens — they should not be the thing that decides when the phase ends, because if they did, the phase would never end, since their loops never terminate. Instead, the phase end is gated by the stimulus: the test or sequence raises an objection while it's generating stimulus and drops it when the stimulus is complete. When the stimulus is done and the objection count returns to zero, the phase ends, and UVM kills the still-running forever loops in the monitors and drivers. That's fine, because their work is done once there's no more stimulus to observe or drive — there's nothing left for them to do. So the division is that the components defining the work — the test and sequences — hold objections to keep the phase alive for exactly as long as the work is in flight, while the perpetual infrastructure components — monitors and drivers — run forever but don't object, so they follow the stimulus rather than dictating the phase length. This is why a forever loop alone doesn't keep the phase alive, and why you must raise an objection in the component that owns the work. It also explains a common confusion: people expect a running task to hold the phase open, but the mechanism is the objection count, not task activity. Activity without an objection doesn't matter to phase termination.
The phase ends immediately, before your work runs, because the objection count is zero at the start and a run-time phase ends as soon as the count is zero. When run_phase begins, no objection has been raised yet, so the count is zero. UVM checks this and, finding nothing objecting to the phase ending, ends the phase right away. If your test forks a stimulus sequence but never raises an objection, the phase ends in the same moment, before the forked sequence gets a chance to execute. The result is a zero-time test: no stimulus is driven, no transactions appear on the interface, the scoreboard checks nothing, and the log shows run_phase starting and ending with no activity in between. The test may even pass, but vacuously — it checked nothing, so passing is meaningless. The tell is exactly this: a test that finishes in zero or near-zero simulation time with no stimulus and no checking. This is the most fundamental objection mistake, and it demonstrates why objections exist — without one, the phase has nothing holding it open, so it ends instantly. The fix is to raise an objection before the work and drop it after: raise_objection before starting the stimulus holds the phase open, the stimulus runs to completion while the objection is outstanding, and drop_objection after lets the phase end once the work is done. Then the phase lives exactly as long as the stimulus runs. The lesson is that you must raise an objection to keep the phase alive while there's work, because the phase waits only for what objects, and the count starts at zero. Forgetting the raise is a classic bug precisely because nothing errors — the test runs, it just runs nothing — so you catch it by checking that simulation time actually elapses with real stimulus.
Exercises
- State the problem. Explain why ending a run-time phase is hard, and why a fixed timeout and waiting for tasks both fail.
- Trace the count. For a test with two components raising and dropping, sketch the objection count over time and mark when the phase ends.
- Diagnose the zero-time test. A test finishes instantly with no stimulus. Name the cause and the fix.
- Place the raise and drop. Write the run_phase bracketing of a stimulus sequence with raise and drop, and explain the lifetime.
Summary
- An objection is a vote that "work is still in progress — don't end the phase yet." A component raises one when it starts work and drops it when it finishes.
- The framework counts outstanding objections; the phase stays alive while the count is greater than zero and ends when it returns to zero — when no component is objecting — so the many concurrent components collectively decide when the phase may end.
- Objections solve a real problem: a run-time phase has no fixed end time (duration is test-dependent) and its work is distributed (no single component knows when all is done), so a fixed timeout (cuts off or wastes) and waiting for tasks to return (monitors fork forever) both fail.
- The phase's lifetime is exactly from the first raise to the last drop — the duration of the work; no objection raised means the count is zero and the phase ends immediately (before the work runs).
- The durable rule of thumb: a run-time phase ends as soon as the objection count is zero — so raise an objection (
raise_objectionbefore the work) to hold the phase open while there's work, and drop it (drop_objectionafter) when done; termination is work-driven and collective, not a fixed timeout or waiting for forever-tasks, and without an objection the phase ends instantly, before your stimulus runs.
Next — Raising an Objection: the mechanism in detail — how raise_objection works, where and when to call it (the test, the sequence, or a component), the role of the optional description and count, and the patterns for raising objections correctly so the phase is held open for exactly the work that matters.