feat(core): add scheduledExecution subject and events - #323
Conversation
A scheduledExecution is a registered intent to enqueue a target execution at a future time. It covers the lifecycle before enqueueing: the schedule is registered, possibly amended, and then either fires, is canceled, or does not fire at all. The existing queued predicates represent work already accepted into an execution queue, which is the earliest moment the vocabulary can express today. A schedule registered now to fire next week is invisible until it materializes as a queued execution, by which point the registration, every amendment, and any cancellation have left no trace. Making the schedule a subject records who scheduled an execution, whether and by whom it was changed or canceled, and whether it fired and when. Five predicates: created, modified, canceled, triggered, missed. triggered is the hand-off that subsequently produces the target's own queued event; missed distinguishes a schedule that never fired from an execution that fired and then failed. On triggered, author is required when trigger.type is manual and MUST NOT be present when it is schedule: the absence of author is what tells a consumer that elapsed time, rather than a person, fired the execution. trigger.type is required so neither branch can be evaded. The trigger object is reused byte-identically from the Testing vocabulary, including its existing schedule value. No new common objects are introduced. Event versions start at 0.1.0-draft. Signed-off-by: Dadisi Sanyika <dadisi@solduara.com>
BaLiKfromUA
left a comment
There was a problem hiding this comment.
Very excited to see this PR!
Hope this idea will be accepted to the spec.
Posting some comments/question and will keep an eye on this PR to provide more feedback later.
|
|
||
| The existing `queued` predicates represent work already accepted into an execution queue. A schedule registered today to fire next week is not visible in the event stream until it materializes as a queued execution, by which point the registration, any amendment to it, and any cancellation have left no trace. Making the schedule a subject records that history as it happens. | ||
|
|
||
| When a `scheduledExecution` fires, it produces the target's own `queued` event. Whether the resulting execution succeeds is reported by the target's events, not by this subject. |
There was a problem hiding this comment.
Is it a hard requirement that execution triggers only queued event? If so, why?
For my use-cases it would be better to produce started events for some scenarios, and queued for others.
There was a problem hiding this comment.
I assume this is because we have only scheduled time which most likely is not the same to actual execution time due to potential delays.
But from other side, it's described below that timestamp can be not the same as scheduledTime
| | author | `Object` | The actor the producer attributes the registration to | `{"id": "alice", "source": "/scheduler/prod"}` | ✅ | | ||
| | changeWindow | `Object` | A change window this execution is bound to, if any | `{"id": "win-maint-0812", "source": "/scheduler/prod"}` | | | ||
|
|
||
| ### [`scheduledExecution Modified`](conformance/scheduledexecution_modified.json) |
There was a problem hiding this comment.
As I mentioned in #305 (comment) , I think spec needs to explain/recommend what consumers should do if multiple modified events were produced for the same schedule.
This is not the rare situation where same schedule got changed in a short period of time.
As simple approach, spec can just say "last modification wins" but maybe there is a better approach.
| | target | `Object` | The execution to be enqueued, as it stands after the change | `{"id": "orders-svc", "source": "/prod/deploys", "type": "deployment"}` | ✅ | | ||
| | scheduledTime | `Timestamp` | The firing time as it stands after the change | `2026-08-12T02:00:00Z` | ✅ | | ||
| | author | `Object` | The actor the producer attributes the change to | `{"id": "alice", "source": "/scheduler/prod"}` | ✅ | | ||
| | changeWindow | `Object` | A change window this execution is bound to, if any | `{"id": "win-maint-0812", "source": "/scheduler/prod"}` | | |
There was a problem hiding this comment.
nit: should we also have non-required reason for schedule modification?
| |-------|------|-------------|----------| | ||
| | id | `String` | See [id](spec.md#id-subject)| `sched-deploy-orders-9281` | | ||
| | source | `URI-Reference` | See [source](spec.md#source-subject) | | | ||
| | target | `Object` | The execution to be enqueued. `type` names what is scheduled. | `{"id": "orders-svc", "source": "/prod/deploys", "type": "deployment"}` | |
There was a problem hiding this comment.
More schema question rather then conceptually:
Do we need somehow to limit and validate "types" that can be targeted?
I believe this idea has been mentioned before but not sure that it's implemented or planned to be implemented
There was a problem hiding this comment.
actually see it now that it's a design decision:
type is a free string — build, deployment, changeWindow — rather than a closed enum, so the subject is not coupled to a fixed list of schedulable things.
Changes
Adds a
scheduledExecutionsubject to the Core vocabulary, withcreated,modified,canceled,triggeredandmissedpredicates.A
scheduledExecutionis a registered intent to enqueue a target execution — a build, a deployment, a change window opening — at a future time. It covers the lifecycle before enqueueing: the schedule is registered, possibly changed, and then either fires, is canceled, or does not fire at all.Why a new subject
This came from an operator running scheduled deployments who needed the event stream to answer six questions: who scheduled a build or deployment, whether the schedule was subsequently changed, who changed it, whether it was canceled, who canceled it, and whether it actually fired and when.
The existing
queuedpredicates cannot answer any of them.build.queuedand theservicedeployment events represent work already accepted into an execution queue — the earliest moment the vocabulary can currently express. A schedule registered today to fire next Tuesday is invisible until it materializes as a queued execution, and by then the registration, every amendment to it, and any cancellation have left no trace.Making the schedule itself a subject puts that history in the stream.
triggeredis the hand-off: the occurrence that subsequently produces the existingqueuedevent for the target.authorontriggeredauthoris required whentrigger.typeismanual, and MUST NOT be present for any other value. The schema encodes both directions, and requirestrigger.typeso neither branch can be evaded.The biconditional is the point. Because the absence of
authoris what tells a consumer that no actor fired the execution, a time-fired event must not carry one — attaching the schedule's creator there would misreport a weeks-old registration as a person running something today. A pipeline or an event is likewise not a person, so those values carry noauthoreither.Manual firing is ordinary, not an exception. A scheduler's "Run Now" is the normal way an author tests a schedule before trusting it to fire unattended. It is therefore an attribute of
triggeredrather than a separate predicate, which would rest on the premise that "Run Now" is unusual. Audit interest in off-schedule firing of a sensitive target is a consumer policy — filter ontrigger.typeand target — not a vocabulary distinction.missedmissedis a live producer reporting that a scheduled execution did not fire. It distinguishes a schedule that never fired from an execution that fired and then failed; the latter is reported by the target's own events.Emission is at the producer's discretion and the vocabulary sets no tolerance window. A normative threshold was considered and not taken: arrival time cannot be predicted accurately in a distributed system, so any threshold would encode a guarantee the transport cannot make. Producers know their own scheduling semantics, and consumers should read
scheduledTimeagainst the event's own timestamp rather than rely on a standard grace period.A producer that was itself unavailable at
scheduledTimeemits nothing at all, including nomissed. That gap is found by querying for the event that is absent, which is the same mechanism that detects any expected occurrence that never arrived.Design decisions
Two times are carried, and both matter.
scheduledTimeis the intended time;context.timestampis when the occurrence happened. Ontriggered, the difference between them is the on-time-versus-late record. The producing scheduler or controller is identified bycontext.source, which every CDEvent already carries, so no separate field names it.authorrecords attributed identity, not authenticated identity. It is an addressing key: the actor the producing system attributes the action to. Proving the actor is the producer's responsibility.targetcarries atypealongsideidandsource, so consumers can route on the kind of thing scheduled, and so target references are shaped consistently across subjects.typeis a free string —build,deployment,changeWindow— rather than a closed enum, so the subject is not coupled to a fixed list of schedulable things.triggeredExecutionis a field, not a link. Replacing it with a relation link to the enqueued execution was considered. Linking is optional in CDEvents and an event should be complete without it: a consumer reading atriggeredevent should not have to consult a second mechanism to learn what was enqueued, particularly when the scheduler knows exactly what it just enqueued and can say so directly.The subject is treated as a sequence of actions, not a definition. It was put to us that the lifecycle here is mostly definitional and might not warrant occurrence events. Registering a schedule is an action taken at a point in time by an identifiable actor, and it produces something that did not exist beforehand; canceling it removes that thing. Each is an occurrence an auditor may need to account for, and each is reported as it happens.
No field for event publication time. Publication timing is a property of the transport carrying the event, not of the thing that happened. The intended time and the occurrence time are both present already.
No attempt identifier. A retry is a new occurrence with its own identity. This is also not specific to
scheduledExecution; if attempt identity is wanted, it belongs in the context for every subject.The
triggerobject is reused byte-identically from the Testing vocabulary, including its existingschedulevalue. No new common objects are introduced.Out of scope
approvalalready covers accountable authorization decisions. A schedule requiring sign-off is two occurrences.Notes on this change
Event versions start at
0.1.0-draft.schemas/_defs/entries andx-cdevents-semanticsare not included here. The predicate definitions this subject would need overlap with those already being added by #319 and with further subjects we are proposing; adding them here would collide. We will follow up with them once #319 lands.Submitter Checklist
As the author of this PR, please check off the items in this checklist:
0.1.0-draft; the spec version is unchanged