name: layer-workflows description: Use when creating or modifying code in nomarr/workflows/. Workflows implement use cases, accept dependencies as parameters, orchestrate components, and return DTOs.
Workflows Layer
Purpose: Implement core use cases ("what Nomarr does").
Workflows contain the story of how Nomarr performs operations. They are recipes that:
- Accept dependencies as parameters (DB, config, ML backends)
- Orchestrate components to perform work
- Return DTOs
Allowed Imports
# ✅ Allowed
from nomarr.persistence import Database
from nomarr.components.ml import compute_embeddings, run_inference
from nomarr.components.tagging import predictions_to_tags
from nomarr.helpers.dto import ProcessFileResult
Forbidden Imports
# ❌ NEVER import these in workflows
from nomarr.services import ProcessingService # Workflows don't call services
from nomarr.interfaces import ... # No interface imports
from pydantic import BaseModel # No Pydantic
File & Function Naming
- File:
verb_object_wf.py(e.g.,scan_library_direct_wf.py,process_file_wf.py) - Function:
verb_object_workflow(...)as the primary entrypoint - One public workflow per file
- Everything else is
_private_helper
Complexity Rule: Clear Sequences
Workflows should read like a recipe:
def process_file_workflow(
db: Database,
file_path: str,
models_dir: str,
namespace: str,
) -> ProcessFileResult:
# Step 1: Load file
file_record = load_file_from_db(db, file_path)
# Step 2: Compute embeddings
embeddings = compute_all_embeddings(file_path, models_dir)
# Step 3: Run inference
predictions = run_inference_for_heads(embeddings, models_dir)
# Step 4: Convert to tags
tags = predictions_to_tags(predictions, namespace)
# Step 5: Write tags
write_tags_to_db(db, file_record.id, tags)
return ProcessFileResult(file=file_path, tags_written=len(tags))
Judge by clarity, not line count. Many component calls are fine if they form a clear sequence.
Accept All Dependencies as Parameters
Workflows receive everything via parameters—no global config reading:
# ✅ Good - dependencies injected
def process_file_workflow(
db: Database,
file_path: str,
models_dir: str,
) -> ProcessFileResult:
...
# ❌ Bad - reading globals
def process_file_workflow(file_path: str) -> ProcessFileResult:
from nomarr.config import db, models_dir # NO GLOBALS
...
When to Extract
Extract to a component if:
- Workflow is doing non-trivial computation itself
- Complex branching logic embedded in workflow
- Heavy domain math/ML/statistics
Split into private helpers if:
- Workflow becomes hard to read
- Large, reusable sub-sequences
# Before - hard to read
def complex_workflow(db: Database, ...) -> Result:
# 50 lines of discovery
# 50 lines of validation
# 50 lines of processing
# After - split with private helpers
def complex_workflow(db: Database, ...) -> Result:
discovered = _discover_and_validate(db, ...)
processed = _process_files(db, discovered, ...)
return Result(...)
Size Guidelines
- Soft limit: ~300–400 LOC per workflow module
- If multiple exported workflows with different user stories → split files
- Exception: analytics-style modules can group related read-only workflows
Validation Checklist
Before committing workflow code, verify:
- Does this file import from services or interfaces? → Violation
- Does this file import Pydantic? → Violation
- Does this workflow read global config? → Accept as parameter instead
- Is the workflow doing heavy computation? → Extract to component
- Does the function return a DTO? → Required
- Is there one public workflow per file? → Required
- Does the file name end in
_wf.py? → Convention
Layer Scripts
This skill includes validation scripts in .github/skills/layer-workflows/scripts/:
lint.py
Runs all linters on the workflows layer:
python .github/skills/layer-workflows/scripts/lint.py
Executes: ruff, mypy, vulture, bandit, radon, lint-imports
check_naming.py
Validates workflows naming conventions:
python .github/skills/layer-workflows/scripts/check_naming.py
Checks:
- Files must end in
_wf.py - Each file must have exactly one
*_workflowfunction - Internal helpers must be
_private