Plans, plan stubs, and the @plan decorator#

Bluesky distinguishes between plans and plan stubs. Both are generator functions that yield messages for the RunEngine; the technical difference is what they yield.

This page explains the distinction, when it matters, and how the @plan decorator helps catch a common mistake.

The technical distinction#

Kind

What it does

Examples in bluesky

plan

Publishes Bluesky documents. Yields open_run / create / save / close_run messages.

bp.count, bp.scan, bp.rel_scan, bp.list_scan

plan stub

Does not publish documents. Yields set, read, wait, mv, sleep, etc.

bps.mv, bps.abs_set, bps.sleep, bps.null

Plans bracket a run; the RunEngine assigns a run UID and scan_id, and every document the plan yields becomes part of that run. After close_run, the RunEngine considers the run finished, fires “stop” subscribers (file writers flush, etc.), and the catalog gains one new entry.

Plan stubs run as part of a plan, but do not start or end a run on their own. They are the building blocks. bp.scan is internally a collection of bps.mv (move) and bps.trigger_and_read (acquire) stubs wrapped in open_run / close_run.

Why the distinction matters#

For using Bluesky, the distinction barely matters. You can pass either to RE(...):

RE(bp.scan([detector], motor, 0, 10, 11))   # a plan
RE(bps.mv(motor, 5))                        # a plan stub

Both work. The difference is what gets saved to the catalog:

  • The bp.scan produces a complete Bluesky run – a catalog entry with metadata, an event stream, and the data.

  • The bps.mv produces no catalog entry. The motor moves; nothing is recorded.

For writing Bluesky code, the distinction matters a lot:

  • Authoring a plan stub is easy: write a generator function that yields stubs. No open_run / close_run needed; the caller will provide those.

  • Authoring a plan requires you to call open_run / create / save / close_run (or use a helper like bluesky.preprocessors.run_decorator). Otherwise the data you acquire never gets attached to a run.

Most user code is plan stubs. Composing them into a plan is usually a matter of calling an existing bp.* plan, not writing one yourself.

The @plan decorator#

bluesky.utils.plan wraps your function so that, if you call it without RE(...) (or yield from), Python prints a warning shortly after you press Enter – usually right next to the next prompt.

It catches the common mistake of typing my_plan(...) at the IPython prompt instead of RE(my_plan(...)).

Compare:

# Without @plan
def my_plan():
    yield from bps.mv(motor, 5)

my_plan()    # silently does nothing; no warning
from bluesky.utils import plan

@plan
def my_plan():
    yield from bps.mv(motor, 5)

my_plan()
# RuntimeWarning: plan `my_plan` was never iterated,
#                 did you mean to use `yield from`?

The warning shows up shortly after the prompt returns – typically mixed in with the next prompt line. The traceback it prints points at your call site, not at internal Bluesky code, so you can see exactly which line you typed.

Convention in this repo#

All plans and plan stubs we author are decorated with @plan. See the AGENTS.md > @plan decorator on our own plans section. Examples in this repo:

  • LaserOptics.move_in / move_out – plan stubs as device methods

  • sim_count_plan, sim_print_plan, sim_rel_scan_plan – plans

The decorator works for both plans and plan stubs; it does not distinguish them. It also works on instance methods (self is passed through normally).

What @plan does not do#

  • It does not turn a non-generator function into a plan. The decorated function still has to yield something for the RunEngine to do.

  • It does not validate the message stream.

  • It does not prevent you from calling the function without RE(...) – it only warns. A tight loop or a script that exits quickly may finish before the warning is printed, so you may not see it in non-interactive contexts.

  • It does not perform any work at decoration time; the cost is one Plan object wrapper per call.

See also#

  • The RunEngine – why RE(...) exists at all.

  • Run a scan – using plans interactively.

  • Add a plan – writing your own plans, with the @plan decorator applied per repo convention.