{"id":46553969,"url":"https://github.com/i2y/edda","last_synced_at":"2026-03-07T04:02:04.675Z","repository":{"id":325732452,"uuid":"1100914477","full_name":"i2y/edda","owner":"i2y","description":"a durable execution framework for Python","archived":false,"fork":false,"pushed_at":"2026-02-03T23:28:48.000Z","size":5363,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-04T11:42:52.874Z","etag":null,"topics":["agentic","cloudevents","durable-execution","llm-agent","mcp","pydantic","python"],"latest_commit_sha":null,"homepage":"https://i2y.github.io/edda/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/i2y.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-20T23:50:47.000Z","updated_at":"2026-02-03T23:28:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/i2y/edda","commit_stats":null,"previous_names":["i2y/edda"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/i2y/edda","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fedda","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fedda/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fedda/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fedda/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/i2y","download_url":"https://codeload.github.com/i2y/edda/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fedda/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30207392,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T03:24:23.086Z","status":"ssl_error","status_checked_at":"2026-03-07T03:23:11.444Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agentic","cloudevents","durable-execution","llm-agent","mcp","pydantic","python"],"created_at":"2026-03-07T04:02:01.382Z","updated_at":"2026-03-07T04:02:04.660Z","avatar_url":"https://github.com/i2y.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Edda\n\n**Edda** - Norse mythology poetic narratives that preserve ancient sagas and legends\n\n\u003e Lightweight durable execution framework - no separate server required\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![Documentation](https://img.shields.io/badge/docs-latest-green.svg)](https://i2y.github.io/edda/)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/i2y/edda)\n\n## Overview\n\nEdda 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.\n\n**Perfect for**: Order processing, distributed transactions (Saga pattern), AI agent orchestration, and any workflow that must survive crashes.\n\nFor detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.github.io/edda/)\n\n## Key Features\n\n- ✨ **Lightweight Library**: Runs in your application process - no separate server infrastructure\n- 🔄 **Durable Execution**: Deterministic replay with workflow history for automatic crash recovery\n- 🎯 **Workflow \u0026 Activity**: Clear separation between orchestration logic and business logic\n- 🔁 **Saga Pattern**: Automatic compensation on failure with `@on_failure` decorator\n- 🌐 **Multi-worker Execution**: Run workflows safely across multiple servers or containers\n- 🔒 **Pydantic Integration**: Type-safe workflows with automatic validation\n- 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery\n- ☁️ **CloudEvents Support**: Native support for CloudEvents protocol\n- ⏱️ **Event \u0026 Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker\n- 📬 **Channel-based Messaging**: Actor-model style communication with competing (job queue) and broadcast (fan-out) modes\n- ⚡ **Instant Notifications**: PostgreSQL LISTEN/NOTIFY for near-instant event delivery (optional)\n- 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol\n- 🧠 **Mirascope Integration**: Durable LLM calls\n- 🦙 **LlamaIndex Integration**: Make LlamaIndex Workflows durable with crash recovery\n- 📊 **pydantic-graph Integration**: Durable graph-based workflows (experimental)\n- 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)\n\n## Use Cases\n\nEdda excels at orchestrating **long-running workflows** that must survive failures:\n\n- **🏢 Long-Running Jobs**: Order processing, data pipelines, batch jobs - from minutes to days, weeks, or even months\n- **🔄 Distributed Transactions**: Coordinate microservices with automatic compensation (Saga pattern)\n- **🤖 AI Agent Workflows**: Orchestrate multi-step AI tasks (LLM calls, tool usage, long-running inference)\n- **📡 Event-Driven Workflows**: React to external events with guaranteed delivery and automatic retry\n\n### Business Process Automation\n\nEdda's waiting functions make it ideal for time-based and event-driven business processes:\n\n- **📧 User Onboarding**: Send reminders if users haven't completed setup after N days\n- **🎁 Campaign Processing**: Evaluate conditions and notify winners after campaign ends\n- **💳 Payment Reminders**: Send escalating reminders before payment deadlines\n- **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders\n\n**Waiting functions**:\n- `sleep(seconds)`: Wait for a relative duration\n- `sleep_until(target_time)`: Wait until an absolute datetime (e.g., campaign end date)\n- `wait_event(event_type)`: Wait for external events (near real-time response)\n\n```python\n@workflow\nasync def onboarding_reminder(ctx: WorkflowContext, user_id: str):\n    await sleep(ctx, seconds=3*24*60*60)  # Wait 3 days\n    if not await check_completed(ctx, user_id):\n        await send_reminder(ctx, user_id)\n```\n\n**Key benefit**: Workflows **never lose progress** - crashes and restarts are handled automatically through deterministic replay.\n\n## Architecture\n\nEdda runs as a lightweight library in your applications, with all workflow state stored in a shared database:\n\n```mermaid\n%%{init: {'theme':'base', 'themeVariables': {'primaryTextColor':'#1a1a1a', 'secondaryTextColor':'#1a1a1a', 'tertiaryTextColor':'#1a1a1a', 'textColor':'#1a1a1a', 'nodeTextColor':'#1a1a1a'}}}%%\ngraph TB\n    subgraph ext[\"External Systems\"]\n        API[REST API\u003cbr/\u003eClients]\n        CE[CloudEvents\u003cbr/\u003eProducer]\n    end\n\n    subgraph cluster[\"Your Multiple Instances\"]\n        subgraph pod1[\"order-service Pod 1\"]\n            W1[Edda Workflow]\n        end\n        subgraph pod2[\"order-service Pod 2\"]\n            W2[Edda Workflow]\n        end\n        subgraph pod3[\"order-service Pod 3\"]\n            W3[Edda Workflow]\n        end\n    end\n\n    DB[(Shared Database\u003cbr/\u003ePostgreSQL/MySQL\u003cbr/\u003eSQLite: single-process only)]\n\n    API --\u003e|\"workflow.start()\u003cbr/\u003e(Direct Invocation)\"| W1\n    API --\u003e|\"workflow.start()\u003cbr/\u003e(Direct Invocation)\"| W2\n    CE --\u003e|\"POST /\u003cbr/\u003e(CloudEvents)\"| W1\n    CE --\u003e|\"POST /\u003cbr/\u003e(CloudEvents)\"| W3\n\n    W1 \u003c--\u003e|Workflow\u003cbr/\u003eState| DB\n    W2 \u003c--\u003e|Workflow\u003cbr/\u003eState| DB\n    W3 \u003c--\u003e|Workflow\u003cbr/\u003eState| DB\n\n    style DB fill:#e1f5ff\n    style W1 fill:#fff4e6\n    style W2 fill:#fff4e6\n    style W3 fill:#fff4e6\n```\n\n**Key Points**:\n\n- Multiple workers can run simultaneously across different pods/servers\n- Each workflow instance runs on only one worker at a time (automatic coordination)\n- `wait_event()` and `sleep()` free up worker resources while waiting, resume on any worker when event arrives or timer expires\n- Automatic crash recovery with stale lock cleanup and workflow auto-resume\n\n## Quick Start\n\n```python\nfrom edda import EddaApp, workflow, activity, WorkflowContext\n\n@activity\nasync def process_payment(ctx: WorkflowContext, amount: float):\n    # Durable execution - automatically recorded in history\n    print(f\"Processing payment: ${amount}\")\n    return {\"status\": \"paid\", \"amount\": amount}\n\n@workflow\nasync def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):\n    # Workflow orchestrates activities with automatic retry on crash\n    result = await process_payment(ctx, amount)\n    return {\"order_id\": order_id, **result}\n\n# Simplified example - production code needs:\n# 1. await app.initialize() before starting workflows\n# 2. try-finally with await app.shutdown() for cleanup\n# 3. PostgreSQL or MySQL for multi-process/multi-pod deployments\napp = EddaApp(db_url=\"sqlite:///workflow.db\")\n\n# Start workflow\ninstance_id = await order_workflow.start(order_id=\"ORD-123\", amount=99.99)\n```\n\n**What happens on crash?**\n\n1. Activities already executed return cached results from history\n2. Workflow resumes from the last checkpoint\n3. No manual intervention required\n\n## Installation\n\nInstall Edda from PyPI using uv:\n\n```bash\n# Basic installation (includes SQLite support)\nuv add edda-framework\n\n# With PostgreSQL support\nuv add edda-framework --extra postgresql\n\n# With MySQL support\nuv add edda-framework --extra mysql\n\n# With Viewer UI\nuv add edda-framework --extra viewer\n\n# With PostgreSQL instant notifications (LISTEN/NOTIFY)\nuv add edda-framework --extra postgres-notify\n\n# With LlamaIndex Workflow integration\nuv add edda-framework --extra llamaindex\n\n# With pydantic-graph integration (experimental)\nuv add edda-framework --extra graph\n\n# All extras (PostgreSQL, MySQL, Viewer UI)\nuv add edda-framework --extra postgresql --extra mysql --extra viewer\n```\n\n### Installing from GitHub (Development Versions)\n\nInstall the latest development version directly from GitHub:\n\n```bash\n# Using uv (latest from main branch)\nuv add git+https://github.com/i2y/edda.git\n\n# Using pip\npip install git+https://github.com/i2y/edda.git\n```\n\n**Install specific version or branch:**\n\n```bash\n# Specific tag/release\nuv add git+https://github.com/i2y/edda.git@v0.1.0\npip install git+https://github.com/i2y/edda.git@v0.1.0\n\n# Specific branch\nuv add git+https://github.com/i2y/edda.git@feature-branch\npip install git+https://github.com/i2y/edda.git@feature-branch\n\n# With extras (PostgreSQL, Viewer)\nuv add \"git+https://github.com/i2y/edda.git[postgresql,viewer]\"\npip install \"git+https://github.com/i2y/edda.git[postgresql,viewer]\"\n```\n\n**Database Drivers**:\n- **SQLite**: Included by default (via `aiosqlite`)\n  - Single-process deployments only (supports multiple async workers within one process, not multiple processes/pods)\n- **PostgreSQL**: Add `--extra postgresql` for `asyncpg` driver\n  - **Recommended for production**\n- **MySQL**: Add `--extra mysql` for `aiomysql` driver\n  - **Recommended for production**\n- **Viewer UI**: Add `--extra viewer` for workflow visualization\n\n### Database Selection Guide\n\n| Database | Use Case | Multi-Pod Support | Production Ready | Notes |\n|----------|----------|-------------------|------------------|-------|\n| **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) |\n| **PostgreSQL** | Production, multi-process/multi-pod systems | ✅ Yes | ✅ Yes | **Recommended for production** - Full support for database-based exclusive control and concurrent workflows |\n| **MySQL** | Production with existing MySQL infrastructure | ✅ Yes | ✅ Yes | Suitable for production - Good choice if you already use MySQL |\n\n**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.\n\n\u003e **Tip**: For PostgreSQL, install the `postgres-notify` extra for near-instant event delivery using LISTEN/NOTIFY instead of polling.\n\n### Database Schema Migration\n\n**Automatic Migration (Default)**\n\nEdda automatically applies database migrations at startup. No manual commands needed:\n\n```python\nfrom edda import EddaApp\n\n# Migrations are applied automatically\napp = EddaApp(db_url=\"postgresql://user:pass@localhost/dbname\")\n```\n\nThis is safe in multi-worker environments - Edda handles concurrent startup gracefully.\n\n**Manual Migration with dbmate (Optional)**\n\nFor explicit schema control, you can disable auto-migration and use [dbmate](https://github.com/amacneil/dbmate):\n\n```python\n# Disable auto-migration\napp = EddaApp(\n    db_url=\"postgresql://...\",\n    auto_migrate=False  # Use dbmate-managed schema\n)\n```\n\n```bash\n# Install dbmate\nbrew install dbmate  # macOS\n\n# Add schema submodule\ngit submodule add https://github.com/durax-io/schema.git schema\n\n# Run migration manually\nDATABASE_URL=\"postgresql://user:pass@localhost/dbname\" dbmate -d ./schema/db/migrations/postgresql up\n```\n\n\u003e **Note**: Edda's auto-migration uses the same SQL files as dbmate, maintaining full compatibility.\n\n### Development Installation\n\nIf you want to contribute to Edda or modify the framework itself:\n\n```bash\n# Clone repository\ngit clone https://github.com/i2y/edda.git\ncd edda\nuv sync --all-extras\n```\n\n### Running Tests\n\nRun Edda's test suite:\n\n```bash\n# Run tests\nuv run pytest\n\n# Run with coverage\nuv run pytest --cov=edda\n```\n\n## Core Concepts\n\n### Workflows and Activities\n\n**Activity**: A unit of work that performs business logic. Activity results are recorded in history.\n\n**Workflow**: Orchestration logic that coordinates activities. Workflows can be replayed from history after crashes.\n\n```python\nfrom edda import workflow, activity, WorkflowContext\n\n@activity\nasync def send_email(ctx: WorkflowContext, email: str, message: str):\n    # Business logic - this will be recorded\n    print(f\"Sending email to {email}\")\n    return {\"sent\": True}\n\n@workflow\nasync def user_signup(ctx: WorkflowContext, email: str):\n    # Orchestration logic\n    await send_email(ctx, email, \"Welcome!\")\n    return {\"status\": \"completed\"}\n```\n\n**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`).\n\n### Durable Execution\n\nEdda ensures workflow progress is never lost through **deterministic replay**:\n\n1. **Activity results are recorded** in a history table\n2. **On crash recovery**, workflows resume from the last checkpoint\n3. **Already-executed activities** return cached results from history\n4. **New activities** continue from where the workflow left off\n\n```python\n@workflow\nasync def long_running_workflow(ctx: WorkflowContext, user_id: str):\n    # Activity 1: Recorded in history\n    result1 = await create_user(ctx, user_id)\n\n    # If process crashes here, activity won't re-execute on restart\n\n    # Activity 2: Continues from history on restart\n    result2 = await send_welcome_email(ctx, result1[\"email\"])\n\n    return result2\n```\n\n**Key guarantees**:\n- Activities execute **exactly once** (results cached in history)\n- Workflows can survive **arbitrary crashes**\n- No manual checkpoint management required\n\n### Automatic Activity Retry\n\nActivities automatically retry with exponential backoff when errors occur, improving reliability without manual error handling:\n\n```python\nfrom edda import activity, WorkflowContext\n\n@activity\nasync def call_external_api(ctx: WorkflowContext, url: str):\n    # Automatically retries up to 5 times with exponential backoff\n    # Delays: 1s, 2s, 4s, 8s, 16s\n    response = await httpx.get(url, timeout=10)\n    return response.json()\n```\n\n**Default retry policy**:\n- **5 attempts** (including initial)\n- **Exponential backoff**: 1s, 2s, 4s, 8s, 16s between attempts\n- **Max delay**: 60 seconds\n- **Total duration**: 5 minutes maximum\n\n**Custom retry policies** for specific activities:\n\n```python\nfrom edda import activity, RetryPolicy, WorkflowContext\n\n@activity(retry_policy=RetryPolicy(\n    max_attempts=3,\n    initial_interval=0.5,\n    backoff_coefficient=2.0,\n    max_interval=10.0,\n    max_duration=60.0\n))\nasync def flaky_operation(ctx: WorkflowContext, data: dict):\n    # Custom: 3 attempts, delays 0.5s, 1s, 2s\n    return await external_service.process(data)\n```\n\n**Application-level default policy**:\n\n```python\nfrom edda import EddaApp, RetryPolicy\n\napp = EddaApp(\n    db_url=\"sqlite:///workflow.db\",\n    default_retry_policy=RetryPolicy(\n        max_attempts=10,\n        initial_interval=2.0\n    )\n)\n```\n\n**Non-retryable errors** with `TerminalError`:\n\n```python\nfrom edda import activity, TerminalError, WorkflowContext\n\n@activity\nasync def validate_user(ctx: WorkflowContext, user_id: str):\n    user = await get_user(user_id)\n    if user is None:\n        # Immediately fail without retry (user doesn't exist)\n        raise TerminalError(f\"User {user_id} not found\")\n    return user\n```\n\n**Retry metadata for observability**:\n\nRetry information is automatically embedded in activity history for monitoring:\n\n```python\n{\n    \"event_type\": \"ActivityCompleted\",\n    \"event_data\": {\n        \"activity_name\": \"call_external_api\",\n        \"result\": {...},\n        \"retry_metadata\": {\n            \"total_attempts\": 3,\n            \"total_duration_ms\": 7200,\n            \"last_error\": {...},\n            \"exhausted\": False,\n            \"errors\": [...]\n        }\n    }\n}\n```\n\n**Policy resolution order**:\n1. Activity-level policy (`@activity(retry_policy=...)`)\n2. Application-level policy (`EddaApp(default_retry_policy=...)`)\n3. Framework default (5 attempts, exponential backoff)\n\n### Compensation (Saga Pattern)\n\nWhen a workflow fails, Edda automatically executes compensation functions for **already-executed activities in reverse order**. This implements the Saga pattern for distributed transaction rollback.\n\n**Key behavior**:\n- Compensation functions run in **reverse order** of activity execution\n- Only **already-executed activities** are compensated\n- If Activity A and B completed, then C fails → B and A compensations run (in that order)\n\n```python\nfrom edda import activity, on_failure, compensation, workflow, WorkflowContext\n\n@compensation\nasync def cancel_reservation(ctx: WorkflowContext, item_id: str):\n    # Automatically called on workflow failure (reverse order)\n    print(f\"Cancelled reservation for {item_id}\")\n    return {\"cancelled\": True}\n\n@activity\n@on_failure(cancel_reservation)\nasync def reserve_inventory(ctx: WorkflowContext, item_id: str):\n    print(f\"Reserved {item_id}\")\n    return {\"reserved\": True}\n\n@workflow\nasync def order_workflow(ctx: WorkflowContext, item1: str, item2: str):\n    await reserve_inventory(ctx, item1)  # Step 1: Reserve item1\n    await reserve_inventory(ctx, item2)  # Step 2: Reserve item2\n    await charge_payment(ctx)            # Step 3: If this fails...\n    # Compensation runs: cancel item2 → cancel item1 (reverse order)\n```\n\n### Multi-worker Execution\n\nMultiple workers can safely process workflows using database-based exclusive control. This means:\n\n- Edda uses database-based locks (not Redis or ZooKeeper)\n- Each workflow instance runs on only one worker at a time\n- If a worker crashes, another worker automatically resumes\n- No additional infrastructure required\n\n```python\n# Worker 1 and Worker 2 can run simultaneously\n# Only one will acquire the lock for each workflow instance\n\napp = EddaApp(\n    db_url=\"postgresql://localhost/workflows\",  # Shared database for coordination\n    service_name=\"order-service\",\n    # Connection pool settings (optional)\n    pool_size=5,        # Concurrent connections\n    max_overflow=10,    # Additional burst capacity\n    # Batch processing (optional)\n    max_workflows_per_batch=10,  # Or \"auto\" / \"auto:cpu\" for dynamic scaling\n)\n```\n\n**Features**:\n- Each workflow instance runs on only one worker at a time (automatic coordination)\n- Automatic stale lock cleanup (5-minute timeout)\n- Crashed workflows automatically resume on any available worker\n\n### Pydantic Integration\n\nType-safe workflows with automatic validation:\n\n```python\nfrom pydantic import BaseModel, Field\nfrom edda import workflow, WorkflowContext\n\nclass OrderItem(BaseModel):\n    item_id: str\n    quantity: int = Field(..., ge=1)\n    price: float = Field(..., gt=0)\n\n@workflow\nasync def process_order(ctx: WorkflowContext, items: list[OrderItem]) -\u003e dict:\n    # Automatic validation before workflow starts\n    total = sum(item.price * item.quantity for item in items)\n    return {\"total\": total, \"item_count\": len(items)}\n```\n\n### Transactional Outbox\n\nActivities are automatically transactional by default, ensuring atomicity:\n\n```python\nfrom edda import activity, send_event_transactional, WorkflowContext\n\n@activity  # Automatically transactional\nasync def create_order(ctx: WorkflowContext, order_id: str):\n    # All operations in a single transaction:\n    # 1. Activity execution\n    # 2. History recording\n    # 3. Event publishing (outbox table)\n\n    await send_event_transactional(\n        ctx,\n        event_type=\"order.created\",\n        event_source=\"order-service\",\n        event_data={\"order_id\": order_id}\n    )\n\n    return {\"order_id\": order_id}\n```\n\n**Custom Database Operations** - Use `ctx.session` for your database operations:\n\n```python\n@activity  # Edda manages the transaction\nasync def process_payment(ctx: WorkflowContext, order_id: str, amount: float):\n    # Access Edda-managed session (same database as Edda)\n    session = ctx.session\n\n    # Your business logic\n    payment = Payment(order_id=order_id, amount=amount)\n    session.add(payment)\n\n    # Edda event (same transaction)\n    await send_event_transactional(\n        ctx,\n        event_type=\"payment.processed\",\n        event_source=\"payment-service\",\n        event_data={\"order_id\": order_id, \"amount\": amount}\n    )\n\n    # Edda automatically commits: your data + Edda's outbox (atomic!)\n    return {\"payment_id\": f\"PAY-{order_id}\"}\n```\n\n## Event Integration\n\nEdda provides optional event-driven capabilities for workflows that need to wait for external events.\n\n### CloudEvents Support\n\nNative support for CloudEvents protocol:\n\n```python\nfrom edda import EddaApp\n\napp = EddaApp(\n    db_url=\"sqlite:///workflow.db\",\n    service_name=\"order-service\",\n    outbox_enabled=True  # Enable transactional outbox\n)\n\n# Accepts CloudEvents at any HTTP path\n```\n\n**CloudEvents handling**:\n- All HTTP requests (any path) are accepted as CloudEvents\n- Events without matching workflow handlers are silently discarded\n- Special endpoint: `POST /cancel/{instance_id}` for workflow cancellation\n- Automatic CloudEvents validation and parsing\n- Works with CloudEvents-compatible systems (Knative Eventing, CloudEvents SDKs, etc.)\n\n**CloudEvents HTTP Binding compliance**:\n- **202 Accepted**: Event accepted for asynchronous processing (success)\n- **400 Bad Request**: CloudEvents parsing/validation error (non-retryable)\n- **500 Internal Server Error**: Internal error (retryable)\n- Error responses include `error_type` and `retryable` flags for client retry logic\n\n### Event \u0026 Timer Waiting\n\nWorkflows 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:\n\n```python\nfrom edda import workflow, wait_event, send_event, WorkflowContext\n\n@workflow\nasync def payment_workflow(ctx: WorkflowContext, order_id: str):\n    # Send payment request event\n    await send_event(\"payment.requested\", \"payment-service\", {\"order_id\": order_id})\n\n    # Wait for payment completion event (process-releasing)\n    payment_event = await wait_event(ctx, \"payment.completed\")\n\n    return payment_event.data\n```\n\n**ReceivedEvent attributes**: The `wait_event()` function returns a `ReceivedEvent` object:\n\n```python\nevent = await wait_event(ctx, \"payment.completed\")\namount = event.data[\"amount\"]           # Event payload (dict or bytes)\nsource = event.metadata.source          # CloudEvents source\nevent_type = event.metadata.type        # CloudEvents type\nextensions = event.extensions           # CloudEvents extensions\n```\n\n**Timeout handling with EventTimeoutError**:\n\n```python\nfrom edda import wait_event, EventTimeoutError\n\ntry:\n    event = await wait_event(ctx, \"payment.completed\", timeout_seconds=60)\nexcept EventTimeoutError:\n    # Handle timeout (e.g., cancel order, send reminder)\n    await cancel_order(ctx, order_id)\n```\n\n**sleep() for time-based waiting**:\n\n```python\nfrom edda import sleep\n\n@workflow\nasync def order_with_timeout(ctx: WorkflowContext, order_id: str):\n    # Create order\n    await create_order(ctx, order_id)\n\n    # Wait 60 seconds for payment\n    await sleep(ctx, seconds=60)\n\n    # Check payment status\n    return await check_payment(ctx, order_id)\n```\n\n**Multi-worker continuation behavior**:\n- `wait_event()` releases the workflow lock atomically\n- Event delivery acquires the lock and resumes on any available worker\n- Safe for multi-pod/multi-container environments (K8s, Docker Compose, etc.)\n- No worker is blocked while waiting for events or timers\n\n**For technical details**, see [Multi-Worker Continuations](local-docs/distributed-coroutines.md).\n\n### Channel-based Messaging\n\nEdda provides channel-based messaging for workflow-to-workflow communication with two delivery modes:\n\n```python\nfrom edda import workflow, subscribe, receive, publish, send_to, WorkflowContext\n\n# Job Worker - processes jobs exclusively (competing mode)\n@workflow\nasync def job_worker(ctx: WorkflowContext, worker_id: str):\n    # Subscribe with competing mode - each job goes to ONE worker only\n    await subscribe(ctx, channel=\"jobs\", mode=\"competing\")\n\n    while True:\n        job = await receive(ctx, channel=\"jobs\")  # Get next job\n        await process_job(ctx, job.data)\n        await ctx.recur(worker_id)  # Continue processing\n\n# Notification Handler - receives ALL messages (broadcast mode)\n@workflow\nasync def notification_handler(ctx: WorkflowContext, handler_id: str):\n    # Subscribe with broadcast mode - ALL handlers receive each message\n    await subscribe(ctx, channel=\"notifications\", mode=\"broadcast\")\n\n    while True:\n        msg = await receive(ctx, channel=\"notifications\")\n        await send_notification(ctx, msg.data)\n        await ctx.recur(handler_id)\n\n# Publish to channel (all subscribers or one competing subscriber)\nawait publish(ctx, channel=\"jobs\", data={\"task\": \"send_report\"})\n\n# Direct message to specific workflow instance\nawait send_to(ctx, instance_id=\"workflow-123\", channel=\"approval\", data={\"approved\": True})\n```\n\n**Delivery modes**:\n- **`competing`**: Each message goes to exactly ONE subscriber (job queue/task distribution)\n- **`broadcast`**: Each message goes to ALL subscribers (notifications/fan-out)\n\n**Key features**:\n- **Channel-based messaging**: Messages are delivered to workflows waiting on specific channels\n- **Competing vs Broadcast**: Choose semantics per subscription\n- **Direct messaging**: `send_to()` for workflow-to-workflow communication\n- **Database-backed**: All messages are persisted for durability\n- **Lock-first delivery**: Safe for multi-worker environments\n\n### Workflow Recurrence\n\nLong-running workflows can use `ctx.recur()` to restart with fresh history while maintaining the same instance ID. This is essential for workflows that run indefinitely (job workers, notification handlers, etc.):\n\n```python\nfrom edda import workflow, subscribe, receive, WorkflowContext\n\n@workflow\nasync def job_worker(ctx: WorkflowContext, worker_id: str):\n    await subscribe(ctx, channel=\"jobs\", mode=\"competing\")\n\n    # Process one job\n    job = await receive(ctx, channel=\"jobs\")\n    await process_job(ctx, job.data)\n\n    # Archive history and restart with same instance_id\n    # Prevents unbounded history growth\n    await ctx.recur(worker_id)\n```\n\n**Key benefits**:\n- **Prevents history growth**: Archives old history, starts fresh\n- **Maintains instance ID**: Same workflow continues logically\n- **Preserves subscriptions**: Channel subscriptions survive recurrence\n- **Enables infinite loops**: Essential for long-running workers\n\n### ASGI Integration\n\nEdda runs as an ASGI application:\n\n```bash\n# Run standalone\nuvicorn demo_app:application --port 8001\n```\n\n**Mounting to existing ASGI apps:**\n\nYou can mount EddaApp to any path in existing ASGI frameworks:\n\n```python\nfrom fastapi import FastAPI\nfrom edda import EddaApp\n\n# Create FastAPI app\napi = FastAPI()\n\n# Create Edda app\nedda_app = EddaApp(db_url=\"sqlite:///workflow.db\")\n\n# Mount Edda at /workflows path\napi.mount(\"/workflows\", edda_app)\n\n# Now Edda handles all requests under /workflows/*\n# POST /workflows/any-path -\u003e CloudEvents handler\n# POST /workflows/cancel/{instance_id} -\u003e Cancellation\n```\n\nThis works with any ASGI framework (Starlette, FastAPI, Quart, etc.)\n\n### WSGI Integration\n\nFor WSGI environments (gunicorn, uWSGI, Flask, Django), use the WSGI adapter:\n\n```python\nfrom edda import EddaApp\nfrom edda.wsgi import create_wsgi_app\n\n# Create Edda app\nedda_app = EddaApp(db_url=\"sqlite:///workflow.db\")\n\n# Convert to WSGI\nwsgi_application = create_wsgi_app(edda_app)\n```\n\n**Running with WSGI servers:**\n\n```bash\n# With Gunicorn\ngunicorn demo_app:wsgi_application --workers 4\n\n# With uWSGI\nuwsgi --http :8000 --wsgi-file demo_app.py --callable wsgi_application\n```\n\n**Sync Activities**: For WSGI environments or legacy codebases, you can write synchronous activities:\n\n```python\nfrom edda import activity, WorkflowContext\n\n@activity\ndef process_payment(ctx: WorkflowContext, amount: float) -\u003e dict:\n    # Sync function - automatically executed in thread pool\n    # No async/await needed!\n    return {\"status\": \"paid\", \"amount\": amount}\n\n@workflow\nasync def payment_workflow(ctx: WorkflowContext, order_id: str) -\u003e dict:\n    # Workflows still use async (for deterministic replay)\n    result = await process_payment(ctx, 99.99)\n    return result\n```\n\n**Performance note**: ASGI servers (uvicorn, hypercorn) are recommended for better performance with Edda's async architecture. WSGI support is provided for compatibility with existing infrastructure and users who prefer synchronous programming.\n\n## MCP Integration\n\nEdda integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI assistants like Claude to interact with your durable workflows as long-running tools.\n\n### Quick Example\n\n```python\nfrom edda.integrations.mcp import EddaMCPServer\nfrom edda import WorkflowContext, activity\n\n# Create MCP server\nserver = EddaMCPServer(\n    name=\"Order Service\",\n    db_url=\"postgresql://user:pass@localhost/orders\",\n)\n\n@activity\nasync def process_payment(ctx: WorkflowContext, amount: float):\n    return {\"status\": \"paid\", \"amount\": amount}\n\n@server.durable_tool(description=\"Process customer order\")\nasync def process_order(ctx: WorkflowContext, order_id: str):\n    await process_payment(ctx, 99.99)\n    return {\"status\": \"completed\", \"order_id\": order_id}\n\n# Deploy with uvicorn\nif __name__ == \"__main__\":\n    import uvicorn\n    uvicorn.run(server.asgi_app(), host=\"0.0.0.0\", port=8000)\n```\n\n### Auto-Generated Tools\n\nEach `@durable_tool` automatically generates **four MCP tools**:\n\n1. **Main tool** (`process_order`): Starts the workflow, returns instance ID\n2. **Status tool** (`process_order_status`): Checks workflow progress with completed activity count and suggested poll interval\n3. **Result tool** (`process_order_result`): Gets final result when completed\n4. **Cancel tool** (`process_order_cancel`): Cancels workflow if running or waiting, executes compensation handlers\n\nThis enables AI assistants to work with workflows that take minutes, hours, or even days to complete, with full control over the workflow lifecycle.\n\n### MCP Prompts\n\nDefine reusable prompt templates that can access workflow state:\n\n```python\nfrom mcp.server.fastmcp.prompts.base import UserMessage\nfrom mcp.types import TextContent\n\n@server.prompt(description=\"Analyze a workflow execution\")\nasync def analyze_workflow(instance_id: str) -\u003e UserMessage:\n    \"\"\"Generate analysis prompt for a specific workflow.\"\"\"\n    instance = await server.storage.get_instance(instance_id)\n    history = await server.storage.get_history(instance_id)\n\n    text = f\"\"\"Analyze this workflow:\n**Status**: {instance['status']}\n**Activities**: {len(history)}\n**Result**: {instance.get('output_data')}\n\nPlease provide insights and optimization suggestions.\"\"\"\n\n    return UserMessage(content=TextContent(type=\"text\", text=text))\n```\n\nAI clients can use these prompts to generate context-aware analysis of your workflows.\n\n**For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).\n\n## Observability Hooks\n\nExtend Edda with custom observability without coupling to specific tools:\n\n```python\nfrom edda import EddaApp\n\nclass MyHooks:\n    async def on_workflow_start(self, instance_id, workflow_name, input_data):\n        print(f\"Workflow {workflow_name} started: {instance_id}\")\n\n    async def on_workflow_complete(self, instance_id, workflow_name, result):\n        print(f\"Workflow {workflow_name} completed\")\n\n    async def on_activity_complete(self, instance_id, activity_id, activity_name, result, cache_hit):\n        print(f\"Activity {activity_name} completed (cache_hit={cache_hit})\")\n\napp = EddaApp(\n    db_url=\"sqlite:///workflow.db\",\n    service_name=\"my-service\",\n    hooks=MyHooks()\n)\n```\n\n`MyHooks` implements the `WorkflowHooks` Protocol through structural subtyping. See integration examples in the examples directory.\n\n## Serialization\n\nEdda supports both **JSON (dict)** and **binary (bytes)** data for event storage and transport, allowing you to choose based on your needs.\n\n### JSON Data Support\n\nFor debugging and human-readable logs, use JSON dict format:\n\n```python\nfrom google.protobuf import json_format\nfrom edda import send_event, wait_event\n\n# Send: Protobuf → JSON dict\nmsg = OrderCreated(order_id=\"123\", amount=99.99)\nawait send_event(\"order.created\", \"orders\", json_format.MessageToDict(msg))\n\n# Receive: JSON dict → Protobuf\nevent = await wait_event(ctx, \"payment.completed\")\npayment = json_format.ParseDict(event.data, PaymentCompleted())\n```\n\n**Benefits**:\n- ✅ Human-readable in database and logs\n- ✅ Easy debugging and troubleshooting\n- ✅ Full Viewer UI compatibility\n- ✅ CloudEvents Structured Content Mode compatible\n\n### Binary Data Support\n\nFor maximum performance and zero storage overhead, Edda stores binary data directly in database BLOB columns:\n\n```python\nfrom edda import send_event, wait_event\n\n# Send binary data (e.g., Protobuf)\nmsg = OrderCreated(order_id=\"123\", amount=99.99)\nawait send_event(\"order.created\", \"orders\", msg.SerializeToString())  # bytes → BLOB\n\n# Receive binary data\nevent = await wait_event(ctx, \"payment.completed\")\npayment = PaymentCompleted()\npayment.ParseFromString(event.data)  # bytes from BLOB\n```\n\n**Benefits**:\n- ✅ Zero storage overhead (100 bytes → 100 bytes, not 133 bytes with base64)\n- ✅ Maximum performance (no encoding/decoding)\n- ✅ Native BLOB storage (SQLite, PostgreSQL, MySQL)\n- ✅ CloudEvents Binary Content Mode compatible\n\n### Choosing Between JSON and Binary Mode\n\nBoth modes are equally valid for production use:\n\n- **JSON Mode**: Human-readable, excellent observability, Viewer UI support\n  - Use when debugging, monitoring, and data inspection are priorities\n\n- **Binary Mode**: Zero serialization overhead, smaller storage\n  - Use when payload size or serialization performance are critical\n  - Ideal for high-throughput scenarios (\u003e1000 events/sec)\n\n**Both modes are first-class citizens** - choose based on your specific requirements, not environment.\n\n## Next Steps\n\n- **[Getting Started](https://edda-framework.dev/getting-started/installation/)**: Installation and setup guide\n- **[Core Concepts](https://edda-framework.dev/getting-started/concepts/)**: Learn about workflows, activities, and durable execution\n- **[Examples](https://edda-framework.dev/examples/simple/)**: See Edda in action with real-world examples\n- **[FastAPI Integration](https://edda-framework.dev/examples/fastapi-integration/)**: Integrate with FastAPI (direct invocation + CloudEvents)\n- **[Transactional Outbox](https://edda-framework.dev/core-features/transactional-outbox/)**: Reliable event publishing with guaranteed delivery\n- **[Viewer UI](https://edda-framework.dev/viewer-ui/setup/)**: Visualize and monitor your workflows\n- **[Lifecycle Hooks](https://edda-framework.dev/core-features/hooks/)**: Add observability and monitoring with custom hooks\n- **[CloudEvents HTTP Binding](https://edda-framework.dev/core-features/events/cloudevents-http-binding/)**: CloudEvents specification compliance and error handling\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## Support\n\n- GitHub Issues: https://github.com/i2y/edda/issues\n- Documentation: https://github.com/i2y/edda#readme\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi2y%2Fedda","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fi2y%2Fedda","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi2y%2Fedda/lists"}