Skip to main content

πŸ›οΈ ValkyrAI Workflow Engine v2.0 β€” System Architecture

The N8N Killer: Built on ThorAPI, Hardened for Production


🎯 High-Level Architecture​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CLIENT LAYER β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ React Flow β”‚ β”‚ DLQ Browser β”‚ β”‚ Execution β”‚ β”‚
β”‚ β”‚ Studio β”‚ β”‚ & Replay UI β”‚ β”‚ Monitor β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ RTK Query Hooks β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”‚ HTTPS/JSON
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ REST API LAYER β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ WorkflowExecution API β”‚ Run API β”‚ DLQ API β”‚ β”‚
β”‚ β”‚ /WorkflowExecution β”‚ /Run β”‚ /DeadLetterQueueβ”‚ β”‚
β”‚ β”‚ - POST /{id}/execute β”‚ - POST /{id}/heartbeat β”‚ β”‚
β”‚ β”‚ - POST /{id}/cancel β”‚ - GET /{id} β”‚ β”‚
β”‚ β”‚ - POST /{id}/pause β”‚ β”‚ β”‚
β”‚ β”‚ - POST /{id}/resume β”‚ /DeadLetterQueue β”‚
β”‚ β”‚ β”‚ - POST /{id}/requeue β”‚
β”‚ β”‚ β”‚ - POST /{id}/discard β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ Spring Security β”‚ β”‚ Spring MVC β”‚
β”‚ RBAC + ACL β”‚ β”‚ @RestController β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚ β”‚
↓ ↓ ↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ SERVICE LAYER β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ValkyrWorkflowExecutionService (Orchestration) β”‚ β”‚
β”‚ β”‚ - execute(workflowId, triggerType, payload) β”‚ β”‚
β”‚ β”‚ - pause/resume/cancel operations β”‚ β”‚
β”‚ β”‚ - state transition management β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β”€β†’ ValkyrRunService (Idempotency Engine) β”‚
β”‚ β”‚ β”œβ”€ generateIdempotencyKey() [SHA-256] β”‚
β”‚ β”‚ β”œβ”€ acquireLease() [DB row lock] β”‚
β”‚ β”‚ β”œβ”€ updateHeartbeat() [extend lease] β”‚
β”‚ β”‚ β”œβ”€ completeRun() [store outputs] β”‚
β”‚ β”‚ β”œβ”€ failRun() [classify error] β”‚
β”‚ β”‚ └─ reapZombieRuns() [@Scheduled 30s] β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β”€β†’ ValkyrDLQService (Error Recovery) β”‚
β”‚ β”‚ β”œβ”€ quarantine() [failed runs] β”‚
β”‚ β”‚ β”œβ”€ requeue() [with overrides] β”‚
β”‚ β”‚ └─ discard() [operator notes] β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β”€β†’ ValkyrCircuitBreakerService (Resilience) β”‚
β”‚ β”‚ β”œβ”€ isCircuitOpen() β”‚
β”‚ β”‚ β”œβ”€ recordFailure() β”‚
β”‚ β”‚ └─ openCircuit() / closeCircuit() β”‚
β”‚ β”‚ β”‚
β”‚ └──→ ValkyrRunnerService (Execution Engine) β”‚
β”‚ β”œβ”€ pollAndExecute() [@Scheduled 100ms] β”‚
β”‚ β”œβ”€ executeWithHeartbeat() β”‚
β”‚ └─ enforceResourceLimits() β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ValkyrBudgetService & ValkyrQuotaService β”‚ β”‚
β”‚ β”‚ - checkBudget() [kill switch] β”‚ β”‚
β”‚ β”‚ - checkQuota() [rate limits] β”‚ β”‚
β”‚ β”‚ - trackCost() [per-principal] β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”‚ JPA/Hibernate
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ REPOSITORY LAYER β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ValkyrRunRepository (Custom Queries) β”‚ β”‚
β”‚ β”‚ - findByIdempotencyKey() [O(1) lookup] β”‚ β”‚
β”‚ β”‚ - findByLeaseUntilBefore() [zombie detection] β”‚ β”‚
β”‚ β”‚ - findByStateOrderByCreated() [FIFO queue] β”‚ β”‚
β”‚ β”‚ - findRunsReadyForRetry() [auto-retry] β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ValkyrDLQRepository (DLQ Queries) β”‚ β”‚
β”‚ β”‚ - findByResolution() β”‚ β”‚
β”‚ β”‚ - countByResolution() β”‚ β”‚
β”‚ β”‚ - findByFailureType() β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ + WorkflowExecutionRepository, BudgetRepository, β”‚
β”‚ QuotaRepository, CircuitBreakerStateRepository β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”‚ JDBC
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DATA LAYER β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ PostgreSQL Database (ACID Transactions) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ Tables: β”‚ β”‚
β”‚ β”‚ - workflow_execution (state tracking) β”‚ β”‚
β”‚ β”‚ - run (idempotency + lease) β”‚ β”‚
β”‚ β”‚ - dead_letter_queue (quarantine) β”‚ β”‚
β”‚ β”‚ - circuit_breaker_state (resilience) β”‚ β”‚
β”‚ β”‚ - budget (cost control) β”‚ β”‚
β”‚ β”‚ - quota (rate limiting) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ Indexes: β”‚ β”‚
β”‚ β”‚ - idx_run_idempotency (UNIQUE) β”‚ β”‚
β”‚ β”‚ - idx_run_lease_until (zombie reaper) β”‚ β”‚
β”‚ β”‚ - idx_run_state (queue polling) β”‚ β”‚
β”‚ β”‚ - idx_dlq_resolution (operator queries) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

