Author a new Bluesky plan for the Bluesky MCP server: the plan-file format (PLAN_METADATA/PARAMS/build_plan), the allowlist the validator enforces, and the author -> validate -> run -> contribute workflow. Use when asked to write, draft, or author a new Bluesky plan, or when an existing plan needs editing before re-validation. NOT for operating an already-registered plan (use the operating-bluesky-scans skill).
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Author a new Bluesky plan for the Bluesky MCP server: the plan-file format (PLAN_METADATA/PARAMS/build_plan), the allowlist the validator enforces, and the author -> validate -> run -> contribute workflow. Use when asked to write, draft, or author a new Bluesky plan, or when an existing plan needs editing before re-validation. NOT for operating an already-registered plan (use the operating-bluesky-scans skill).
summary
Author, validate, and launch a session-tier Bluesky plan
Writing Bluesky Plans
Author a new plan as a plain-text file, get it machine-validated in a
sandbox with no hardware access, then launch it through the normal
author -> validate -> run -> contribute workflow. A plan you write is inert until validate_plan records a
pass for its exact content — nothing you author here is ever imported or
run directly.
The plan-file format
A plan file is a single Python module exposing exactly three things:
PLAN_METADATA — a plain dict with five required keys, all required
(a plan missing one is rejected at load time, not defaulted):
name (str) — the plan's name.
description (str) — human-readable summary.
category (str) — free-text grouping shown to operators (e.g.
"accelerator").
required_devices (list[str]) — names of the PARAMS fields that name
devices the plan drives or reads (e.g. ["correctors", "detectors"]).
writes (bool) — whether the plan moves a device (vs. read-only).
Authoring metadata only; it has no effect on whether writes actually
happen — that is governed entirely by control_system.writes_enabled.
PARAMS — a pydantic.BaseModel subclass declaring the plan's own
parameters (device names, ranges, point counts, ...). Use Field(...)
constraints and a model_validator where it helps (e.g. rejecting a
device named as both a driven setpoint and a read detector).
build_plan(devices, params) — a callable taking devices: dict[str, Any] (resolved by string name, injected by the bridge — never free names
in a namespace) and params: PARAMS, returning a bluesky generator
(typically built with bluesky.plan_stubs/bluesky.plans/
bluesky.preprocessors).
Study the two shipped plans for the full worked pattern — do not
invent new accelerator physics:
orm (src/osprey/services/bluesky_bridge/plans_core/orm.py)
— sweeps each corrector over a bounded current range, reading every BPM
detector at each point, to measure an orbit-response matrix.
grid_scan (src/osprey/services/bluesky_bridge/plans_core/grid_scan.py)
— steps a set of setpoint devices over a rectangular grid, reading a set of
detectors at every grid point.
These are the ONLY accelerator scan patterns this framework ships. Never
propose or author a BBA (beam-based alignment) or tune-scan plan — they are
explicitly out of scope.
The allowlist the validator enforces
validate_plan runs your file's body through three ordered stages,
any of which can reject it outright before the next ever runs:
Static import allowlist — only these imports are permitted:
bluesky.plan_stubs, bluesky.plans, bluesky.preprocessors
(submodule-exact — bare import bluesky or bluesky.utils is
rejected).
numpy, scipy, math, statistics, time, collections,
itertools, functools, pydantic.
CA/connector pattern scan — rejects any body matching caput(,
caget(, epics., aioca, caproto, write_channel(, read_channel(,
_osprey_connector, or PV(. Ordinary numeric/stdlib calls that merely
share a method name (numpy.put(...), dict.get(...), queue.put(...))
are NOT flagged — device I/O only ever happens through the devices dict
build_plan is handed, never through a raw control-system import.
Mock-device dry run — actually builds and drives your build_plan
generator to completion against in-process mock devices, in a subprocess
with EPICS_CA_* neutralized. This is an authoring-quality check ("does
it actually run"), not the containment boundary — containment is stages 1
and 2 plus the load/launch gates that key off the validation record.
Foot-gun: use bps.sleep(...), never time.sleep(...).time.sleep
blocks the RunEngine's worker thread for its whole duration — no other plan
step, status update, or stop request can be serviced until it returns.
bluesky.plan_stubs.sleep(...) yields a message the RunEngine schedules
cooperatively, so the run stays responsive. time is on the import
allowlist for ordinary bookkeeping (computing a delay, timestamping) — it is
never a substitute for bps.sleep inside a plan's own control flow.
Workflow: author -> validate -> run -> contribute
Author — write_plan(name, category, required_devices, writes, body, description=""). body is your PARAMS + build_plan
source (no PLAN_METADATA block — the bridge assembles and prepends one
from your other arguments). Writes a session-tier file; reaches no
hardware. Re-authoring the same name overwrites the file and drops any
prior passing validation (its content hash changes).
Validate — validate_plan(name, sample_args=None, dry_run_timeout=30.0). Validates the file's CURRENT on-disk content
(never a body you pass directly) through the three stages above.
sample_args should supply realistic PARAMS field values so the dry
run's mock devices match what your plan expects. A pass is what makes the
plan loadable at all — an unvalidated session plan is never listed,
loaded, or launchable.
Confirm it's live — list_plans() to see the plan appear with
provenance: "session" alongside its metadata.
Run — stage the validated plan into the shared draft with
set_draft(plan_name, plan_args_patch=...) (motion-safe, no device
touched — it only fills the plan panel and returns a revision), then
launch_run(draft_revision) is the sole launch path: it re-checks the
validation record against the file's current hash, requires
control_system.writes_enabled, and needs human approval. Use
get_run(run_id) / get_run_data(run_id, ...) to watch it run. The
operating-bluesky-scans skill covers this run flow in full — staging the
complete configuration, launching at a pinned revision, 409 recovery, and
stopping.
Contribute to the permanent catalog — a session plan stays
session-tier (least trusted, most ephemeral) until a human reviews it and
contributes it into a facility catalog directory; that is a separate
follow-up step, not something this skill or any MCP tool does
automatically.
Anti-patterns
Never import or reference EPICS/CA/connector internals directly
(epics, caput/caget, _osprey_connector, raw PV names) — all device
I/O goes through the devices dict build_plan receives.
Never use time.sleep(...) inside a plan body — use bps.sleep(...).
Never propose a BBA or tune-scan plan — orm and grid_scan are the
only scan patterns this framework ships.
Never hard-code a facility device name inside build_plan — resolve
every device by string name through the injected devices dict, exactly
like both exemplars.
Never treat a passing dry run as proof the plan is safe against real
hardware — it proves the plan runs, not that its device motion is
physically sound. Human approval at launch is the real backstop.
Never include a from __future__ import ... line in your body — the
bridge always prepends a generated PLAN_METADATA assignment ahead of it,
so it can never be the file's first statement (a hard Python requirement);
modern type hints (list[str], dict[str, Any]) work without it on
Python 3.9+.