pyworkflow-engine 0.1.7__py3-none-any.whl

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.
Files changed (196) hide show
  1. dashboard/backend/app/__init__.py +1 -0
  2. dashboard/backend/app/config.py +32 -0
  3. dashboard/backend/app/controllers/__init__.py +6 -0
  4. dashboard/backend/app/controllers/run_controller.py +86 -0
  5. dashboard/backend/app/controllers/workflow_controller.py +33 -0
  6. dashboard/backend/app/dependencies/__init__.py +5 -0
  7. dashboard/backend/app/dependencies/storage.py +50 -0
  8. dashboard/backend/app/repositories/__init__.py +6 -0
  9. dashboard/backend/app/repositories/run_repository.py +80 -0
  10. dashboard/backend/app/repositories/workflow_repository.py +27 -0
  11. dashboard/backend/app/rest/__init__.py +8 -0
  12. dashboard/backend/app/rest/v1/__init__.py +12 -0
  13. dashboard/backend/app/rest/v1/health.py +33 -0
  14. dashboard/backend/app/rest/v1/runs.py +133 -0
  15. dashboard/backend/app/rest/v1/workflows.py +41 -0
  16. dashboard/backend/app/schemas/__init__.py +23 -0
  17. dashboard/backend/app/schemas/common.py +16 -0
  18. dashboard/backend/app/schemas/event.py +24 -0
  19. dashboard/backend/app/schemas/hook.py +25 -0
  20. dashboard/backend/app/schemas/run.py +54 -0
  21. dashboard/backend/app/schemas/step.py +28 -0
  22. dashboard/backend/app/schemas/workflow.py +31 -0
  23. dashboard/backend/app/server.py +87 -0
  24. dashboard/backend/app/services/__init__.py +6 -0
  25. dashboard/backend/app/services/run_service.py +240 -0
  26. dashboard/backend/app/services/workflow_service.py +155 -0
  27. dashboard/backend/main.py +18 -0
  28. docs/concepts/cancellation.mdx +362 -0
  29. docs/concepts/continue-as-new.mdx +434 -0
  30. docs/concepts/events.mdx +266 -0
  31. docs/concepts/fault-tolerance.mdx +370 -0
  32. docs/concepts/hooks.mdx +552 -0
  33. docs/concepts/limitations.mdx +167 -0
  34. docs/concepts/schedules.mdx +775 -0
  35. docs/concepts/sleep.mdx +312 -0
  36. docs/concepts/steps.mdx +301 -0
  37. docs/concepts/workflows.mdx +255 -0
  38. docs/guides/cli.mdx +942 -0
  39. docs/guides/configuration.mdx +560 -0
  40. docs/introduction.mdx +155 -0
  41. docs/quickstart.mdx +279 -0
  42. examples/__init__.py +1 -0
  43. examples/celery/__init__.py +1 -0
  44. examples/celery/durable/docker-compose.yml +55 -0
  45. examples/celery/durable/pyworkflow.config.yaml +12 -0
  46. examples/celery/durable/workflows/__init__.py +122 -0
  47. examples/celery/durable/workflows/basic.py +87 -0
  48. examples/celery/durable/workflows/batch_processing.py +102 -0
  49. examples/celery/durable/workflows/cancellation.py +273 -0
  50. examples/celery/durable/workflows/child_workflow_patterns.py +240 -0
  51. examples/celery/durable/workflows/child_workflows.py +202 -0
  52. examples/celery/durable/workflows/continue_as_new.py +260 -0
  53. examples/celery/durable/workflows/fault_tolerance.py +210 -0
  54. examples/celery/durable/workflows/hooks.py +211 -0
  55. examples/celery/durable/workflows/idempotency.py +112 -0
  56. examples/celery/durable/workflows/long_running.py +99 -0
  57. examples/celery/durable/workflows/retries.py +101 -0
  58. examples/celery/durable/workflows/schedules.py +209 -0
  59. examples/celery/transient/01_basic_workflow.py +91 -0
  60. examples/celery/transient/02_fault_tolerance.py +257 -0
  61. examples/celery/transient/__init__.py +20 -0
  62. examples/celery/transient/pyworkflow.config.yaml +25 -0
  63. examples/local/__init__.py +1 -0
  64. examples/local/durable/01_basic_workflow.py +94 -0
  65. examples/local/durable/02_file_storage.py +132 -0
  66. examples/local/durable/03_retries.py +169 -0
  67. examples/local/durable/04_long_running.py +119 -0
  68. examples/local/durable/05_event_log.py +145 -0
  69. examples/local/durable/06_idempotency.py +148 -0
  70. examples/local/durable/07_hooks.py +334 -0
  71. examples/local/durable/08_cancellation.py +233 -0
  72. examples/local/durable/09_child_workflows.py +198 -0
  73. examples/local/durable/10_child_workflow_patterns.py +265 -0
  74. examples/local/durable/11_continue_as_new.py +249 -0
  75. examples/local/durable/12_schedules.py +198 -0
  76. examples/local/durable/__init__.py +1 -0
  77. examples/local/transient/01_quick_tasks.py +87 -0
  78. examples/local/transient/02_retries.py +130 -0
  79. examples/local/transient/03_sleep.py +141 -0
  80. examples/local/transient/__init__.py +1 -0
  81. pyworkflow/__init__.py +256 -0
  82. pyworkflow/aws/__init__.py +68 -0
  83. pyworkflow/aws/context.py +234 -0
  84. pyworkflow/aws/handler.py +184 -0
  85. pyworkflow/aws/testing.py +310 -0
  86. pyworkflow/celery/__init__.py +41 -0
  87. pyworkflow/celery/app.py +198 -0
  88. pyworkflow/celery/scheduler.py +315 -0
  89. pyworkflow/celery/tasks.py +1746 -0
  90. pyworkflow/cli/__init__.py +132 -0
  91. pyworkflow/cli/__main__.py +6 -0
  92. pyworkflow/cli/commands/__init__.py +1 -0
  93. pyworkflow/cli/commands/hooks.py +640 -0
  94. pyworkflow/cli/commands/quickstart.py +495 -0
  95. pyworkflow/cli/commands/runs.py +773 -0
  96. pyworkflow/cli/commands/scheduler.py +130 -0
  97. pyworkflow/cli/commands/schedules.py +794 -0
  98. pyworkflow/cli/commands/setup.py +703 -0
  99. pyworkflow/cli/commands/worker.py +413 -0
  100. pyworkflow/cli/commands/workflows.py +1257 -0
  101. pyworkflow/cli/output/__init__.py +1 -0
  102. pyworkflow/cli/output/formatters.py +321 -0
  103. pyworkflow/cli/output/styles.py +121 -0
  104. pyworkflow/cli/utils/__init__.py +1 -0
  105. pyworkflow/cli/utils/async_helpers.py +30 -0
  106. pyworkflow/cli/utils/config.py +130 -0
  107. pyworkflow/cli/utils/config_generator.py +344 -0
  108. pyworkflow/cli/utils/discovery.py +53 -0
  109. pyworkflow/cli/utils/docker_manager.py +651 -0
  110. pyworkflow/cli/utils/interactive.py +364 -0
  111. pyworkflow/cli/utils/storage.py +115 -0
  112. pyworkflow/config.py +329 -0
  113. pyworkflow/context/__init__.py +63 -0
  114. pyworkflow/context/aws.py +230 -0
  115. pyworkflow/context/base.py +416 -0
  116. pyworkflow/context/local.py +930 -0
  117. pyworkflow/context/mock.py +381 -0
  118. pyworkflow/core/__init__.py +0 -0
  119. pyworkflow/core/exceptions.py +353 -0
  120. pyworkflow/core/registry.py +313 -0
  121. pyworkflow/core/scheduled.py +328 -0
  122. pyworkflow/core/step.py +494 -0
  123. pyworkflow/core/workflow.py +294 -0
  124. pyworkflow/discovery.py +248 -0
  125. pyworkflow/engine/__init__.py +0 -0
  126. pyworkflow/engine/events.py +879 -0
  127. pyworkflow/engine/executor.py +682 -0
  128. pyworkflow/engine/replay.py +273 -0
  129. pyworkflow/observability/__init__.py +19 -0
  130. pyworkflow/observability/logging.py +234 -0
  131. pyworkflow/primitives/__init__.py +33 -0
  132. pyworkflow/primitives/child_handle.py +174 -0
  133. pyworkflow/primitives/child_workflow.py +372 -0
  134. pyworkflow/primitives/continue_as_new.py +101 -0
  135. pyworkflow/primitives/define_hook.py +150 -0
  136. pyworkflow/primitives/hooks.py +97 -0
  137. pyworkflow/primitives/resume_hook.py +210 -0
  138. pyworkflow/primitives/schedule.py +545 -0
  139. pyworkflow/primitives/shield.py +96 -0
  140. pyworkflow/primitives/sleep.py +100 -0
  141. pyworkflow/runtime/__init__.py +21 -0
  142. pyworkflow/runtime/base.py +179 -0
  143. pyworkflow/runtime/celery.py +310 -0
  144. pyworkflow/runtime/factory.py +101 -0
  145. pyworkflow/runtime/local.py +706 -0
  146. pyworkflow/scheduler/__init__.py +9 -0
  147. pyworkflow/scheduler/local.py +248 -0
  148. pyworkflow/serialization/__init__.py +0 -0
  149. pyworkflow/serialization/decoder.py +146 -0
  150. pyworkflow/serialization/encoder.py +162 -0
  151. pyworkflow/storage/__init__.py +54 -0
  152. pyworkflow/storage/base.py +612 -0
  153. pyworkflow/storage/config.py +185 -0
  154. pyworkflow/storage/dynamodb.py +1315 -0
  155. pyworkflow/storage/file.py +827 -0
  156. pyworkflow/storage/memory.py +549 -0
  157. pyworkflow/storage/postgres.py +1161 -0
  158. pyworkflow/storage/schemas.py +486 -0
  159. pyworkflow/storage/sqlite.py +1136 -0
  160. pyworkflow/utils/__init__.py +0 -0
  161. pyworkflow/utils/duration.py +177 -0
  162. pyworkflow/utils/schedule.py +391 -0
  163. pyworkflow_engine-0.1.7.dist-info/METADATA +687 -0
  164. pyworkflow_engine-0.1.7.dist-info/RECORD +196 -0
  165. pyworkflow_engine-0.1.7.dist-info/WHEEL +5 -0
  166. pyworkflow_engine-0.1.7.dist-info/entry_points.txt +2 -0
  167. pyworkflow_engine-0.1.7.dist-info/licenses/LICENSE +21 -0
  168. pyworkflow_engine-0.1.7.dist-info/top_level.txt +5 -0
  169. tests/examples/__init__.py +0 -0
  170. tests/integration/__init__.py +0 -0
  171. tests/integration/test_cancellation.py +330 -0
  172. tests/integration/test_child_workflows.py +439 -0
  173. tests/integration/test_continue_as_new.py +428 -0
  174. tests/integration/test_dynamodb_storage.py +1146 -0
  175. tests/integration/test_fault_tolerance.py +369 -0
  176. tests/integration/test_schedule_storage.py +484 -0
  177. tests/unit/__init__.py +0 -0
  178. tests/unit/backends/__init__.py +1 -0
  179. tests/unit/backends/test_dynamodb_storage.py +1554 -0
  180. tests/unit/backends/test_postgres_storage.py +1281 -0
  181. tests/unit/backends/test_sqlite_storage.py +1460 -0
  182. tests/unit/conftest.py +41 -0
  183. tests/unit/test_cancellation.py +364 -0
  184. tests/unit/test_child_workflows.py +680 -0
  185. tests/unit/test_continue_as_new.py +441 -0
  186. tests/unit/test_event_limits.py +316 -0
  187. tests/unit/test_executor.py +320 -0
  188. tests/unit/test_fault_tolerance.py +334 -0
  189. tests/unit/test_hooks.py +495 -0
  190. tests/unit/test_registry.py +261 -0
  191. tests/unit/test_replay.py +420 -0
  192. tests/unit/test_schedule_schemas.py +285 -0
  193. tests/unit/test_schedule_utils.py +286 -0
  194. tests/unit/test_scheduled_workflow.py +274 -0
  195. tests/unit/test_step.py +353 -0
  196. tests/unit/test_workflow.py +243 -0
