Plan format (TOML)
Plans are TOML documents. The structured editor (Targets / Constraints / Focus / etc.) generates them; you can also edit them directly.
Top-level
[meta]
name = "M42 LRGB"
notes = "test imaging plan"
author = "Pete" # optional
created = "2026-06-28" # optional, ISO time
version = 1
[defaults] # optional: default args shared by steps
binning = 1
[variables] # optional: initial variable values
target_name = "M42"
[[steps]]
kind = "..."
A plan is a list of [[steps]]. Each step has a kind and per-kind args. The optional [defaults] and [variables] tables give you plan-wide default values and starting variables you can reference from steps.
Step kinds
notes— comment / no-op (also used to embed plan-wide policy)set— assign one or more variables (kind = "set", then anykey = value)wait— sleepsecondswait_until— wait for a time or a condition (see below)slew— move mount (namedtovia planetarium, orra_h/dec_deg);precise = trueplate-solves and recenters after the slewfilter— switch filter wheel bynameorposition(no-op if no wheel is connected)expose— take framesdither— nudge the mount via PHD2 between framesfocus— move the focuser to a positionpark/unpark/track— mount commands (trackneedson = true/false)flat_cover— open or close a flat-panel cover (action = "open"|"close")warm_camera— warm the camera up (ramp the cooler off)run_script— run an external command or scriptrotate— rotate to a sky position angle (pa_deg) via plate-solvingstart_guiding/stop_guiding— start or stop PHD2 guidingrepeat— repeat a body N timesloop_targets— iterate over a list of targets
expose
[[steps]]
kind = "expose"
count = 20
exposure_s = 300
binning = 1 # optional; omit to use [camera] default_binning
gain = 100 # optional
offset = 30 # optional
light = true # optional; false for calibration frames
object_name = "M42" # optional; defaults to the current target name
filter_name = "Ha" # optional; recorded in the file header
dither
A dither step nudges the mount through PHD2 so hot pixels and walking noise average out. It only fires when guiding and dithering are enabled for the plan (Guiding tab) and PHD2 control is on — otherwise it's a harmless no-op that just records the request. The nudge size defaults to the plan's dither pixels; override it with amount_px. The settle gate (settle pixels / time / timeout) comes from the plan's guiding settings.
You usually don't need explicit dither steps: when guiding + dithering are on, Starship automatically dithers every N frames (the "dither every N exposures" setting) between subs, no matter how they're grouped.
focus
A focus step moves the focuser to a set position:
[[steps]]
kind = "focus"
position = 2500
relative = false # optional; true moves by an offset instead
Automatic refocus (running a real autofocus during the run) is driven by the plan-level focus policy, not by the focus step — see below. A focus step with autofocus = true is reserved and not yet wired.
wait_until
wait_until blocks until the first matching condition is met (checked in this order):
safe = true— wait until conditions are safe to observealt_min_degwithra_h/dec_deg— wait until the target climbs above that altitude (needs your site coordinates)time(orwhen) — an absolute ISO timetime_local = "HH:MM"— the next occurrence of a local wall-clock time
Add timeout_min to give up after that many minutes (the step then fails, so its error policy decides what happens next; 0 means fail immediately). Omit it to wait indefinitely.
Plan-level policy
Policy from the structured editor's tabs is encoded in a single hidden notes step at the top:
[[steps]]
kind = "notes"
label = "plan policy"
_policy = { constraints = { altitude_min_deg = 30.0, ... }, focus = { ... }, cooling = { ... }, meridian = { ... }, guiding = { ... }, on_end = { ... }, phases = { ... } }
The executor reads this on plan start and applies the policies as the run progresses. The blocks cover:
constraints— altitude / hour-angle limits and anend_by_utccutoff; a target that drifts out of limits is skipped, not aborted.focus— automatic refocus triggers:every_n_frames,every_x_minutes,delta_temp_c(focuser temperature drift), andon_filter_change, plus afocuser_positionfallback for rigs without autofocus. When a trigger fires and autofocus is calibrated, a real refocus runs (and streams its live V-curve to the run view); otherwise the focuser moves to the fallback position.cooling— enable the cooler at start (with a setpoint and optional wait-to-settle) and warm up at the end.meridian-mode = "inherit" | "off" | "halt" | "flip"(inherit, the default, follows[mount].flip_mode;halt/flipoverride it and the run monitor warns when that widens the mount setting), amax_minutes_pastsafety clamp, andrecenter_after_flipto plate-solve and recenter after a flip.guiding— start/stop guiding and dithering (settle gates, dither pixels/cadence).phases— actions to run at run boundaries (on start / on suspend / on resume / on end / on error), e.g. warm the camera and close the cover when the run ends.
You can edit any of this directly in the TOML tab if you want fine control.
Per-step error handling
[[steps]]
kind = "expose"
on_error = "retry" # 'abort' | 'skip' | 'retry'
retry_count = 2
abort stops the sequence, skip logs and moves on, and retry retries up to retry_count times before falling back to skip. Every step can also carry an optional label that shows up in the run journal.
Variable substitution
Loop bodies can reference $target_name, $target_ra_h, $target_dec_deg (bound by loop_targets), the iter counter inside a repeat, and any variable set with a set step or in the [variables] table. Whole-string references preserve type ("$target_ra_h" resolves to a number). Partial interpolation works too ("Imaging $target_name now"). Note that repeat.count itself must be a literal number, not a variable.
Why TOML
Human-readable, line-diffable, easy to share via email or git, no security risk on parse. All Starship config is TOML, so plans match.