edda-framework 0.6.0__tar.gz → 0.7.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.
Files changed (168) hide show
  1. {edda_framework-0.6.0 → edda_framework-0.7.0}/.gitignore +1 -0
  2. {edda_framework-0.6.0 → edda_framework-0.7.0}/PKG-INFO +3 -1
  3. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/index.md +1 -0
  4. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/integrations/mcp.md +2 -0
  5. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/integrations/opentelemetry.md +4 -0
  6. edda_framework-0.7.0/docs/viewer-ui/images/compensation-execution.png +0 -0
  7. edda_framework-0.7.0/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
  8. edda_framework-0.7.0/docs/viewer-ui/images/detail-page-match-case.png +0 -0
  9. edda_framework-0.7.0/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  10. edda_framework-0.7.0/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
  11. edda_framework-0.7.0/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  12. edda_framework-0.7.0/docs/viewer-ui/images/workflow-list-view.png +0 -0
  13. edda_framework-0.7.0/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
  14. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/viewer-ui/setup.md +11 -1
  15. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/viewer-ui/visualization.md +38 -33
  16. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/protocol.py +18 -4
  17. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/sqlalchemy_storage.py +105 -5
  18. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/app.py +552 -126
  19. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/components.py +81 -68
  20. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/data_service.py +42 -3
  21. edda_framework-0.7.0/edda/viewer_ui/theme.py +200 -0
  22. {edda_framework-0.6.0 → edda_framework-0.7.0}/pyproject.toml +4 -2
  23. edda_framework-0.7.0/tests/test_viewer_pagination.py +318 -0
  24. {edda_framework-0.6.0 → edda_framework-0.7.0}/uv.lock +21 -17
  25. {edda_framework-0.6.0 → edda_framework-0.7.0}/zensical.toml +8 -9
  26. edda_framework-0.6.0/docs/markdown.md +0 -98
  27. edda_framework-0.6.0/docs/viewer-ui/images/compensation-execution.png +0 -0
  28. edda_framework-0.6.0/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
  29. edda_framework-0.6.0/docs/viewer-ui/images/detail-overview-panel.png +0 -0
  30. edda_framework-0.6.0/docs/viewer-ui/images/execution-history-panel.png +0 -0
  31. edda_framework-0.6.0/docs/viewer-ui/images/form-generation-example.png +0 -0
  32. edda_framework-0.6.0/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
  33. edda_framework-0.6.0/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  34. edda_framework-0.6.0/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
  35. edda_framework-0.6.0/docs/viewer-ui/images/status-badges-example.png +0 -0
  36. edda_framework-0.6.0/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  37. edda_framework-0.6.0/docs/viewer-ui/images/workflow-list-view.png +0 -0
  38. {edda_framework-0.6.0 → edda_framework-0.7.0}/.github/workflows/ci.yml +0 -0
  39. {edda_framework-0.6.0 → edda_framework-0.7.0}/.github/workflows/docs.yml +0 -0
  40. {edda_framework-0.6.0 → edda_framework-0.7.0}/.github/workflows/release.yml +0 -0
  41. {edda_framework-0.6.0 → edda_framework-0.7.0}/.python-version +0 -0
  42. {edda_framework-0.6.0 → edda_framework-0.7.0}/Justfile +0 -0
  43. {edda_framework-0.6.0 → edda_framework-0.7.0}/LICENSE +0 -0
  44. {edda_framework-0.6.0 → edda_framework-0.7.0}/README.md +0 -0
  45. {edda_framework-0.6.0 → edda_framework-0.7.0}/demo_app.py +0 -0
  46. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/durable-execution/replay.md +0 -0
  47. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
  48. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/events/wait-event.md +0 -0
  49. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/hooks.md +0 -0
  50. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/retry.md +0 -0
  51. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/saga-compensation.md +0 -0
  52. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/transactional-outbox.md +0 -0
  53. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/core-features/workflows-activities.md +0 -0
  54. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/ecommerce.md +0 -0
  55. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/events.md +0 -0
  56. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/fastapi-integration.md +0 -0
  57. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/saga.md +0 -0
  58. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/examples/simple.md +0 -0
  59. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/concepts.md +0 -0
  60. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/first-workflow.md +0 -0
  61. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/installation.md +0 -0
  62. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/getting-started/quick-start.md +0 -0
  63. {edda_framework-0.6.0 → edda_framework-0.7.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
  64. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/__init__.py +0 -0
  65. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/activity.py +0 -0
  66. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/app.py +0 -0
  67. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/compensation.py +0 -0
  68. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/context.py +0 -0
  69. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/events.py +0 -0
  70. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/exceptions.py +0 -0
  71. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/hooks.py +0 -0
  72. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/__init__.py +0 -0
  73. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/mcp/__init__.py +0 -0
  74. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/mcp/decorators.py +0 -0
  75. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/mcp/server.py +0 -0
  76. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/opentelemetry/__init__.py +0 -0
  77. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/integrations/opentelemetry/hooks.py +0 -0
  78. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/locking.py +0 -0
  79. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/outbox/__init__.py +0 -0
  80. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/outbox/relayer.py +0 -0
  81. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/outbox/transactional.py +0 -0
  82. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/pydantic_utils.py +0 -0
  83. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/replay.py +0 -0
  84. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/retry.py +0 -0
  85. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/serialization/__init__.py +0 -0
  86. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/serialization/base.py +0 -0
  87. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/serialization/json.py +0 -0
  88. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/__init__.py +0 -0
  89. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/storage/models.py +0 -0
  90. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/viewer_ui/__init__.py +0 -0
  91. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/visualizer/__init__.py +0 -0
  92. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/visualizer/ast_analyzer.py +0 -0
  93. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/visualizer/mermaid_generator.py +0 -0
  94. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/workflow.py +0 -0
  95. {edda_framework-0.6.0 → edda_framework-0.7.0}/edda/wsgi.py +0 -0
  96. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/__init__.py +0 -0
  97. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/cancellable_workflow.py +0 -0
  98. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/compensation_workflow.py +0 -0
  99. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/event_waiting_app.py +0 -0
  100. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/event_waiting_workflow.py +0 -0
  101. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/event_waiting_workflow_complete.py +0 -0
  102. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/README.md +0 -0
  103. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/order_processing_mcp.py +0 -0
  104. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/prompts_example.py +0 -0
  105. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/remote_server_example.py +0 -0
  106. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/mcp/simple_mcp_server.py +0 -0
  107. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/observability_with_logfire.py +0 -0
  108. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/observability_with_opentelemetry.py +0 -0
  109. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/pydantic_saga.py +0 -0
  110. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/retry_example.py +0 -0
  111. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/retry_with_compensation.py +0 -0
  112. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/simple_workflow.py +0 -0
  113. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/typeddict_example.py +0 -0
  114. {edda_framework-0.6.0 → edda_framework-0.7.0}/examples/with_outbox.py +0 -0
  115. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/__init__.py +0 -0
  116. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/conftest.py +0 -0
  117. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/__init__.py +0 -0
  118. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/__init__.py +0 -0
  119. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_cancel.py +0 -0
  120. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_integration.py +0 -0
  121. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_jsonrpc.py +0 -0
  122. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_prompts.py +0 -0
  123. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_server.py +0 -0
  124. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/opentelemetry/__init__.py +0 -0
  125. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
  126. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_activity.py +0 -0
  127. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_activity_retry.py +0 -0
  128. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_activity_sync.py +0 -0
  129. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_app.py +0 -0
  130. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_ast_analyzer.py +0 -0
  131. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_atomic_wait_event.py +0 -0
  132. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_binary_data.py +0 -0
  133. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_cloudevents_http_binding.py +0 -0
  134. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_compensation.py +0 -0
  135. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
  136. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_concurrent_outbox.py +0 -0
  137. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_context.py +0 -0
  138. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_ctx_session.py +0 -0
  139. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_distributed_event_delivery.py +0 -0
  140. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_events.py +0 -0
  141. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_lock_race_condition.py +0 -0
  142. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_lock_timeout_customization.py +0 -0
  143. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_locking.py +0 -0
  144. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_multidb_storage.py +0 -0
  145. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_outbox.py +0 -0
  146. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_activity.py +0 -0
  147. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_enum.py +0 -0
  148. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_events.py +0 -0
  149. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_saga.py +0 -0
  150. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_pydantic_utils.py +0 -0
  151. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_received_event.py +0 -0
  152. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_replay.py +0 -0
  153. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_retry_policy.py +0 -0
  154. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_saga_parameter_extraction.py +0 -0
  155. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_serialization.py +0 -0
  156. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_skip_locked.py +0 -0
  157. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_stale_workflow_recovery.py +0 -0
  158. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_storage.py +0 -0
  159. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_storage_mysql.py +0 -0
  160. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_storage_postgresql.py +0 -0
  161. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_transactions.py +0 -0
  162. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_viewer_pydantic_form.py +0 -0
  163. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_viewer_start_saga.py +0 -0
  164. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_wait_timer.py +0 -0
  165. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_workflow.py +0 -0
  166. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_workflow_auto_register.py +0 -0
  167. {edda_framework-0.6.0 → edda_framework-0.7.0}/tests/test_workflow_cancellation.py +0 -0
  168. {edda_framework-0.6.0 → edda_framework-0.7.0}/viewer_app.py +0 -0
@@ -99,3 +99,4 @@ site/
99
99
 
100
100
  # Cache directories
101
101
  .cache/
102
+ .nicegui/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Lightweight Durable Execution Framework
5
5
  Project-URL: Homepage, https://github.com/i2y/edda
6
6
  Project-URL: Documentation, https://github.com/i2y/edda#readme
@@ -30,11 +30,13 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
30
30
  Requires-Dist: uvloop>=0.22.1
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: black>=25.9.0; extra == 'dev'
33
+ Requires-Dist: mcp>=1.22.0; extra == 'dev'
33
34
  Requires-Dist: mypy>=1.18.2; extra == 'dev'
34
35
  Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
35
36
  Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
36
37
  Requires-Dist: pytest>=8.4.2; extra == 'dev'
37
38
  Requires-Dist: ruff>=0.14.2; extra == 'dev'
39
+ Requires-Dist: starlette>=0.40.0; extra == 'dev'
38
40
  Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
39
41
  Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
40
42
  Requires-Dist: tsuno>=0.1.3; extra == 'dev'
@@ -46,6 +46,7 @@ Edda's waiting functions make it ideal for time-based and event-driven business
46
46
  - **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
47
47
 
48
48
  **Waiting functions**:
49
+
49
50
  - `wait_timer(duration_seconds)`: Wait for a relative duration
50
51
  - `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
51
52
  - `wait_event(event_type)`: Wait for external events (near real-time response)
@@ -133,6 +133,7 @@ Output: {
133
133
  ```
134
134
 
135
135
  The status tool provides progress metadata for efficient polling:
136
+
136
137
  - **Completed Activities**: Number of activities that have finished
137
138
  - **Suggested Poll Interval**: Recommended wait time before checking again (5000ms for running, 10000ms for waiting)
138
139
 
@@ -169,6 +170,7 @@ Output: {
169
170
  ```
170
171
 
171
172
  The cancel tool:
173
+
172
174
  - Only works on workflows with status `running`, `waiting_for_event`, or `waiting_for_timer`
173
175
  - Automatically executes SAGA compensation transactions to roll back side effects
174
176
  - Returns an error for already completed, failed, or cancelled workflows
@@ -76,11 +76,13 @@ workflow:order_workflow (parent)
76
76
  ## Span Attributes
77
77
 
78
78
  **Workflow Spans**:
79
+
79
80
  - `edda.workflow.instance_id`
80
81
  - `edda.workflow.name`
81
82
  - `edda.workflow.cancelled` (when cancelled)
82
83
 
83
84
  **Activity Spans**:
85
+
84
86
  - `edda.activity.id` (e.g., "reserve_inventory:1")
85
87
  - `edda.activity.name`
86
88
  - `edda.activity.is_replaying`
@@ -107,10 +109,12 @@ When `enable_metrics=True`:
107
109
  OpenTelemetryHooks automatically inherits trace context from multiple sources, with the following priority:
108
110
 
109
111
  1. **Explicit `_trace_context` in input_data** (highest priority)
112
+
110
113
  - Extracted from CloudEvents extension attributes
111
114
  - Useful for cross-service trace propagation
112
115
 
113
116
  2. **Current active span** (e.g., from ASGI/WSGI middleware)
117
+
114
118
  - Automatically detected using `trace.get_current_span()`
115
119
  - Works with OpenTelemetry instrumentation middleware
116
120
 
@@ -44,6 +44,16 @@ Then open http://localhost:8080 in your browser.
44
44
 
45
45
  *The Viewer UI shows all workflow instances with status badges and action buttons*
46
46
 
47
+ **Pydantic Form Generation:**
48
+
49
+ ![Pydantic Form Generation](images/start-workflow-form-pydantic.png)
50
+
51
+ *Auto-generated form fields based on Pydantic model type hints (str, float, int)*
52
+
53
+ ![Workflow Selection](images/workflow-selection-dropdown.png)
54
+
55
+ *Dropdown showing all registered workflows with event_handler=True*
56
+
47
57
  ## Three Ways to Run the Viewer
48
58
 
49
59
  ### Method 1: Command Line (Recommended)
@@ -295,7 +305,7 @@ lsof -ti:8080 | xargs kill -9
295
305
 
296
306
  Once workflows appear, you'll see them with color-coded status badges:
297
307
 
298
- ![Status Badges Example](images/status-badges-example.png)
308
+ ![Status Badges Example](images/workflow-list-view.png)
299
309
 
300
310
  *Workflow instances displayed with status badges (Completed ✅, Running ⏳, Failed ❌, Waiting ⏸️, Cancelled 🚫, etc.)*
301
311
 
@@ -34,15 +34,22 @@ The Edda Viewer UI provides workflow visualization and monitoring.
34
34
 
35
35
  Workflows with conditional logic (if/else, match/case) are visualized with labeled branches:
36
36
 
37
- ![Conditional Branching Diagram](images/conditional-branching-diagram.png)
37
+ ![Workflow Detail Page](images/detail-page-loan-approval.png)
38
38
 
39
- *Example: `loan_approval_workflow` showing conditional branches based on credit score ("sufficient" vs "insufficient" paths)*
39
+ *Example: `loan_approval_workflow` detail page. Note the **Execution Flow diagram** (bottom left) showing conditional branches with "sufficient" vs "insufficient" paths based on credit score.*
40
40
 
41
41
  The diagram clearly shows:
42
+
42
43
  - Decision points (diamond shapes in some diagrams)
43
44
  - Branch labels indicating conditions
44
45
  - Different execution paths based on runtime data
45
46
 
47
+ **Match/Case Pattern:**
48
+
49
+ ![Match Case Workflow](images/detail-page-match-case.png)
50
+
51
+ *Example: `match_case_workflow` showing multiple case branches with different handlers*
52
+
46
53
  ## Viewing Workflows
47
54
 
48
55
  ### Workflow List
@@ -68,42 +75,46 @@ The main page shows all workflow instances:
68
75
  - 🚫 **Cancelled**: Manually cancelled (orange)
69
76
  - 🔄 **Compensating**: Executing compensations (purple)
70
77
 
71
- ![Status Badges Example](images/status-badges-example.png)
78
+ ![Status Badges Example](images/workflow-list-view.png)
72
79
 
73
80
  ### Workflow List View
74
81
 
75
- The main page displays workflow instances as interactive cards:
82
+ The main page displays workflow instances as interactive cards with search and filter capabilities:
76
83
 
77
84
  ![Workflow List View](images/workflow-list-view.png)
78
85
 
86
+ *Workflow list with search filter bar, status filter, date range picker, and pagination controls*
87
+
79
88
  ### Workflow Detail Page
80
89
 
81
- Click on a workflow instance to see:
90
+ Click on a workflow instance to see the detail page:
91
+
92
+ ![Workflow Detail Page](images/detail-page-loan-approval.png)
82
93
 
83
- 1. **Overview Panel**:
84
- - Instance ID, status, timestamps
85
- - Input parameters (JSON)
86
- - Output result (if completed)
94
+ *Workflow detail page showing the Overview Panel (top), Execution Flow diagram (bottom left), and Activity Details panel (bottom right).*
87
95
 
88
- ![Workflow Detail - Overview Panel](images/detail-overview-panel.png)
96
+ 1. **Overview Panel** (top):
89
97
 
90
- 2. **Hybrid Diagram**:
91
- - Visual graph of workflow structure
92
- - Color-coded execution status
93
- - Compensation flow (if applicable)
98
+ - Instance ID, status, timestamps
99
+ - Input parameters (JSON)
100
+ - Output result (if completed)
94
101
 
95
- ![Hybrid Diagram Example](images/hybrid-diagram-example.png)
102
+ 2. **Execution Flow** (bottom left):
96
103
 
97
- 3. **Execution History**:
98
- - Step-by-step execution log
99
- - Activity results
100
- - Event data (for wait_event)
101
- - Error messages (if failed)
104
+ - Visual graph of workflow structure
105
+ - Color-coded execution status
106
+ - Compensation flow (if applicable)
102
107
 
103
- ![Execution History Panel](images/execution-history-panel.png)
108
+ 3. **Activity Details** (bottom right):
109
+
110
+ - Step-by-step execution log
111
+ - Activity results
112
+ - Event data (for wait_event)
113
+ - Error messages (if failed)
104
114
 
105
115
  4. **Actions**:
106
- - **Cancel**: Stop running workflow
116
+
117
+ - **Cancel**: Stop running workflow
107
118
 
108
119
  ## Starting Workflows from Viewer
109
120
 
@@ -113,21 +124,15 @@ The Viewer UI can start workflows with automatic form generation:
113
124
 
114
125
  For Pydantic-based workflows, the Viewer generates input forms automatically.
115
126
 
116
- ![Start Workflow Dialog](images/start-workflow-dialog.png)
127
+ ![Start Workflow Dialog](images/workflow-selection-dropdown.png)
117
128
 
118
- *Example: Workflow selection with auto-generated form*
129
+ *Example: Workflow selection dropdown showing all registered workflows*
119
130
 
120
131
  The form generator supports various field types based on Pydantic model definitions:
121
132
 
122
- ![Form Generation Example](images/form-generation-example.png)
123
-
124
- *Example: Auto-generated form showing different field types (text inputs, number inputs, checkboxes, etc.)*
125
-
126
- For complex workflows with nested models:
127
-
128
133
  ![Nested Pydantic Form](images/nested-pydantic-form.png)
129
134
 
130
- *Example: Auto-generated form with nested Pydantic models (OrderItem list, ShippingAddress)*
135
+ *Auto-generated form with nested Pydantic models (items list, shipping_address) showing text inputs, number inputs, and nested model containers*
131
136
 
132
137
  ### Starting Workflow
133
138
 
@@ -166,7 +171,7 @@ When a workflow is cancelled or fails, and compensations run, the diagram shows:
166
171
 
167
172
  ![Compensation Execution](images/compensation-execution.png)
168
173
 
169
- *Example: `cancellable_booking` workflow after cancellation*
174
+ *Saga compensation flow showing rollback activities (cancel_flight_ticket, cancel_hotel_room)*
170
175
 
171
176
  ## Event Waiting Visualization
172
177
 
@@ -174,7 +179,7 @@ Workflows can wait for external events using `wait_event()`. The Viewer displays
174
179
 
175
180
  ![Event Waiting Visualization](images/wait-event-visualization.png)
176
181
 
177
- *Example: `payment_workflow` waiting for `payment.completed` event*
182
+ *Workflow with wait_event showing event type, timeout, and activity details panel*
178
183
 
179
184
  The diagram shows:
180
185
  - Hexagon node: Event wait point
@@ -238,20 +238,34 @@ class StorageProtocol(Protocol):
238
238
  async def list_instances(
239
239
  self,
240
240
  limit: int = 50,
241
+ page_token: str | None = None,
241
242
  status_filter: str | None = None,
242
- ) -> list[dict[str, Any]]:
243
+ workflow_name_filter: str | None = None,
244
+ instance_id_filter: str | None = None,
245
+ started_after: datetime | None = None,
246
+ started_before: datetime | None = None,
247
+ ) -> dict[str, Any]:
243
248
  """
244
- List workflow instances with optional filtering.
249
+ List workflow instances with cursor-based pagination and filtering.
245
250
 
246
251
  This method JOINs workflow_instances with workflow_definitions to
247
252
  return instances along with their source code.
248
253
 
249
254
  Args:
250
- limit: Maximum number of instances to return
255
+ limit: Maximum number of instances to return per page
256
+ page_token: Cursor for pagination (format: "ISO_DATETIME||INSTANCE_ID")
251
257
  status_filter: Optional status filter (e.g., "running", "completed", "failed")
258
+ workflow_name_filter: Optional workflow name filter (partial match, case-insensitive)
259
+ instance_id_filter: Optional instance ID filter (partial match, case-insensitive)
260
+ started_after: Filter instances started after this datetime (inclusive)
261
+ started_before: Filter instances started before this datetime (inclusive)
252
262
 
253
263
  Returns:
254
- List of workflow instances, ordered by started_at DESC.
264
+ Dictionary containing:
265
+ - instances: List of workflow instances, ordered by started_at DESC
266
+ - next_page_token: Cursor for the next page, or None if no more pages
267
+ - has_more: Boolean indicating if there are more pages
268
+
255
269
  Each instance contains: instance_id, workflow_name, source_hash,
256
270
  owner_service, status, current_activity_id, started_at, updated_at,
257
271
  input_data, source_code, output_data, locked_by, locked_at
@@ -774,11 +774,17 @@ class SQLAlchemyStorage:
774
774
  async def list_instances(
775
775
  self,
776
776
  limit: int = 50,
777
+ page_token: str | None = None,
777
778
  status_filter: str | None = None,
778
- ) -> list[dict[str, Any]]:
779
- """List workflow instances with optional filtering."""
779
+ workflow_name_filter: str | None = None,
780
+ instance_id_filter: str | None = None,
781
+ started_after: datetime | None = None,
782
+ started_before: datetime | None = None,
783
+ ) -> dict[str, Any]:
784
+ """List workflow instances with cursor-based pagination and filtering."""
780
785
  session = self._get_session_for_operation()
781
786
  async with self._session_scope(session) as session:
787
+ # Base query with JOIN
782
788
  stmt = (
783
789
  select(WorkflowInstance, WorkflowDefinition.source_code)
784
790
  .join(
@@ -788,17 +794,105 @@ class SQLAlchemyStorage:
788
794
  WorkflowInstance.source_hash == WorkflowDefinition.source_hash,
789
795
  ),
790
796
  )
791
- .order_by(WorkflowInstance.started_at.desc())
792
- .limit(limit)
797
+ .order_by(
798
+ WorkflowInstance.started_at.desc(),
799
+ WorkflowInstance.instance_id.desc(),
800
+ )
793
801
  )
794
802
 
803
+ # Apply cursor-based pagination (page_token format: "ISO_DATETIME||INSTANCE_ID")
804
+ if page_token:
805
+ # Parse page_token: || separates datetime and instance_id
806
+ separator = "||"
807
+ if separator in page_token:
808
+ cursor_time_str, cursor_id = page_token.split(separator, 1)
809
+ cursor_time = datetime.fromisoformat(cursor_time_str)
810
+ # Use _make_datetime_comparable for SQLite compatibility
811
+ started_at_comparable = self._make_datetime_comparable(
812
+ WorkflowInstance.started_at
813
+ )
814
+ # For SQLite, also wrap the cursor_time in func.datetime()
815
+ cursor_time_comparable: Any
816
+ if self.engine.dialect.name == "sqlite":
817
+ cursor_time_comparable = func.datetime(cursor_time_str)
818
+ else:
819
+ cursor_time_comparable = cursor_time
820
+ # For DESC order, we want rows where (started_at, instance_id) < cursor
821
+ stmt = stmt.where(
822
+ or_(
823
+ started_at_comparable < cursor_time_comparable,
824
+ and_(
825
+ started_at_comparable == cursor_time_comparable,
826
+ WorkflowInstance.instance_id < cursor_id,
827
+ ),
828
+ )
829
+ )
830
+
831
+ # Apply status filter
795
832
  if status_filter:
796
833
  stmt = stmt.where(WorkflowInstance.status == status_filter)
797
834
 
835
+ # Apply workflow name and/or instance ID filter (partial match, case-insensitive)
836
+ # When both filters have the same value (unified search), use OR logic
837
+ if workflow_name_filter and instance_id_filter:
838
+ if workflow_name_filter == instance_id_filter:
839
+ # Unified search: match either workflow name OR instance ID
840
+ stmt = stmt.where(
841
+ or_(
842
+ WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%"),
843
+ WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"),
844
+ )
845
+ )
846
+ else:
847
+ # Separate filters: match both (AND logic)
848
+ stmt = stmt.where(
849
+ WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%")
850
+ )
851
+ stmt = stmt.where(WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"))
852
+ elif workflow_name_filter:
853
+ stmt = stmt.where(WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%"))
854
+ elif instance_id_filter:
855
+ stmt = stmt.where(WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"))
856
+
857
+ # Apply date range filters (use _make_datetime_comparable for SQLite)
858
+ if started_after or started_before:
859
+ started_at_comparable = self._make_datetime_comparable(WorkflowInstance.started_at)
860
+ if started_after:
861
+ started_after_comparable: Any
862
+ if self.engine.dialect.name == "sqlite":
863
+ started_after_comparable = func.datetime(started_after.isoformat())
864
+ else:
865
+ started_after_comparable = started_after
866
+ stmt = stmt.where(started_at_comparable >= started_after_comparable)
867
+ if started_before:
868
+ started_before_comparable: Any
869
+ if self.engine.dialect.name == "sqlite":
870
+ started_before_comparable = func.datetime(started_before.isoformat())
871
+ else:
872
+ started_before_comparable = started_before
873
+ stmt = stmt.where(started_at_comparable <= started_before_comparable)
874
+
875
+ # Fetch limit+1 to determine if there are more pages
876
+ stmt = stmt.limit(limit + 1)
877
+
798
878
  result = await session.execute(stmt)
799
879
  rows = result.all()
800
880
 
801
- return [
881
+ # Determine has_more and next_page_token
882
+ has_more = len(rows) > limit
883
+ if has_more:
884
+ rows = rows[:limit] # Trim to actual limit
885
+
886
+ # Generate next_page_token from last row
887
+ next_page_token: str | None = None
888
+ if has_more and rows:
889
+ last_instance = rows[-1][0]
890
+ # Format: ISO_DATETIME||INSTANCE_ID (using || as separator)
891
+ next_page_token = (
892
+ f"{last_instance.started_at.isoformat()}||{last_instance.instance_id}"
893
+ )
894
+
895
+ instances = [
802
896
  {
803
897
  "instance_id": instance.instance_id,
804
898
  "workflow_name": instance.workflow_name,
@@ -820,6 +914,12 @@ class SQLAlchemyStorage:
820
914
  for instance, source_code in rows
821
915
  ]
822
916
 
917
+ return {
918
+ "instances": instances,
919
+ "next_page_token": next_page_token,
920
+ "has_more": has_more,
921
+ }
922
+
823
923
  # -------------------------------------------------------------------------
824
924
  # Distributed Locking Methods (ALWAYS use separate session/transaction)
825
925
  # -------------------------------------------------------------------------