⚑ Execution Flow: End-to-End​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ USER β”‚ POST /WorkflowExecution/{workflowId}/execute
β”‚ CLIENT β”‚ {params: {...}}
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ WorkflowExecutionController β”‚
β”‚ β”œβ”€ @PreAuthorize("hasPermission(...)") β”‚
β”‚ └─ workflowExecutionService.execute(...) β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ValkyrWorkflowExecutionService β”‚
β”‚ β”œβ”€ Create WorkflowExecution (PENDING β†’ RUNNING) β”‚
β”‚ β”œβ”€ Load Workflow.tasks β”‚
β”‚ └─ For each Task: β”‚
β”‚ β”œβ”€ runService.createRun(execution, task, inputs, 1) β”‚
β”‚ └─ Store Run (PENDING state) β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”‚ (Async polling loop β€” RunnerService)
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ValkyrRunnerService [@Scheduled(fixedDelay = 100ms)] β”‚
β”‚ β”œβ”€ runService.findPendingRuns(limit=10) β”‚
β”‚ └─ For each pending Run: β”‚
β”‚ β”œβ”€ runService.acquireLease(runId, runnerId) β”‚
β”‚ β”‚ β”œβ”€ Check existing lease holder β”‚
β”‚ β”‚ β”œβ”€ Set leaseUntil = now + 2min β”‚
β”‚ β”‚ β”œβ”€ Set state = LEASED β”‚
β”‚ β”‚ └─ Save Run β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€ Start heartbeat thread (every 10s): β”‚
β”‚ β”‚ └─ runService.updateHeartbeat(runId, runnerId) β”‚
β”‚ β”‚ β”œβ”€ Verify lease ownership β”‚
β”‚ β”‚ β”œβ”€ Extend leaseUntil β”‚
β”‚ β”‚ └─ Update heartbeatAt β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€ circuitBreakerService.isCircuitOpen(module)? β”‚
β”‚ β”‚ β”œβ”€ YES β†’ failRun(CIRCUIT_OPEN) β”‚
β”‚ β”‚ └─ NO β†’ continue β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€ budgetService.checkBudget(principal)? β”‚
β”‚ β”‚ β”œβ”€ EXCEEDED β†’ failRun + kill switch β”‚
β”‚ β”‚ └─ OK β†’ continue β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€ quotaService.checkQuota(principal, resource)? β”‚
β”‚ β”‚ β”œβ”€ EXCEEDED β†’ failRun β”‚
β”‚ β”‚ └─ OK β†’ continue β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€ Execute VModule: β”‚
β”‚ β”‚ β”œβ”€ Load ExecModule β”‚
β”‚ β”‚ β”œβ”€ module.execute(inputs) β†’ outputs β”‚
β”‚ β”‚ └─ Track duration & cost β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€ On SUCCESS: β”‚
β”‚ β”‚ └─ runService.completeRun(runId, outputs, ...) β”‚
β”‚ β”‚ β”œβ”€ state = SUCCESS β”‚
β”‚ β”‚ β”œβ”€ Store outputs, duration, cost β”‚
β”‚ β”‚ └─ Release lease β”‚
β”‚ β”‚ β”‚
β”‚ └─ On FAILURE: β”‚
β”‚ β”œβ”€ Classify error type: β”‚
β”‚ β”‚ β”œβ”€ TRANSIENT β†’ failRun(TRANSIENT) β”‚
β”‚ β”‚ β”‚ β”œβ”€ Calculate backoff (exp + jitter) β”‚
β”‚ β”‚ β”‚ └─ Set retryReadyAt timestamp β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”œβ”€ PERMANENT β†’ failRun(PERMANENT) β”‚
β”‚ β”‚ β”‚ └─ runService.promoteToDeadLetterQueue() β”‚
β”‚ β”‚ β”‚ └─ dlqService.quarantine() β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ └─ TIMEOUT β†’ failRun(TIMEOUT) β”‚
β”‚ β”‚ β”‚
β”‚ └─ circuitBreakerService.recordFailure() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Background Jobs (Scheduled) β”‚
β”‚ β”‚
β”‚ ValkyrRunService.reapZombieRuns() [@Scheduled 30s] β”‚
β”‚ β”œβ”€ Find runs where leaseUntil < now β”‚
β”‚ β”œβ”€ AND state IN (LEASED, RUNNING) β”‚
β”‚ β”œβ”€ For each zombie: β”‚
β”‚ β”‚ β”œβ”€ Log warning (runner X crashed) β”‚
β”‚ β”‚ β”œβ”€ state = PENDING β”‚
β”‚ β”‚ β”œβ”€ Clear lease fields β”‚
β”‚ β”‚ └─ Save (will be picked up again) β”‚
β”‚ └─ Auto-recovery in < 30 seconds β”‚
β”‚ β”‚
β”‚ CircuitBreakerService.checkHalfOpen() [@Scheduled 60s] β”‚
β”‚ β”œβ”€ Find circuits in OPEN state β”‚
β”‚ β”œβ”€ If nextRetryAt < now: β”‚
β”‚ β”‚ └─ state = HALF_OPEN (allow 1 test request) β”‚
β”‚ └─ On success β†’ CLOSED, on failure β†’ OPEN β”‚
β”‚ β”‚
β”‚ QuotaService.resetCounters() [@Scheduled hourly/daily] β”‚
β”‚ β”œβ”€ Reset hourly counters at hourResetAt β”‚
β”‚ └─ Reset daily counters at dayResetAt β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🎯 Key Differentiators vs n8n​

