Waiting and cancellation
Let a node hold Running while your coroutine, task, or event finishes, and know exactly what happens when the branch is taken away.
On this page
Work that takes time keeps its node Running until it finishes. When something else takes the branch away, the graph cancels that work instead of leaving it running behind the scenes.
The problem
Section titled “The problem”A door takes a second to open. Bind its coroutine as an ordinary call and the graph starts a new one every frame, so the door never finishes opening and the branch never finishes waiting.
The Platformer beacon is the same shape with a different lever: the wind up belongs to the component, and the graph waits for WarmUpFinished rather than counting frames of its own. Either way the rule is that one activation owns one piece of work, and the node holds Running while that work is outstanding.
When a node waits
Section titled “When a node waits”Three shapes wait, and nothing else does.
- A method whose return is an
IEnumerator, bound as a coroutine. - A method whose return is awaitable, bound as an awaitable.
- An event condition on the GameObject path, which waits for one firing.
Everything else settles inside the evaluation that started it: a property, a field, a message, a member condition, and a method whose return is an ordinary value.
The awaitable contract is shape-based. Any return type with a public instance GetAwaiter(), whose awaiter exposes a public bool IsCompleted and a public GetResult(), qualifies. Task, Task<T>, ValueTask, UniTask, Unity’s Awaitable, and a handle of your own all take the same path, with no per-type special case. A callback API without that shape is not awaitable, so wrap it in one that is.
See it
Section titled “See it”using System.Collections;using System.Threading;using System.Threading.Tasks;using UnityEngine;
public sealed class Door : MonoBehaviour{ public IEnumerator Open() { while (transform.localScale.y > 0.05f) { transform.localScale += Vector3.down * Time.deltaTime; yield return null; } }
public async Task<bool> ReserveAsync(CancellationToken cancellationToken) { await Task.Yield(); cancellationToken.ThrowIfCancellationRequested(); return true; }}Bind Open() and the node holds Running while the door shrinks. Bind ReserveAsync and its CancellationToken row is supplied by the runtime, so the cancellation the graph requests reaches your code.

The same lever, filmed on BeaconWarmUpLive.bqbehavior: the waiting node holds its RUNNING pill for the full wind up, and the branch moves on the tick the event arrives.
-
Add a method node and pick the coroutine or awaitable declaration. The Result card switches to its deferred form.
-
Read While waiting, which is locked to running. The waiting contract is fixed.
-
Set the completion mapping: what a finished call reports, and for a value-returning awaitable, whether to compare or store the completed value.
-
Set Give up after to On and enter a finite number of seconds when the gameplay action has a real deadline. Choose the result the node reports when the budget expires.
-
Leave On interrupt alone. It is locked to interrupted, because a preempted branch did not fail.
How it decides
Section titled “How it decides”Result
- Success
- The coroutine ran to its end, or the awaited call completed, and the mapping for that completion is succeeded.
- Failure
- The completion mapped to failed, or the give-up result you chose was failed.
- Running
- The coroutine or awaiter is outstanding, or an event condition has not fired yet.
One activation, one invocation
- Starting the work stamps an activation generation on the pending slot. A completion that carries an older generation is ignored, so a late callback from before a reset cannot finish the node that came after it.
- The method is not invoked again while its coroutine or awaitable is still outstanding.
- Releasing an activation unsubscribes an event, cancels that invocation’s cancellation token, stops the owned coroutine wrapper, and disposes the iterator when it is disposable.
What cancels pending work
| Reason | Posted when |
|---|---|
| Transition | A state machine transition leaves the state that owned the work |
| Reset | The graph is replaced, or the agent’s runtime is reset |
| Lower priority | A reactive observer preempts the branch that owned the work |
| Self | A Self observer cancels its own guarded branch |
| Timeout | A Time Limit decorator above the work expires |
| Peer failure | A failed parallel state action, or a faulted hosted machine, ends its still-running peers |
| Interruption | The host cancels active work explicitly |
Disabling the agent pauses its member runtime instead: pending deferred work is cancelled, notification subscriptions are dropped, and the pending slots are released. Re-enabling keeps the runtime state it had.
Cancellation is cooperative for a task. The graph stops the polling coroutine it owns and cancels the token for that invocation. A Task that ignores its token keeps running in the background, unobserved, so read the token in code where the work is expensive. A coroutine, by contrast, is genuinely stopped, because the graph owns the Unity coroutine it started.
Give up after is a budget on one wait. Off means the node waits as long as the work does, which is why an event nobody raises waits forever and why Time Limit exists.
Faults
- A thrown member call, a coroutine that throws, and a target lost mid-wait are recorded as faults, each naming the graph, the node, and the member. The trace on the agent holds the most recent entries and does not grow.
- A fault reports the faulted result, which is a runtime fault and never an authored failure. The On fault control on the deferred Result card is authored and recorded in the bake; the runtime reports the fault itself, so treat a fault as a bug to fix rather than a branch to plan around.
- A deferred start that hands back no usable handle, such as a method returning a null
IEnumerator, faults immediately instead of quietly succeeding. - A call that throws commits no outputs. Outputs captured before a wait began stay committed, because they were already handed over.
When it goes wrong
Section titled “When it goes wrong”When it goes wrong
| Symptom | Check | Fix |
|---|---|---|
| The coroutine restarts every frame. | Confirm the Result card is in its deferred form rather than mapping an ordinary return. | Pick the IEnumerator declaration so the node owns the coroutine. |
| The node waits forever. | Nothing completes the work: an event that is never raised, or a task that never finishes. | Turn on Give up after, or wrap the node in Time Limit. |
| Your task keeps running after the branch was interrupted. | The token is supplied, and your code has to observe it. | Take a CancellationToken parameter and check it, since the graph cannot stop a task from outside. |
| The node faults immediately. | Look for a method that returned a null iterator or a null awaitable. | Return a real handle. A missing handle is a fault, not a failure. |
| An old completion lands after a reset. | Generations differ, so the completion is discarded. | Nothing to repair. The new activation waits for its own completion. |
| A give-up result of failed reads as a real failure in the log. | Give-up reports the result you authored. | Choose timeout as the give-up result when you want the reason to survive up the tree. |
- Condition and event: the event side of waiting.
- Interrupt: what takes a running branch away, and why.
- Method: the Result card in its immediate form.