pyworkflow-engine 0.2.7__tar.gz → 0.2.8__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.
Files changed (232) hide show
  1. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/PKG-INFO +1 -1
  2. pyworkflow_engine-0.2.8/docs/concepts/stream-steps.mdx +264 -0
  3. pyworkflow_engine-0.2.8/docs/concepts/stream-workflows.mdx +221 -0
  4. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyproject.toml +1 -1
  5. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/__init__.py +1 -1
  6. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/citus.py +25 -50
  7. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/postgres.py +23 -49
  8. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow_engine.egg-info/SOURCES.txt +2 -0
  9. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/CLAUDE.md +0 -0
  10. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/DISTRIBUTED.md +0 -0
  11. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/LICENSE +0 -0
  12. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/MANIFEST.in +0 -0
  13. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/README.md +0 -0
  14. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/RELEASING.md +0 -0
  15. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/architecture.md +0 -0
  16. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/cancellation.mdx +0 -0
  17. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/continue-as-new.mdx +0 -0
  18. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/events.mdx +0 -0
  19. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/fault-tolerance.mdx +0 -0
  20. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/hooks.mdx +0 -0
  21. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/limitations.mdx +0 -0
  22. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/schedules.mdx +0 -0
  23. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/sleep.mdx +0 -0
  24. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/step-context.mdx +0 -0
  25. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/steps.mdx +0 -0
  26. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/concepts/workflows.mdx +0 -0
  27. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/conventions.md +0 -0
  28. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/guides/brokers.mdx +0 -0
  29. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/guides/cli.mdx +0 -0
  30. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/guides/configuration.mdx +0 -0
  31. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/harness-gaps.md +0 -0
  32. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/introduction.mdx +0 -0
  33. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/layers.md +0 -0
  34. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/docs/quickstart.mdx +0 -0
  35. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/__init__.py +0 -0
  36. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/__init__.py +0 -0
  37. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/docker-compose.yml +0 -0
  38. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  39. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/__init__.py +0 -0
  40. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/basic.py +0 -0
  41. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/batch_processing.py +0 -0
  42. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/cancellation.py +0 -0
  43. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  44. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  45. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/child_workflows.py +0 -0
  46. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  47. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  48. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/hooks.py +0 -0
  49. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/idempotency.py +0 -0
  50. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/long_running.py +0 -0
  51. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/retries.py +0 -0
  52. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/schedules.py +0 -0
  53. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  54. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/durable/workflows/step_context.py +0 -0
  55. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/transient/01_basic_workflow.py +0 -0
  56. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/transient/02_fault_tolerance.py +0 -0
  57. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/transient/__init__.py +0 -0
  58. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  59. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/__init__.py +0 -0
  60. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/01_basic_workflow.py +0 -0
  61. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/02_file_storage.py +0 -0
  62. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/03_retries.py +0 -0
  63. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/04_long_running.py +0 -0
  64. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/05_event_log.py +0 -0
  65. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/06_idempotency.py +0 -0
  66. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/07_hooks.py +0 -0
  67. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/08_cancellation.py +0 -0
  68. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/09_child_workflows.py +0 -0
  69. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  70. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/11_continue_as_new.py +0 -0
  71. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/12_schedules.py +0 -0
  72. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/13_step_context.py +0 -0
  73. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  74. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/durable/__init__.py +0 -0
  75. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/transient/01_quick_tasks.py +0 -0
  76. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/transient/02_retries.py +0 -0
  77. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/transient/03_sleep.py +0 -0
  78. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/examples/local/transient/__init__.py +0 -0
  79. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/aws/__init__.py +0 -0
  80. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/aws/context.py +0 -0
  81. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/aws/handler.py +0 -0
  82. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/aws/testing.py +0 -0
  83. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/celery/__init__.py +0 -0
  84. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/celery/app.py +0 -0
  85. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/celery/loop.py +0 -0
  86. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/celery/scheduler.py +0 -0
  87. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/celery/singleton.py +0 -0
  88. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/celery/tasks.py +0 -0
  89. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/__init__.py +0 -0
  90. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/__main__.py +0 -0
  91. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/__init__.py +0 -0
  92. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/hooks.py +0 -0
  93. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/quickstart.py +0 -0
  94. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/runs.py +0 -0
  95. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/scheduler.py +0 -0
  96. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/schedules.py +0 -0
  97. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/setup.py +0 -0
  98. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/worker.py +0 -0
  99. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/commands/workflows.py +0 -0
  100. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/output/__init__.py +0 -0
  101. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/output/formatters.py +0 -0
  102. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/output/styles.py +0 -0
  103. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/__init__.py +0 -0
  104. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/async_helpers.py +0 -0
  105. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/config.py +0 -0
  106. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/config_generator.py +0 -0
  107. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/discovery.py +0 -0
  108. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/docker_manager.py +0 -0
  109. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/interactive.py +0 -0
  110. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/cli/utils/storage.py +0 -0
  111. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/config.py +0 -0
  112. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/context/__init__.py +0 -0
  113. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/context/aws.py +0 -0
  114. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/context/base.py +0 -0
  115. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/context/local.py +0 -0
  116. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/context/mock.py +0 -0
  117. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/context/step_context.py +0 -0
  118. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/__init__.py +0 -0
  119. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/exceptions.py +0 -0
  120. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/registry.py +0 -0
  121. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/scheduled.py +0 -0
  122. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/step.py +0 -0
  123. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/validation.py +0 -0
  124. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/core/workflow.py +0 -0
  125. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/discovery.py +0 -0
  126. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/engine/__init__.py +0 -0
  127. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/engine/events.py +0 -0
  128. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/engine/executor.py +0 -0
  129. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/engine/replay.py +0 -0
  130. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/observability/__init__.py +0 -0
  131. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/observability/logging.py +0 -0
  132. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/__init__.py +0 -0
  133. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/child_handle.py +0 -0
  134. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/child_workflow.py +0 -0
  135. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/continue_as_new.py +0 -0
  136. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/define_hook.py +0 -0
  137. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/hooks.py +0 -0
  138. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/resume_hook.py +0 -0
  139. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/schedule.py +0 -0
  140. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/shield.py +0 -0
  141. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/sleep.py +0 -0
  142. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/step_checkpoint.py +0 -0
  143. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/primitives/step_hook.py +0 -0
  144. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/runtime/__init__.py +0 -0
  145. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/runtime/base.py +0 -0
  146. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/runtime/celery.py +0 -0
  147. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/runtime/factory.py +0 -0
  148. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/runtime/local.py +0 -0
  149. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/scheduler/__init__.py +0 -0
  150. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/scheduler/local.py +0 -0
  151. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/serialization/__init__.py +0 -0
  152. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/serialization/decoder.py +0 -0
  153. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/serialization/encoder.py +0 -0
  154. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/__init__.py +0 -0
  155. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/base.py +0 -0
  156. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/cassandra.py +0 -0
  157. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/config.py +0 -0
  158. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/dynamodb.py +0 -0
  159. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/file.py +0 -0
  160. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/memory.py +0 -0
  161. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/migrations/__init__.py +0 -0
  162. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/migrations/base.py +0 -0
  163. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/mysql.py +0 -0
  164. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/schemas.py +0 -0
  165. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/storage/sqlite.py +0 -0
  166. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/__init__.py +0 -0
  167. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/checkpoint.py +0 -0
  168. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/consumer.py +0 -0
  169. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/context.py +0 -0
  170. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/decorator.py +0 -0
  171. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/dispatcher.py +0 -0
  172. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/emit.py +0 -0
  173. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/registry.py +0 -0
  174. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/runtime.py +0 -0
  175. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/signal.py +0 -0
  176. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/streams/step_context.py +0 -0
  177. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/utils/__init__.py +0 -0
  178. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/utils/duration.py +0 -0
  179. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/utils/helpers.py +0 -0
  180. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/pyworkflow/utils/schedule.py +0 -0
  181. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/setup.cfg +0 -0
  182. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/__init__.py +0 -0
  183. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_cancellation.py +0 -0
  184. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_cassandra_storage.py +0 -0
  185. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_child_workflows.py +0 -0
  186. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_continue_as_new.py +0 -0
  187. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_dynamodb_storage.py +0 -0
  188. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_fault_tolerance.py +0 -0
  189. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_schedule_storage.py +0 -0
  190. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_schema_migrations.py +0 -0
  191. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_singleton.py +0 -0
  192. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_stream_e2e.py +0 -0
  193. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/integration/test_workflow_suspended.py +0 -0
  194. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/__init__.py +0 -0
  195. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/backends/__init__.py +0 -0
  196. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/backends/test_cassandra_storage.py +0 -0
  197. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/backends/test_citus_storage.py +0 -0
  198. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  199. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/backends/test_postgres_storage.py +0 -0
  200. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/backends/test_sqlite_storage.py +0 -0
  201. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/conftest.py +0 -0
  202. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/storage/__init__.py +0 -0
  203. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/storage/test_migrations.py +0 -0
  204. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_cancellation.py +0 -0
  205. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_child_workflows.py +0 -0
  206. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_cli_worker.py +0 -0
  207. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_continue_as_new.py +0 -0
  208. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_emit.py +0 -0
  209. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_event_limits.py +0 -0
  210. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_executor.py +0 -0
  211. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_fault_tolerance.py +0 -0
  212. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_force_local.py +0 -0
  213. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_hooks.py +0 -0
  214. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_parent_run_id.py +0 -0
  215. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_primitives_from_steps.py +0 -0
  216. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_registry.py +0 -0
  217. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_replay.py +0 -0
  218. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_retention.py +0 -0
  219. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_schedule_schemas.py +0 -0
  220. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_schedule_utils.py +0 -0
  221. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_scheduled_workflow.py +0 -0
  222. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_signal.py +0 -0
  223. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_singleton.py +0 -0
  224. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_step.py +0 -0
  225. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_step_checkpoint.py +0 -0
  226. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_step_context.py +0 -0
  227. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_step_hook.py +0 -0
  228. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_stream_storage.py +0 -0
  229. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_stream_workflow.py +0 -0
  230. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_validation.py +0 -0
  231. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_workflow.py +0 -0
  232. {pyworkflow_engine-0.2.7 → pyworkflow_engine-0.2.8}/tests/unit/test_workflow_suspended.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyworkflow-engine
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow
5
5
  Author: PyWorkflow Contributors