Featuren8nValkyrAI v2.0
Idempotency❌ Noneβœ… SHA-256 keys + DB checks
Lease Management❌ Noneβœ… 2-min leases + heartbeats
Crash Recovery❌ Lost workβœ… Zombie reaper (<30s)
Exactly-Once❌ At-least-onceβœ… Lease + idempotency
DLQ & Replay❌ Manualβœ… Operator-guided w/ overrides
Circuit Breakers❌ Noneβœ… Auto-recovery
Budget Controls❌ Noneβœ… Per-principal + kill switch
Cost Tracking❌ Noneβœ… Per-step token tracking
Rate Limiting⚠️ Basicβœ… Concurrent + hourly/daily
Observability⚠️ Logs onlyβœ… OpenTelemetry + flame charts
Typed I/O❌ Noneβœ… JSONSchema validation
Multi-Tenancy⚠️ Weakβœ… RBAC + ACL + quotas

πŸ” Security Architecture​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Authentication Layer β”‚
β”‚ β”œβ”€ Spring Security β”‚
β”‚ β”œβ”€ JWT tokens (stateless) β”‚
β”‚ └─ Principal (user/system/agent) β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Authorization Layer (RBAC) β”‚
β”‚ β”œβ”€ Roles: ADMIN, USER, SYSTEM, WORKFLOW β”‚
β”‚ β”œβ”€ @PreAuthorize("hasRole('ADMIN')") β”‚
β”‚ └─ Method-level security β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ACL Layer (Fine-Grained) β”‚
β”‚ β”œβ”€ Per-object permissions (READ, WRITE, DELETE, ...)β”‚
β”‚ β”œβ”€ @PreAuthorize("hasPermission(#id, 'execute')") β”‚
β”‚ └─ ValkyrACL service β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Data Encryption β”‚
β”‚ β”œβ”€ @SecureField annotation β”‚
β”‚ β”œβ”€ AES-256 encryption at rest β”‚
β”‚ β”œβ”€ Transparent encrypt/decrypt via AspectJ β”‚
β”‚ └─ KMS integration for key rotation β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“Š Data Flow: Idempotency in Action​

Request: Execute Task with inputs {userId: "123", action: "purchase"}

Step 1: Generate Idempotency Key
─────────────────────────────────
Input: executionId + taskId + moduleId + SHA256({userId:"123",action:"purchase"})
Output: "a3f5c9d8e2b1..." (64-char hex)

Step 2: Check Existing Run
─────────────────────────────────
Query: SELECT * FROM run WHERE idempotency_key = 'a3f5c9d8e2b1...'

