Quick Reference: N8N Killer Workflow Designer
π Get Started in 5 Minutesβ
What You Need to Knowβ
- Mission: Build the most intuitive workflow designer (better than N8N)
- Platform: React + Spring Boot + PostgreSQL + ThorAPI
- Key Challenge: Module chaining (snap together like LEGO)
- Deadline: Production-ready, enterprise-grade
Three Core Files to Readβ
/Users/johnmcmahon/workspace/2025/valkyr/ValkyrAI/N8N_KILLER_WORKFLOW_INITIATIVE.md- Full context/Users/johnmcmahon/workspace/2025/valkyr/ValkyrAI/valor_inference_prompt.txt- Development rules (search "N8N KILLER")/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 repo | Use generated repo methods |
| Access lazy collections in async | Hibernate.initialize() first |
| Throw exceptions in modules | Return error maps |
| Trust client validation | Always validate on server |
| Set relationships manually | Use repository methods |
| No error handling | Handle all exceptions |
| No tests | Write tests first |
| No documentation | Document as you code |
| Hardcode secrets | Use environment variables |
π Success Metricsβ
β
= Complete and tested
β οΈ = In progress
β = Not started
| Metric | Status | Target |
|---|---|---|
| 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β
- Today: Read all three files (N8N_KILLER, valor_inference_prompt, checklist)
- Tomorrow: Start Phase 1 - Build execModuleCatalog with 20+ schemas
- Day 3: Build ExecModuleConfigBuilder component
π¬ Questions?β
Refer to:
- Rules:
valor_inference_prompt.txtsection "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.