@@ -0,0 +1,312 @@
1
+ ---
2
+ title: 'Sleep'
3
+ description: 'Pause workflows for any duration without consuming resources'
4
+ ---
5
+
6
+ ## What is Sleep?
7
+
8
+ The `sleep()` primitive pauses a workflow for a specified duration. Unlike traditional sleep that blocks a thread, PyWorkflow's sleep **suspends** the workflow completely - no resources are consumed during the sleep period.
9
+
10
+ ```python
11
+ from pyworkflow import workflow, sleep
12
+
13
+ @workflow()
14
+ async def reminder_sequence(user_id: str):
15
+ await send_reminder(user_id, "First reminder")
16
+
17
+ # Workflow suspends here - zero resources used
18
+ await sleep("1d")
19
+
20
+ # Resumes automatically after 1 day
21
+ await send_reminder(user_id, "Second reminder")
22
+
23
+ await sleep("7d")
24
+
25
+ await send_reminder(user_id, "Final reminder")
26
+ ```
27
+
28
+ ## How It Works
29
+
30
+ ```
31
+ Workflow Execution
32
+
33
+
34
+ ┌───────────────┐
35
+ │ Execute Steps │
36
+ └───────┬───────┘
37
+
38
+
39
+ ┌───────────────┐
40
+ │ sleep("1d") │
41
+ └───────┬───────┘
42
+
43
+ ├─── 1. Record sleep_started event
44
+
45
+ ├─── 2. Schedule Celery Beat task for wake time
46
+
47
+ ├─── 3. Raise SuspensionSignal
48
+
49
+ └─── 4. Worker is freed
50
+
51
+ │ ... 1 day passes ...
52
+
53
+
54
+ ┌─────────────────┐
55
+ │ Celery Beat │
56
+ │ triggers resume │
57
+ └────────┬────────┘
58
+
59
+
60
+ ┌───────────────┐
61
+ │ Replay events │
62
+ │ Resume work │
63
+ └───────────────┘
64
+ ```
65
+
66
+ ## Duration Formats
67
+
68
+ ### String Format (Recommended)
69
+
70
+ ```python
71
+ await sleep("30s") # 30 seconds
72
+ await sleep("5m") # 5 minutes
73
+ await sleep("2h") # 2 hours
74
+ await sleep("1d") # 1 day
75
+ await sleep("1w") # 1 week
76
+
77
+ # Combined
78
+ await sleep("1h30m") # 1 hour 30 minutes
79
+ await sleep("2d12h") # 2 days 12 hours
80
+ ```
81
+
82
+ ### Timedelta
83
+
84
+ ```python
85
+ from datetime import timedelta
86
+
87
+ await sleep(timedelta(hours=2, minutes=30))
88
+ await sleep(timedelta(days=7))
89
+ ```
90
+
91
+ ### Until Specific Time
92
+
93
+ ```python
94
+ from datetime import datetime
95
+
96
+ # Sleep until a specific datetime
97
+ await sleep(datetime(2025, 12, 25, 9, 0, 0))
98
+
99
+ # Sleep until next Monday at 9 AM
100
+ next_monday = get_next_monday()
101
+ await sleep(next_monday.replace(hour=9, minute=0))
102
+ ```
103
+
104
+ ### Integer (Seconds)
105
+
106
+ ```python
107
+ await sleep(300) # 300 seconds (5 minutes)
108
+ ```
109
+
110
+ ## Zero-Resource Suspension
111
+
112
+ Traditional async sleep blocks a worker:
113
+
114
+ ```python
115
+ # BAD: This holds a worker for 24 hours!
116
+ import asyncio
117
+
118
+ async def traditional_sleep():
119
+ await do_something()
120
+ await asyncio.sleep(86400) # Blocks worker for 24h
121
+ await do_something_else()
122
+ ```
123
+
124
+ PyWorkflow's sleep releases the worker:
125
+
126
+ ```python
127
+ # GOOD: Worker is freed during sleep
128
+ from pyworkflow import sleep
129
+
130
+ @workflow()
131
+ async def efficient_sleep():
132
+ await do_something()
133
+ await sleep("24h") # Worker freed, resumes later
134
+ await do_something_else()
135
+ ```
136
+
137
+ <Note>
138
+ With 100 workflows each sleeping for 1 day, traditional sleep would need 100 workers blocked for 24 hours. PyWorkflow needs 0 workers during the sleep period.
139
+ </Note>
140
+
141
+ ## Use Cases
142
+
143
+ ### Scheduled Reminders
144
+
145
+ ```python
146
+ @workflow()
147
+ async def onboarding_drip(user_id: str):
148
+ await send_email(user_id, "Welcome!")
149
+
150
+ await sleep("1d")
151
+ await send_email(user_id, "Getting started tips")
152
+
153
+ await sleep("3d")
154
+ await send_email(user_id, "Advanced features")
155
+
156
+ await sleep("7d")
157
+ await send_email(user_id, "How's it going?")
158
+ ```
159
+
160
+ ### Delayed Processing
161
+
162
+ ```python
163
+ @workflow()
164
+ async def process_refund(order_id: str):
165
+ await validate_refund_request(order_id)
166
+
167
+ # Wait for potential fraud review
168
+ await sleep("24h")
169
+
170
+ # If not flagged, process refund
171
+ await execute_refund(order_id)
172
+ await notify_customer(order_id)
173
+ ```
174
+
175
+ ### Rate Limiting
176
+
177
+ ```python
178
+ @workflow()
179
+ async def batch_api_calls(items: list):
180
+ for i, item in enumerate(items):
181
+ await call_api(item)
182
+
183
+ # Rate limit: 10 calls per minute
184
+ if (i + 1) % 10 == 0:
185
+ await sleep("1m")
186
+ ```
187
+
188
+ ### Retry with Backoff
189
+
190
+ ```python
191
+ @workflow()
192
+ async def resilient_operation():
193
+ for attempt in range(5):
194
+ try:
195
+ result = await risky_step()
196
+ return result
197
+ except TemporaryError:
198
+ if attempt < 4:
199
+ # Exponential backoff: 1m, 2m, 4m, 8m
200
+ delay = f"{2 ** attempt}m"
201
+ await sleep(delay)
202
+
203
+ raise FatalError("All retries exhausted")
204
+ ```
205
+
206
+ ## Sleep vs Step Timeout
207
+
208
+ Sleep and timeouts serve different purposes:
209
+
210
+ | Feature | Sleep | Timeout |
211
+ |---------|-------|---------|
212
+ | Purpose | Intentional delay | Maximum execution time |
213
+ | Resources | Zero during sleep | Worker is active |
214
+ | Failure | Never fails | Fails if exceeded |
215
+
216
+ ```python
217
+ @step(timeout="30s") # Fails if step takes > 30s
218
+ async def quick_step():
219
+ pass
220
+
221
+ @workflow()
222
+ async def my_workflow():
223
+ await quick_step()
224
+ await sleep("1h") # Intentional 1-hour pause
225
+ await quick_step()
226
+ ```
227
+
228
+ ## Celery Beat Requirement
229
+
230
+ Sleep resumption requires Celery Beat to be running:
231
+
232
+ ```bash
233
+ # Start Celery Beat for scheduled task execution
234
+ celery -A pyworkflow.celery.app beat --loglevel=info
235
+ ```
236
+
237
+ <Warning>
238
+ Without Celery Beat, workflows will suspend but never resume automatically. Make sure Beat is running in production.
239
+ </Warning>
240
+
241
+ ### Docker Compose Setup
242
+
243
+ ```yaml
244
+ services:
245
+ worker:
246
+ command: celery -A pyworkflow.celery.app worker --loglevel=info
247
+
248
+ beat:
249
+ command: celery -A pyworkflow.celery.app beat --loglevel=info
250
+ ```
251
+
252
+ ## Best Practices
253
+
254
+ <AccordionGroup>
255
+ <Accordion title="Use sleep for intentional delays only">
256
+ Don't use sleep as a retry mechanism. Use step retry configuration instead:
257
+
258
+ ```python
259
+ # Good: Use retry config
260
+ @step(max_retries=3, retry_delay="exponential")
261
+ async def my_step():
262
+ pass
263
+
264
+ # Avoid: Manual retry with sleep
265
+ @workflow()
266
+ async def my_workflow():
267
+ for i in range(3):
268
+ try:
269
+ await my_step()
270
+ break
271
+ except:
272
+ await sleep(f"{2**i}m")
273
+ ```
274
+ </Accordion>
275
+
276
+ <Accordion title="Consider timezone implications">
277
+ When sleeping until a specific time, be aware of timezones:
278
+
279
+ ```python
280
+ from datetime import datetime
281
+ import pytz
282
+
283
+ # Explicit timezone
284
+ eastern = pytz.timezone("US/Eastern")
285
+ wake_time = eastern.localize(datetime(2025, 1, 15, 9, 0))
286
+ await sleep(wake_time)
287
+ ```
288
+ </Accordion>
289
+
290
+ <Accordion title="Keep sleep durations reasonable">
291
+ Very long sleeps (months, years) work but consider if a different approach is better:
292
+
293
+ ```python
294
+ # Works, but consider alternatives
295
+ await sleep("365d")
296
+
297
+ # Alternative: Schedule a new workflow
298
+ schedule_workflow(annual_review, run_at="2026-01-01")
299
+ ```
300
+ </Accordion>
301
+ </AccordionGroup>
302
+
303
+ ## Next Steps
304
+
305
+ <CardGroup cols={2}>
306
+ <Card title="Workflows" icon="diagram-project" href="/concepts/workflows">
307
+ Learn about workflow orchestration.
308
+ </Card>
309
+ <Card title="Deployment" icon="rocket" href="/guides/deployment">
310
+ Set up Celery Beat in production.
311
+ </Card>
312
+ </CardGroup>
@@ -0,0 +1,301 @@
1
+ ---
2
+ title: 'Steps'
3
+ description: 'Isolated, retryable units of work that form the building blocks of workflows'
4
+ ---
5
+
6
+ ## What is a Step?
7
+
8
+ A step is an isolated, retryable unit of work within a workflow. Steps are where the actual business logic executes - calling APIs, processing data, sending emails, etc. Each step runs on a Celery worker and can be retried independently if it fails.
9
+
10
+ ```python
11
+ from pyworkflow import step
12
+
13
+ @step()
14
+ async def send_email(to: str, subject: str, body: str):
15
+ """Send an email - retries automatically on failure."""
16
+ async with EmailClient() as client:
17
+ await client.send(to=to, subject=subject, body=body)
18
+ return {"sent": True, "to": to}
19
+ ```
20
+
21
+ ## Key Characteristics
22
+
23
+ <CardGroup cols={2}>
24
+ <Card title="Isolated" icon="box">
25
+ Each step runs independently with its own retry policy and timeout.
26
+ </Card>
27
+ <Card title="Retryable" icon="rotate">
28
+ Failed steps automatically retry with configurable backoff strategies.
29
+ </Card>
30
+ <Card title="Cached" icon="database">
31
+ Completed step results are cached and replayed during workflow resumption.
32
+ </Card>
33
+ <Card title="Distributed" icon="server">
34
+ Steps execute on Celery workers, distributing load across your cluster.
35
+ </Card>
36
+ </CardGroup>
37
+
38
+ ## Creating Steps
39
+
40
+ <Tabs>
41
+ <Tab title="Decorator">
42
+ ```python
43
+ from pyworkflow import step
44
+
45
+ @step()
46
+ async def fetch_user(user_id: str):
47
+ async with httpx.AsyncClient() as client:
48
+ response = await client.get(f"/api/users/{user_id}")
49
+ return response.json()
50
+ ```
51
+ </Tab>
52
+ <Tab title="Class">
53
+ ```python
54
+ from pyworkflow import Step
55
+
56
+ class FetchUserStep(Step):
57
+ async def execute(self, user_id: str):
58
+ async with httpx.AsyncClient() as client:
59
+ response = await client.get(f"/api/users/{user_id}")
60
+ return response.json()
61
+
62
+ # Usage in workflow
63
+ user = await FetchUserStep()(user_id)
64
+ ```
65
+ </Tab>
66
+ </Tabs>
67
+
68
+ ### Configuration Options
69
+
70
+ <Tabs>
71
+ <Tab title="Decorator">
72
+ ```python
73
+ @step(
74
+ name="fetch_user_data", # Custom step name
75
+ max_retries=5, # Retry up to 5 times
76
+ retry_delay="exponential", # Exponential backoff
77
+ timeout="30s" # Step timeout
78
+ )
79
+ async def fetch_user(user_id: str):
80
+ pass
81
+ ```
82
+ </Tab>
83
+ <Tab title="Class">
84
+ ```python
85
+ class FetchUserStep(Step):
86
+ name = "fetch_user_data"
87
+ max_retries = 5
88
+ retry_delay = "exponential"
89
+ timeout = "30s"
90
+
91
+ async def execute(self, user_id: str):
92
+ pass
93
+ ```
94
+ </Tab>
95
+ </Tabs>
96
+
97
+ | Option | Type | Default | Description |
98
+ |--------|------|---------|-------------|
99
+ | `name` | `str` | Function/class name | Unique identifier for the step |
100
+ | `max_retries` | `int` | `3` | Maximum retry attempts |
101
+ | `retry_delay` | `str` | `"fixed"` | Retry strategy: `"fixed"`, `"exponential"`, or duration |
102
+ | `timeout` | `str` | `"60s"` | Maximum step execution time |
103
+
104
+ ## Retry Strategies
105
+
106
+ ### Fixed Delay
107
+
108
+ Retry with a constant delay between attempts:
109
+
110
+ ```python
111
+ @step(max_retries=3, retry_delay="30s")
112
+ async def call_api():
113
+ # Retries at: 30s, 60s, 90s
114
+ pass
115
+ ```
116
+
117
+ ### Exponential Backoff
118
+
119
+ Retry with increasing delays (recommended for external APIs):
120
+
121
+ ```python
122
+ @step(max_retries=5, retry_delay="exponential")
123
+ async def call_external_api():
124
+ # Retries at: 1s, 2s, 4s, 8s, 16s
125
+ pass
126
+ ```
127
+
128
+ ### Custom Delay in Error
129
+
130
+ Specify retry delay when raising an error:
131
+
132
+ ```python
133
+ from pyworkflow import step, RetryableError
134
+
135
+ @step(max_retries=3)
136
+ async def rate_limited_api():
137
+ response = await call_api()
138
+
139
+ if response.status_code == 429:
140
+ retry_after = response.headers.get("Retry-After", "60")
141
+ raise RetryableError(
142
+ "Rate limited",
143
+ retry_after=f"{retry_after}s"
144
+ )
145
+
146
+ return response.json()
147
+ ```
148
+
149
+ ## Error Handling
150
+
151
+ ### RetryableError
152
+
153
+ Use for transient failures that should be retried:
154
+
155
+ ```python
156
+ from pyworkflow import step, RetryableError
157
+
158
+ @step(max_retries=3)
159
+ async def fetch_data():
160
+ try:
161
+ return await external_api.get_data()
162
+ except ConnectionError:
163
+ raise RetryableError("Connection failed")
164
+ except TimeoutError:
165
+ raise RetryableError("Request timed out", retry_after="10s")
166
+ ```
167
+
168
+ ### FatalError
169
+
170
+ Use for permanent failures that should stop the workflow:
171
+
172
+ ```python
173
+ from pyworkflow import step, FatalError
174
+
175
+ @step()
176
+ async def validate_input(data: dict):
177
+ if "email" not in data:
178
+ raise FatalError("Email is required")
179
+
180
+ if not is_valid_email(data["email"]):
181
+ raise FatalError(f"Invalid email: {data['email']}")
182
+
183
+ return data
184
+ ```
185
+
186
+ ### Error Flow
187
+
188
+ ```
189
+ Step Execution
190
+
191
+ ├─── Success ───────────────────────> Return Result
192
+
193
+ └─── Exception
194
+
195
+ ├─── FatalError ────────────> Workflow Failed
196
+
197
+ └─── RetryableError / Other
198
+
199
+ ├─── Retries Left ──> Wait & Retry
200
+
201
+ └─── No Retries ────> Workflow Failed
202
+ ```
203
+
204
+ ## Step Results and Caching
205
+
206
+ When a workflow resumes after suspension, completed steps are not re-executed. Instead, their cached results are returned:
207
+
208
+ ```python
209
+ @workflow()
210
+ async def my_workflow():
211
+ # First run: executes the step
212
+ # After resume: returns cached result
213
+ user = await fetch_user("user_123")
214
+
215
+ await sleep("1h")
216
+
217
+ # After 1 hour, workflow resumes
218
+ # fetch_user is NOT called again - cached result is used
219
+ await send_email(user["email"], "Hello!")
220
+ ```
221
+
222
+ <Warning>
223
+ Step results must be serializable. PyWorkflow supports common Python types (dict, list, str, int, datetime, etc.) and uses cloudpickle for complex objects.
224
+ </Warning>
225
+
226
+ ## Parallel Step Execution
227
+
228
+ Execute multiple steps concurrently using `asyncio.gather()`:
229
+
230
+ ```python
231
+ import asyncio
232
+ from pyworkflow import workflow, step
233
+
234
+ @step()
235
+ async def fetch_user(user_id: str):
236
+ return await api.get_user(user_id)
237
+
238
+ @step()
239
+ async def fetch_orders(user_id: str):
240
+ return await api.get_orders(user_id)
241
+
242
+ @step()
243
+ async def fetch_preferences(user_id: str):
244
+ return await api.get_preferences(user_id)
245
+
246
+ @workflow()
247
+ async def load_dashboard(user_id: str):
248
+ # All three steps run in parallel
249
+ user, orders, preferences = await asyncio.gather(
250
+ fetch_user(user_id),
251
+ fetch_orders(user_id),
252
+ fetch_preferences(user_id)
253
+ )
254
+
255
+ return {
256
+ "user": user,
257
+ "orders": orders,
258
+ "preferences": preferences
259
+ }
260
+ ```
261
+
262
+ ## Best Practices
263
+
264
+ <AccordionGroup>
265
+ <Accordion title="Keep steps small and focused">
266
+ Each step should do one thing well. This makes retries more efficient - if a step fails, only that specific operation is retried.
267
+ </Accordion>
268
+
269
+ <Accordion title="Make steps idempotent">
270
+ Steps may be retried, so ensure they can be safely re-executed. Use idempotency keys when calling external APIs.
271
+
272
+ ```python
273
+ @step()
274
+ async def charge_payment(order_id: str, amount: float):
275
+ # Use order_id as idempotency key
276
+ return await stripe.charges.create(
277
+ amount=amount,
278
+ idempotency_key=f"charge-{order_id}"
279
+ )
280
+ ```
281
+ </Accordion>
282
+
283
+ <Accordion title="Use appropriate timeouts">
284
+ Set timeouts based on expected execution time. External API calls should have shorter timeouts than data processing steps.
285
+ </Accordion>
286
+
287
+ <Accordion title="Handle errors explicitly">
288
+ Distinguish between retryable and fatal errors. Don't retry errors that will never succeed.
289
+ </Accordion>
290
+ </AccordionGroup>
291
+
292
+ ## Next Steps
293
+
294
+ <CardGroup cols={2}>
295
+ <Card title="Events" icon="timeline" href="/concepts/events">
296
+ Learn how event sourcing enables durability and replay.
297
+ </Card>
298
+ <Card title="Error Handling" icon="shield-check" href="/guides/error-handling">
299
+ Deep dive into retry strategies and error handling.
300
+ </Card>
301
+ </CardGroup>