Skip to content

A workflow declares what its runs are about - #134

Merged
czpython merged 1 commit into
mainfrom
eng-777-subject-is-a-class
Jul 28, 2026
Merged

A workflow declares what its runs are about#134
czpython merged 1 commit into
mainfrom
eng-777-subject-is-a-class

Conversation

@czpython

Copy link
Copy Markdown
Owner

A subject was spelled three ways, and which one an author wrote depended on where they were standing:

@subscribe(WorkflowEvent.RUNNING, workflow=Build, subject=WorkItem)   # a class
class Ship(Extension):  subject_type = "work_item"                    # a string
class Build(Workflow):  ...                                           # nothing at all

The one place that produces a run said nothing about what the run is about, so the read side searched for it — and the search only ever found subjects that happened to have a table.

A workflow declares its subject

class Build(Workflow):
    subject = WorkItem

class Profile(Workflow):
    subject = ProjectRepo        # same extension, different subject

class PullRequestReview(Workflow):
    subject = PullRequest        # no table

class RefreshTokens(Workflow):
    ...                          # about nothing, and says so by silence

A run is one episode in the life of something that outlives it. Druks can only show that life, or offer what may be done to it, before any run about it exists, if the workflow says what kind of thing it is about — and the workflow is the only place that noun is known at import time.

The declaration is the lookup, so the mapper walk in Workflow.subject is gone rather than replaced. start(), Workflow.cancel() and Gate.answer() hold a caller to it: a wrong subject, a missing one, and an unexpected one all fail before anything is enqueued. Subscribers derive their subject class from workflow=, so the declaration has to be true.

What moved

where the subject was spelled before after
subscriber, with workflow= subject=WorkItem derived — and no longer written beside it
subscriber, without workflow= subject=WorkItem kept — "any workflow about a work item"
extension subject_type = "work_item" deleted — derived from its workflows
workflow nothing subject = WorkItem

Subject is now subclassable and derives its subject_type from the class name, the way StoredSubject already did; it stays a separate frozen dataclass, with no base shared with the ORM. Both answer subject_type, id, identity and label, and both now answer get_for_subject_id(), get_summary() and list_summaries() — the summary and listing Extension used to carry, moved to the class that owns the data.

An extension's mounted subject types follow from its workflows, so Ship mounts two: ProjectRepo gets the board and page it never had, and its profiling runs stop being stamped, deduped and queryable with nowhere to show it. Review's PullRequest becomes a real class that owns its own id parsing, which leaves its schemas.py as response fields only. The route-collision guard now runs once per mounted subject rather than once per extension.

Known cost

subject = WorkItem is a plain class attribute and buys zero type-checking — self.subject is still Any, and Build.start(subject=a_repo) type-checks clean. Runtime enforcement is the guarantee.

Class names become durable: renaming a subject class changes its subject_type, and with it route prefixes, dedup keys, event identities and replay lookup. There is no alias and no override.

Acting on another extension's subject is now an import edge between pluggable apps. The alternative was two extensions agreeing on a magic string and composing two different ids for one thing.

@czpython
czpython force-pushed the eng-777-subject-is-a-class branch 3 times, most recently from 52ec75f to 60e8fc6 Compare July 28, 2026 05:48
A subject was spelled three ways depending on where the author stood: a
class on a subscriber, a string on the extension, and nothing at all on
the workflow that produces the run. The read side had to search for it,
and the search only ever found subjects that happened to have a table.

A workflow declares `subject = SomeSubjectClass`. That declaration is
the lookup, so the mapper walk is gone; `Subject` is subclassable and
derives its `subject_type` from the class name, the way `StoredSubject`
already did. `start()`, `Workflow.cancel()` and `Gate.answer()` hold a
caller to it.

`Extension.subject_type` is deleted — an extension's subjects follow
from its workflows, and each gets its board and page. Summary and
listing move onto the subject class, where the data is: ship's
`WorkItem`, review's new `PullRequest`, and `ProjectRepo`, which gets a
board it never had. `subject=` on `@subscribe` is derived from
`workflow=` and no longer written beside it.
@czpython
czpython force-pushed the eng-777-subject-is-a-class branch from 60e8fc6 to 764508d Compare July 28, 2026 05:52
@czpython
czpython merged commit fcfd24f into main Jul 28, 2026
2 checks passed
@czpython
czpython deleted the eng-777-subject-is-a-class branch July 28, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant