Skip to main content

Quick Reference: N8N Killer Workflow Designer

πŸš€ Get Started in 5 Minutes​

What You Need to Know​

  1. Mission: Build the most intuitive workflow designer (better than N8N)
  2. Platform: React + Spring Boot + PostgreSQL + ThorAPI
  3. Key Challenge: Module chaining (snap together like LEGO)
  4. Deadline: Production-ready, enterprise-grade

Three Core Files to Read​

  1. /Users/johnmcmahon/workspace/2025/valkyr/ValkyrAI/N8N_KILLER_WORKFLOW_INITIATIVE.md - Full context
  2. /Users/johnmcmahon/workspace/2025/valkyr/ValkyrAI/valor_inference_prompt.txt - Development rules (search "N8N KILLER")
  3. /Users/johnmcmahon/workspace/2025/valkyr/ValkyrAI/DEVELOPMENT_CHECKLIST.md - Task list

πŸ”΄ CRITICAL RULES (Violating = Severe Penalty)​

Rule #1: NEVER Set IDs on New Objects​

// ❌ WRONG - This breaks JPA
ExecModule m = new ExecModule();
m.setId(UUID.randomUUID()); // FORBIDDEN!
repo.save(m); // JPA will throw error

// βœ… CORRECT - Let JPA generate the ID
ExecModule m = new ExecModule();
// Don't set ID
repo.save(m); // JPA auto-generates UUID

Rule #2: Use ThorAPI Generated Services ONLY​

// ❌ WRONG - Custom query
@Query("SELECT * FROM exec_module WHERE task_id = ?1")
List<ExecModule> findByTaskId(UUID taskId);

// βœ… CORRECT - Use generated repo
ExecModuleRepository repo; // auto-generated by ThorAPI
List<ExecModule> modules = repo.findByTaskId(taskId);

Rule #3: Eager Load Before Async​

// ❌ WRONG - LazyInitializationException in async thread
return CompletableFuture.supplyAsync(() -> {
workflow.getTasks().forEach(...); // CRASHES!
});

// βœ… CORRECT - Hydrate within transaction
@Transactional
public void loadSchedules() {
List<Workflow> wfs = findAll();
wfs.forEach(w -> {
Hibernate.initialize(w.getTasks());
w.getTasks().forEach(t -> Hibernate.initialize(t.getModules()));
});
// NOW safe to pass to async
}

Rule #4: Validate Server-Side, ALWAYS​

// βœ… ALWAYS validate input on backend
@PostMapping("/Workflow")
public ResponseEntity<?> create(@RequestBody Workflow wf) {
ValidationResult validation = validator.validate(wf);
if (!validation.isValid()) {
return ResponseEntity.badRequest().body(validation.errors);
}
// Safe to save
return ResponseEntity.ok(repo.save(wf));
}

Rule #5: Error Handling in Modules (Not Exceptions)​

// βœ… Return error maps, don't throw
@Override
public Map<String, Object> execute(Map<String, Object> input) {
try {
// Do work
return Map.of("status", "success", "result", ...);
} catch (Exception e) {
// Return error map
return Map.of("status", "error", "message", e.getMessage());
}
}

πŸ“‹ Phase Breakdown​

Phase 1: UX (Days 1-2)​

  • Build ExecModule catalog with 20+ schemas
  • Create unified config editor component
  • Add module chaining visualization
  • Implement drag-to-reorder

Phase 2: Server (Days 2-3)​

  • Tighten workflow execution (proper data flow)
  • Fix ExecModule CRUD (no ID issues)
  • Fix Task service (module ordering)
  • Implement validation

Phase 3: APIs (Days 3-4)​

  • REST API lookup service
  • QBE auto-complete for module inputs
  • API browser component

Phase 4: Lifecycle (Days 4-5)​

  • Create workflow flow
  • Edit workflow with dirty-state tracking
  • Auto-save with debounce
  • Workflow validation

Phase 5: Testing (Days 5-6)​

  • Unit tests (80%+ coverage)
  • Integration tests
  • E2E manual testing

πŸ› οΈ Common Patterns​

Pattern: Create New Workflow (No ID!)​

Backend:

@PostMapping("/Workflow")
public ResponseEntity<Workflow> create(@RequestBody Workflow wf) {
if (wf.getId() != null) throw new BadRequest("ID must be null");
Workflow saved = repo.save(wf); // JPA sets ID
return ResponseEntity.created(...).body(saved);
}

Frontend:

const workflow = { name: "My Workflow" }; // NO id
const result = await createWorkflow(workflow);
const workflowId = result.id; // Use server's ID

