---
phase: 03-workflow-management
type: execute
---

<objective>
Implement workflow file operations: utility functions for filename handling and commands for pull, push, and create.

Purpose: Enable local-first workflow editing by pulling workflows to JSON files, pushing changes back, and creating new workflows from files.
Output: Utility functions for file handling and three CLI commands (pull, push, create) that support the local workflow development workflow.
</objective>

<execution_context>
~/.claude/get-shit-done/workflows/execute-phase.md
./summary.md
~/.claude/get-shit-done/references/tdd.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-workflow-management/03-01-SUMMARY.md
@.planning/phases/03-workflow-management/03-02-SUMMARY.md
@src/n8n_cli/commands/workflow.py
@src/n8n_cli/client/resources/workflows.py
@REQUIREMENTS.md
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create utility functions for filename handling</name>
  <files>src/n8n_cli/utils.py, tests/test_utils.py</files>
  <test-first>
Test: sanitize_filename() converts names to kebab-case, find_workflow_file() finds files by name or path
Cases:
- sanitize_filename("My Workflow!") → "my-workflow"
- sanitize_filename("Test  --  Workflow") → "test-workflow"
- find_workflow_file("existing.json") → Path("existing.json")
- find_workflow_file("My Workflow") → Path("workflows/my-workflow.json") if exists
- find_workflow_file("nonexistent") → None
  </test-first>
  <action>
Create src/n8n_cli/utils.py with two functions:

1. sanitize_filename(name: str) -> str:
   - Convert to lowercase
   - Replace non-alphanumeric chars with hyphens using re.sub(r"[^a-z0-9]+", "-", name)
   - Strip leading/trailing hyphens
   - Return sanitized name

2. find_workflow_file(name_or_path: str, search_dir: Path | None = None) -> Path | None:
   - If Path(name_or_path).exists(), return it directly
   - Otherwise, search in search_dir (default: "workflows") for files matching sanitized name
   - Use search_dir.rglob("*.json") to find all JSON files recursively
   - Match by sanitize_filename(file.stem) == sanitize_filename(name_or_path)
   - Return first match or None

Import from pathlib and re. Follow REQUIREMENTS.md specification exactly.
  </action>
  <verify>pytest tests/test_utils.py -v passes with 100% coverage</verify>
  <done>Utility functions work, sanitize_filename converts names correctly, find_workflow_file resolves paths and names</done>
</task>

<task type="auto">
  <name>Task 2: Implement workflow pull command</name>
  <files>src/n8n_cli/commands/workflow.py, tests/commands/test_workflow.py</files>
  <action>
Implement `workflow pull` command in workflow.py:
- Arguments: names (List[str], optional) - workflow names/IDs to pull
- Options: -p/--project (str), --all (flag), -v/--verbose (flag)
- Logic:
  1. If --all: Pull all workflows from client.workflows.list()
  2. If -p only (no names): Pull all workflows from that project (filter by project_id - note: will show "Project filtering not yet implemented" for now)
  3. If names provided: Pull specific workflows (resolve by ID or find_by_name)
  4. For each workflow: Save to workflows/{sanitized-name}.json using json.dumps(workflow.model_dump(), indent=2)
  5. Create workflows/ directory if it doesn't exist (using Path("workflows").mkdir(exist_ok=True))
  6. With -v: Show progress for each file written
  7. Success message: "Pulled {count} workflow(s) to workflows/"

File naming: Use sanitize_filename(workflow.name) + ".json". Preserve the workflow ID in the saved JSON (this enables push to work).
  </action>
  <verify>pytest tests/commands/test_workflow.py::test_pull -v passes, manual test creates JSON files</verify>
  <done>workflow pull command works, saves workflows to JSON files with sanitized names, preserves IDs, handles --all flag</done>
</task>

<task type="auto">
  <name>Task 3: Implement workflow push and create commands</name>
  <files>src/n8n_cli/commands/workflow.py, tests/commands/test_workflow.py</files>
  <action>
Implement two commands in workflow.py:

1. `workflow push`:
   - Argument: files (List[Path]) - one or more JSON files to push
   - Options: --dry-run (flag), -v/--verbose (flag)
   - Logic:
     a. For each file: Load JSON with json.loads(file.read_text())
     b. Check if "id" field exists in JSON
     c. If no ID: Error "Workflow has no ID. Use 'workflow create' instead."
     d. If ID exists: Call client.workflows.update(workflow["id"], workflow)
     e. With --dry-run: Show what would be pushed without making API calls
     f. Success message per file: "Pushed {workflow['name']} ({workflow['id']})"

2. `workflow create`:
   - Argument: file (Path) - JSON file to create from
   - Options: -p/--project (str), -f/--force (flag to create even if ID exists), -v/--verbose (flag)
   - Logic:
     a. Load JSON from file
     b. If "id" in JSON and not --force: Error "Workflow already has ID. Use 'workflow push' to update."
     c. Remove "id" field if present (API will assign new ID)
     d. Call client.workflows.create(workflow)
     e. CRITICAL: Update the local file with the new ID: workflow["id"] = response.id; file.write_text(json.dumps(workflow, indent=2))
     f. Success message: "Created workflow: {response.name} ({response.id})" + "Updated {file} with new ID"

Import json and pathlib.Path. Both commands should use typer.Argument() for file paths with proper type hints.
  </action>
  <verify>pytest tests/commands/test_workflow.py::test_push and test_create pass, manual tests work</verify>
  <done>workflow push updates existing workflows from files, workflow create creates new workflows and updates local files with IDs</done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] pytest tests/test_utils.py passes with 100% coverage
- [ ] pytest tests/commands/test_workflow.py passes
- [ ] mypy src/n8n_cli/utils.py and src/n8n_cli/commands/workflow.py pass
- [ ] ruff check passes
- [ ] Manual test: n8n workflow pull creates JSON files in workflows/
- [ ] Manual test: n8n workflow create updates local file with new ID
- [ ] Manual test: n8n workflow push updates cloud workflow
</verification>

<success_criteria>

- All tasks completed
- All verification checks pass
- Utility functions (sanitize_filename, find_workflow_file) work correctly
- Three file operation commands work: pull, push, create
- Local files are properly formatted JSON with IDs preserved
- workflow create updates local files with new IDs (critical behavior)
  </success_criteria>

<output>
After completion, create `.planning/phases/03-workflow-management/03-03-SUMMARY.md`:

# Phase 3 Plan 3: File Operation Commands Summary

**[Substantive one-liner - what shipped]**

## Accomplishments

- [Key outcomes]

## Files Created/Modified

- `src/n8n_cli/utils.py` - Utility functions for filename sanitization and file finding
- `src/n8n_cli/commands/workflow.py` - Added pull/push/create commands
- `tests/test_utils.py` - Tests for utility functions
- `tests/commands/test_workflow.py` - Tests for file operation commands

## Decisions Made

[Key decisions and rationale, or "None"]

## Issues Encountered

[Problems and resolutions, or "None"]

## Next Step

Ready for 03-04-PLAN.md (Advanced Workflow Commands)
</output>