6
6
  License: MIT
@@ -0,0 +1,264 @@
1
+ ---
2
+ title: 'Stream Steps'
3
+ description: 'Reactive, long-lived steps that subscribe to signals on a stream and resume on each arrival'
4
+ ---
5
+
6
+ ## What is a Stream Step?
7
+
8
+ A stream step is a long-lived reactive unit that subscribes to a named stream and runs in response to **signals**. Unlike a regular `@step` — which runs once when called from a workflow — a `@stream_step` is materialized as a subscription row when its parent [stream workflow](/concepts/stream-workflows) starts, and is then re-invoked each time a matching signal arrives, until it explicitly terminates.
9
+
10
+ ```python
11
+ from pyworkflow import stream_step
12
+ from pyworkflow.streams import get_current_signal, set_result, terminate
13
+
14
+ @stream_step(stream="agent_comms", signals=["task.assigned"])
15
+ async def worker():
16
+ signal = await get_current_signal()
17
+ if signal is None:
18
+ # First start — no signal yet, just register and wait.
19
+ return
20
+
21
+ task_id = signal.payload["task_id"]
22
+ result = await do_work(task_id)
23
+
24
+ await set_result({"task_id": task_id, "result": result})
25
+ await terminate()
26
+ ```
27
+
28
+ ## Key Characteristics
29
+
30
+ <CardGroup cols={2}>
31
+ <Card title="Signal-driven" icon="bolt">
32
+ The step body runs once on start and again on each matching signal arrival.
33
+ </Card>
34
+ <Card title="Stateful" icon="database">
35
+ Use `save_checkpoint()` / `get_checkpoint()` to carry state across resumes without rebuilding from scratch.
36
+ </Card>
37
+ <Card title="Suspendable" icon="pause">
38
+ `suspend()` parks the step and bubbles a `SuspensionSignal` up to the parent workflow — perfect for HITL.
39
+ </Card>
40
+ <Card title="Schema-aware" icon="shield-check">
41
+ Bind Pydantic models to signal types and the dispatcher validates payloads before delivery.
42
+ </Card>
43
+ </CardGroup>
44
+
45
+ ## The Two Code Paths
46
+
47
+ A stream step has **two** code paths the runtime invokes at different times:
48
+
49
+ 1. **`on_signal` callback** — runs on every matching signal arrival. It receives the raw `Signal` and a `StreamStepContext`. Its job is to decide whether to `resume()`, `cancel()`, `terminate()`, or `suspend()` — typically very lightweight.
50
+ 2. **The lifecycle function** (the decorated function itself) — runs on first start and again on each explicit `ctx.resume()`. This is where the actual work happens.
51
+
52
+ If you don't pass an `on_signal`, the default callback (`_auto_resume_on_signal`) just calls `ctx.resume()` for every matching signal — which is the right thing 90% of the time.
53
+
54
+ ```python
55
+ from pyworkflow import stream_step
56
+ from pyworkflow.streams import get_current_signal, terminate
57
+
58
+ # Custom on_signal: only resume on the high-priority variant.
59
+ async def gate_priority(signal, ctx):
60
+ if signal.payload.get("priority") == "high":
61
+ await ctx.resume()
62
+ # Otherwise: do nothing — the signal is acked and the step stays waiting.
63
+
64
+ @stream_step(
65
+ stream="agent_comms",
66
+ signals=["task.created"],
67
+ on_signal=gate_priority,
68
+ )
69
+ async def priority_worker():
70
+ signal = await get_current_signal()
71
+ if signal is None:
72
+ return
73
+ await handle_high_priority(signal.payload)
74
+ await terminate()
75
+ ```
76
+
77
+ ## Defining Signals
78
+
79
+ The `signals=` argument accepts either a plain list of names or a dict mapping names to Pydantic schemas.
80
+
81
+ <Tabs>
82
+ <Tab title="List of names">
83
+ ```python
84
+ @stream_step(
85
+ stream="agent_comms",
86
+ signals=["task.created", "task.updated", "task.cancelled"],
87
+ )
88
+ async def planner():
89
+ signal = await get_current_signal()
90
+ ...
91
+ ```
92
+ </Tab>
93
+ <Tab title="Pydantic schemas">
94
+ ```python
95
+ from pydantic import BaseModel
96
+
97
+ class TaskAssigned(BaseModel):
98
+ task_id: str
99
+ assignee: str
100
+ priority: int
101
+
102
+ @stream_step(
103
+ stream="agent_comms",
104
+ signals={"task.assigned": TaskAssigned},
105
+ )
106
+ async def worker():
107
+ signal = await get_current_signal()
108
+ if signal:
109
+ # signal.payload is a validated dict matching TaskAssigned
110
+ await assign(signal.payload["task_id"], signal.payload["assignee"])
111
+ ```
112
+ </Tab>
113
+ </Tabs>
114
+
115
+ <Note>
116
+ When a schema is bound to a signal type, payloads that fail validation are rejected by the dispatcher and the step is **not** invoked for that signal.
117
+ </Note>
118
+
119
+ ## Lifecycle Primitives
120
+
121
+ These helpers, imported from `pyworkflow.streams`, are how the lifecycle function communicates with the dispatcher. They all set state that the dispatcher reads **after** the function returns — they don't immediately mutate the subscription row.
122
+
123
+ | Primitive | Purpose |
124
+ |---|---|
125
+ | `await get_current_signal()` | Returns the `Signal` that triggered this resume (or `None` on first start). |
126
+ | `await set_result(value)` | Attach a result payload to the subscription. Surfaced via `StreamWorkflowResult.get_result(...)`. |
127
+ | `await terminate()` | Mark the step `terminated` — it will not be invoked again. |
128
+ | `await suspend(reason, resume_signals=None)` | Mark the step `suspended` — bubbles a `SuspensionSignal` up to the parent workflow (HITL). |
129
+ | `await save_checkpoint(data)` | Persist a JSON-serializable dict for the next resume. |
130
+ | `await get_checkpoint()` | Load the most recently saved checkpoint dict (or `None`). |
131
+
132
+ ```python
133
+ from pyworkflow.streams import (
134
+ get_current_signal,
135
+ get_checkpoint,
136
+ save_checkpoint,
137
+ set_result,
138
+ suspend,
139
+ terminate,
140
+ )
141
+
142
+ @stream_step(stream="reviews", signals=["review.submitted", "human.approved"])
143
+ async def review_loop():
144
+ signal = await get_current_signal()
145
+ state = (await get_checkpoint()) or {"submissions": 0}
146
+
147
+ if signal is None:
148
+ return # initial start
149
+
150
+ if signal.signal_type == "review.submitted":
151
+ state["submissions"] += 1
152
+ await save_checkpoint(state)
153
+
154
+ if state["submissions"] >= 3:
155
+ # Park the step until a human reviews it.
156
+ await suspend(
157
+ reason="hitl:needs_review",
158
+ resume_signals=["human.approved"],
159
+ )
160
+ return
161
+
162
+ if signal.signal_type == "human.approved":
163
+ await set_result({"approved": True, "submissions": state["submissions"]})
164
+ await terminate()
165
+ ```
166
+
167
+ ## The `Signal` Object
168
+
169
+ `get_current_signal()` returns a `Signal` dataclass:
170
+
171
+ ```python
172
+ @dataclass
173
+ class Signal:
174
+ signal_id: str
175
+ stream_id: str
176
+ signal_type: str
177
+ payload: Any # dict (validated against schema if configured)
178
+ published_at: datetime
179
+ sequence: int | None # Per-stream ordering, assigned by storage
180
+ source_run_id: str | None # run_id of the workflow that called emit()
181
+ stream_run_id: str | None # Scoping key for this stream run
182
+ metadata: dict[str, Any]
183
+ ```
184
+
185
+ The `sequence` field gives you a strict ordering across all signals on the stream — useful for deduping or detecting gaps. `source_run_id` is set automatically when `emit()` is called from inside a workflow context.
186
+
187
+ ## Emitting Signals from a Step
188
+
189
+ Stream steps can emit signals back to the same or another stream — this is how multi-actor pipelines fan out work between participants.
190
+
191
+ ```python
192
+ from pyworkflow.streams import emit, get_current_signal, terminate
193
+
194
+ @stream_step(stream="orders", signals=["order.placed"])
195
+ async def fulfillment():
196
+ signal = await get_current_signal()
197
+ if signal is None:
198
+ return
199
+
200
+ order_id = signal.payload["order_id"]
201
+ await ship(order_id)
202
+
203
+ # Notify the billing stream that this order is ready to invoice.
204
+ await emit("billing", "order.shipped", {"order_id": order_id})
205
+
206
+ await terminate()
207
+ ```
208
+
209
+ `emit()` automatically picks up the current `stream_run_id` from context, so signals stay scoped to the same run unless you pass an explicit override.
210
+
211
+ ## The `StreamStepContext` (for `on_signal` callbacks)
212
+
213
+ When you pass a custom `on_signal=...`, the callback receives `(signal, ctx)` where `ctx` is a `StreamStepContext`:
214
+
215
+ | Method | Effect |
216
+ |---|---|
217
+ | `await ctx.resume()` | Trigger the lifecycle function with `signal` as the current signal. |
218
+ | `await ctx.terminate()` | Mark the step terminated without invoking the lifecycle function. |
219
+ | `await ctx.suspend(reason, resume_signals=None)` | Park the step; bubbles to the parent workflow as a `SuspensionSignal`. |
220
+ | `await ctx.cancel(reason=None)` | Cancel the step (terminal, with a reason). |
221
+
222
+ `ctx` also exposes `ctx.status`, `ctx.run_id`, and `ctx.stream_id` for inspection.
223
+
224
+ ```python
225
+ async def cancel_on_kill(signal, ctx):
226
+ if signal.signal_type == "stream.kill":
227
+ await ctx.cancel(reason="received kill signal")
228
+ return
229
+ await ctx.resume()
230
+
231
+ @stream_step(
232
+ stream="agent_comms",
233
+ signals=["task.created", "stream.kill"],
234
+ on_signal=cancel_on_kill,
235
+ )
236
+ async def killable_worker():
237
+ ...
238
+ ```
239
+
240
+ ## Terminal States
241
+
242
+ Every stream step subscription is in one of these states. The aggregate of all step states determines whether the parent stream workflow is `running`, `completed`, or `suspended` — see [Stream Workflows: Aggregate Lifecycle](/concepts/stream-workflows#aggregate-lifecycle).
243
+
244
+ | State | Meaning |
245
+ |---|---|
246
+ | `waiting` | Subscribed and parked, ready for the next matching signal. |
247
+ | `running` | Lifecycle function is currently executing on a worker. |
248
+ | `suspended` | `suspend()` was called — bubbles `SuspensionSignal` up to the parent. |
249
+ | `terminated` | `terminate()` was called — the step is done and will never run again. |
250
+ | `cancelled` | `cancel()` was called from an `on_signal` callback. |
251
+
252
+ ## Best Practices
253
+
254
+ - **Keep `on_signal` callbacks lightweight.** They run for every signal, including ones you ignore — do filtering there, do work in the lifecycle function.
255
+ - **Always handle the `signal is None` case.** The first invocation has no triggering signal; that's the registration phase. Use it to seed state, not to do work.
256
+ - **Use `set_result()` instead of `save_checkpoint()` for output the parent needs.** `save_checkpoint` is for *internal* state across resumes; `set_result` is the parent-facing return value.
257
+ - **Call `terminate()` or `suspend()` when you're done.** A step that just `return`s without setting a terminal state stays in `waiting` and keeps the parent stream workflow alive.
258
+ - **Don't rely on globals.** Step lifecycles can run on different workers between resumes — persist anything you need via `save_checkpoint()`.
259
+
260
+ ## Related Concepts
261
+
262
+ - [Stream Workflows](/concepts/stream-workflows) — the parent runtime that hosts and drives stream steps
263
+ - [Steps](/concepts/steps) — the regular, run-once step model for sequential workflows
264
+ - [Hooks](/concepts/hooks) — the suspension primitive that stream workflows use under the hood
@@ -0,0 +1,221 @@
1
+ ---
2
+ title: 'Stream Workflows'
3
+ description: 'Long-lived, reactive workflows built from stream steps that react to signals on a named channel'
4
+ ---
5
+
6
+ ## What is a Stream Workflow?
7
+
8
+ A stream workflow is a **long-lived, reactive unit of orchestration** built on top of the signal pub/sub system. Where a `@workflow` runs top-to-bottom and completes, a `@stream_workflow` defines a named channel ("stream") that one or more `@stream_step`s subscribe to. Each step reacts to incoming signals, can suspend and be resumed by new signals, and can publish results back.
9
+
10
+ ```python
11
+ from pyworkflow import stream_workflow, stream_step, emit, run_stream_workflow, workflow
12
+
13
+ @stream_workflow(name="agent_comms")
14
+ async def agent_comms():
15
+ """Defines the 'agent_comms' channel. Body runs once on start."""
16
+ await emit("agent_comms", "agent.started", {"ts": "now"})
17
+
18
+ @stream_step(stream="agent_comms", signals=["task.assigned"])
19
+ async def worker():
20
+ from pyworkflow.streams import get_current_signal, set_result, terminate
21
+ signal = await get_current_signal()
22
+ if signal is None:
23
+ return # initial start, no signal yet
24
+ await set_result({"task_id": signal.payload["task_id"]})
25
+ await terminate()
26
+
27
+ @workflow()
28
+ async def run_agent(run_id: str):
29
+ # Drive the stream workflow from inside a durable @workflow.
30
+ result = await run_stream_workflow(agent_comms, stream_run_id=run_id)
31
+ return result.step_results
32
+ ```
33
+
34
+ ## Key Characteristics
35
+
36
+ <CardGroup cols={2}>
37
+ <Card title="Reactive" icon="bolt">
38
+ Steps react to signals arriving on the stream instead of running top-to-bottom.
39
+ </Card>
40
+ <Card title="Long-lived" icon="infinity">
41
+ A stream workflow stays alive until every subscribed step reaches a terminal state.
42
+ </Card>
43
+ <Card title="Durable Suspension" icon="pause">
44
+ The parent `@workflow` is released via `hook()` while steps wait for signals — no worker is pinned.
45
+ </Card>
46
+ <Card title="Scoped" icon="layer-group">
47
+ Each `stream_run_id` isolates its own set of subscriptions, signals, and results.
48
+ </Card>
49
+ </CardGroup>
50
+
51
+ ## When to Use
52
+
53
+ Use a stream workflow when your process is **event-driven and multi-actor** — e.g. a planner step waiting for task results from N worker steps, a supervisor loop waiting on human-in-the-loop feedback, or long-running agents that react to external signals over hours or days.
54
+
55
+ Use a regular `@workflow` when your process is a linear sequence of steps that run to completion.
56
+
57
+ | Feature | `@workflow` | `@stream_workflow` |
58
+ |---|---|---|
59
+ | Execution model | Top-to-bottom | Signal-driven, reactive |
60
+ | Step activation | Explicit `await step()` | Subscription to signal types |
61
+ | Termination | Return from function | All steps reach terminal state |
62
+ | Suspension | `sleep` / `hook` | `hook` + signal arrival |
63
+ | Best for | Sequential business logic | Agents, multi-actor coordination, HITL loops |
64
+
65
+ ## Defining a Stream Workflow
66
+
67
+ ```python
68
+ from pyworkflow import stream_workflow, emit
69
+
70
+ @stream_workflow(name="order_pipeline")
71
+ async def order_pipeline():
72
+ """
73
+ The body runs once at startup, after all @stream_step subscriptions
74
+ for this stream have been materialized. Use it to emit the initial
75
+ signal that kicks the pipeline off.
76
+ """
77
+ await emit("order_pipeline", "pipeline.started", {})
78
+ ```
79
+
80
+ <Note>
81
+ The stream name defaults to the function name when `name=` is omitted. Every `@stream_step` with matching `stream="..."` subscribes to this channel.
82
+ </Note>
83
+
84
+ ## Running from a Parent Workflow
85
+
86
+ Stream workflows are driven from inside a regular `@workflow` via `run_stream_workflow()`. This is what gives them durability: the parent workflow records the stream run, suspends via `hook()` between signals, and the dispatcher resumes the parent once the stream reaches a terminal aggregate state.
87
+
88
+ ```python
89
+ from pyworkflow import workflow, run_stream_workflow
90
+ from pyworkflow.streams import StreamWorkflowResult
91
+
92
+ @workflow()
93
+ async def agent_run(job_id: str):
94
+ async def init():
95
+ # Optional: seed initial state before the stream body runs
96
+ await seed_job(job_id)
97
+
98
+ result: StreamWorkflowResult = await run_stream_workflow(
99
+ order_pipeline,
100
+ stream_run_id=f"stream_{job_id}",
101
+ init=init,
102
+ )
103
+
104
+ # Read step outputs published via set_result()
105
+ planner_output = result.get_result("planner")
106
+ return {"job_id": job_id, "planner": planner_output}
107
+ ```
108
+
109
+ ### Signature
110
+
111
+ ```python
112
+ await run_stream_workflow(
113
+ stream_workflow_func, # the @stream_workflow-decorated function
114
+ *,
115
+ stream_run_id: str, # unique scope key for this run
116
+ init: Callable | None = None, # optional async init callback
117
+ storage = None, # defaults to context / global config
118
+ ) -> StreamWorkflowResult
119
+ ```
120
+
121
+ ## Aggregate Lifecycle
122
+
123
+ A stream run is in one of three aggregate states derived from its subscription rows:
124
+
125
+ | Aggregate | Condition | Parent workflow behavior |
126
+ |---|---|---|
127
+ | `running` | At least one step is `waiting` or `running` | Parent is suspended via `hook()`, worker released |
128
+ | `completed` | Every step is `terminated` | `run_stream_workflow` returns `StreamWorkflowResult(status="completed")` |
129
+ | `suspended` | At least one step `suspended`, none `waiting`/`running` | Parent re-raises `SuspensionSignal` (bubbles up HITL) |
130
+
131
+ ```
132
+ run_stream_workflow(...)
133
+
134
+
135
+ ┌────────────────────────┐
136
+ │ Ensure subscriptions │
137
+ │ exist for every step │
138
+ └───────┬────────────────┘
139
+
140
+
141
+ ┌────────────────────────┐
142
+ │ Run init() + body() │ (first run only)
143
+ └───────┬────────────────┘
144
+
145
+
146
+ ┌────────────────────────┐
147
+ │ Compute aggregate │
148
+ └───────┬────────────────┘
149
+
150
+ ├─ "completed" ──────► Return StreamWorkflowResult
151
+
152
+ ├─ "suspended" ──────► Raise SuspensionSignal (HITL)
153
+
154
+ └─ "running"
155
+
156
+
157
+ ┌────────────────────────┐
158
+ │ await hook(...) │ Parent worker released
159
+ │ Record parent link │
160
+ └───────┬────────────────┘
161
+ │ ... signals flow, steps execute ...
162
+
163
+
164
+ ┌──────────────────────────────┐
165
+ │ Dispatcher sees terminal agg │
166
+ │ Calls resume_hook(token) │
167
+ └───────┬──────────────────────┘
168
+
169
+
170
+ Parent re-enters run_stream_workflow,
171
+ recomputes aggregate, returns or re-raises.
172
+ ```
173
+
174
+ ## StreamWorkflowResult
175
+
176
+ ```python
177
+ @dataclass
178
+ class StreamWorkflowResult:
179
+ status: str # "completed"
180
+ step_states: dict[str, str] # step_run_id -> final status
181
+ step_results: dict[str, Any] # step_run_id -> set_result(...) payload
182
+
183
+ def get_result(self, step_name: str) -> Any:
184
+ """Look up a result by step name (matches against step_run_id)."""
185
+ ```
186
+
187
+ Any value a step published via `await set_result(...)` in its lifecycle body is attached to the subscription row and surfaced here — the parent workflow does not need to read checkpoints.
188
+
189
+ ## Backend Support
190
+
191
+ Stream workflows are fully supported on:
192
+
193
+ - **PostgreSQL** — the primary target (migrations v5/v6/v7 add the required columns)
194
+ - **Citus** — inherits from Postgres, with reference-table distribution
195
+ - **InMemory** — for unit tests
196
+
197
+ Other SQL backends (SQLite, MySQL) and NoSQL backends (DynamoDB, Cassandra) accept the stream API for signature compatibility but do **not** scope subscriptions by `stream_run_id`. Running concurrent stream runs on those backends will cross-contaminate subscription state — stick to Postgres/Citus for production streams.
198
+
199
+ ## Scheduled Signals
200
+
201
+ Use `schedule_signal()` to emit a signal after a delay. A Celery beat task (`pyworkflow.streams.drain_scheduled_signals`, runs every 2s) polls the `scheduled_signals` table and emits due rows.
202
+
203
+ ```python
204
+ from pyworkflow.streams import schedule_signal
205
+
206
+ await schedule_signal(
207
+ stream_id="agent_comms",
208
+ signal_type="supervisor.wakeup",
209
+ payload={"reason": "periodic_check"},
210
+ delay_seconds=300, # 5 minutes
211
+ stream_run_id="stream_job_42",
212
+ )
213
+ ```
214
+
215
+ This replaces in-process polling loops inside stream workflows — the scheduler is durable across worker restarts.
216
+
217
+ ## Related Concepts
218
+
219
+ - [Stream Steps](/concepts/stream-steps) — the reactive units that subscribe to streams
220
+ - [Hooks](/concepts/hooks) — stream workflows use `hook()` internally for parent suspension
221
+ - [Workflows](/concepts/workflows) — stream workflows are driven from inside regular `@workflow`s
@@ -7,7 +7,7 @@ packages = [{include = "pyworkflow"}]
7
7
 
8
8
  [project]
9
9
  name = "pyworkflow-engine"
10
- version = "0.2.7"
10
+ version = "0.2.8"
11
11
  description = "A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -29,7 +29,7 @@ Quick Start:
29
29
  >>> run_id = await start(my_workflow, "Alice")
30
30
  """
31
31
 
32
- __version__ = "0.2.7"
32
+ __version__ = "0.2.8"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -116,22 +116,16 @@ class CitusMigrationRunner(PostgresMigrationRunner):
116
116
  # so this is a no-op but we still record the version.
117
117
  pass
118
118
  elif migration.version == 5:
119
- # V5: Add stream_run_id to stream_subscriptions + scheduled_signals table
120
- await conn.execute("""
121
- DO $$
122
- BEGIN
123
- IF EXISTS (
124
- SELECT FROM information_schema.tables
125
- WHERE table_name = 'stream_subscriptions'
126
- ) AND NOT EXISTS (
127
- SELECT 1 FROM information_schema.columns
128
- WHERE table_name = 'stream_subscriptions'
129
- AND column_name = 'stream_run_id'
130
- ) THEN
131
- ALTER TABLE stream_subscriptions ADD COLUMN stream_run_id TEXT NULL;
132
- END IF;
133
- END $$
134
- """)
119
+ # V5: Add stream_run_id to stream_subscriptions + scheduled_signals table.
120
+ # IMPORTANT: Use top-level ALTER (NOT inside a DO $$ block) so Citus
121
+ # propagates the DDL to all worker shards via the ProcessUtility hook.
122
+ # DDL inside PL/pgSQL DO blocks is executed via SPI and bypasses the
123
+ # hook, which would leave worker shards missing the column while the
124
+ # coordinator catalog appears correct.
125
+ await conn.execute(
126
+ "ALTER TABLE stream_subscriptions "
127
+ "ADD COLUMN IF NOT EXISTS stream_run_id TEXT NULL"
128
+ )
135
129
  await conn.execute(
136
130
  "CREATE INDEX IF NOT EXISTS idx_subscriptions_stream_run "
137
131
  "ON stream_subscriptions(stream_id, stream_run_id)"
@@ -164,41 +158,22 @@ class CitusMigrationRunner(PostgresMigrationRunner):
164
158
  if not already:
165
159
  await conn.execute("SELECT create_reference_table('scheduled_signals')")
166
160
  elif migration.version == 6:
167
- # V6: Add parent_run_id + parent_hook_token to stream_subscriptions
168
- await conn.execute("""
169
- DO $$
170
- BEGIN
171
- IF EXISTS (
172
- SELECT FROM information_schema.tables
173
- WHERE table_name = 'stream_subscriptions'
174
- ) AND NOT EXISTS (
175
- SELECT 1 FROM information_schema.columns
176
- WHERE table_name = 'stream_subscriptions'
177
- AND column_name = 'parent_run_id'
178
- ) THEN
179
- ALTER TABLE stream_subscriptions
180
- ADD COLUMN parent_run_id TEXT NULL,
181
- ADD COLUMN parent_hook_token TEXT NULL;
182
- END IF;
183
- END $$
184
- """)
161
+ # V6: Add parent_run_id + parent_hook_token to stream_subscriptions.
162
+ # Top-level ALTERs so Citus propagates to all worker shards (see V5 note).
163
+ await conn.execute(
164
+ "ALTER TABLE stream_subscriptions "
165
+ "ADD COLUMN IF NOT EXISTS parent_run_id TEXT NULL"
166
+ )
167
+ await conn.execute(
168
+ "ALTER TABLE stream_subscriptions "
169
+ "ADD COLUMN IF NOT EXISTS parent_hook_token TEXT NULL"
170
+ )
185
171
  elif migration.version == 7:
186
- await conn.execute("""
187
- DO $$
188
- BEGIN
189
- IF EXISTS (
190
- SELECT FROM information_schema.tables
191
- WHERE table_name = 'stream_subscriptions'
192
- ) AND NOT EXISTS (
193
- SELECT 1 FROM information_schema.columns
194
- WHERE table_name = 'stream_subscriptions'
195
- AND column_name = 'result'
196
- ) THEN
197
- ALTER TABLE stream_subscriptions
198
- ADD COLUMN result JSONB NULL;
199
- END IF;
200
- END $$
201
- """)
172
+ # V7: Add result column. Top-level ALTER for Citus propagation (see V5 note).
173
+ await conn.execute(
174
+ "ALTER TABLE stream_subscriptions "
175
+ "ADD COLUMN IF NOT EXISTS result JSONB NULL"
176
+ )
202
177
  elif migration.up_func:
203
178
  await migration.up_func(conn)
204
179
  elif migration.up_sql and migration.up_sql != "SELECT 1":