Pattern: Module Configuration Validation​

interface FormField {
name: string;
type: "text" | "email" | "select" | "json" | "apiLookup";
required: boolean;
validation?: (value: any) => string | null;
}

const validateConfig = (fields: FormField[], config: any) => {
const errors: Record<string, string> = {};

fields.forEach((field) => {
if (field.required && !config[field.name]) {
errors[field.name] = `${field.name} is required`;
}
if (field.validation && config[field.name]) {
const error = field.validation(config[field.name]);
if (error) errors[field.name] = error;
}
});

return { valid: Object.keys(errors).length === 0, errors };
};

Pattern: Async with Auth Context​

final Authentication auth = SecurityContextHolder.getContext().getAuthentication();

return CompletableFuture.supplyAsync(() -> {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(auth);
SecurityContextHolder.setContext(ctx);

try {
// Do work
persistState(workflow);
} finally {
SecurityContextHolder.clearContext();
}
});

@Transactional(propagation = Propagation.REQUIRES_NEW)
void persistState(Workflow wf) {
repo.save(wf);
}

Pattern: Module Data Flow​

Map<String, Object> globalState = new HashMap<>();

// Module A execution
Map<String, Object> outputA = moduleA.execute(globalState);
globalState.putAll(outputA); // Merge output

// Module B execution (receives merged state)
Map<String, Object> outputB = moduleB.execute(globalState);
globalState.putAll(outputB); // Merge output

πŸ“Š Key Data Model​

Workflow
β”œβ”€ id (UUID, auto-generated by JPA)
β”œβ”€ name (String)
β”œβ”€ description (String)
β”œβ”€ status (ACTIVE, PAUSED, DRAFT, ERROR)
β”œβ”€ createdDate (DateTime)
β”œβ”€ updatedDate (DateTime, for conflict detection)
└─ tasks: List<Task>
β”œβ”€ id (UUID, auto-generated)
β”œβ”€ workflowId (FK)
β”œβ”€ name (String)
β”œβ”€ role (SYSTEM, ASSISTANT, USER)
β”œβ”€ taskOrder (Float: 1.0, 2.0, 3.0...)
β”œβ”€ status (RUNNING, STOPPED, READY, ERROR)
└─ modules: List<ExecModule>
β”œβ”€ id (UUID, auto-generated)
β”œβ”€ taskId (FK)
β”œβ”€ name (String)
β”œβ”€ className (String: com.valkyrlabs.workflow.modules.*)
β”œβ”€ moduleType (String: Email, REST, Stripe, etc.)
β”œβ”€ moduleOrder (Float: 1.0, 2.0, 2.5...)
β”œβ”€ moduleData (JSON string with config)
β”œβ”€ status (READY, RUNNING, GOOD, ERROR, WARNING)
β”œβ”€ config (Map<String, Object>, persisted as JSON)
└─ integrationAccount (Optional FK)

Key: Module execution order is determined by moduleOrder (float), not insertion order.


🎨 Component Tree​

WorkflowStudio (main)
β”œβ”€ WorkflowCanvas (ReactFlow)
β”‚ β”œβ”€ TaskNode (task with nested modules)
β”‚ β”‚ β”œβ”€ ModuleNode (each module)
β”‚ β”‚ └─ ModuleChainViewer (show chain in inspector)
β”‚ └─ TaskEdges
β”œβ”€ FloatingExecModulesToolbar (palette)
β”œβ”€ InspectorPanel (right side)
β”‚ β”œβ”€ SelectedNodeDetails
β”‚ β”œβ”€ ModulesList (with drag-reorder)
β”‚ └─ ExecModuleEditModal
β”‚ └─ ExecModuleConfigBuilder (form)
└─ BottomConsole (logs)

πŸ”— File Locations​

Frontend (TypeScript/React)​

web/typescript/valkyr_labs_com/src/
β”œβ”€ components/WorkflowStudio/
β”‚ β”œβ”€ execModuleCatalog.ts (NEW - schemas)
β”‚ β”œβ”€ ExecModuleConfigBuilder.tsx (NEW - form)
β”‚ β”œβ”€ ModuleChainViewer.tsx (NEW - chain viz)
β”‚ β”œβ”€ ApiLookupComponent.tsx (NEW - QBE)
β”‚ β”œβ”€ ApiBrowserComponent.tsx (NEW - API browser)
β”‚ β”œβ”€ index.tsx (MODIFY)
β”‚ β”œβ”€ ExecModuleEditModal.tsx (MODIFY)
β”‚ β”œβ”€ InspectorPanel.tsx (MODIFY)
β”‚ └─ WorkflowCanvas.tsx (MODIFY)
└─ redux/features/workflows/
β”œβ”€ workflowSlice.ts (MODIFY - dirty state)
└─ WorkflowService.ts (MODIFY - auto-save)

