"""WorkflowExportable: shares a scheduled-task/workflow recipe (steps, schedule shape, actions, model). The workflow store lives on the eric/workflow branch and is NOT on eric/dev yet, so every store touch is lazy: on a build without it, export finds nothing and import fails with a clear message, and the module still imports cleanly. It lights up the moment the workflow forward-port lands. Safety: an imported workflow must never silently start running on someone else's machine, so the schedule is forced off on import (the importer re-arms it). The sharer's phone numbers (text/call escalation) are stripped as PII, and run history / session + dashboard linkage are dropped.""" from __future__ import annotations from backend.apps.swarm.exportable import DepRef, ExportContext, RemapTable from backend.apps.swarm.models import EntityType, Requirement, RequirementKind P_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"} # Run-state, machine-linkage, and identifiers that must not ride along. P_DROP_FIELDS = { "id", "source_session_id", "dashboard_id", "edit_agent_session_id", "last_run_at", "last_run_status", "last_run_id", "next_run_at", "created_at", "updated_at", "cost_cap_usd_monthly", } def sanitize_workflow(data: dict) -> dict: out = {k: v for k, v in data.items() if k not in P_DROP_FIELDS} sched = dict(out.get("schedule") or {}) if sched: sched["enabled"] = False sched["runs_count"] = 0 sched["next_run_at"] = None sched["ends_at"] = None out["schedule"] = sched perms = [] for tier in out.get("permissions") or []: t = dict(tier) t["phone"] = None # the sharer's number; the importer sets their own perms.append(t) if perms: out["permissions"] = perms return out class WorkflowExportable: type = EntityType.workflow def __init__(self, local_id: str, name: str, data: dict): self.local_id = local_id self.name = name self.p_data = data @classmethod def load(cls, local_id: str) -> "WorkflowExportable | None": store = p_store() if store is None: return None wf = store.get_workflow(local_id) if wf is None: return None data = wf.model_dump(mode="json") return cls(local_id, data.get("title") or "Untitled workflow", data) def serialize(self, ctx: ExportContext) -> dict: return sanitize_workflow(self.p_data) def files(self) -> dict[str, bytes]: return {} def dependencies(self) -> list[DepRef]: return [] def requirements(self) -> list[Requirement]: reqs: list[Requirement] = [] for name in (self.p_data.get("actions") or {}).get("configured_sets") or []: reqs.append(Requirement( kind=RequirementKind.mcp_action, key=name, label=name, detail="This workflow uses this action.", )) mode = self.p_data.get("mode") or "agent" if mode in P_BUILTIN_MODES and mode != "agent": reqs.append(Requirement( kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode", detail="A built-in mode this workflow runs in.", )) provider = self.p_data.get("provider") or "anthropic" reqs.append(Requirement( kind=RequirementKind.api_key, key=provider, label=f"A {provider} model", detail="Set up this provider to run the workflow.", )) return reqs @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: store = p_store() model = p_model() if store is None or model is None: from backend.apps.swarm.ziputil import BundleError raise BundleError("this build doesn't support workflows yet; please update OpenSwarm") clean = sanitize_workflow(payload) clean.pop("id", None) # fresh id via the model's default_factory wf = model(**clean) store.save_workflow(wf) return wf.id @classmethod def rollback(cls, local_id: str) -> None: store = p_store() if store is not None: try: store.delete_workflow(local_id) except Exception: pass def p_store(): try: from backend.apps.workflows import storage return storage except Exception: return None def p_model(): try: from backend.apps.workflows.models import Workflow return Workflow except Exception: return None