diff --git a/README.md b/README.md
index aad3c65..80c45bb 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ Progi Monitoring gives you a live view of every running and completed task — s
**4. Optimize as you go**
-Tweak playbooks between runs. Because workflows live in a database and survive context resets, every future task picks up your changes automatically — your process gets sharper with each iteration.
+Tweak playbooks in Progi Monitoring between runs. Because workflows live in a database and survive context resets, every future task picks up your changes automatically — your process gets sharper with each iteration.
---
@@ -88,7 +88,6 @@ Tweak playbooks between runs. Because workflows live in a database and survive c
| `get_playbook_authoring_prompt` | Return the Pass 2 system prompt for authoring a step's playbook (injects workflow context) |
| `save_workflow` | Persist a new workflow, its steps, and playbooks |
| `list_workflows` | Return all workflows with their ordered steps |
-| `update_playbook` | Replace the playbook content for a step |
Authoring is two passes: Pass 1 turns a plain-language description into a structured skeleton; Pass 2 authors each step's playbook. `save_workflow` persists both.
diff --git a/docs/README.md b/docs/README.md
index 9acfedf..1788700 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -30,21 +30,15 @@ Before I save this, a few questions:
Questions would be specific to your use case of course.
-Then, Progi saves the playbooks:
+Then, Progi saves the workflow:
```md
Now let me save the skeleton first, then I'll get the playbook authoring prompts for each step.
Progi [save_workflow]
-Progi [update_playbook]
-Progi [update_playbook]
-Progi [update_playbook]
-...
-Progi [update_playbook]
-
```
-Finally, Progi saves your complete workflow:
+Finally, Progi confirms your complete workflow:
```
Done. The Agentic Fix-Up workflow is saved with 4 steps and full playbooks.
diff --git a/progi/db.py b/progi/db.py
index fc75318..bb884bc 100644
--- a/progi/db.py
+++ b/progi/db.py
@@ -935,6 +935,39 @@ def get_workflow_with_playbooks(cfg: Config, workflow_id: int) -> dict[str, Any]
}
+def export_workflow(cfg: Config, workflow_id: int) -> dict[str, Any]:
+ """Return a workflow in the save_workflow-compatible export format."""
+ wf = get_workflow_with_playbooks(cfg, workflow_id)
+ step_by_id = {s["id"]: s for s in wf["steps"]}
+ return {
+ "name": wf["name"],
+ "description": wf["description"],
+ "process": [
+ {
+ "order": s["order"],
+ "name": s["name"],
+ "input_spec": s["input_spec"],
+ "output_spec": s["output_spec"],
+ }
+ for s in wf["steps"]
+ ],
+ "edges": [
+ {
+ "from": step_by_id[e["from_step_id"]]["name"],
+ "to": step_by_id[e["to_step_id"]]["name"],
+ "condition": e["condition"],
+ "priority": e["priority"],
+ }
+ for e in wf["edges"]
+ ],
+ "playbooks": {
+ s["name"]: s["playbook"]
+ for s in wf["steps"]
+ if s["playbook"] is not None
+ },
+ }
+
+
def get_step_detail(cfg: Config, workflow_id: int, step_id: int) -> dict[str, Any]:
"""Return a single step with its playbook, and derived prev/next steps."""
engine = get_engine(cfg)
diff --git a/progi/mcp_server.py b/progi/mcp_server.py
index 7086fef..c45fc73 100644
--- a/progi/mcp_server.py
+++ b/progi/mcp_server.py
@@ -7,8 +7,7 @@
- **Work loop**: create_task, list_tasks,
start_or_continue_task, update_progress_notes, submit_output.
- **Workflow authoring**: get_process_skeleton_prompt,
- get_playbook_authoring_prompt, save_workflow, list_workflows,
- update_playbook.
+ get_playbook_authoring_prompt, save_workflow, list_workflows.
IMPORTANT (stdio hygiene): when running over stdio, stdout is the MCP protocol
channel. Never `print()` to stdout. Use logging configured to stderr (see
@@ -235,11 +234,6 @@ def list_workflows() -> dict:
return {"workflows": db.list_workflows(_cfg), "monitoring_url": _monitoring_url("/workflows")}
-@mcp.tool(title="Update Playbook")
-def update_playbook(step_id: int, content: str) -> dict:
- """Replace the playbook content for a step."""
- return db.update_playbook(_cfg, step_id, content)
-
def run(cfg: Config | None = None) -> None:
"""Run the MCP server over stdio. Blocks until the client disconnects."""
diff --git a/progi/web/routers/workflows.py b/progi/web/routers/workflows.py
index 6542394..d7fd591 100644
--- a/progi/web/routers/workflows.py
+++ b/progi/web/routers/workflows.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import json
+
from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, Response
from fastapi.templating import Jinja2Templates
@@ -78,6 +80,21 @@ def step_detail(workflow_id: int, step_id: int, request: Request):
)
+@router.get("/workflows/{workflow_id}/export")
+def export_workflow(workflow_id: int, request: Request):
+ cfg = request.app.state.cfg
+ try:
+ data = db.export_workflow(cfg, workflow_id)
+ except ValueError as exc:
+ raise HTTPException(status_code=404, detail=str(exc))
+ filename = data["name"].replace(" ", "_") + ".json"
+ return Response(
+ content=json.dumps(data, indent=2),
+ media_type="application/json",
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
+ )
+
+
@router.patch("/workflows/{workflow_id}", response_class=JSONResponse)
def rename_workflow(
workflow_id: int,
diff --git a/progi/web/templates/pages/workflows.html b/progi/web/templates/pages/workflows.html
index e6b6056..61ef1a7 100644
--- a/progi/web/templates/pages/workflows.html
+++ b/progi/web/templates/pages/workflows.html
@@ -106,6 +106,11 @@
@click.stop="startRename(wf)"
class="w-full text-left px-3 py-1.5 text-xs text-muted hover:bg-surface-1 hover:text-primary transition-colors"
>Rename
+ Export JSON