edda-framework 0.1.0__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.
@@ -0,0 +1,748 @@
1
+ Metadata-Version: 2.4
2
+ Name: edda-framework
3
+ Version: 0.1.0
4
+ Summary: Lightweight Durable Execution Framework
5
+ Project-URL: Homepage, https://github.com/i2y/edda
6
+ Project-URL: Documentation, https://github.com/i2y/edda#readme
7
+ Project-URL: Repository, https://github.com/i2y/edda
8
+ Project-URL: Issues, https://github.com/i2y/edda/issues
9
+ Author-email: Yasushi Itoh <6240399+i2y@users.noreply.github.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: cloudevents,distributed,durable-execution,event-driven,knative,microservices,orchestration,workflow
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
+ Classifier: Topic :: System :: Distributed Computing
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: aiosqlite>=0.21.0
24
+ Requires-Dist: cloudevents>=1.12.0
25
+ Requires-Dist: httpx>=0.28.1
26
+ Requires-Dist: pydantic>=2.0.0
27
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
28
+ Requires-Dist: uvloop>=0.22.1
29
+ Provides-Extra: dev
30
+ Requires-Dist: black>=25.9.0; extra == 'dev'
31
+ Requires-Dist: mypy>=1.18.2; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.4.2; extra == 'dev'
35
+ Requires-Dist: ruff>=0.14.2; extra == 'dev'
36
+ Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
37
+ Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
38
+ Requires-Dist: tsuno>=0.1.3; extra == 'dev'
39
+ Provides-Extra: mysql
40
+ Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
41
+ Provides-Extra: postgresql
42
+ Requires-Dist: asyncpg>=0.30.0; extra == 'postgresql'
43
+ Provides-Extra: server
44
+ Requires-Dist: tsuno>=0.1.3; extra == 'server'
45
+ Provides-Extra: viewer
46
+ Requires-Dist: nicegui>=2.8.0; extra == 'viewer'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # Edda
50
+
51
+ **Edda** - Norse mythology poetic narratives that preserve ancient sagas and legends
52
+
53
+ > Lightweight durable execution framework - no separate server required
54
+
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
56
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
57
+ [![Documentation](https://img.shields.io/badge/docs-latest-green.svg)](https://i2y.github.io/edda/)
58
+
59
+ ## Overview
60
+
61
+ Edda is a lightweight durable execution framework for Python that runs as a **library** in your application - no separate workflow server required. It provides automatic crash recovery through deterministic replay, allowing **long-running workflows** to survive process restarts and failures without losing progress.
62
+
63
+ **Perfect for**: Order processing, distributed transactions (Saga pattern), AI agent orchestration, and any workflow that must survive crashes.
64
+
65
+ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.github.io/edda/)
66
+
67
+ ## Key Features
68
+
69
+ - ✨ **Lightweight Library**: Runs in your application process - no separate server infrastructure
70
+ - 🔄 **Durable Execution**: Deterministic replay with workflow history for automatic crash recovery
71
+ - 🎯 **Workflow & Activity**: Clear separation between orchestration logic and business logic
72
+ - 🔁 **Saga Pattern**: Automatic compensation on failure with `@on_failure` decorator
73
+ - 🌐 **Multi-worker Execution**: Run workflows safely across multiple servers or containers
74
+ - 🔒 **Pydantic Integration**: Type-safe workflows with automatic validation
75
+ - 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
76
+ - ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
77
+ - ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
78
+
79
+ ## Use Cases
80
+
81
+ Edda excels at orchestrating **long-running workflows** that must survive failures:
82
+
83
+ - **🏢 Long-Running Jobs**: Order processing, data pipelines, batch jobs - from minutes to days, weeks, or even months
84
+ - **🔄 Distributed Transactions**: Coordinate microservices with automatic compensation (Saga pattern)
85
+ - **🤖 AI Agent Workflows**: Orchestrate multi-step AI tasks (LLM calls, tool usage, long-running inference)
86
+ - **📡 Event-Driven Workflows**: React to external events with guaranteed delivery and automatic retry
87
+
88
+ **Key benefit**: Workflows **never lose progress** - crashes and restarts are handled automatically through deterministic replay.
89
+
90
+ ## Architecture
91
+
92
+ Edda runs as a lightweight library in your applications, with all workflow state stored in a shared database:
93
+
94
+ ```mermaid
95
+ %%{init: {'theme':'base', 'themeVariables': {'primaryTextColor':'#1a1a1a', 'secondaryTextColor':'#1a1a1a', 'tertiaryTextColor':'#1a1a1a', 'textColor':'#1a1a1a', 'nodeTextColor':'#1a1a1a'}}}%%
96
+ graph TB
97
+ subgraph ext["External Systems"]
98
+ API[REST API<br/>Clients]
99
+ CE[CloudEvents<br/>Producer]
100
+ end
101
+
102
+ subgraph cluster["Your Multiple Instances"]
103
+ subgraph pod1["order-service Pod 1"]
104
+ W1[Edda Workflow]
105
+ end
106
+ subgraph pod2["order-service Pod 2"]
107
+ W2[Edda Workflow]
108
+ end
109
+ subgraph pod3["order-service Pod 3"]
110
+ W3[Edda Workflow]
111
+ end
112
+ end
113
+
114
+ DB[(Shared Database<br/>PostgreSQL/MySQL<br/>SQLite: single-process only)]
115
+
116
+ API -->|"workflow.start()<br/>(Direct Invocation)"| W1
117
+ API -->|"workflow.start()<br/>(Direct Invocation)"| W2
118
+ CE -->|"POST /<br/>(CloudEvents)"| W1
119
+ CE -->|"POST /<br/>(CloudEvents)"| W3
120
+
121
+ W1 <-->|Workflow<br/>State| DB
122
+ W2 <-->|Workflow<br/>State| DB
123
+ W3 <-->|Workflow<br/>State| DB
124
+
125
+ style DB fill:#e1f5ff
126
+ style W1 fill:#fff4e6
127
+ style W2 fill:#fff4e6
128
+ style W3 fill:#fff4e6
129
+ ```
130
+
131
+ **Key Points**:
132
+
133
+ - Multiple workers can run simultaneously across different pods/servers
134
+ - Each workflow instance runs on only one worker at a time (automatic coordination)
135
+ - `wait_event()` and `wait_timer()` free up worker resources while waiting, resume on any worker when event arrives or timer expires
136
+ - Automatic crash recovery with stale lock cleanup and workflow auto-resume
137
+
138
+ ## Quick Start
139
+
140
+ ```python
141
+ from edda import EddaApp, workflow, activity, WorkflowContext
142
+
143
+ @activity
144
+ async def process_payment(ctx: WorkflowContext, amount: float):
145
+ # Durable execution - automatically recorded in history
146
+ print(f"Processing payment: ${amount}")
147
+ return {"status": "paid", "amount": amount}
148
+
149
+ @workflow
150
+ async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):
151
+ # Workflow orchestrates activities with automatic retry on crash
152
+ result = await process_payment(ctx, amount)
153
+ return {"order_id": order_id, **result}
154
+
155
+ # Simplified example - production code needs:
156
+ # 1. await app.initialize() before starting workflows
157
+ # 2. try-finally with await app.shutdown() for cleanup
158
+ # 3. PostgreSQL or MySQL for multi-process/multi-pod deployments
159
+ app = EddaApp(db_url="sqlite:///workflow.db")
160
+
161
+ # Start workflow
162
+ instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
163
+ ```
164
+
165
+ **What happens on crash?**
166
+
167
+ 1. Activities already executed return cached results from history
168
+ 2. Workflow resumes from the last checkpoint
169
+ 3. No manual intervention required
170
+
171
+ ## Installation
172
+
173
+ Install Edda from PyPI using uv:
174
+
175
+ ```bash
176
+ # Basic installation (includes SQLite support)
177
+ uv add edda-framework
178
+
179
+ # With PostgreSQL support
180
+ uv add edda-framework --extra postgresql
181
+
182
+ # With MySQL support
183
+ uv add edda-framework --extra mysql
184
+
185
+ # With Viewer UI
186
+ uv add edda-framework --extra viewer
187
+
188
+ # All extras (PostgreSQL, MySQL, Viewer UI)
189
+ uv add edda-framework --extra postgresql --extra mysql --extra viewer
190
+ ```
191
+
192
+ ### Installing from GitHub (Development Versions)
193
+
194
+ Install the latest development version directly from GitHub:
195
+
196
+ ```bash
197
+ # Using uv (latest from main branch)
198
+ uv add git+https://github.com/i2y/edda.git
199
+
200
+ # Using pip
201
+ pip install git+https://github.com/i2y/edda.git
202
+ ```
203
+
204
+ **Install specific version or branch:**
205
+
206
+ ```bash
207
+ # Specific tag/release
208
+ uv add git+https://github.com/i2y/edda.git@v0.1.0
209
+ pip install git+https://github.com/i2y/edda.git@v0.1.0
210
+
211
+ # Specific branch
212
+ uv add git+https://github.com/i2y/edda.git@feature-branch
213
+ pip install git+https://github.com/i2y/edda.git@feature-branch
214
+
215
+ # With extras (PostgreSQL, Viewer)
216
+ uv add "git+https://github.com/i2y/edda.git[postgresql,viewer]"
217
+ pip install "git+https://github.com/i2y/edda.git[postgresql,viewer]"
218
+ ```
219
+
220
+ **Database Drivers**:
221
+ - **SQLite**: Included by default (via `aiosqlite`)
222
+ - Single-process deployments only (supports multiple async workers within one process, not multiple processes/pods)
223
+ - **PostgreSQL**: Add `--extra postgresql` for `asyncpg` driver
224
+ - **Recommended for production**
225
+ - **MySQL**: Add `--extra mysql` for `aiomysql` driver
226
+ - **Recommended for production**
227
+ - **Viewer UI**: Add `--extra viewer` for workflow visualization
228
+
229
+ ### Database Selection Guide
230
+
231
+ | Database | Use Case | Multi-Pod Support | Production Ready | Notes |
232
+ |----------|----------|-------------------|------------------|-------|
233
+ | **SQLite** | Development, testing, single-process deployments | ❌ No | ⚠️ Limited | Supports multiple async workers within one process, but not multiple processes/pods (K8s, Docker Compose with multiple replicas) |
234
+ | **PostgreSQL** | Production, multi-process/multi-pod systems | ✅ Yes | ✅ Yes | **Recommended for production** - Full support for database-based exclusive control and concurrent workflows |
235
+ | **MySQL** | Production with existing MySQL infrastructure | ✅ Yes | ✅ Yes | Suitable for production - Good choice if you already use MySQL |
236
+
237
+ **Important**: For multi-process or multi-pod deployments (K8s, Docker Compose with multiple replicas, etc.), you **must** use PostgreSQL or MySQL. SQLite supports multiple async workers within a single process, but its table-level locking makes it unsuitable for multi-process/multi-pod scenarios.
238
+
239
+ ### Development Installation
240
+
241
+ If you want to contribute to Edda or modify the framework itself:
242
+
243
+ ```bash
244
+ # Clone repository
245
+ git clone https://github.com/i2y/edda.git
246
+ cd kairo
247
+ uv sync --all-extras
248
+ ```
249
+
250
+ ### Running Tests
251
+
252
+ Run Edda's test suite:
253
+
254
+ ```bash
255
+ # Run tests
256
+ uv run pytest
257
+
258
+ # Run with coverage
259
+ uv run pytest --cov=edda
260
+ ```
261
+
262
+ ## Core Concepts
263
+
264
+ ### Workflows and Activities
265
+
266
+ **Activity**: A unit of work that performs business logic. Activity results are recorded in history.
267
+
268
+ **Workflow**: Orchestration logic that coordinates activities. Workflows can be replayed from history after crashes.
269
+
270
+ ```python
271
+ from edda import workflow, activity, WorkflowContext
272
+
273
+ @activity
274
+ async def send_email(ctx: WorkflowContext, email: str, message: str):
275
+ # Business logic - this will be recorded
276
+ print(f"Sending email to {email}")
277
+ return {"sent": True}
278
+
279
+ @workflow
280
+ async def user_signup(ctx: WorkflowContext, email: str):
281
+ # Orchestration logic
282
+ await send_email(ctx, email, "Welcome!")
283
+ return {"status": "completed"}
284
+ ```
285
+
286
+ **Activity IDs**: Activities are automatically identified with IDs like `"send_email:1"` for deterministic replay. Manual IDs are only needed for concurrent execution (e.g., `asyncio.gather`). See [MIGRATION_GUIDE_ACTIVITY_ID.md](MIGRATION_GUIDE_ACTIVITY_ID.md) for details.
287
+
288
+ ### Durable Execution
289
+
290
+ Edda ensures workflow progress is never lost through **deterministic replay**:
291
+
292
+ 1. **Activity results are recorded** in a history table
293
+ 2. **On crash recovery**, workflows resume from the last checkpoint
294
+ 3. **Already-executed activities** return cached results from history
295
+ 4. **New activities** continue from where the workflow left off
296
+
297
+ ```python
298
+ @workflow
299
+ async def long_running_workflow(ctx: WorkflowContext, user_id: str):
300
+ # Activity 1: Recorded in history
301
+ result1 = await create_user(ctx, user_id)
302
+
303
+ # If process crashes here, activity won't re-execute on restart
304
+
305
+ # Activity 2: Continues from history on restart
306
+ result2 = await send_welcome_email(ctx, result1["email"])
307
+
308
+ return result2
309
+ ```
310
+
311
+ **Key guarantees**:
312
+ - Activities execute **exactly once** (results cached in history)
313
+ - Workflows can survive **arbitrary crashes**
314
+ - No manual checkpoint management required
315
+
316
+ ### Automatic Activity Retry
317
+
318
+ Activities automatically retry with exponential backoff when errors occur, improving reliability without manual error handling:
319
+
320
+ ```python
321
+ from edda import activity, WorkflowContext
322
+
323
+ @activity
324
+ async def call_external_api(ctx: WorkflowContext, url: str):
325
+ # Automatically retries up to 5 times with exponential backoff
326
+ # Delays: 1s, 2s, 4s, 8s, 16s
327
+ response = await httpx.get(url, timeout=10)
328
+ return response.json()
329
+ ```
330
+
331
+ **Default retry policy**:
332
+ - **5 attempts** (including initial)
333
+ - **Exponential backoff**: 1s, 2s, 4s, 8s, 16s between attempts
334
+ - **Max delay**: 60 seconds
335
+ - **Total duration**: 5 minutes maximum
336
+
337
+ **Custom retry policies** for specific activities:
338
+
339
+ ```python
340
+ from edda import activity, RetryPolicy, WorkflowContext
341
+
342
+ @activity(retry_policy=RetryPolicy(
343
+ max_attempts=3,
344
+ initial_interval=0.5,
345
+ backoff_coefficient=2.0,
346
+ max_interval=10.0,
347
+ max_duration=60.0
348
+ ))
349
+ async def flaky_operation(ctx: WorkflowContext, data: dict):
350
+ # Custom: 3 attempts, delays 0.5s, 1s, 2s
351
+ return await external_service.process(data)
352
+ ```
353
+
354
+ **Application-level default policy**:
355
+
356
+ ```python
357
+ from edda import EddaApp, RetryPolicy
358
+
359
+ app = EddaApp(
360
+ db_url="sqlite:///workflow.db",
361
+ default_retry_policy=RetryPolicy(
362
+ max_attempts=10,
363
+ initial_interval=2.0
364
+ )
365
+ )
366
+ ```
367
+
368
+ **Non-retryable errors** with `TerminalError`:
369
+
370
+ ```python
371
+ from edda import activity, TerminalError, WorkflowContext
372
+
373
+ @activity
374
+ async def validate_user(ctx: WorkflowContext, user_id: str):
375
+ user = await get_user(user_id)
376
+ if user is None:
377
+ # Immediately fail without retry (user doesn't exist)
378
+ raise TerminalError(f"User {user_id} not found")
379
+ return user
380
+ ```
381
+
382
+ **Retry metadata for observability**:
383
+
384
+ Retry information is automatically embedded in activity history for monitoring:
385
+
386
+ ```python
387
+ {
388
+ "event_type": "ActivityCompleted",
389
+ "event_data": {
390
+ "activity_name": "call_external_api",
391
+ "result": {...},
392
+ "retry_metadata": {
393
+ "total_attempts": 3,
394
+ "total_duration_ms": 7200,
395
+ "last_error": {...},
396
+ "exhausted": False,
397
+ "errors": [...]
398
+ }
399
+ }
400
+ }
401
+ ```
402
+
403
+ **Policy resolution order**:
404
+ 1. Activity-level policy (`@activity(retry_policy=...)`)
405
+ 2. Application-level policy (`EddaApp(default_retry_policy=...)`)
406
+ 3. Framework default (5 attempts, exponential backoff)
407
+
408
+ ### Compensation (Saga Pattern)
409
+
410
+ When a workflow fails, Edda automatically executes compensation functions for **already-executed activities in reverse order**. This implements the Saga pattern for distributed transaction rollback.
411
+
412
+ **Key behavior**:
413
+ - Compensation functions run in **reverse order** of activity execution
414
+ - Only **already-executed activities** are compensated
415
+ - If Activity A and B completed, then C fails → B and A compensations run (in that order)
416
+
417
+ ```python
418
+ from edda import activity, on_failure, compensation, workflow, WorkflowContext
419
+
420
+ @compensation
421
+ async def cancel_reservation(ctx: WorkflowContext, item_id: str):
422
+ # Automatically called on workflow failure (reverse order)
423
+ print(f"Cancelled reservation for {item_id}")
424
+ return {"cancelled": True}
425
+
426
+ @activity
427
+ @on_failure(cancel_reservation)
428
+ async def reserve_inventory(ctx: WorkflowContext, item_id: str):
429
+ print(f"Reserved {item_id}")
430
+ return {"reserved": True}
431
+
432
+ @workflow
433
+ async def order_workflow(ctx: WorkflowContext, item1: str, item2: str):
434
+ await reserve_inventory(ctx, item1) # Step 1: Reserve item1
435
+ await reserve_inventory(ctx, item2) # Step 2: Reserve item2
436
+ await charge_payment(ctx) # Step 3: If this fails...
437
+ # Compensation runs: cancel item2 → cancel item1 (reverse order)
438
+ ```
439
+
440
+ ### Multi-worker Execution
441
+
442
+ Multiple workers can safely process workflows using database-based exclusive control. This means:
443
+
444
+ - Edda uses database-based locks (not Redis or ZooKeeper)
445
+ - Each workflow instance runs on only one worker at a time
446
+ - If a worker crashes, another worker automatically resumes
447
+ - No additional infrastructure required
448
+
449
+ ```python
450
+ # Worker 1 and Worker 2 can run simultaneously
451
+ # Only one will acquire the lock for each workflow instance
452
+
453
+ app = EddaApp(
454
+ db_url="postgresql://localhost/workflows", # Shared database for coordination
455
+ service_name="order-service"
456
+ )
457
+ ```
458
+
459
+ **Features**:
460
+ - Each workflow instance runs on only one worker at a time (automatic coordination)
461
+ - Automatic stale lock cleanup (5-minute timeout)
462
+ - Crashed workflows automatically resume on any available worker
463
+
464
+ ### Pydantic Integration
465
+
466
+ Type-safe workflows with automatic validation:
467
+
468
+ ```python
469
+ from pydantic import BaseModel, Field
470
+ from edda import workflow, WorkflowContext
471
+
472
+ class OrderItem(BaseModel):
473
+ item_id: str
474
+ quantity: int = Field(..., ge=1)
475
+ price: float = Field(..., gt=0)
476
+
477
+ @workflow
478
+ async def process_order(ctx: WorkflowContext, items: list[OrderItem]) -> dict:
479
+ # Automatic validation before workflow starts
480
+ total = sum(item.price * item.quantity for item in items)
481
+ return {"total": total, "item_count": len(items)}
482
+ ```
483
+
484
+ ### Transactional Outbox
485
+
486
+ Activities are automatically transactional by default, ensuring atomicity:
487
+
488
+ ```python
489
+ from edda import activity, send_event_transactional, WorkflowContext
490
+
491
+ @activity # Automatically transactional
492
+ async def create_order(ctx: WorkflowContext, order_id: str):
493
+ # All operations in a single transaction:
494
+ # 1. Activity execution
495
+ # 2. History recording
496
+ # 3. Event publishing (outbox table)
497
+
498
+ await send_event_transactional(
499
+ ctx,
500
+ event_type="order.created",
501
+ event_source="order-service",
502
+ event_data={"order_id": order_id}
503
+ )
504
+
505
+ return {"order_id": order_id}
506
+ ```
507
+
508
+ **Custom Database Operations** - Use `ctx.session` for your database operations:
509
+
510
+ ```python
511
+ @activity # Edda manages the transaction
512
+ async def process_payment(ctx: WorkflowContext, order_id: str, amount: float):
513
+ # Access Edda-managed session (same database as Edda)
514
+ session = ctx.session
515
+
516
+ # Your business logic
517
+ payment = Payment(order_id=order_id, amount=amount)
518
+ session.add(payment)
519
+
520
+ # Edda event (same transaction)
521
+ await send_event_transactional(
522
+ ctx,
523
+ event_type="payment.processed",
524
+ event_source="payment-service",
525
+ event_data={"order_id": order_id, "amount": amount}
526
+ )
527
+
528
+ # Edda automatically commits: your data + Edda's outbox (atomic!)
529
+ return {"payment_id": f"PAY-{order_id}"}
530
+ ```
531
+
532
+ ## Event Integration
533
+
534
+ Edda provides optional event-driven capabilities for workflows that need to wait for external events.
535
+
536
+ ### CloudEvents Support
537
+
538
+ Native support for CloudEvents protocol:
539
+
540
+ ```python
541
+ from edda import EddaApp
542
+
543
+ app = EddaApp(
544
+ db_url="sqlite:///workflow.db",
545
+ service_name="order-service",
546
+ outbox_enabled=True # Enable transactional outbox
547
+ )
548
+
549
+ # Accepts CloudEvents at any HTTP path
550
+ ```
551
+
552
+ **CloudEvents handling**:
553
+ - All HTTP requests (any path) are accepted as CloudEvents
554
+ - Events without matching workflow handlers are silently discarded
555
+ - Special endpoint: `POST /cancel/{instance_id}` for workflow cancellation
556
+ - Automatic CloudEvents validation and parsing
557
+ - Works with CloudEvents-compatible systems (Knative Eventing, CloudEvents SDKs, etc.)
558
+
559
+ **CloudEvents HTTP Binding compliance**:
560
+ - **202 Accepted**: Event accepted for asynchronous processing (success)
561
+ - **400 Bad Request**: CloudEvents parsing/validation error (non-retryable)
562
+ - **500 Internal Server Error**: Internal error (retryable)
563
+ - Error responses include `error_type` and `retryable` flags for client retry logic
564
+
565
+ ### Event & Timer Waiting
566
+
567
+ Workflows can wait for external events or timers without consuming worker resources. While waiting, the workflow state is persisted to the database and can be resumed by **any available worker** when the event arrives or timer expires:
568
+
569
+ ```python
570
+ from edda import workflow, wait_event, send_event, WorkflowContext
571
+
572
+ @workflow
573
+ async def payment_workflow(ctx: WorkflowContext, order_id: str):
574
+ # Send payment request event
575
+ await send_event("payment.requested", "payment-service", {"order_id": order_id})
576
+
577
+ # Wait for payment completion event (process-releasing)
578
+ payment_event = await wait_event(ctx, "payment.completed")
579
+
580
+ return payment_event.data
581
+ ```
582
+
583
+ **wait_timer() for time-based waiting**:
584
+
585
+ ```python
586
+ from edda import wait_timer
587
+
588
+ @workflow
589
+ async def order_with_timeout(ctx: WorkflowContext, order_id: str):
590
+ # Create order
591
+ await create_order(ctx, order_id)
592
+
593
+ # Wait 60 seconds for payment
594
+ await wait_timer(ctx, duration_seconds=60)
595
+
596
+ # Check payment status
597
+ return await check_payment(ctx, order_id)
598
+ ```
599
+
600
+ **Multi-worker continuation behavior**:
601
+ - `wait_event()` releases the workflow lock atomically
602
+ - Event delivery acquires the lock and resumes on any available worker
603
+ - Safe for multi-pod/multi-container environments (K8s, Docker Compose, etc.)
604
+ - No worker is blocked while waiting for events or timers
605
+
606
+ **For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).
607
+
608
+ ### ASGI Integration
609
+
610
+ Edda runs as an ASGI application:
611
+
612
+ ```bash
613
+ # Run standalone
614
+ uvicorn demo_app:application --port 8001
615
+ ```
616
+
617
+ **Mounting to existing ASGI apps:**
618
+
619
+ You can mount EddaApp to any path in existing ASGI frameworks:
620
+
621
+ ```python
622
+ from fastapi import FastAPI
623
+ from edda import EddaApp
624
+
625
+ # Create FastAPI app
626
+ api = FastAPI()
627
+
628
+ # Create Edda app
629
+ edda_app = EddaApp(db_url="sqlite:///workflow.db")
630
+
631
+ # Mount Edda at /workflows path
632
+ api.mount("/workflows", edda_app)
633
+
634
+ # Now Edda handles all requests under /workflows/*
635
+ # POST /workflows/any-path -> CloudEvents handler
636
+ # POST /workflows/cancel/{instance_id} -> Cancellation
637
+ ```
638
+
639
+ This works with any ASGI framework (Starlette, FastAPI, Quart, etc.)
640
+
641
+ ## Observability Hooks
642
+
643
+ Extend Edda with custom observability without coupling to specific tools:
644
+
645
+ ```python
646
+ from edda import EddaApp
647
+
648
+ class MyHooks:
649
+ async def on_workflow_start(self, instance_id, workflow_name, input_data):
650
+ print(f"Workflow {workflow_name} started: {instance_id}")
651
+
652
+ async def on_workflow_complete(self, instance_id, workflow_name, result):
653
+ print(f"Workflow {workflow_name} completed")
654
+
655
+ async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):
656
+ print(f"Activity {activity_name} completed (cache_hit={cache_hit})")
657
+
658
+ app = EddaApp(
659
+ db_url="sqlite:///workflow.db",
660
+ service_name="my-service",
661
+ hooks=MyHooks()
662
+ )
663
+ ```
664
+
665
+ `MyHooks` implements the `WorkflowHooks` Protocol through structural subtyping. See integration examples in the examples directory.
666
+
667
+ ## Serialization
668
+
669
+ Edda supports both **JSON (dict)** and **binary (bytes)** data for event storage and transport, allowing you to choose based on your needs.
670
+
671
+ ### JSON Data Support
672
+
673
+ For debugging and human-readable logs, use JSON dict format:
674
+
675
+ ```python
676
+ from google.protobuf import json_format
677
+ from edda import send_event, wait_event
678
+
679
+ # Send: Protobuf → JSON dict
680
+ msg = OrderCreated(order_id="123", amount=99.99)
681
+ await send_event("order.created", "orders", json_format.MessageToDict(msg))
682
+
683
+ # Receive: JSON dict → Protobuf
684
+ event = await wait_event(ctx, "payment.completed")
685
+ payment = json_format.ParseDict(event.data, PaymentCompleted())
686
+ ```
687
+
688
+ **Benefits**:
689
+ - ✅ Human-readable in database and logs
690
+ - ✅ Easy debugging and troubleshooting
691
+ - ✅ Full Viewer UI compatibility
692
+ - ✅ CloudEvents Structured Content Mode compatible
693
+
694
+ ### Binary Data Support
695
+
696
+ For maximum performance and zero storage overhead, Edda stores binary data directly in database BLOB columns:
697
+
698
+ ```python
699
+ from edda import send_event, wait_event
700
+
701
+ # Send binary data (e.g., Protobuf)
702
+ msg = OrderCreated(order_id="123", amount=99.99)
703
+ await send_event("order.created", "orders", msg.SerializeToString()) # bytes → BLOB
704
+
705
+ # Receive binary data
706
+ event = await wait_event(ctx, "payment.completed")
707
+ payment = PaymentCompleted()
708
+ payment.ParseFromString(event.data) # bytes from BLOB
709
+ ```
710
+
711
+ **Benefits**:
712
+ - ✅ Zero storage overhead (100 bytes → 100 bytes, not 133 bytes with base64)
713
+ - ✅ Maximum performance (no encoding/decoding)
714
+ - ✅ Native BLOB storage (SQLite, PostgreSQL, MySQL)
715
+ - ✅ CloudEvents Binary Content Mode compatible
716
+
717
+ ### Choosing Between JSON and Binary Mode
718
+
719
+ Both modes are equally valid for production use:
720
+
721
+ - **JSON Mode**: Human-readable, excellent observability, Viewer UI support
722
+ - Use when debugging, monitoring, and data inspection are priorities
723
+
724
+ - **Binary Mode**: Zero serialization overhead, smaller storage
725
+ - Use when payload size or serialization performance are critical
726
+ - Ideal for high-throughput scenarios (>1000 events/sec)
727
+
728
+ **Both modes are first-class citizens** - choose based on your specific requirements, not environment.
729
+
730
+ ## Next Steps
731
+
732
+ - **[Getting Started](https://edda-framework.dev/getting-started/installation/)**: Installation and setup guide
733
+ - **[Core Concepts](https://edda-framework.dev/getting-started/concepts/)**: Learn about workflows, activities, and durable execution
734
+ - **[Examples](https://edda-framework.dev/examples/simple/)**: See Edda in action with real-world examples
735
+ - **[FastAPI Integration](https://edda-framework.dev/examples/fastapi-integration/)**: Integrate with FastAPI (direct invocation + CloudEvents)
736
+ - **[Transactional Outbox](https://edda-framework.dev/core-features/transactional-outbox/)**: Reliable event publishing with guaranteed delivery
737
+ - **[Viewer UI](https://edda-framework.dev/viewer-ui/setup/)**: Visualize and monitor your workflows
738
+ - **[Lifecycle Hooks](https://edda-framework.dev/core-features/hooks/)**: Add observability and monitoring with custom hooks
739
+ - **[CloudEvents HTTP Binding](https://edda-framework.dev/core-features/events/cloudevents-http-binding/)**: CloudEvents specification compliance and error handling
740
+
741
+ ## License
742
+
743
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
744
+
745
+ ## Support
746
+
747
+ - GitHub Issues: https://github.com/i2y/edda/issues
748
+ - Documentation: https://github.com/i2y/edda#readme