{"id":49178652,"url":"https://github.com/npow/sagaflow","last_synced_at":"2026-05-09T05:11:20.184Z","repository":{"id":353136284,"uuid":"1218134699","full_name":"npow/sagaflow","owner":"npow","description":"Run durable agent workflows that outlive your session","archived":false,"fork":false,"pushed_at":"2026-05-06T08:21:57.000Z","size":742,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-06T10:50:10.768Z","etag":null,"topics":["ai-agents","anthropic","claude-code","durable-execution","llm","orchestration","python","temporal","workflow"],"latest_commit_sha":null,"homepage":null,"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/npow.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":"2026-04-22T15:09:00.000Z","updated_at":"2026-05-06T08:21:56.000Z","dependencies_parsed_at":"2026-04-28T06:00:30.912Z","dependency_job_id":null,"html_url":"https://github.com/npow/sagaflow","commit_stats":null,"previous_names":["npow/sagaflow"],"tags_count":56,"template":false,"template_full_name":null,"purl":"pkg:github/npow/sagaflow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/npow%2Fsagaflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/npow%2Fsagaflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/npow%2Fsagaflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/npow%2Fsagaflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/npow","download_url":"https://codeload.github.com/npow/sagaflow/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/npow%2Fsagaflow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32807904,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-08T08:22:46.396Z","status":"online","status_checked_at":"2026-05-09T02:00:06.633Z","response_time":123,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["ai-agents","anthropic","claude-code","durable-execution","llm","orchestration","python","temporal","workflow"],"created_at":"2026-04-23T00:01:11.393Z","updated_at":"2026-05-09T05:11:20.171Z","avatar_url":"https://github.com/npow.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sagaflow\n\n[![CI](https://github.com/npow/sagaflow/actions/workflows/ci.yml/badge.svg)](https://github.com/npow/sagaflow/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/sagaflow.svg)](https://pypi.org/project/sagaflow/)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nDurable execution for long-running agent workflows, on top of [Temporal](https://temporal.io/).\n\nYou write a Python workflow that calls models, runs tools, and writes artifacts. sagaflow runs each step as a Temporal activity, so when the worker dies — or a 40-minute fan-out crashes halfway — the next launch resumes from the last completed step instead of starting over. Results land in `~/.sagaflow/INBOX.md` whether or not you're still attached to the session that started them.\n\n## Quick start\n\n```bash\npip install sagaflow\ntemporal server start-dev \u0026\nexport ANTHROPIC_API_KEY=sk-ant-...\n\nsagaflow launch hello-world --name alice --await\n# → hello, alice\n```\n\nKill the terminal mid-run and re-launch the same workflow ID: it picks up from where the worker died.\n\n## What you get\n\n- **Resumes after crashes.** Activity-level checkpointing via Temporal — workers, sessions, and laptops can all die without losing in-flight work.\n- **Decoupled from the caller.** Fire-and-forget submissions land in an append-only inbox; a session-start hook surfaces unread results next time you open Claude Code.\n- **Provider-agnostic transport.** Anthropic SDK by default; point `ANTHROPIC_BASE_URL` at Bedrock, a model gateway, or any compatible proxy.\n- **Auto-managed worker.** First `sagaflow launch` spawns a worker daemon; `sagaflow doctor` reports health.\n\n## Install\n\n```bash\npip install sagaflow\n```\n\nRequirements:\n- Python 3.11+\n- [Temporal CLI](https://docs.temporal.io/cli) running locally: `brew install temporal \u0026\u0026 temporal server start-dev`\n- An Anthropic API key (or a compatible proxy via `ANTHROPIC_BASE_URL`)\n\n## Authoring a skill\n\nA skill is a directory under `~/.claude/skills/\u003cskill-name\u003e/` containing three things:\n\n1. **`workflow.py`** — a Temporal workflow class (the durable orchestration)\n2. **`__init__.py`** — a `register()` function that wires the workflow into sagaflow\n3. **`prompts/*.md`** — the system/user prompts the workflow's activities load\n\nHere is the complete `hello-world` skill (the one `sagaflow launch hello-world --name alice` runs).\n\n**`~/.claude/skills/hello-world/workflow.py`** — the durable workflow:\n\n```python\nfrom dataclasses import dataclass\nfrom datetime import timedelta\n\nfrom temporalio import workflow\n\nwith workflow.unsafe.imports_passed_through():\n    from sagaflow.durable.activities import (\n        EmitFindingInput, SpawnSubagentInput, WriteArtifactInput,\n    )\n    from sagaflow.durable.retry_policies import HAIKU_POLICY\n\n\n@dataclass(frozen=True)\nclass HelloWorldInput:\n    run_id: str\n    name: str\n    inbox_path: str\n    run_dir: str\n    greeter_system_prompt: str\n    greeter_user_prompt: str\n\n\n@workflow.defn(name=\"HelloWorldWorkflow\")\nclass HelloWorldWorkflow:\n    @workflow.run\n    async def run(self, inp: HelloWorldInput) -\u003e str:\n        prompt_path = f\"{inp.run_dir}/prompt.txt\"\n        await workflow.execute_activity(\n            \"write_artifact\",\n            WriteArtifactInput(path=prompt_path, content=inp.greeter_user_prompt),\n            start_to_close_timeout=timedelta(seconds=10),\n            retry_policy=HAIKU_POLICY,\n        )\n        parsed = await workflow.execute_activity(\n            \"spawn_subagent\",\n            SpawnSubagentInput(\n                role=\"greeter\", tier_name=\"HAIKU\",\n                system_prompt=inp.greeter_system_prompt,\n                user_prompt_path=prompt_path,\n                max_tokens=64, tools_needed=False,\n            ),\n            start_to_close_timeout=timedelta(seconds=600),\n            heartbeat_timeout=timedelta(seconds=120),\n            retry_policy=HAIKU_POLICY,\n        )\n        greeting = parsed.get(\"GREETING\", \"hello\")\n        await workflow.execute_activity(\n            \"emit_finding\",\n            EmitFindingInput(\n                inbox_path=inp.inbox_path, run_id=inp.run_id,\n                skill=\"hello-world\", status=\"DONE\", summary=greeting,\n                timestamp_iso=workflow.now().isoformat(timespec=\"seconds\"),\n            ),\n            start_to_close_timeout=timedelta(seconds=10),\n            retry_policy=HAIKU_POLICY,\n        )\n        return greeting\n```\n\nEach `execute_activity` call is a checkpoint. If the worker dies between them, replay resumes from the last completed one.\n\n**`~/.claude/skills/hello-world/__init__.py`** — registration:\n\n```python\nfrom typing import Any\n\nfrom sagaflow.durable.activities import emit_finding, spawn_subagent, write_artifact\nfrom sagaflow.prompts import load_prompt\nfrom sagaflow.registry import SkillRegistry, SkillSpec\n\nfrom skills.hello_world.workflow import HelloWorldInput, HelloWorldWorkflow\n\n\ndef _build_input(*, run_id, run_dir, inbox_path, cli_args: dict[str, Any]) -\u003e HelloWorldInput:\n    name = str(cli_args.get(\"name\", \"world\"))\n    return HelloWorldInput(\n        run_id=run_id, name=name,\n        inbox_path=inbox_path, run_dir=run_dir,\n        greeter_system_prompt=load_prompt(__file__, \"greeter.system\"),\n        greeter_user_prompt=load_prompt(__file__, \"greeter.user\", substitutions={\"name\": name}),\n    )\n\n\ndef register(registry: SkillRegistry) -\u003e None:\n    registry.register(SkillSpec(\n        name=\"hello-world\",\n        workflow_cls=HelloWorldWorkflow,\n        activities=[write_artifact, emit_finding, spawn_subagent],\n        build_input=_build_input,\n    ))\n```\n\n`register()` is what the worker calls at startup to discover the skill. `_build_input` translates CLI args (`--name alice`) into the workflow's input dataclass and loads prompts from disk.\n\n**`~/.claude/skills/hello-world/prompts/greeter.system.md`**:\n\n```\nYou are a greeter. Output a greeting using the format\nSTRUCTURED_OUTPUT_START / GREETING|\u003ctext\u003e / STRUCTURED_OUTPUT_END.\nDo not include any other text.\n```\n\n**`~/.claude/skills/hello-world/prompts/greeter.user.md`**:\n\n```\nGreet $name\n```\n\nThat's the whole skill. `sagaflow launch hello-world --name alice` finds the registration, builds the input, hands the workflow to Temporal, and the worker runs it durably.\n\n## CLI\n\n```bash\nsagaflow launch \u003cname\u003e --arg key=value [--await]   # submit a workflow\nsagaflow inbox                                     # list unread results\nsagaflow dismiss \u003crun-id\u003e                          # mark as read\nsagaflow doctor                                    # diagnose temporal/worker/hook\n```\n\n## How it works\n\n```\nsagaflow launch\n   │\n   ▼\npreflight (install hook, spawn worker if missing)\n   │\n   ▼\nTemporal (localhost:7233) ── workflow ID ── worker daemon\n                                              │\n                                              ▼\n                                         activities:\n                                          • model calls\n                                          • file I/O\n                                          • inbox emit\n                                              │\n                                              ▼\n                              ~/.sagaflow/INBOX.md  +  desktop notify\n                                              │\n                                              ▼\n                                  next session: SessionStart\n                                  hook surfaces unread runs\n```\n\nIf the worker crashes mid-run, the next `sagaflow launch` (or the next worker poll) resumes from the last completed activity. Activities that already succeeded don't re-execute.\n\n## Development\n\n```bash\ngit clone https://github.com/npow/sagaflow\ncd sagaflow\npython -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -e \".[dev]\"\n\nruff check sagaflow tests\nmypy sagaflow\npytest\n\n# Opt-in end-to-end tests (require live Temporal + real Anthropic access)\nSAGAFLOW_E2E=1 pytest\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnpow%2Fsagaflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnpow%2Fsagaflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnpow%2Fsagaflow/lists"}