A run is either launched by hand or bound to a pass. A schedule binds a run request to a pass, with a start relative to its AOS. The scheduler, a stateless service, watches the passes and the schedules, evaluates each schedule again whenever its pass changes, and submits the run once, at its start, if it still fits in the pass. Recurring rules create such schedules by themselves for every pass that matches a filter.
Where planning is mandatory (hazardous_requires_plan), a run that sends a hazardous
telecommand starts only from a schedule, and a supervisor validates its plan beforehand: see
Hazardous Confirmations.
A schedule#
schedule:
- id: sch-0142
pass: gs-a-2026-10-02T1014Z-sat1
start: aos + 30 s
run: "Hot standby test"
targets: {sat: sat1-fm}
inputs: {tcu: TCU2}| Field | Required | Meaning |
|---|---|---|
id | yes | Identifier, a token (letters, digits, -, _) |
pass | yes | Pass, by its stored identifier or that of one of its contributions |
start | yes | Start relative to the AOS: aos, or aos + <duration> |
run | yes | Procedure |
targets | yes | Target of each role; one of them must be the target of the pass |
inputs | no | Input values, as in a run request (28 V, TCU2, true) |
library | no | Library of the procedure, when several have a procedure of that name |
environment | no | Environment; by default the last environment of the target of the pass |
start is aos or aos + followed by a duration: aos + 30 s, aos + 2 min, aos+90s. Any
other form (los - 30 s) is refused.
The MCS fills in the rest of the schedule, which the API and the CLI show:
| Field | Meaning |
|---|---|
state | scheduled, at_risk, cancelled, fired or missed |
reasons | Why it is at risk, cancelled or missed |
target | Target of the pass |
ir | Hash of the resolved run in stellar_ir |
max_duration | Maximum duration of the run, computed at compile time |
sends | Whether the run sends telecommands |
needs_validation | Whether a supervisor must validate the plan before it starts |
validation | The validation of the plan, while it holds |
fire_at | Start time on the current pass |
run_id | Run submitted, once fired |
by, updated_at | Author and last change |
Creating a schedule#
stellar schedules add schedules.yamlstellar schedules add posts each entry of schedule: to POST /v1/schedules, and each entry
of rules: to POST /v1/schedule-rules (see Recurring rules). A file can
hold both.
At creation, the MCS:
- finds the pass (a pass named by the identifier of a contribution is recorded under its stored identifier) and checks that a role of the schedule targets it;
- resolves the run request on the current configuration, with
planset to the pass, and stores the resolved run instellar_ir; - records its maximum duration, whether it sends telecommands, and whether it needs a validation;
- checks that it fits in the pass (below), and that its start is in the future.
A schedule that does not fit, a start already past, an unknown pass or a request that does not
resolve is refused with 422 schedule::refused, with the reasons. A body that does not parse is
refused with 422 schedule::invalid. The environment of the run decides who may create it (see
Identity and Roles).
A schedule not fired yet can be replaced: post it again under the same identifier. Replacing a schedule drops its validation. A fired schedule cannot be replaced.
Fitting in the pass#
A schedule is scheduled only when all these rules hold on its pass as it is:
- Before the LOS. The run, from its start and for its maximum duration, ends before LOS minus
scheduler.margin(30 s by default). - In order. It does not start before the possible end of a run scheduled before it on the same pass. Several runs share a pass, one after the other.
- Booked in orbit. In
in_orbit, the pass isbooked. - Uplink. A run that sends telecommands needs a pass with
uplink. - Validated. A schedule that needs a validation has one that still holds.
The maximum duration of a procedure is computed at compile time, retries included; see Safety Rules and Maximum Duration.
States#
stateDiagram-v2
[*] --> scheduled
[*] --> at_risk: needs a validation
scheduled --> at_risk: pass changed, validation fell
at_risk --> scheduled: pass corrected, plan validated
scheduled --> fired: start reached, run fits
scheduled --> missed: rest of the pass too short
at_risk --> missed: start reached while at risk
scheduled --> cancelled: pass or operator
at_risk --> cancelled: pass or operator
fired --> [*]
missed --> [*]
cancelled --> [*]| State | Meaning |
|---|---|
scheduled | Will start at its time |
at_risk | Will not start unless its pass changes again, or its plan is validated; see reasons |
cancelled | Its pass was cancelled, or an operator cancelled it |
fired | Its run was submitted; run_id names it |
missed | Its time came while at risk, or the rest of the pass was too short: nothing was sent |
cancelled, fired and missed are final. An at_risk schedule becomes scheduled again when
its pass is corrected. A schedule whose pass is unknown is at_risk.
Continuous evaluation#
The scheduler watches stellar_passes and stellar_schedule. It evaluates every schedule not
final at each change and at least every second, and writes a change of state with its reasons by
a conditional write: a concurrent change by an operator is evaluated again at the next round.
At its start time, a scheduled schedule is fired if the run still fits in the rest of the
pass — from now, for its maximum duration, before LOS minus the margin. Otherwise it is missed.
Firing once#
The scheduler submits the run on stellar.run.submit with Nats-Msg-Id =
<schedule>-<pass>, and by = schedule <id>. JetStream deduplicates a second submission: a
scheduler instance that submitted the run and stopped before recording it finds the duplicate on
its next round, and the schedule is fired. Several scheduler instances can run at the same time.
The run is then an ordinary run: follow it with stellar watch <run>, answer its questions,
suspend it. See Running Procedures.
Cancelling a schedule#
stellar schedules cancel sch-0142POST /v1/schedules/{id}/cancel cancels a schedule that is not final, as the operator of
X-Stellar-User (--as). Its reason records who cancelled it. A final schedule answers
409 schedule::final, a schedule changed meanwhile 409 schedule::changed.
Plans validated by a supervisor#
In an environment where a supervisor confirms hazardous telecommands (hazardous_confirmation
contains supervisor), a schedule whose run sends a hazardous telecommand is marked
needs_validation at creation. It stays at_risk ("awaiting the validation of the plan by a
supervisor") and does not start until a validation holds.
stellar schedules --as bob --role supervisor validate sch-0142 # with identity: declared
STELLAR_TOKEN=… stellar schedules validate sch-0142 # with identity: jwtPOST /v1/schedules/{id}/validate is reserved to an identity with the supervisor role, allowed
by the environment of the run (403 schedule::not-supervisor otherwise). It records what was
validated:
{"by": "bob", "at": "…", "pass": "gs-a-2026-10-02T1014Z-sat1",
"aos": "2026-10-02T10:14:32Z", "los": "2026-10-02T10:24:05Z", "ir": "5f2c…"}At every evaluation, the scheduler compares the validation with the schedule and its pass: same
resolved run (procedure, inputs, targets), same pass, same AOS and LOS. At the first difference,
the validation falls for good ("the validation of bob fell: the window of pass … changed") and the
schedule is at_risk again; a new validation is needed. A schedule still without a validation at
its start is missed, and nothing is sent.
Recurring rules#
A rule makes a schedule on every pass of a target that matches its filter. It suits routine activities, such as a housekeeping dump on every booked pass above 20°:
rules:
- id: hk
target: sat1-fm
filter: {status: booked, min_elevation: 20 deg}
start: aos + 30 s
run: "Housekeeping dump"
targets: {sat: sat1-fm}
inputs: {tcu: TCU1}| Field | Required | Meaning |
|---|---|---|
id | yes | Identifier, a token |
target | yes | Target of the passes; a role must target it |
filter | no | Passes it applies to; every pass of the target without |
start, run, targets, inputs, library, environment | As in a schedule |
| Filter key | Matches |
|---|---|
status | Passes of this status (booked) |
min_elevation | Passes whose maximum elevation is at least this angle (20 deg) |
station | Passes over this station |
uplink | Passes with (true) or without (false) uplink |
A cancelled pass never matches.
At creation, the rule's request is resolved on the current configuration. A procedure that sends
a hazardous telecommand is refused: it cannot run from a rule. Rules are stored in the
stellar_schedule_rules bucket.
At each evaluation, the scheduler creates the schedule <rule>-<pass> (with by = rule <id>)
for each matching pass that has none yet and whose start is in the future. It goes through the
same checks as an explicit schedule; a refusal (a run that does not fit, for instance) is logged
and nothing is scheduled for that pass. Removing a rule keeps the schedules it already made.
stellar schedules rules # the rules
stellar schedules remove-rule hk # no new schedule from hkListing schedules#
stellar schedules # schedules not final
stellar schedules --pass gs-a-2026-10-02T1014Z-sat1
stellar schedules --all # fired, missed and cancelled toosch-0142 gs-a-2026-10-02T1014Z-sat1 2026-10-02T10:15:02Z « Hot standby test » scheduled
hk-gs-a-2026-10-02T1327Z-sat1 gs-a-2026-10-02T1327Z-sat1 2026-10-02T13:27:32Z « Housekeeping dump » at_risk: pass `gs-a-2026-10-02T1327Z-sat1` is receive-only and the run sends telecommandsThe options of stellar schedules (--pass, --all, --api, --as, --role, --token) come
before its subcommand.
CLI and API#
| CLI | API | Effect |
|---|---|---|
stellar schedules [--pass P] [--all] | GET /v1/schedules (?pass=, ?all=true) | List schedules, by start |
| — | GET /v1/schedules/{id} | One schedule |
stellar schedules add <file> | POST /v1/schedules, POST /v1/schedule-rules | Create or replace schedules and rules |
stellar schedules cancel <id> | POST /v1/schedules/{id}/cancel | Cancel a schedule |
stellar schedules validate <id> | POST /v1/schedules/{id}/validate | Validate the plan, as a supervisor |
stellar schedules rules | GET /v1/schedule-rules | List rules |
stellar schedules remove-rule <id> | DELETE /v1/schedule-rules/{id} | Remove a rule |
The scheduler counts the runs it submits in the Prometheus counter
stellar_scheduler_runs_total, labelled by outcome (fired, missed).