pegasus-workflows-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ .venv/
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: pegasus-workflows-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK and CLI for authoring, packaging, and publishing Pegasus workflows.
5
+ Author: DolasDev
6
+ License: UNLICENSED
7
+ Keywords: automation,pegasus,temporal,workflows
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: httpx<1,>=0.27
12
+ Requires-Dist: temporalio<2,>=1.7
13
+ Requires-Dist: typer<1,>=0.12
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
16
+ Requires-Dist: ruff>=0.6; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Pegasus Workflows SDK
20
+
21
+ `pegasus-workflows-sdk` is the Python SDK and CLI for authoring, packaging, and
22
+ publishing **Pegasus workflows** — Temporal workflows that automate
23
+ cross-domain operations (move lifecycle, billing follow-ups, dispatch
24
+ decisions) against the Pegasus public API.
25
+
26
+ Phase 1 ships the **developer flow**: write a workflow locally, run it against a
27
+ Dockerized Temporal, package it, and upload it. There is no server-side
28
+ execution yet — the API stores the artifact and lists it.
29
+
30
+ ## Install
31
+
32
+ ```
33
+ pip install pegasus-workflows-sdk
34
+ ```
35
+
36
+ This installs the `pegasus-workflows` CLI. **Python 3.11+** is required. Pin the
37
+ version in your project's requirements for reproducible builds, e.g.
38
+ `pegasus-workflows-sdk==0.1.0`.
39
+
40
+ ### Interim / unreleased install (git)
41
+
42
+ The repository is public, so you can install straight from a tagged commit
43
+ without waiting for a PyPI release — useful for an unreleased fix, or before the
44
+ first PyPI publish lands:
45
+
46
+ ```
47
+ pip install "pegasus-workflows-sdk @ git+https://github.com/DolasDev/pegasus@sdk-python-v0.1.0#subdirectory=packages/workflows-sdk-python"
48
+ ```
49
+
50
+ Swap the `@sdk-python-v0.1.0` tag for `@main` to track the latest unreleased
51
+ SDK. This clones the whole monorepo to build one subdirectory, so prefer the
52
+ PyPI install for everyday use.
53
+
54
+ ## Quick start
55
+
56
+ ```
57
+ pegasus-workflows init demo
58
+ cd demo
59
+ pegasus-workflows test demo
60
+ pegasus-workflows package
61
+ pegasus-workflows push --token=vnd_... --base-url=http://localhost:3000
62
+ ```
63
+
64
+ ## Authoring
65
+
66
+ Import the Temporal authoring primitives from `pegasus_workflows` and mark your
67
+ workflow class with `@pegasus_workflow`:
68
+
69
+ ```python
70
+ from datetime import timedelta
71
+ from pegasus_workflows import activity, pegasus_workflow, workflow
72
+
73
+ @activity.defn
74
+ async def greet(name: str) -> str:
75
+ return f"Hello, {name}!"
76
+
77
+ @pegasus_workflow(name="demo", version="0.1.0")
78
+ class HelloWorkflow:
79
+ @workflow.run
80
+ async def run(self, name: str = "world") -> str:
81
+ return await workflow.execute_activity(
82
+ greet, name, start_to_close_timeout=timedelta(seconds=10)
83
+ )
84
+ ```
85
+
86
+ `@pegasus_workflow` wraps `temporalio.workflow.defn` and records the
87
+ `(name, version)` used by the manifest.
88
+
89
+ ### Input contract: how `run()` receives its argument
90
+
91
+ Your `run()` method receives a **single positional argument** whose shape depends on how the workflow
92
+ was started:
93
+
94
+ **1. Trigger-fired (domain-event trigger)** — the dispatcher passes the full event envelope:
95
+
96
+ ```python
97
+ {
98
+ "domainEventId": "<uuid>",
99
+ "eventType": "quote.accepted", # the event type that fired the trigger
100
+ "occurredAt": "<ISO-8601>",
101
+ "payload": {"quoteId": "<id>", "moveId": "<id>"} # entity ids, camelCase
102
+ }
103
+ ```
104
+
105
+ Read entity ids from `arg["payload"]["quoteId"]` etc. The `payload` is a pointer, not a full snapshot
106
+ — always re-fetch authoritative state from the Pegasus API using those ids rather than relying on the
107
+ payload alone.
108
+
109
+ **2. Manual run** — `POST /api/v1/workflows/:id/run` passes:
110
+
111
+ ```python
112
+ {"executionId": "<uuid>", "input": <user-supplied dict>}
113
+ ```
114
+
115
+ Read your business data from `arg["input"]` (e.g. `arg["input"]["quote_id"]`).
116
+
117
+ **3. CLI test** — `pegasus-workflows test <name>` passes a raw string for local-dev parity.
118
+
119
+ Your `run()` should handle all three shapes. A module-level helper (not a method) is the recommended
120
+ pattern — it stays unit-testable without a Temporal worker context:
121
+
122
+ ```python
123
+ def _resolve_quote_id(payload: dict | str) -> str:
124
+ if isinstance(payload, str):
125
+ return payload
126
+ event_payload = payload.get("payload") if isinstance(payload, dict) else None
127
+ if isinstance(event_payload, dict) and event_payload.get("quoteId"):
128
+ return str(event_payload["quoteId"])
129
+ inner = payload.get("input") if isinstance(payload, dict) else None
130
+ if isinstance(inner, dict) and inner.get("quote_id"):
131
+ return str(inner["quote_id"])
132
+ return "quote-unknown"
133
+ ```
134
+
135
+ ## The manifest — `pegasus-workflows.toml`
136
+
137
+ Every project has a `pegasus-workflows.toml` at its root. Each `[[workflow]]`
138
+ table is packaged into its own artifact and uploaded as a distinct
139
+ `(name, version)` row:
140
+
141
+ ```toml
142
+ [[workflow]]
143
+ name = "demo" # ^[a-z0-9][a-z0-9_-]{0,63}$
144
+ version = "0.1.0" # semver
145
+ entry_points = ["demo.workflow:HelloWorkflow"] # non-empty
146
+ source_dir = "demo" # optional, defaults to name
147
+ description = "..." # optional
148
+ ```
149
+
150
+ These rules mirror the server's `ManifestSchema` exactly, so `package`/`push`
151
+ fail fast locally before any HTTP call.
152
+
153
+ ## CLI
154
+
155
+ | Command | What it does |
156
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
157
+ | `pegasus-workflows init <name>` | Scaffold a new workflow project. |
158
+ | `pegasus-workflows package` | Zip each declared workflow into `dist/<name>-<version>.zip`. |
159
+ | `pegasus-workflows push --token=<vnd_…> [--base-url=…]` | Package, then `upload-url` → S3 PUT → finalize. |
160
+ | `pegasus-workflows test <workflow>` | Start local Temporal and run the workflow with a stub input. |
161
+ | `pegasus-workflows integration-config validate <id> [-C <dir>]` | Dry-run the publish gate for a config (no write). |
162
+ | `pegasus-workflows integration-config publish <id> [-C <dir>]` | Gate then publish a new config version. |
163
+ | `pegasus-workflows integration-config pull <id> [-C <dir>] [--stdout]` | Fetch the active config; write the editable surface to disk. |
164
+ | `pegasus-workflows integration-config versions <id>` | List the config version history (newest first). |
165
+ | `pegasus-workflows integration-config rollback <id> <version>` | Re-publish a prior version (re-runs the gate). |
166
+
167
+ `push` reads the token from `--token` or the `PEGASUS_WORKFLOW_TOKEN`
168
+ environment variable. The token is a `vnd_*` Pegasus API key whose service
169
+ account holds the `workflow_developer` role.
170
+
171
+ ### Authoring an integration-validator config
172
+
173
+ The `integration-config` group manages an integration's declarative **mapping +
174
+ rules** (the DB-backed authoring surface; see
175
+ `apps/api/src/handlers/integration-validation/config.ts`). The editable surface
176
+ lives as three JSON files in a working directory (`-C`, default `.`):
177
+ `mapping.json`, `rules.json`, `corpus.json`. The round-trip is pull → edit →
178
+ validate → publish:
179
+
180
+ ```
181
+ pegasus-workflows integration-config pull weichert -C ./weichert
182
+ # …edit mapping.json / rules.json…
183
+ pegasus-workflows integration-config validate weichert -C ./weichert
184
+ pegasus-workflows integration-config publish weichert -C ./weichert
185
+ ```
186
+
187
+ `publish`/`rollback` require the token's tenant to be the **platform tenant** to
188
+ write GLOBAL (visibility is derived server-side) and to carry the
189
+ `PublishIntegrationConfig` action; they are gated by the server's
190
+ `INTEGRATION_CONFIG_PUBLISH_ENABLED` switch. `validate` and `pull` are
191
+ read-level and never gated.
192
+
193
+ ## Local Temporal
194
+
195
+ `pegasus-workflows test` needs a Temporal server. The repo root ships
196
+ `docker-compose.temporal.yml` (Temporal server + Temporal UI on `7233` / `8080`)
197
+ purely as a local-dev aid — no production connection. `test` runs
198
+ `docker compose -f docker-compose.temporal.yml up -d` automatically if Temporal
199
+ is not already reachable on `127.0.0.1:7233`. To start it by hand:
200
+
201
+ ```
202
+ docker compose -f docker-compose.temporal.yml up -d
203
+ ```
204
+
205
+ The Temporal Web UI is then at <http://localhost:8080>.
206
+
207
+ ## Release
208
+
209
+ The SDK is published to PyPI by `.github/workflows/release-sdk-python.yml` on
210
+ `sdk-python-v*` tags via PyPI **trusted publishing** (OIDC — no API token).
211
+
212
+ To cut a release:
213
+
214
+ 1. Bump `version` in `pyproject.toml` and commit it on `main`.
215
+ 2. Tag the release commit and push the tag, e.g.
216
+ `git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0`.
217
+
218
+ The workflow then lints, audits, tests, builds, and uploads the sdist + wheel.
219
+
220
+ **One-time setup (before the first release):** a PyPI project owner must add a
221
+ pending publisher at `pegasus-workflows-sdk` → Publishing → owner `DolasDev`,
222
+ repo `pegasus`, workflow `release-sdk-python.yml`, environment `pypi`. Until
223
+ that exists the `publish` job fails at the upload step, and tenants must use the
224
+ [git install](#interim--unreleased-install-git) above.
@@ -0,0 +1,206 @@
1
+ # Pegasus Workflows SDK
2
+
3
+ `pegasus-workflows-sdk` is the Python SDK and CLI for authoring, packaging, and
4
+ publishing **Pegasus workflows** — Temporal workflows that automate
5
+ cross-domain operations (move lifecycle, billing follow-ups, dispatch
6
+ decisions) against the Pegasus public API.
7
+
8
+ Phase 1 ships the **developer flow**: write a workflow locally, run it against a
9
+ Dockerized Temporal, package it, and upload it. There is no server-side
10
+ execution yet — the API stores the artifact and lists it.
11
+
12
+ ## Install
13
+
14
+ ```
15
+ pip install pegasus-workflows-sdk
16
+ ```
17
+
18
+ This installs the `pegasus-workflows` CLI. **Python 3.11+** is required. Pin the
19
+ version in your project's requirements for reproducible builds, e.g.
20
+ `pegasus-workflows-sdk==0.1.0`.
21
+
22
+ ### Interim / unreleased install (git)
23
+
24
+ The repository is public, so you can install straight from a tagged commit
25
+ without waiting for a PyPI release — useful for an unreleased fix, or before the
26
+ first PyPI publish lands:
27
+
28
+ ```
29
+ pip install "pegasus-workflows-sdk @ git+https://github.com/DolasDev/pegasus@sdk-python-v0.1.0#subdirectory=packages/workflows-sdk-python"
30
+ ```
31
+
32
+ Swap the `@sdk-python-v0.1.0` tag for `@main` to track the latest unreleased
33
+ SDK. This clones the whole monorepo to build one subdirectory, so prefer the
34
+ PyPI install for everyday use.
35
+
36
+ ## Quick start
37
+
38
+ ```
39
+ pegasus-workflows init demo
40
+ cd demo
41
+ pegasus-workflows test demo
42
+ pegasus-workflows package
43
+ pegasus-workflows push --token=vnd_... --base-url=http://localhost:3000
44
+ ```
45
+
46
+ ## Authoring
47
+
48
+ Import the Temporal authoring primitives from `pegasus_workflows` and mark your
49
+ workflow class with `@pegasus_workflow`:
50
+
51
+ ```python
52
+ from datetime import timedelta
53
+ from pegasus_workflows import activity, pegasus_workflow, workflow
54
+
55
+ @activity.defn
56
+ async def greet(name: str) -> str:
57
+ return f"Hello, {name}!"
58
+
59
+ @pegasus_workflow(name="demo", version="0.1.0")
60
+ class HelloWorkflow:
61
+ @workflow.run
62
+ async def run(self, name: str = "world") -> str:
63
+ return await workflow.execute_activity(
64
+ greet, name, start_to_close_timeout=timedelta(seconds=10)
65
+ )
66
+ ```
67
+
68
+ `@pegasus_workflow` wraps `temporalio.workflow.defn` and records the
69
+ `(name, version)` used by the manifest.
70
+
71
+ ### Input contract: how `run()` receives its argument
72
+
73
+ Your `run()` method receives a **single positional argument** whose shape depends on how the workflow
74
+ was started:
75
+
76
+ **1. Trigger-fired (domain-event trigger)** — the dispatcher passes the full event envelope:
77
+
78
+ ```python
79
+ {
80
+ "domainEventId": "<uuid>",
81
+ "eventType": "quote.accepted", # the event type that fired the trigger
82
+ "occurredAt": "<ISO-8601>",
83
+ "payload": {"quoteId": "<id>", "moveId": "<id>"} # entity ids, camelCase
84
+ }
85
+ ```
86
+
87
+ Read entity ids from `arg["payload"]["quoteId"]` etc. The `payload` is a pointer, not a full snapshot
88
+ — always re-fetch authoritative state from the Pegasus API using those ids rather than relying on the
89
+ payload alone.
90
+
91
+ **2. Manual run** — `POST /api/v1/workflows/:id/run` passes:
92
+
93
+ ```python
94
+ {"executionId": "<uuid>", "input": <user-supplied dict>}
95
+ ```
96
+
97
+ Read your business data from `arg["input"]` (e.g. `arg["input"]["quote_id"]`).
98
+
99
+ **3. CLI test** — `pegasus-workflows test <name>` passes a raw string for local-dev parity.
100
+
101
+ Your `run()` should handle all three shapes. A module-level helper (not a method) is the recommended
102
+ pattern — it stays unit-testable without a Temporal worker context:
103
+
104
+ ```python
105
+ def _resolve_quote_id(payload: dict | str) -> str:
106
+ if isinstance(payload, str):
107
+ return payload
108
+ event_payload = payload.get("payload") if isinstance(payload, dict) else None
109
+ if isinstance(event_payload, dict) and event_payload.get("quoteId"):
110
+ return str(event_payload["quoteId"])
111
+ inner = payload.get("input") if isinstance(payload, dict) else None
112
+ if isinstance(inner, dict) and inner.get("quote_id"):
113
+ return str(inner["quote_id"])
114
+ return "quote-unknown"
115
+ ```
116
+
117
+ ## The manifest — `pegasus-workflows.toml`
118
+
119
+ Every project has a `pegasus-workflows.toml` at its root. Each `[[workflow]]`
120
+ table is packaged into its own artifact and uploaded as a distinct
121
+ `(name, version)` row:
122
+
123
+ ```toml
124
+ [[workflow]]
125
+ name = "demo" # ^[a-z0-9][a-z0-9_-]{0,63}$
126
+ version = "0.1.0" # semver
127
+ entry_points = ["demo.workflow:HelloWorkflow"] # non-empty
128
+ source_dir = "demo" # optional, defaults to name
129
+ description = "..." # optional
130
+ ```
131
+
132
+ These rules mirror the server's `ManifestSchema` exactly, so `package`/`push`
133
+ fail fast locally before any HTTP call.
134
+
135
+ ## CLI
136
+
137
+ | Command | What it does |
138
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
139
+ | `pegasus-workflows init <name>` | Scaffold a new workflow project. |
140
+ | `pegasus-workflows package` | Zip each declared workflow into `dist/<name>-<version>.zip`. |
141
+ | `pegasus-workflows push --token=<vnd_…> [--base-url=…]` | Package, then `upload-url` → S3 PUT → finalize. |
142
+ | `pegasus-workflows test <workflow>` | Start local Temporal and run the workflow with a stub input. |
143
+ | `pegasus-workflows integration-config validate <id> [-C <dir>]` | Dry-run the publish gate for a config (no write). |
144
+ | `pegasus-workflows integration-config publish <id> [-C <dir>]` | Gate then publish a new config version. |
145
+ | `pegasus-workflows integration-config pull <id> [-C <dir>] [--stdout]` | Fetch the active config; write the editable surface to disk. |
146
+ | `pegasus-workflows integration-config versions <id>` | List the config version history (newest first). |
147
+ | `pegasus-workflows integration-config rollback <id> <version>` | Re-publish a prior version (re-runs the gate). |
148
+
149
+ `push` reads the token from `--token` or the `PEGASUS_WORKFLOW_TOKEN`
150
+ environment variable. The token is a `vnd_*` Pegasus API key whose service
151
+ account holds the `workflow_developer` role.
152
+
153
+ ### Authoring an integration-validator config
154
+
155
+ The `integration-config` group manages an integration's declarative **mapping +
156
+ rules** (the DB-backed authoring surface; see
157
+ `apps/api/src/handlers/integration-validation/config.ts`). The editable surface
158
+ lives as three JSON files in a working directory (`-C`, default `.`):
159
+ `mapping.json`, `rules.json`, `corpus.json`. The round-trip is pull → edit →
160
+ validate → publish:
161
+
162
+ ```
163
+ pegasus-workflows integration-config pull weichert -C ./weichert
164
+ # …edit mapping.json / rules.json…
165
+ pegasus-workflows integration-config validate weichert -C ./weichert
166
+ pegasus-workflows integration-config publish weichert -C ./weichert
167
+ ```
168
+
169
+ `publish`/`rollback` require the token's tenant to be the **platform tenant** to
170
+ write GLOBAL (visibility is derived server-side) and to carry the
171
+ `PublishIntegrationConfig` action; they are gated by the server's
172
+ `INTEGRATION_CONFIG_PUBLISH_ENABLED` switch. `validate` and `pull` are
173
+ read-level and never gated.
174
+
175
+ ## Local Temporal
176
+
177
+ `pegasus-workflows test` needs a Temporal server. The repo root ships
178
+ `docker-compose.temporal.yml` (Temporal server + Temporal UI on `7233` / `8080`)
179
+ purely as a local-dev aid — no production connection. `test` runs
180
+ `docker compose -f docker-compose.temporal.yml up -d` automatically if Temporal
181
+ is not already reachable on `127.0.0.1:7233`. To start it by hand:
182
+
183
+ ```
184
+ docker compose -f docker-compose.temporal.yml up -d
185
+ ```
186
+
187
+ The Temporal Web UI is then at <http://localhost:8080>.
188
+
189
+ ## Release
190
+
191
+ The SDK is published to PyPI by `.github/workflows/release-sdk-python.yml` on
192
+ `sdk-python-v*` tags via PyPI **trusted publishing** (OIDC — no API token).
193
+
194
+ To cut a release:
195
+
196
+ 1. Bump `version` in `pyproject.toml` and commit it on `main`.
197
+ 2. Tag the release commit and push the tag, e.g.
198
+ `git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0`.
199
+
200
+ The workflow then lints, audits, tests, builds, and uploads the sdist + wheel.
201
+
202
+ **One-time setup (before the first release):** a PyPI project owner must add a
203
+ pending publisher at `pegasus-workflows-sdk` → Publishing → owner `DolasDev`,
204
+ repo `pegasus`, workflow `release-sdk-python.yml`, environment `pypi`. Until
205
+ that exists the `publish` job fails at the upload step, and tenants must use the
206
+ [git install](#interim--unreleased-install-git) above.
@@ -0,0 +1,113 @@
1
+ """Pegasus Workflows SDK.
2
+
3
+ Authoring surface for Pegasus workflows. Re-exports the Temporal authoring
4
+ primitives (``workflow`` and ``activity``) so workflow authors only ever
5
+ import from :mod:`pegasus_workflows`, and adds the :func:`pegasus_workflow`
6
+ decorator which stashes ``(name, version)`` metadata used by the CLI's
7
+ ``package`` step and the ``pegasus-workflows.toml`` manifest.
8
+
9
+ Example
10
+ -------
11
+ .. code-block:: python
12
+
13
+ from pegasus_workflows import pegasus_workflow, workflow
14
+
15
+ @pegasus_workflow(name="send_quote_followup", version="0.1.0")
16
+ class SendQuoteFollowup:
17
+ @workflow.run
18
+ async def run(self, quote_id: str) -> str:
19
+ return f"followed up on {quote_id}"
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable
25
+ from typing import TYPE_CHECKING, Any, TypeVar
26
+
27
+ from temporalio import activity, workflow
28
+
29
+ if TYPE_CHECKING:
30
+ from .api import PegasusApiError, PegasusClient
31
+ from .manifest import Manifest, ManifestError, load_manifest
32
+
33
+ __all__ = [
34
+ "activity",
35
+ "workflow",
36
+ "pegasus_workflow",
37
+ "PegasusClient",
38
+ "PegasusApiError",
39
+ "Manifest",
40
+ "ManifestError",
41
+ "load_manifest",
42
+ "WORKFLOW_META_ATTR",
43
+ ]
44
+
45
+ # api.py pulls in httpx (and transitively urllib), which Temporal's workflow
46
+ # sandbox restricts. Workflow files import the authoring primitives from this
47
+ # package, so importing the package must NOT eagerly import api/manifest —
48
+ # otherwise httpx lands in the sandboxed workflow module graph and validation
49
+ # fails. PegasusClient/Manifest/etc. are therefore exposed lazily and only
50
+ # resolved when actually referenced (i.e. inside activities or the CLI).
51
+ _LAZY_EXPORTS = {
52
+ "PegasusClient": "api",
53
+ "PegasusApiError": "api",
54
+ "Manifest": "manifest",
55
+ "ManifestError": "manifest",
56
+ "load_manifest": "manifest",
57
+ }
58
+
59
+
60
+ def __getattr__(name: str) -> Any:
61
+ """Lazily resolve API/manifest exports (PEP 562 module __getattr__)."""
62
+ module_name = _LAZY_EXPORTS.get(name)
63
+ if module_name is None:
64
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
65
+ import importlib
66
+
67
+ module = importlib.import_module(f".{module_name}", __name__)
68
+ return getattr(module, name)
69
+
70
+ #: Attribute name under which :func:`pegasus_workflow` stores its metadata
71
+ #: dict on the decorated class.
72
+ WORKFLOW_META_ATTR = "__pegasus_workflow__"
73
+
74
+ _T = TypeVar("_T")
75
+
76
+
77
+ def pegasus_workflow(
78
+ *, name: str, version: str, description: str | None = None
79
+ ) -> Callable[[_T], _T]:
80
+ """Mark a class as a Pegasus workflow.
81
+
82
+ Wraps :func:`temporalio.workflow.defn` so the class is a valid Temporal
83
+ workflow definition, and records ``(name, version, description)`` on the
84
+ class under :data:`WORKFLOW_META_ATTR`. The CLI reads this metadata for
85
+ introspection; the authoritative manifest still lives in
86
+ ``pegasus-workflows.toml``.
87
+
88
+ Args:
89
+ name: Workflow name. Must match ``^[a-z0-9][a-z0-9_-]{0,63}$``.
90
+ version: Semantic version, e.g. ``1.2.3`` or ``1.2.3-beta.1``.
91
+ description: Optional human-readable description.
92
+
93
+ Returns:
94
+ A decorator that returns the (Temporal-registered) class unchanged.
95
+ """
96
+
97
+ def decorator(cls: _T) -> _T:
98
+ # Register under the PEGASUS name, not the Python class name: the
99
+ # API starts executions with `client.workflow.start(workflow.name,
100
+ # ...)` (apps/api/src/lib/start-workflow-execution.ts), so the
101
+ # Temporal workflow *type* must equal the manifest name or the
102
+ # worker rejects every task with "Workflow class <name> is not
103
+ # registered". (Caught live in the Phase 3 staging smoke — the
104
+ # class-name registration had been latent since Phase 1.)
105
+ defined: Any = workflow.defn(name=name)(cls) # type: ignore[arg-type]
106
+ setattr(
107
+ defined,
108
+ WORKFLOW_META_ATTR,
109
+ {"name": name, "version": version, "description": description},
110
+ )
111
+ return defined # type: ignore[return-value]
112
+
113
+ return decorator