Backend (Java)​

valkyrai/src/main/java/com/valkyrlabs/
β”œβ”€ workflow/service/
β”‚ β”œβ”€ ValkyrWorkflowService.java (MODIFY)
β”‚ β”œβ”€ ValkyrExecModuleService.java (MODIFY)
β”‚ β”œβ”€ ValkyrTaskService.java (MODIFY)
β”‚ └─ WorkflowValidator.java (NEW)
β”œβ”€ workflow/controller/
β”‚ └─ WorkflowController.java (MODIFY)
└─ workflow/config/
└─ ExecModuleSchemaProvider.java (NEW)

πŸ§ͺ Testing Quick Start​

Frontend Test Template​

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import ExecModuleConfigBuilder from "./ExecModuleConfigBuilder";

describe("ExecModuleConfigBuilder", () => {
it("should render form fields for module type", () => {
const module = { className: "EmailModule", moduleData: "{}" };
render(<ExecModuleConfigBuilder module={module} onChange={jest.fn()} />);

expect(screen.getByLabelText("Recipient Email")).toBeInTheDocument();
expect(screen.getByLabelText("Subject")).toBeInTheDocument();
});

it("should validate required fields", async () => {
const module = { className: "EmailModule", moduleData: "{}" };
const onChange = jest.fn();

render(<ExecModuleConfigBuilder module={module} onChange={onChange} />);

const saveButton = screen.getByText("Save");
await userEvent.click(saveButton);

expect(screen.getByText("Recipient Email is required")).toBeInTheDocument();
});
});

Backend Test Template​

@SpringBootTest
@Transactional
class ValkyrWorkflowServiceTest {
@Autowired
private ValkyrWorkflowService workflowService;

@Autowired
private WorkflowRepository workflowRepo;

@Test
void testCreateWorkflowGeneratesId() {
Workflow wf = new Workflow();
wf.setName("Test Workflow");
// DON'T set wf.setId(...)

Workflow saved = workflowService.saveOrUpdate(wf);

assertThat(saved.getId()).isNotNull();
assertThat(workflowRepo.findById(saved.getId())).isPresent();
}

@Test
void testExecuteTaskWithModuleChain() {
// Create workflow with 2 modules
Workflow wf = createTestWorkflow();
Task task = wf.getTasks().get(0);

// Execute
BranchOutcome result = workflowService.executeTask(task, new HashMap<>(), ...);

assertThat(result.status).isEqualTo("success");
assertThat(result.state).containsEntry("moduleAOutput", "value");
}
}

🚨 Common Mistakes to Avoid​

❌ Wrongβœ… Correct
m.setId(UUID.randomUUID())Leave ID null, let JPA generate
Custom @Query in repoUse generated repo methods
Access lazy collections in asyncHibernate.initialize() first
Throw exceptions in modulesReturn error maps
Trust client validationAlways validate on server
Set relationships manuallyUse repository methods
No error handlingHandle all exceptions
No testsWrite tests first
No documentationDocument as you code
Hardcode secretsUse environment variables

πŸ“ˆ Success Metrics​

βœ… = Complete and tested
⚠️ = In progress
❌ = Not started

MetricStatusTarget
Module config forms❌20+ types
Module chaining works❌Visual + functional
Data flows correctly❌Between all modules
Create workflow works❌No ID errors
Edit workflow works❌Dirty tracking, auto-save
Workflows execute❌All topologies
Tests pass❌80%+ coverage
No LazyInit errors❌Zero occurrences
Production ready❌Ready to deploy

🎯 Next 3 Steps​

  1. Today: Read all three files (N8N_KILLER, valor_inference_prompt, checklist)
  2. Tomorrow: Start Phase 1 - Build execModuleCatalog with 20+ schemas
  3. Day 3: Build ExecModuleConfigBuilder component

πŸ’¬ Questions?​

Refer to:

  • Rules: valor_inference_prompt.txt section "N8N KILLER WORKFLOW DESIGNER INITIATIVE"
  • Architecture: N8N_KILLER_WORKFLOW_INITIATIVE.md
  • Tasks: DEVELOPMENT_CHECKLIST.md
  • Code Examples: IMPLEMENTATION_ROADMAP_DETAILED.md

Remember: Every rule is there for a reason. Follow them to the letter.