diff --git a/docs/superpowers/plans/2026-07-20-workflow-builder-v2.md b/docs/superpowers/plans/2026-07-20-workflow-builder-v2.md
new file mode 100644
index 0000000..5462af4
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-20-workflow-builder-v2.md
@@ -0,0 +1,747 @@
+# Workflow Builder v2 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Rebuild the workflow builder to match the approved mockup with drag-and-drop, base-step editing/deletion (cascade), step input parameters, an edit-workflow modal, runs navigation, and fix the save crash.
+
+**Architecture:** Backend gains input-parameter fields on step/ref/resolved models, cascade delete, runner env injection, and a workflow-update handler that returns the updated workflow. Frontend adds `bash`/`pwsh`/`signal` tokens and a `Modal` primitive, then rebuilds the builder page (dotted canvas, 340px node cards, wire env chips, kicker/field inspector, HTML5 drag-and-drop), plus Edit-base-step and Edit-workflow modals and a runs list page.
+
+**Tech Stack:** Go (gin, mongo-driver v2), Next.js 16 app-router + react-query + Tailwind, HTML5 Drag-and-Drop, MongoDB.
+
+## Global Constraints
+
+- **No tests this iteration** — no `*_test.go` or frontend tests. Verify with `go build ./...`, `go vet ./...`, `npm run build`.
+- Mongo access pattern: `db.Col("collection_name")` + `context.WithTimeout`. Follow `server/internal/services/workflows.go`.
+- Audit every mutation with `services.LogEvent(action, actor, serverID, targetID, message)`.
+- Interpreter literals are `"bash"` and `"powershell"`.
+- Go module path: `github.com/mrhid6/vantage`.
+- Visual target: approved mockup. Builder palette adds amber signal `#f5a524` (`signal`), `#241800` (`signal-ink`), bash `#3fb950`, pwsh `#5b9bff`; keep existing `surface`/`surface-2`/`border`/`text-primary`/`text-secondary`/`danger`/`accent` tokens for panels. Node cards 340px; dotted-grid canvas; dashed-amber `passes` chips on wires; kicker/field inspector.
+- Frontend uses `@/lib/api` typed client, `@/components/ui`, react-query. Input styling follows the existing `inputClass` pattern in `web/app/secrets/page.tsx`.
+
+---
+
+## Task 1: Models + step CRUD + cascade delete
+
+**Files:**
+- Modify: `server/internal/models/workflow.go`
+- Modify: `server/internal/services/workflows.go`
+
+**Interfaces:**
+- Produces: `models.InputParam{Name,Default,Description}`; `WorkflowStep.DeclaredInputs`, `WorkflowStepRef.Inputs`, `ResolvedStep.Inputs`. `DeleteStep(stepID)` now cascades to workflows.
+
+- [ ] **Step 1: Add the model fields**
+
+In `server/internal/models/workflow.go` add:
+
+```go
+type InputParam struct {
+ Name string `bson:"name" json:"name"`
+ Default string `bson:"default" json:"default"`
+ Description string `bson:"description" json:"description"`
+}
+```
+
+- In `WorkflowStep`, add after `DeclaredOutputs`: `DeclaredInputs []InputParam `bson:"declared_inputs" json:"declared_inputs"``
+- In `WorkflowStepRef`, add: `Inputs map[string]string `bson:"inputs,omitempty" json:"inputs,omitempty"``
+- In `ResolvedStep`, add: `Inputs map[string]string `bson:"inputs" json:"inputs"``
+
+- [ ] **Step 2: Persist declared_inputs in CreateStep/UpdateStep**
+
+In `server/internal/services/workflows.go`:
+- In `CreateStep`, after the `SecretRefs` nil-guard add:
+
+```go
+ if s.DeclaredInputs == nil {
+ s.DeclaredInputs = []models.InputParam{}
+ }
+```
+
+- In `UpdateStep`'s `$set`, add: `"declared_inputs": s.DeclaredInputs,`
+
+- [ ] **Step 3: Cascade DeleteStep to workflows**
+
+Replace the body of `DeleteStep` with a version that also strips the step from every workflow:
+
+```go
+func DeleteStep(stepID string) error {
+ ctx, cancel := wfCtx()
+ defer cancel()
+ if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID}); err != nil {
+ return err
+ }
+ // Cascade: remove this step from every workflow that references it, re-sequencing orders.
+ cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID})
+ if err != nil {
+ return err
+ }
+ defer cur.Close(ctx)
+ var wfs []models.Workflow
+ if err := cur.All(ctx, &wfs); err != nil {
+ return err
+ }
+ for _, w := range wfs {
+ kept := make([]models.WorkflowStepRef, 0, len(w.Steps))
+ for _, ref := range w.Steps {
+ if ref.StepID == stepID {
+ continue
+ }
+ kept = append(kept, ref)
+ }
+ for i := range kept {
+ kept[i].Order = i
+ }
+ if _, err := db.Col("workflows").UpdateOne(ctx,
+ bson.M{"workflow_id": w.WorkflowID},
+ bson.M{"$set": bson.M{"steps": kept, "updated_at": time.Now()}},
+ ); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+```
+
+Confirm `models` and `time` are imported in this file (they are).
+
+- [ ] **Step 4: Verify build**
+
+Run: `cd server && go build ./... && go vet ./...`
+Expected: success.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add server/internal/models/workflow.go server/internal/services/workflows.go
+git commit -m "feat(workflows): step input params model + cascade step delete"
+```
+
+---
+
+## Task 2: Runner input injection + update handler returns workflow
+
+**Files:**
+- Modify: `server/internal/services/workflow_runner.go`
+- Modify: `server/internal/api/workflows.go`
+
+**Interfaces:**
+- Consumes: `models.ResolvedStep.Inputs`, `WorkflowStep.DeclaredInputs`, `WorkflowStepRef.Inputs` (Task 1).
+- Produces: runner injects input env; `PUT /api/workflows/:id` returns the `Workflow`.
+
+- [ ] **Step 1: Resolve inputs in `resolveSteps`**
+
+In `server/internal/services/workflow_runner.go`, in `resolveSteps`, after loading `lib` and before/where the `ResolvedStep` is built, compute the input env and set it. Add inside the loop (after `lib, err := getStep(...)` succeeds):
+
+```go
+ inputs := map[string]string{}
+ for _, p := range lib.DeclaredInputs {
+ if ref.Inputs != nil {
+ if v, ok := ref.Inputs[p.Name]; ok {
+ inputs[p.Name] = v
+ continue
+ }
+ }
+ inputs[p.Name] = p.Default
+ }
+```
+
+and set `Inputs: inputs,` on the `models.ResolvedStep{...}` literal.
+
+- [ ] **Step 2: Inject inputs into cmdEnv in `runServer`**
+
+In `runServer`, where `cmdEnv` is built (currently: copy `runEnv`, then overlay `secretVals`), change the layering so inputs are the base layer:
+
+```go
+ cmdEnv := map[string]string{}
+ for k, v := range step.Inputs {
+ cmdEnv[k] = v
+ }
+ for k, v := range runEnv {
+ cmdEnv[k] = v
+ }
+ for k, v := range secretVals {
+ cmdEnv[k] = v
+ }
+```
+
+(Inputs first = lowest precedence; upstream outputs override; secrets win. Keep the existing `secretVals`/`allSecrets` masking logic unchanged.)
+
+- [ ] **Step 3: `updateWorkflow` handler returns the workflow**
+
+In `server/internal/api/workflows.go`, change `updateWorkflow` so that after a successful `services.UpdateWorkflow`, it re-fetches and returns the workflow:
+
+```go
+func updateWorkflow(c *gin.Context) {
+ var w models.Workflow
+ if err := c.ShouldBindJSON(&w); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+ if err := services.UpdateWorkflow(c.Param("id"), w); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ services.LogEvent("workflow.updated", actorFromCtx(c), "", c.Param("id"), "workflow updated")
+ updated, err := services.GetWorkflow(c.Param("id"))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, updated)
+}
+```
+
+- [ ] **Step 4: Verify build**
+
+Run: `cd server && go build ./... && go vet ./...`
+Expected: success.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add server/internal/services/workflow_runner.go server/internal/api/workflows.go
+git commit -m "feat(workflows): inject step inputs into env; update returns workflow"
+```
+
+---
+
+## Task 3: Frontend tokens, Modal primitive, API types
+
+**Files:**
+- Modify: `web/tailwind.config.ts`
+- Create: `web/components/ui/Modal.tsx`
+- Modify: `web/components/ui/index.ts`
+- Modify: `web/lib/api.ts`
+
+**Interfaces:**
+- Produces: tokens `bash`/`pwsh`/`signal`/`signal-ink`; `