IF found AND state = SUCCESS:
βœ… Return cached outputs (no duplicate work)
Exit

IF found AND state IN (RUNNING, LEASED):
⚠️ Duplicate request detected, return existing run
Exit

ELSE:
➑️ Continue to Step 3

Step 3: Create New Run
─────────────────────────────────
INSERT INTO run (
id, execution_id, task_id, state, idempotency_key, ...
) VALUES (
uuid(), execution_id, task_id, 'PENDING', 'a3f5c9d8e2b1...', ...
)

Commit transaction

Step 4: Runner Picks Up Run
─────────────────────────────────
SELECT * FROM run WHERE state = 'PENDING' ORDER BY created_date LIMIT 10

FOR EACH run:
BEGIN TRANSACTION
UPDATE run
SET state = 'LEASED',
lease_until = NOW() + INTERVAL '2 minutes',
leased_by = 'runner-pod-123'
WHERE id = run.id
AND (lease_until IS NULL OR lease_until &lt; NOW())
COMMIT

IF rows_affected = 1:
βœ… Lease acquired
ELSE:
❌ Another runner got it, skip

Step 5: Execute with Heartbeat
─────────────────────────────────
Start heartbeat thread (every 10s):
UPDATE run
SET heartbeat_at = NOW(),
lease_until = NOW() + INTERVAL '2 minutes'
WHERE id = run.id AND leased_by = 'runner-pod-123'

Execute module.execute(inputs)

On success:
UPDATE run
SET state = 'SUCCESS',
finished_at = NOW(),
outputs = '{"orderId":"abc123"}',
duration_ms = 1234,
cost_tokens = 0.05,
lease_until = NULL,
leased_by = NULL
WHERE id = run.id

Step 6: Future Request (Same Inputs)
─────────────────────────────────
Same idempotency key: 'a3f5c9d8e2b1...'

Query: SELECT * FROM run WHERE idempotency_key = 'a3f5c9d8e2b1...'
Result: state = 'SUCCESS', outputs = '{"orderId":"abc123"}'

βœ… Return cached outputs immediately (no re-execution)

πŸš€ Scalability Architecture​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Load Balancer (ALB/nginx) β”‚
β”‚ β”œβ”€ Route to available API pods β”‚
β”‚ └─ Health checks: /actuator/health β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
↓ ↓ ↓ ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ API Pod 1β”‚ β”‚ API Pod 2β”‚ β”‚ API Pod 3β”‚ β”‚ API Pod Nβ”‚
β”‚ (Stateless) β”‚ (Stateless) β”‚ (Stateless) β”‚ (Stateless)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Shared Database (PostgreSQL) β”‚
β”‚ β”œβ”€ Connection pooling (HikariCP) β”‚
β”‚ β”œβ”€ Read replicas for queries β”‚
β”‚ └─ Write leader for mutations β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Runner Pods (Scheduled Workers) β”‚
β”‚ β”œβ”€ Auto-scaling: scale by queue depth β”‚
β”‚ β”œβ”€ Each pod polls queue independently β”‚
β”‚ β”œβ”€ Lease-based coordination (no leader election) β”‚
β”‚ └─ Horizontal: 1 β†’ N pods (linear scale) β”‚
β”‚ β”‚
β”‚ Runner Pod 1 (runnerId: runner-abc-123) β”‚
β”‚ └─ @Scheduled pollAndExecute() every 100ms β”‚
β”‚ β”œβ”€ Fetch 10 pending runs β”‚
β”‚ β”œβ”€ Acquire leases (DB row lock) β”‚
β”‚ β”œβ”€ Execute with heartbeat β”‚
β”‚ └─ Release on completion β”‚
β”‚ β”‚
β”‚ Runner Pod 2 (runnerId: runner-def-456) β”‚
β”‚ └─ (same) β”‚
β”‚ β”‚
β”‚ Runner Pod N (runnerId: runner-xyz-789) β”‚
β”‚ └─ (same) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Performance Targets:
- 10k concurrent executions
- 250k active tasks
- P95 dispatch &lt; 75ms
- Runner CPU &lt; 70% at scale

🎯 Summary​

ValkyrAI Workflow Engine v2.0 is architected for production resilience with:

  1. Exactly-once execution via idempotency keys + lease management
  2. Automatic crash recovery via zombie reaper (<30s)
  3. Operator-friendly DLQ with replay + input overrides
  4. Cost control via budgets + quotas + kill switches
  5. Horizontal scaling via stateless design + DB coordination
  6. Security by default via RBAC + ACL + field encryption

This is the n8n killer. Built on ThorAPI. Shipping Q4 2025. πŸš€