Scheduler
The Schedule module is Starship's autonomous, multi-night brain. You give it a list of targets and how many frames you want of each; it decides — night after night — what to shoot next, points there, images, and keeps a running tally so it picks up exactly where it left off the next clear night. Left running under a blockscript, it will work a whole target list to completion across as many nights as it takes, opening and closing the roof, waiting out clouds, and avoiding the Moon, with no one watching.
This guide is task-first: how to set it up and run it. The scoring internals and the full config reference are at the end.
---
The mental model
- A project is one target (name + coordinates + a few rules).
- A project owns one or more goals — a goal is *"N frames in filter X at E
seconds."* A mono camera has several goals per target (L, R, G, B, or Ha/OIII/SII); a one-shot-colour (OSC) camera has a single goal.
- The ledger counts the good frames you've actually banked for each goal. It
lives in a database (scheduler.db) and survives restarts and reboots, so a campaign always resumes — it never re-shoots what it already has.
- A template (recipe) is how every target is imaged — the slew/focus/guide/
dither cadence — with the target itself left as a variable the scheduler fills in.
- A strategy decides the order: interleave the best target right now, or finish
one target before moving on.
You build the target list once, choose a couple of campaign settings, and start it.
---
Quick start (single night, supervised by you)
- Set your site first. Equipment → Site / Location. With
[site]left at 0,0
the scheduler refuses to start ("[site] latitude/longitude required") and all the altitude/Moon math is meaningless. This is the #1 cause of a scheduler that won't run.
- Open Schedule in the left nav.
- In Add a target, type a target into Target name (e.g.
M42) and click
Resolve — it fills RA h and Dec ° and previews the target right now: its altitude (and whether it's above your min alt), and — if Moon avoidance is on — its separation from the Moon, the Moon's illumination, and whether broadband/narrowband can shoot it tonight (any filter ok / narrowband only / moon-washed). You can also type RA (decimal hours) and Dec (decimal degrees) by hand.
- Set Priority (1–10), Min alt ° (don't shoot below this), the
completion mode, and pick a recipe if you saved one. Then add the goals: each row is Filter, Exp s, Frames — click + filter to add more (L+R+G+B or Ha+OIII+SII in one go).
- Click Add project. Repeat for each target.
- In Campaign settings, choose a Strategy, optionally set a **Default
recipe and tick Moon avoidance, and click Save**.
- Click Simulate tonight to see the plan (nothing moves), then Start to run
it for real. Stop halts it.
A readiness stepper across the top walks you through those steps in order — Location → Strategy → Targets → Filters & frames → Simulate & start — colouring each green as it's satisfied, with a one-line banner telling you the next thing to do. Start stays locked until your site is set, you have at least one target, and every active target has filters and frame counts. Below the stepper, a live now strip shows the run state, roof open/closed, dark/daylight, the Moon, and tonight's progress; when a visit is running it adds a NOW IMAGING line with the target and filter.
That covers a single night you're keeping an eye on. For a hands-off multi-night campaign, see Running unattended below.
---
Defining targets and goals
The Add a target form (target fields on top, then one row per filter):
| Field | Meaning | Default | |---|---|---| | Target name | Name + the plan label | — | | Resolve | Looks up RA/Dec by name and previews altitude + Moon right now | — | | RA h | Right ascension, decimal hours | — | | Dec ° | Declination, decimal degrees | — | | Priority | 1–10, feeds the score | 5 | | Min alt ° | Eligibility floor — never imaged below this | 30 | | Completion mode | spread or finish first (see below) | spread | | Recipe | The per-target template; "default recipe" = built-in | default | | Filter | A goal's filter — type L/R/G/B/Ha… (mono) or your OSC filter name | L | | Exp s | Sub exposure for that goal | 120 | | Frames | How many good frames you want (the goal target) | 30 |
Two things to know:
- Add as many filter goals as you like right on the form. Click + filter for
another row — a mono target gets L+R+G+B or Ha+OIII+SII in one submit. There are no binning or gain boxes here on purpose — binning/gain come from the recipe. If you do need a per-goal binning/gain override, add that goal via the API (/api/scheduler/goals/add, which takes binning/gain).
- Mono vs OSC is just what you type in Filter. There's no camera-type switch —
a mono rig gets a goal per filter; an OSC rig gets one goal (e.g. filter OSC).
Each project row in the Projects list shows priority, min-alt and mode, a per-filter progress bar (Ha 40/60), the frame tally, and — while the campaign is live — a status badge in plain language explaining exactly why it is or isn't imaging right now: imaging now, paused, complete, still rising / too low (with the current altitude vs the floor), Moon too close (with the separation and Moon %), waiting for the meridian, sets in N min, or ready — waiting its turn. Each row has pause/resume and delete. Paused (inactive) projects are skipped entirely; deleting a project removes its history.
The ledger only counts confirmed frames. A goal's tally advances by the number of exposures the sequencer journal actually confirmed ok after each visit — not by the number requested. A failed or partial visit advances it by exactly what landed, so the campaign always converges on the true target count. Over-shooting a goal is clamped.
---
Strategy — the order it works in
Set globally in Campaign settings → Strategy:
- Dispatch — best target now (default). Every ~45 minutes it re-scores all
eligible targets and shoots the current best, then re-scores. This interleaves targets across the night — grabbing whatever is highest/most urgent — and is the right default for a mixed list.
- Finish target. It stays on one target until that target's goals are complete
or it sinks below its min altitude, then moves to the next best. Use this when you'd rather fully bank one object before starting another.
There's also a per-project completion mode that shapes the score:
- spread (default) — boosts neglected projects, so work spreads evenly
across your whole list.
- finish first — boosts nearly-done projects, so they get drained first.
(Strategy is the night-level behaviour; completion mode is a per-target nudge inside the score. They compose.)
---
Recipes (templates) — the "variable target"
A recipe is a normal sequence — your slew → autofocus → guiding → dither/refocus cadence and options — but with the target left as a variable the scheduler fills from your project list. Build the recipe once; every scheduled target runs through it.
To create one:
- In the Sequencer editor, build a sequence the way you want every target shot.
Put the target you want to be the variable first.
- Click Save as template, give it a name. Starship flags the first target as the
scheduler's placeholder and saves the recipe under plans/templates/.
- Back on Schedule, pick that recipe per-project (the Recipe dropdown) or set
it as the campaign Default recipe.
When the scheduler picks a target, it drops that target's name/RA/Dec into the placeholder and injects its remaining per-filter goals as the exposures — so the recipe's focus/guide/dither logic is preserved and only the target and frame counts change. Recipe resolution order: a project's own recipe → the campaign default recipe → the built-in default.
One rule worth knowing: set the altitude floor on the project (Min alt °), not in the recipe — the scheduler forces the recipe's altitude constraint to match the project's floor so it never picks a target the run then skips.
---
Saved schedules — reuse a whole target list
Where a recipe is how every target is shot, a schedule is a named snapshot of which targets — your entire project pool (targets, priorities, min-alts, modes, recipes, and all their filter goals) saved as one portable document you can bring back any time.
On the Saved schedules card: type a name (and an optional description) and click Save current to snapshot the pool as it stands. Each saved schedule then offers:
- Recall — adds its targets to whatever is already in the pool.
- Replace — clears the current pool first, then loads this schedule's targets.
- ✕ — delete the saved schedule.
A recalled schedule always starts a fresh campaign at 0 frames — the frame tallies are stripped on save, so recalling a "winter broadband" or "narrowband survey" list re-runs it clean rather than resuming an old ledger. (To resume a campaign you never deleted, you don't need this at all — the live pool and its ledger already persist; see below.) Schedules are files under plans/schedules/, so they're shareable just like recipes and sequences.
Saved schedules also feed the unattended path: a blockscript's run_scheduler action can name one to load before the campaign begins (see Running unattended).
---
Moon avoidance
Off by default. Tick Moon avoidance in Campaign settings (or [scheduler] moon_avoidance = true) to enable per-filter Moon gating. With it on, a goal is only shot when the Moon is down, or the target is far enough from a not-too-bright Moon. Broadband and narrowband get different limits:
| | Min separation | Max Moon illumination | |---|---|---| | Broadband | moon_min_sep_deg (30°) | moon_max_illum (1.0 = no cap) | | Narrowband | moon_min_sep_deg_nb (10°) | moon_max_illum_nb (1.0) |
Narrowband is recognised by filter name (narrowband_filters — Ha,OIII,SII,S2,O3,NB,SHO,HOO,Halpha, case-insensitive). If every open goal on a target is Moon-blocked right now, that target drops out of contention until the Moon sets or moves — so under a bright Moon you'll see narrowband targets keep working while broadband ones wait for the Moon to set. That's the gate doing its job; to shoot broadband anyway, raise moon_max_illum.
By default the required separation scales with Moon phase (moon_sep_scales_with_illum, on): a thin crescent needs far less clearance than a full Moon. The separation above is the full-Moon requirement; at 30% illumination a broadband target only needs ~9° (30° × 0.30), so a slim crescent stops needlessly blocking targets the old fixed rule would have rejected. Turn it off to use the fixed separation regardless of phase.
---
The roof
By default ([scheduler] manage_roof = true) the scheduler runs the roof itself: it opens and unparks when it's dark and safe, and parks then closes at dawn, on an unsafe verdict, when all goals are done, or when you stop it. (Parking before closing keeps the OTA clear of the moving roof.) CloudWatcher remains hardwired to the roof as the independent hardware backstop.
If a blockscript is supervising the night, turn this off and let the blockscript's own open_roof/close_roof own the roof instead (see next).
---
Running unattended (all night, many nights)
The Schedule page's Start runs the scheduler stand-alone — good while you watch. For a truly hands-off campaign that cycles night after night until the whole list is done, wrap it in a blockscript:
- Build your target list and recipes as above.
- Open the Blockscripts editor. Create a blockscript, put your STARTUP
actions (unpark, cool, start guiding, etc.) and SHUTDOWN/safety actions in, and in the RUNNING block add one action of type run_scheduler. Its one optional field is a schedule dropdown: leave it on current pool to run the live project list, or pick a saved schedule to load that target list first.
- Start the blockscript. It walks STARTUP → RUNNING;
run_schedulerlaunches the
dispatcher, which images the best eligible target every clear, dark, safe night — swapping targets through your recipe — until every active project's goals are filled. Then it signals the blockscript, which runs SHUTDOWN and ends cleanly.
Under a blockscript the scheduler runs each visit as an injected run and lets the blockscript own the roof/mount/park and the safety response — they never fight over the hardware. Stand-alone, the scheduler uses its own darkness/safety/roof gates (manage_roof).
Auto-start on boot. At the top of the blockscript list, the ⟳ Auto-start on boot selector (saved to [blockscript].auto_start) names a blockscript to relaunch automatically when Starship starts — so an interrupted campaign resumes from the persistent ledger. Caveat: this only fires if Starship's process is already running at boot; on a fresh power-up something at the OS level still has to launch Starship first (a Windows startup task / service). Without that, auto-start has nothing to run inside. Set it to "(save to apply)" and Save config for it to take effect.
---
Simulate tonight
Simulate tonight runs the same pick/score logic as the live dispatcher, driven by a clock that doesn't wait, and draws tonight's plan as a colour-coded timeline strip — one bar per visit across the night, coloured by target, with the filter labelled and the altitude on hover. Below it, a "Not scheduled tonight — why" panel lists every active target that didn't make the plan with the reason (too low, Moon-blocked, complete, or simply "up, but didn't win a slot"). Nothing moves.
Two things the tonight-sim does not model, by design: it ignores Moon avoidance (it plans as if the gate were off) and it uses the real Sun for darkness but not the soft transit relaxation. For a faithful multi-night run with real Sun and Moon, moon gating, roof cycling and weather closures, there's a campaign simulation (scripts/_campaign_sim.py) that plays a whole week forward. Use both to sanity-check a plan before you trust a night to it. Ask Pete additionally flags targets that never rise from your latitude or have malformed goals.
---
How it decides (scoring reference)
Each eligible target gets a score = the sum of four weighted terms (weights in [scheduler]):
- Altitude (
w_altitude1.0) —sin(altitude); higher in the sky scores more. - Setting urgency (
w_setting0.6) — `1 / (1 + hours until it drops below min
alt)`; a target about to set is grabbed first.
- Priority (
w_priority1.0) — your 1–10 priority / 10. - Completion (
w_completion0.5) — uses1 − completionfor spread projects
(favour neglected) or completion for finish first (favour nearly-done).
- Meridian (
w_transit0.5) — peaks when the target is at transit and falls to 0
at ± transit_window_min (default ±120 min). This pulls each target toward the meridian, where airmass is lowest. Set transit_window_min = 0 to switch it off.
Eligibility (checked before scoring): the project is active, has frames remaining, is above its min altitude, has at least one goal not currently Moon-blocked, and — if transit_gate is on — is within the transit window. Hysteresis (0.15): the current target keeps going unless a challenger beats it by that margin — this stops two near-equal targets ping-ponging. A visit is chunk_minutes long, capped by what the goal still needs and the time left until the target sets or dawn (near set/dawn a visit shrinks to zero and is skipped).
Don't waste a clear night (allow_relax, default off): when nothing meets every constraint, the scheduler can relax the soft ones in order — first the transit window, then Moon avoidance — and shoot the best of what's left, rather than idling. With it off it idles until a target qualifies.
---
Config reference ([scheduler])
| Knob | Default | Effect | |---|---|---| | strategy | dispatch | dispatch (re-score, interleave) or finish_target (stay on one) | | chunk_minutes | 45 | Visit length before re-scoring (5–240) | | dark_sun_alt_deg | −18 | Sun must be below this to image; −12 = nautical | | hysteresis | 0.15 | Margin a challenger must beat the current target by | | precise_slews | true | Plate-solved slew-and-centre per target | | wait_for_safe | true | Wait out UNSAFE/clouds and resume, rather than abort | | manage_roof | true | Scheduler opens/closes the roof (off → blockscript owns it) | | roof_park_before_close | true | Park the mount before closing the roof | | default_template | "" | Recipe for projects with none of their own | | moon_avoidance | false | Master switch for per-filter Moon gating | | moon_min_sep_deg / _nb | 30 / 10 | Min target–Moon separation, broadband / narrowband | | moon_max_illum / _nb | 1.0 / 1.0 | Max Moon illumination, broadband / narrowband | | narrowband_filters | Ha,OIII,SII,… | Filter names treated as narrowband | | moon_sep_scales_with_illum | true | Scale required Moon separation by illumination (phase-aware) | | w_altitude / w_setting / w_priority / w_completion | 1.0 / 0.6 / 1.0 / 0.5 | Score weights | | w_transit | 0.5 | Meridian-preference weight | | transit_window_min | 120 | ± minutes from transit the meridian boost spans (0 = off) | | transit_gate | false | Make the transit window a hard cut, not just a preference | | allow_relax | false | When nothing qualifies, relax transit then Moon rather than idle | | db_path | scheduler.db | The project + ledger store |
Per-project (set on the Schedule page, not in [scheduler]): priority, min_alt_deg, completion_mode, template.
---
Gotchas
- Site coordinates are mandatory — 0,0 blocks Start and Simulate.
- The form adds one goal; add more filters via the recipe or the goals API
(binning/gain live there, not on the page).
- The operator always wins — a manual sequencer run blocks scheduled visits
(sequencer busy); the scheduler waits rather than fight for the camera.
- The ledger persists in
scheduler.db— restarting or rebooting resumes the
campaign; at most the one in-progress visit re-shoots (its frames are already on disk, not double-counted).
- Darkness and safety are re-checked, never assumed — twilight ending or the
safety verdict going UNKNOWN parks the loop in a waiting state; it does not push frames into bad sky.
See also the Blockscript, Sequencer, Safety supervisor, and Ask Pete topics.