Ver Fonte

Initial commit: SmartBotic workflow automation platform

Features:
- C++20 microservices architecture (Database, WebServer, Runner)
- gRPC communication between services
- React/TypeScript frontend with workflow editor
- QuickJS-based JavaScript node execution engine
- Credential management with encrypted storage
- OCR workflow nodes for pdftoimageapi integration
- HTTP request node with binary download support

Key components:
- lib/: Shared libraries (common utils, config, logging, storage, credentials)
- src/database/: In-memory database service with WAL persistence
- src/webserver/: HTTP REST API, WebSocket, JWT auth
- src/runner/: Workflow execution engine with hot-reload
- nodes/: Built-in workflow node definitions
- webui/: React frontend with ReactFlow workflow editor
- proto/: Protocol Buffer definitions for gRPC
fszontagh há 6 meses atrás
commit
bc77b6424c
100 ficheiros alterados com 19354 adições e 0 exclusões
  1. 55 0
      .gitignore
  2. 137 0
      CLAUDE.md
  3. 263 0
      CMakeLists.txt
  4. 54 0
      cmake/CompilerFlags.cmake
  5. 66 0
      cmake/Dependencies.cmake
  6. 34 0
      cmake/FindPackages.cmake
  7. 31 0
      config/database.json
  8. 24 0
      config/runner.json
  9. 29 0
      config/webserver.json
  10. 452 0
      docs/nodes.md
  11. 94 0
      lib/common/error.cpp
  12. 153 0
      lib/common/error.hpp
  13. 266 0
      lib/common/string_utils.cpp
  14. 60 0
      lib/common/string_utils.hpp
  15. 104 0
      lib/common/time_utils.cpp
  16. 60 0
      lib/common/time_utils.hpp
  17. 52 0
      lib/common/uuid.cpp
  18. 20 0
      lib/common/uuid.hpp
  19. 166 0
      lib/config/config_loader.cpp
  20. 103 0
      lib/config/config_loader.hpp
  21. 121 0
      lib/credentials/credential_client.cpp
  22. 45 0
      lib/credentials/credential_client.hpp
  23. 531 0
      lib/credentials/credential_store.cpp
  24. 63 0
      lib/credentials/credential_store.hpp
  25. 327 0
      lib/credentials/credential_types.cpp
  26. 156 0
      lib/credentials/credential_types.hpp
  27. 379 0
      lib/crypto/aes_gcm.cpp
  28. 70 0
      lib/crypto/aes_gcm.hpp
  29. 85 0
      lib/logging/logger.cpp
  30. 124 0
      lib/logging/logger.hpp
  31. 343 0
      lib/storage/storage_client.cpp
  32. 114 0
      lib/storage/storage_client.hpp
  33. 373 0
      nodes/core/http-request.js
  34. 268 0
      nodes/core/if-condition.js
  35. 182 0
      nodes/core/loop.js
  36. 90 0
      nodes/core/wait.js
  37. 121 0
      nodes/imap/imap-fetch.js
  38. 164 0
      nodes/imap/imap-modify.js
  39. 190 0
      nodes/imap/imap-search.js
  40. 148 0
      nodes/imap/imap-trigger.js
  41. 106 0
      nodes/ocr/ocr-delete-job.js
  42. 102 0
      nodes/ocr/ocr-job-stats.js
  43. 126 0
      nodes/ocr/ocr-job-status.js
  44. 100 0
      nodes/ocr/ocr-limitations.js
  45. 140 0
      nodes/ocr/ocr-list-jobs.js
  46. 114 0
      nodes/ocr/ocr-retry-job.js
  47. 223 0
      nodes/ocr/ocr-upload.js
  48. 69 0
      nodes/test/utils-test.js
  49. 45 0
      nodes/triggers/click-trigger.js
  50. 49 0
      proto/common.proto
  51. 66 0
      proto/credentials.proto
  52. 298 0
      proto/runner.proto
  53. 204 0
      proto/storage.proto
  54. 156 0
      proto/workflow.proto
  55. 602 0
      src/database/database_service.cpp
  56. 131 0
      src/database/database_service.hpp
  57. 365 0
      src/database/document.hpp
  58. 64 0
      src/database/main.cpp
  59. 830 0
      src/database/memory_store.cpp
  60. 136 0
      src/database/memory_store.hpp
  61. 159 0
      src/database/persistence/snapshot.cpp
  62. 53 0
      src/database/persistence/snapshot.hpp
  63. 243 0
      src/database/persistence/wal.cpp
  64. 71 0
      src/database/persistence/wal.hpp
  65. 142 0
      src/runner/collection_permissions.cpp
  66. 66 0
      src/runner/collection_permissions.hpp
  67. 2048 0
      src/runner/engine/script_engine.cpp
  68. 153 0
      src/runner/engine/script_engine.hpp
  69. 121 0
      src/runner/error_handler.cpp
  70. 71 0
      src/runner/error_handler.hpp
  71. 313 0
      src/runner/imap/imap_client.cpp
  72. 134 0
      src/runner/imap/imap_client.hpp
  73. 64 0
      src/runner/main.cpp
  74. 336 0
      src/runner/node_registry.cpp
  75. 115 0
      src/runner/node_registry.hpp
  76. 679 0
      src/runner/runner_service.cpp
  77. 139 0
      src/runner/runner_service.hpp
  78. 1445 0
      src/runner/workflow_engine.cpp
  79. 229 0
      src/runner/workflow_engine.hpp
  80. 237 0
      src/tools/migrate_nodes.cpp
  81. 143 0
      src/webserver/api/auth_controller.cpp
  82. 42 0
      src/webserver/api/auth_controller.hpp
  83. 197 0
      src/webserver/api/credential_controller.cpp
  84. 51 0
      src/webserver/api/credential_controller.hpp
  85. 246 0
      src/webserver/api/database_controller.cpp
  86. 39 0
      src/webserver/api/database_controller.hpp
  87. 191 0
      src/webserver/api/execution_controller.cpp
  88. 37 0
      src/webserver/api/execution_controller.hpp
  89. 333 0
      src/webserver/api/node_controller.cpp
  90. 46 0
      src/webserver/api/node_controller.hpp
  91. 173 0
      src/webserver/api/runner_controller.cpp
  92. 34 0
      src/webserver/api/runner_controller.hpp
  93. 226 0
      src/webserver/api/user_controller.cpp
  94. 37 0
      src/webserver/api/user_controller.hpp
  95. 134 0
      src/webserver/api/webhook_controller.cpp
  96. 32 0
      src/webserver/api/webhook_controller.hpp
  97. 322 0
      src/webserver/api/workflow_controller.cpp
  98. 55 0
      src/webserver/api/workflow_controller.hpp
  99. 101 0
      src/webserver/auth/auth_middleware.cpp
  100. 54 0
      src/webserver/auth/auth_middleware.hpp

+ 55 - 0
.gitignore

@@ -0,0 +1,55 @@
+# Build artifacts
+build/
+build-*/
+cmake-build-*/
+
+# Cache
+.cache/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Dependencies
+webui/node_modules/
+
+# Compiled/generated
+*.o
+*.a
+*.so
+*.dylib
+
+# Logs
+*.log
+logs/
+
+# Runtime data
+data/
+*.db
+*.wal
+snapshots/
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Config overrides (keep templates)
+config/*.local.json
+
+# Package lock (optional - uncomment if you want to ignore)
+# webui/package-lock.json
+
+# Vite cache
+webui/.vite/
+webui/dist/
+
+# Test coverage
+coverage/

+ 137 - 0
CLAUDE.md

@@ -0,0 +1,137 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+SmartBotic is a workflow automation and execution platform with a microservices architecture. The backend is C++20 with gRPC communication, and the frontend is React/TypeScript.
+
+## Build Commands
+
+### C++ Backend
+```bash
+# Debug build
+mkdir build && cd build
+cmake -DCMAKE_BUILD_TYPE=Debug ..
+make
+
+# Release build
+cmake -DCMAKE_BUILD_TYPE=Release ..
+make
+
+# With Address Sanitizer
+cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON ..
+make
+```
+
+### Frontend (webui/)
+```bash
+cd webui
+npm install
+npm run dev      # Dev server (port 3000, proxies to localhost:8090)
+npm run build    # Production build
+npm run lint     # ESLint
+```
+
+## Running Services
+
+Services must start in order due to dependencies:
+
+```bash
+# 1. Database service first (gRPC port 9010)
+./build/smartbotic-database
+
+# 2. WebServer (HTTP port 8090, gRPC port 9002)
+./build/smartbotic-webserver
+
+# 3. Runner(s) (gRPC port 9011)
+./build/smartbotic-runner
+
+# Or use systemd
+systemctl --user start smartbotic.target
+journalctl --user -u smartbotic-* -f
+```
+
+## Architecture
+
+### Three Microservices
+
+1. **Database Service** (src/database/) - gRPC storage server with in-memory store, WAL persistence, and snapshots
+2. **WebServer Service** (src/webserver/) - HTTP REST API, WebSocket for real-time updates, JWT auth, runner registry
+3. **Runner Service** (src/runner/) - Workflow execution engine with QuickJS for JavaScript node evaluation
+
+### Data Flow
+```
+WebUI (React) → HTTP/WebSocket → WebServer → gRPC → Database
+                                     ↓
+                              gRPC → Runner(s) → executes workflows
+```
+
+### Key Directories
+- `lib/` - Shared libraries (common utilities, config loader, logging, storage client)
+- `proto/` - Protocol Buffer definitions for gRPC services
+- `src/` - Microservice implementations
+- `nodes/` - Built-in workflow node definitions (JavaScript modules)
+- `webui/` - React frontend
+- `config/` - Runtime JSON configuration files
+
+### Node System
+
+Workflow nodes are JavaScript modules in `nodes/` with this interface:
+```javascript
+module.exports = {
+  configSchema: { /* JSON Schema */ },
+  inputSchema: { /* JSON Schema */ },
+  outputSchema: { /* JSON Schema */ },
+  execute: async (config, input, context) => { /* returns output */ }
+}
+```
+
+Nodes are loaded by the Runner service and executed in QuickJS. Hot-reload is supported.
+
+**See [docs/nodes.md](docs/nodes.md) for complete node creation guide.**
+
+### Migrating Nodes to Database
+
+After creating or modifying nodes in `nodes/`, migrate them to the running database:
+
+```bash
+# 1. Login to get JWT token
+TOKEN=$(curl -s http://localhost:8090/api/v1/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{"username": "admin", "password": "admin"}' | jq -r '.accessToken')
+
+# 2. Migrate nodes from filesystem to database
+curl -X POST http://localhost:8090/api/v1/nodes/migrate \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{"nodesPath": "./nodes"}'
+
+# 3. Verify nodes are loaded
+curl -s http://localhost:8090/api/v1/nodes \
+  -H "Authorization: Bearer $TOKEN" | jq '.nodes[] | "\(.id) - \(.name)"'
+```
+
+Runners automatically receive node updates via gRPC streaming - no restart required.
+
+### Configuration
+
+Services load JSON configs from `config/` directory:
+- `database.json` - Storage settings, WAL/snapshot intervals
+- `webserver.json` - HTTP port, JWT settings, runner load balancing
+- `runner.json` - Runner ID, max concurrent executions, hot reload settings
+
+Environment variables can override config values using `${VAR_NAME:default}` syntax.
+
+## C++ Standards
+
+- C++20 required (strict compliance, no extensions)
+- Compiler warnings: Wall, Wextra, Wpedantic
+- Key libraries: gRPC, Protobuf, cpp-httplib, QuickJS, spdlog, nlohmann/json, bcrypt
+
+## Frontend Stack
+
+- React 18.2 + TypeScript + Vite
+- State: Zustand + React Query
+- UI: TailwindCSS + Lucide icons
+- Workflow editor: ReactFlow

+ 263 - 0
CMakeLists.txt

@@ -0,0 +1,263 @@
+cmake_minimum_required(VERSION 3.20)
+project(SmartBotic VERSION 1.0.0 LANGUAGES CXX C)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# Include CMake modules
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+
+include(CompilerFlags)
+include(Dependencies)
+include(FindPackages)
+
+# Common library
+add_library(smartbotic_common STATIC
+    lib/common/uuid.cpp
+    lib/common/time_utils.cpp
+    lib/common/error.cpp
+    lib/common/string_utils.cpp
+)
+target_include_directories(smartbotic_common PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+)
+target_link_libraries(smartbotic_common PUBLIC
+    nlohmann_json::nlohmann_json
+    OpenSSL::Crypto
+)
+
+# Logging library
+add_library(smartbotic_logging STATIC
+    lib/logging/logger.cpp
+)
+target_include_directories(smartbotic_logging PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+)
+target_link_libraries(smartbotic_logging PUBLIC
+    spdlog::spdlog
+    smartbotic_common
+)
+
+# Config library
+add_library(smartbotic_config STATIC
+    lib/config/config_loader.cpp
+)
+target_include_directories(smartbotic_config PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+)
+target_link_libraries(smartbotic_config PUBLIC
+    smartbotic_common
+    smartbotic_logging
+    nlohmann_json::nlohmann_json
+)
+
+# Storage client library
+add_library(smartbotic_storage STATIC
+    lib/storage/storage_client.cpp
+)
+target_include_directories(smartbotic_storage PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(smartbotic_storage PUBLIC
+    smartbotic_common
+    smartbotic_logging
+    smartbotic_proto
+)
+
+# Crypto library
+add_library(smartbotic_crypto STATIC
+    lib/crypto/aes_gcm.cpp
+)
+target_include_directories(smartbotic_crypto PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+)
+target_link_libraries(smartbotic_crypto PUBLIC
+    smartbotic_common
+    OpenSSL::Crypto
+    nlohmann_json::nlohmann_json
+)
+
+# Credentials library
+add_library(smartbotic_credentials STATIC
+    lib/credentials/credential_types.cpp
+    lib/credentials/credential_store.cpp
+    lib/credentials/credential_client.cpp
+)
+target_include_directories(smartbotic_credentials PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(smartbotic_credentials PUBLIC
+    smartbotic_common
+    smartbotic_logging
+    smartbotic_crypto
+    smartbotic_storage
+    smartbotic_proto
+    CURL::libcurl
+)
+
+# Proto library
+add_library(smartbotic_proto STATIC
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/common.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/common.grpc.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/storage.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/storage.grpc.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/workflow.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/workflow.grpc.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/runner.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/runner.grpc.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/credentials.pb.cc
+    ${CMAKE_CURRENT_BINARY_DIR}/proto/credentials.grpc.pb.cc
+)
+target_include_directories(smartbotic_proto PUBLIC
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(smartbotic_proto PUBLIC
+    gRPC::grpc++
+    protobuf::libprotobuf
+)
+
+# Generate proto files
+find_program(PROTOC protoc REQUIRED)
+find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED)
+
+set(PROTO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto)
+set(PROTO_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/proto)
+file(MAKE_DIRECTORY ${PROTO_OUT_DIR})
+
+set(PROTO_FILES common storage workflow runner credentials)
+foreach(PROTO ${PROTO_FILES})
+    add_custom_command(
+        OUTPUT
+            ${PROTO_OUT_DIR}/${PROTO}.pb.cc
+            ${PROTO_OUT_DIR}/${PROTO}.pb.h
+            ${PROTO_OUT_DIR}/${PROTO}.grpc.pb.cc
+            ${PROTO_OUT_DIR}/${PROTO}.grpc.pb.h
+        COMMAND ${PROTOC}
+            --proto_path=${PROTO_DIR}
+            --cpp_out=${PROTO_OUT_DIR}
+            --grpc_out=${PROTO_OUT_DIR}
+            --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN}
+            ${PROTO_DIR}/${PROTO}.proto
+        DEPENDS ${PROTO_DIR}/${PROTO}.proto
+        COMMENT "Generating protobuf files for ${PROTO}.proto"
+    )
+endforeach()
+
+# Database service
+add_executable(smartbotic-database
+    src/database/main.cpp
+    src/database/memory_store.cpp
+    src/database/database_service.cpp
+    src/database/persistence/wal.cpp
+    src/database/persistence/snapshot.cpp
+)
+target_include_directories(smartbotic-database PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/src
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(smartbotic-database PRIVATE
+    smartbotic_common
+    smartbotic_logging
+    smartbotic_config
+    smartbotic_proto
+    gRPC::grpc++
+)
+
+# WebServer service
+add_executable(smartbotic-webserver
+    src/webserver/main.cpp
+    src/webserver/http_server.cpp
+    src/webserver/websocket_server.cpp
+    src/webserver/webserver_service.cpp
+    src/webserver/auth/bcrypt_utils.cpp
+    src/webserver/auth/jwt_utils.cpp
+    src/webserver/auth/auth_store.cpp
+    src/webserver/auth/auth_middleware.cpp
+    src/webserver/runners/runner_registry.cpp
+    src/webserver/runners/load_balancer.cpp
+    src/webserver/nodes/node_store.cpp
+    src/webserver/grpc/node_sync_service.cpp
+    src/webserver/grpc/credential_service.cpp
+    src/webserver/api/auth_controller.cpp
+    src/webserver/api/credential_controller.cpp
+    src/webserver/api/user_controller.cpp
+    src/webserver/api/workflow_controller.cpp
+    src/webserver/api/execution_controller.cpp
+    src/webserver/api/node_controller.cpp
+    src/webserver/api/runner_controller.cpp
+    src/webserver/api/webhook_controller.cpp
+    src/webserver/api/database_controller.cpp
+)
+target_include_directories(smartbotic-webserver PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/src
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(smartbotic-webserver PRIVATE
+    smartbotic_common
+    smartbotic_logging
+    smartbotic_config
+    smartbotic_storage
+    smartbotic_credentials
+    smartbotic_proto
+    httplib::httplib
+    websockets
+    bcrypt_lib
+    gRPC::grpc++
+    OpenSSL::SSL
+    OpenSSL::Crypto
+)
+
+# Runner service
+add_executable(smartbotic-runner
+    src/runner/main.cpp
+    src/runner/workflow_engine.cpp
+    src/runner/node_registry.cpp
+    src/runner/error_handler.cpp
+    src/runner/runner_service.cpp
+    src/runner/collection_permissions.cpp
+    src/runner/engine/script_engine.cpp
+    src/runner/imap/imap_client.cpp
+)
+target_include_directories(smartbotic-runner PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/src
+    ${CMAKE_CURRENT_BINARY_DIR}
+)
+target_link_libraries(smartbotic-runner PRIVATE
+    smartbotic_common
+    smartbotic_logging
+    smartbotic_config
+    smartbotic_storage
+    smartbotic_credentials
+    smartbotic_proto
+    quickjs_lib
+    CURL::libcurl
+    gRPC::grpc++
+)
+
+# Migration tool
+add_executable(smartbotic-migrate-nodes
+    src/tools/migrate_nodes.cpp
+)
+target_include_directories(smartbotic-migrate-nodes PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/lib
+)
+target_link_libraries(smartbotic-migrate-nodes PRIVATE
+    nlohmann_json::nlohmann_json
+    CURL::libcurl
+)
+
+# Install targets
+install(TARGETS
+    smartbotic-database
+    smartbotic-webserver
+    smartbotic-runner
+    smartbotic-migrate-nodes
+    RUNTIME DESTINATION bin
+)
+
+install(DIRECTORY nodes/ DESTINATION share/smartbotic/nodes)
+install(DIRECTORY config/ DESTINATION etc/smartbotic)

+ 54 - 0
cmake/CompilerFlags.cmake

@@ -0,0 +1,54 @@
+# Compiler flags for SmartBotic
+
+# Warning flags
+if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
+    add_compile_options(
+        -Wall
+        -Wextra
+        -Wpedantic
+        -Wno-unused-parameter
+        -Wno-missing-field-initializers
+    )
+
+    # Debug flags
+    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+        add_compile_options(-g -O0 -DDEBUG)
+        # Note: _GLIBCXX_DEBUG is disabled because it causes ABI incompatibility
+        # with pre-compiled system libraries (gRPC, etc.)
+        # add_compile_definitions(_GLIBCXX_DEBUG)
+    endif()
+
+    # Release flags
+    if(CMAKE_BUILD_TYPE STREQUAL "Release")
+        add_compile_options(-O3 -DNDEBUG)
+        add_compile_options(-flto)
+        add_link_options(-flto)
+    endif()
+
+    # RelWithDebInfo flags
+    if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
+        add_compile_options(-O2 -g -DNDEBUG)
+    endif()
+endif()
+
+# MSVC flags (for potential Windows support)
+if(MSVC)
+    add_compile_options(/W4 /permissive-)
+    add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
+
+    if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+        add_compile_options(/Od /Zi)
+    else()
+        add_compile_options(/O2)
+    endif()
+endif()
+
+# Enable colored output
+if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
+    add_compile_options(-fdiagnostics-color=always)
+elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+    add_compile_options(-fcolor-diagnostics)
+endif()
+
+# Position independent code
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)

+ 66 - 0
cmake/Dependencies.cmake

@@ -0,0 +1,66 @@
+# External dependencies via FetchContent
+
+include(FetchContent)
+
+cmake_policy(SET CMP0135 NEW)
+
+# cpp-httplib v0.18.3 (header-only)
+FetchContent_Declare(
+    httplib
+    URL https://github.com/yhirose/cpp-httplib/archive/refs/tags/v0.18.3.tar.gz
+    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
+)
+FetchContent_MakeAvailable(httplib)
+
+# QuickJS - using bellard's official release
+FetchContent_Declare(
+    quickjs
+    URL https://bellard.org/quickjs/quickjs-2024-01-13.tar.xz
+    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
+)
+FetchContent_MakeAvailable(quickjs)
+
+# Build QuickJS as a static library
+add_library(quickjs_lib STATIC
+    ${quickjs_SOURCE_DIR}/quickjs.c
+    ${quickjs_SOURCE_DIR}/libbf.c
+    ${quickjs_SOURCE_DIR}/libregexp.c
+    ${quickjs_SOURCE_DIR}/libunicode.c
+    ${quickjs_SOURCE_DIR}/cutils.c
+)
+target_include_directories(quickjs_lib PUBLIC ${quickjs_SOURCE_DIR})
+target_compile_definitions(quickjs_lib PRIVATE
+    CONFIG_VERSION="2024-01-13"
+    CONFIG_BIGNUM
+)
+# Disable some warnings for QuickJS C code
+if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
+    target_compile_options(quickjs_lib PRIVATE
+        -Wno-sign-compare
+        -Wno-unused-variable
+        -Wno-implicit-fallthrough
+        -Wno-missing-field-initializers
+    )
+endif()
+
+# libbcrypt for password hashing - download only, don't use their CMakeLists
+FetchContent_Declare(
+    bcrypt
+    URL https://github.com/trusch/libbcrypt/archive/refs/heads/master.tar.gz
+    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
+)
+FetchContent_GetProperties(bcrypt)
+if(NOT bcrypt_POPULATED)
+    FetchContent_Populate(bcrypt)
+endif()
+
+add_library(bcrypt_lib STATIC
+    ${bcrypt_SOURCE_DIR}/src/bcrypt.c
+    ${bcrypt_SOURCE_DIR}/src/crypt_blowfish.c
+    ${bcrypt_SOURCE_DIR}/src/crypt_gensalt.c
+    ${bcrypt_SOURCE_DIR}/src/wrapper.c
+)
+target_include_directories(bcrypt_lib PUBLIC
+    ${bcrypt_SOURCE_DIR}/include
+    ${bcrypt_SOURCE_DIR}/include/bcrypt
+)

+ 34 - 0
cmake/FindPackages.cmake

@@ -0,0 +1,34 @@
+# Find system packages
+
+# Required packages
+find_package(PkgConfig REQUIRED)
+find_package(Threads REQUIRED)
+find_package(OpenSSL REQUIRED)
+find_package(CURL REQUIRED)
+
+# nlohmann-json
+find_package(nlohmann_json 3.11 QUIET)
+if(NOT nlohmann_json_FOUND)
+    pkg_check_modules(nlohmann_json REQUIRED IMPORTED_TARGET nlohmann_json)
+    add_library(nlohmann_json::nlohmann_json ALIAS PkgConfig::nlohmann_json)
+endif()
+
+# spdlog
+find_package(spdlog QUIET)
+if(NOT spdlog_FOUND)
+    pkg_check_modules(spdlog REQUIRED IMPORTED_TARGET spdlog)
+    add_library(spdlog::spdlog ALIAS PkgConfig::spdlog)
+endif()
+
+# gRPC and Protobuf
+find_package(gRPC CONFIG QUIET)
+if(NOT gRPC_FOUND)
+    pkg_check_modules(grpc++ REQUIRED IMPORTED_TARGET grpc++)
+    add_library(gRPC::grpc++ ALIAS PkgConfig::grpc++)
+endif()
+
+find_package(Protobuf REQUIRED)
+
+# libwebsockets
+pkg_check_modules(websockets REQUIRED IMPORTED_TARGET libwebsockets)
+add_library(websockets ALIAS PkgConfig::websockets)

+ 31 - 0
config/database.json

@@ -0,0 +1,31 @@
+{
+  "grpc_port": 9010,
+  "data_directory": "${DATA_DIR:./data/database}",
+  "persistence": {
+    "wal_sync_interval_ms": 100,
+    "snapshot_interval_sec": 300,
+    "wal_size_trigger_mb": 10
+  },
+  "versioning": {
+    "enabled": false,
+    "default_config": {
+      "max_versions": 50,
+      "version_ttl_ms": 2592000000
+    },
+    "collection_overrides": {
+      "workflows": {
+        "enabled": true,
+        "max_versions": 100,
+        "version_ttl_ms": 7776000000,
+        "keep_on_delete": true
+      },
+      "executions": {
+        "enabled": false
+      }
+    }
+  },
+  "logging": {
+    "level": "${LOG_LEVEL:info}",
+    "file": "${LOG_FILE:}"
+  }
+}

+ 24 - 0
config/runner.json

@@ -0,0 +1,24 @@
+{
+  "grpc_port": 9011,
+  "runner_id": "${RUNNER_ID:runner-1}",
+  "webserver_address": "${WEBSERVER_ADDRESS:localhost:8090}",
+  "node_sync_address": "${NODE_SYNC_ADDRESS:localhost:9012}",
+  "credential_service_address": "${CREDENTIAL_SERVICE_ADDRESS:localhost:9013}",
+  "database_address": "${DATABASE_ADDRESS:localhost:9010}",
+  "node_sync": {
+    "enabled": true,
+    "reconnect_interval_ms": 5000
+  },
+  "registration": {
+    "heartbeat_interval_sec": 10,
+    "max_concurrent_executions": 10
+  },
+  "execution": {
+    "default_timeout_ms": 60000,
+    "max_memory_per_script_mb": 64
+  },
+  "logging": {
+    "level": "${LOG_LEVEL:info}",
+    "file": "${LOG_FILE:}"
+  }
+}

+ 29 - 0
config/webserver.json

@@ -0,0 +1,29 @@
+{
+  "http_port": 8090,
+  "node_sync_port": 9012,
+  "credential_service_port": 9013,
+  "static_files_path": "${WEBUI_PATH:./webui/dist}",
+  "database_address": "${DATABASE_ADDRESS:localhost:9010}",
+  "runners": {
+    "load_balancing": "least-connections",
+    "heartbeat_timeout_sec": 30,
+    "offline_removal_sec": 60
+  },
+  "auth": {
+    "jwt_secret": "${JWT_SECRET:dev-secret-change-in-production}",
+    "access_token_lifetime_sec": 900,
+    "refresh_token_lifetime_sec": 604800
+  },
+  "credentials": {
+    "master_key": "${CREDENTIALS_MASTER_KEY:dev-key-change-in-production}",
+    "pbkdf2_iterations": 100000
+  },
+  "cors": {
+    "enabled": true,
+    "origins": ["*"]
+  },
+  "logging": {
+    "level": "${LOG_LEVEL:info}",
+    "file": "${LOG_FILE:}"
+  }
+}

+ 452 - 0
docs/nodes.md

@@ -0,0 +1,452 @@
+# Creating Workflow Nodes
+
+This guide explains how to create custom workflow nodes for SmartBotic.
+
+## Node File Structure
+
+Nodes are JavaScript modules located in `nodes/<category>/` directories. Each node file must export:
+
+- `configSchema` - JSON Schema for node configuration
+- `inputSchema` - JSON Schema for input data
+- `outputSchema` - JSON Schema for output data
+- `execute` - Async function that performs the node's action
+
+## Basic Template
+
+```javascript
+/**
+ * @node my-node-id
+ * @name My Node Name
+ * @category my-category
+ * @version 1.0.0
+ * @description What this node does
+ * @icon icon-name
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        myOption: {
+            type: 'string',
+            title: 'My Option',
+            description: 'Description of this option',
+            default: 'default-value'
+        }
+    },
+    required: ['myOption']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        data: {
+            type: 'any',
+            description: 'Input data'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        result: {
+            type: 'string',
+            description: 'Output result'
+        }
+    }
+};
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+
+    async execute(config, input, context) {
+        // Your node logic here
+        return {
+            result: 'success'
+        };
+    }
+};
+```
+
+## JSDoc Metadata Tags
+
+The comment block at the top of the file defines node metadata:
+
+| Tag | Required | Description |
+|-----|----------|-------------|
+| `@node` | Yes | Unique node identifier (kebab-case) |
+| `@name` | Yes | Display name shown in UI |
+| `@category` | Yes | Category for grouping (e.g., `http`, `data`, `email`) |
+| `@version` | Yes | Semantic version (e.g., `1.0.0`) |
+| `@description` | Yes | Brief description of node functionality |
+| `@icon` | No | Lucide icon name (e.g., `globe`, `mail`, `database`) |
+| `@trigger` | No | Add this tag if the node is a trigger (starts workflows) |
+
+## Configuration Schema
+
+The `configSchema` defines what options users can configure in the node editor.
+
+### Supported Field Types
+
+```javascript
+// String field
+myString: {
+    type: 'string',
+    title: 'Label',
+    description: 'Help text',
+    default: 'default value'
+}
+
+// Number field
+myNumber: {
+    type: 'number',
+    title: 'Count',
+    default: 10
+}
+
+// Boolean field
+myBoolean: {
+    type: 'boolean',
+    title: 'Enable Feature',
+    default: false
+}
+
+// Dropdown (enum)
+myChoice: {
+    type: 'string',
+    title: 'Select Option',
+    enum: ['option1', 'option2', 'option3'],
+    default: 'option1'
+}
+
+// Object field
+myHeaders: {
+    type: 'object',
+    title: 'Headers',
+    additionalProperties: { type: 'string' }
+}
+```
+
+### Dynamic Options (Credentials)
+
+To create a credential selector:
+
+```javascript
+credentialId: {
+    type: 'string',
+    title: 'Credential',
+    description: 'Select authentication credential',
+    dynamicOptions: {
+        source: 'credentials',
+        filter: { type: 'bearer' }  // Filter by credential type
+    }
+}
+```
+
+Available credential types: `bearer`, `api_key`, `basic`, `imap`
+
+## Custom Outputs
+
+Define multiple output ports:
+
+```javascript
+const outputs = [
+    { name: 'success', displayName: 'Success', type: 'object', color: '#10b981' },
+    { name: 'error', displayName: 'Error', type: 'object', color: '#ef4444' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+    execute
+};
+```
+
+## Available APIs
+
+Inside `execute()`, you have access to the `smartbotic` global object:
+
+### Logging
+
+```javascript
+smartbotic.log.debug('Debug message');
+smartbotic.log.info('Info message');
+smartbotic.log.warn('Warning message');
+smartbotic.log.error('Error message');
+```
+
+### HTTP Requests
+
+```javascript
+const response = smartbotic.http.request({
+    method: 'GET',  // GET, POST, PUT, PATCH, DELETE
+    url: 'https://api.example.com/data',
+    headers: { 'Authorization': 'Bearer token' },
+    body: JSON.stringify({ key: 'value' }),
+    timeout: 30000,
+    followRedirects: true
+});
+
+// Response: { status, headers, data }
+```
+
+### Storage (Database)
+
+```javascript
+// Insert document
+const result = smartbotic.storage.insert('collection', { key: 'value' });
+// Returns: { success, id }
+
+// Get document
+const doc = smartbotic.storage.get('collection', 'document-id');
+// Returns: { found, document }
+
+// Query documents
+const results = smartbotic.storage.query('collection', { field: 'value' });
+// Returns: { success, documents }
+
+// Update document
+smartbotic.storage.update('collection', 'document-id', { key: 'new-value' });
+
+// Delete document
+smartbotic.storage.delete('collection', 'document-id');
+```
+
+### Credentials
+
+```javascript
+const auth = smartbotic.credentials.get(credentialId);
+if (auth.success) {
+    // auth.headerName = 'Authorization' or 'X-API-Key'
+    // auth.headerValue = 'Bearer <token>' or '<api-key>'
+    headers[auth.headerName] = auth.headerValue;
+}
+```
+
+### Utilities
+
+```javascript
+// Generate UUID
+const id = smartbotic.utils.uuid();
+
+// Sleep (pause execution)
+smartbotic.utils.sleep(1000);  // milliseconds, max 300000 (5 min)
+
+// Template interpolation
+const result = smartbotic.utils.interpolate('Hello {{name}}', { name: 'World' });
+
+// Base64 encoding/decoding
+const encoded = smartbotic.utils.base64Encode('data');
+const decoded = smartbotic.utils.base64Decode(encoded);
+
+// SHA256 hash
+const hash = smartbotic.utils.sha256('data');  // Returns hex string
+
+// Object utilities
+const picked = smartbotic.utils.pick(obj, ['key1', 'key2']);
+const omitted = smartbotic.utils.omit(obj, ['unwantedKey']);
+```
+
+## Important Guidelines
+
+### Indentation
+
+Use **4 spaces** for indentation (consistent with existing nodes).
+
+### Avoid Inline Comments
+
+Do NOT use `//` comments inside schema definitions. The parser may incorrectly interpret them:
+
+```javascript
+// BAD - comment may break parsing
+const configSchema = {
+    type: 'object',
+    properties: {
+        url: {
+            type: 'string',
+            default: 'http://localhost:3000'  // This is the default  <- AVOID
+        }
+    }
+};
+
+// GOOD - no inline comments in schema
+const configSchema = {
+    type: 'object',
+    properties: {
+        url: {
+            type: 'string',
+            default: 'http://localhost:3000'
+        }
+    }
+};
+```
+
+### String Quotes
+
+Use **single quotes** for string values in schemas. The parser converts them to JSON:
+
+```javascript
+// GOOD
+title: 'My Title'
+
+// AVOID - embedded quotes can cause issues
+description: 'Use "quotes" carefully'  // May break parsing
+```
+
+### Error Handling
+
+**IMPORTANT**: Throw errors instead of returning `success: false`. This ensures the workflow fails properly:
+
+```javascript
+async execute(config, input, context) {
+    // Validate required inputs
+    if (!config.requiredField) {
+        throw new Error('Required field is missing');
+    }
+
+    // Make API call
+    const response = smartbotic.http.request({
+        method: 'GET',
+        url: config.url,
+        headers: headers
+    });
+
+    // Check response status - throw on error
+    if (response.status < 200 || response.status >= 300) {
+        const errorMsg = typeof response.data === 'string'
+            ? response.data
+            : JSON.stringify(response.data);
+        throw new Error(`API error (${response.status}): ${errorMsg}`);
+    }
+
+    // Return data on success (no try/catch needed for most cases)
+    return {
+        data: response.data
+    };
+}
+```
+
+**Do NOT** catch errors just to return `{ success: false }` - let them propagate so the workflow fails.
+
+## Migrating Nodes to Database
+
+After creating or modifying node files, migrate them to the running database:
+
+```bash
+# Get authentication token
+TOKEN=$(curl -s http://localhost:8090/api/v1/auth/login \
+  -H "Content-Type: application/json" \
+  -d '{"username": "admin", "password": "admin"}' | jq -r '.accessToken')
+
+# Migrate nodes
+curl -X POST http://localhost:8090/api/v1/nodes/migrate \
+  -H "Authorization: Bearer $TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{"nodesPath": "./nodes"}'
+```
+
+Runners automatically receive node updates via gRPC streaming.
+
+## Example: HTTP Download Node
+
+```javascript
+/**
+ * @node download-file
+ * @name Download File
+ * @category http
+ * @version 1.0.0
+ * @description Download a file from URL
+ * @icon download
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        url: {
+            type: 'string',
+            title: 'URL',
+            description: 'URL to download from'
+        },
+        timeout: {
+            type: 'number',
+            title: 'Timeout (ms)',
+            default: 30000
+        }
+    },
+    required: ['url']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        url: { type: 'string', description: 'Override URL from input' }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        success: { type: 'boolean' },
+        data: { type: 'string' },
+        contentType: { type: 'string' }
+    }
+};
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+
+    async execute(config, input, context) {
+        const url = input.url || config.url;
+
+        smartbotic.log.info(`Downloading from ${url}`);
+
+        try {
+            const response = smartbotic.http.request({
+                method: 'GET',
+                url: url,
+                timeout: config.timeout
+            });
+
+            if (response.status < 200 || response.status >= 300) {
+                throw new Error(`HTTP ${response.status}`);
+            }
+
+            return {
+                success: true,
+                data: response.data,
+                contentType: response.headers['content-type'] || ''
+            };
+        } catch (error) {
+            smartbotic.log.error(`Download failed: ${error.message}`);
+            return {
+                success: false,
+                error: error.message
+            };
+        }
+    }
+};
+```
+
+## Trigger Nodes
+
+Trigger nodes start workflow executions. Add `@trigger` to the JSDoc:
+
+```javascript
+/**
+ * @node my-trigger
+ * @name My Trigger
+ * @category triggers
+ * @version 1.0.0
+ * @description Triggers workflow on event
+ * @icon zap
+ * @trigger
+ */
+```
+
+Triggers typically don't have input schemas (they generate initial data).

+ 94 - 0
lib/common/error.cpp

@@ -0,0 +1,94 @@
+#include "common/error.hpp"
+#include <sstream>
+
+namespace smartbotic::common {
+
+int errorCodeToHttpStatus(ErrorCode code) {
+    switch (code) {
+        case ErrorCode::InvalidArgument:
+        case ErrorCode::ValidationError:
+        case ErrorCode::QueryError:
+            return 400;
+
+        case ErrorCode::Unauthenticated:
+        case ErrorCode::InvalidCredentials:
+        case ErrorCode::TokenExpired:
+        case ErrorCode::TokenInvalid:
+        case ErrorCode::SessionExpired:
+            return 401;
+
+        case ErrorCode::PermissionDenied:
+        case ErrorCode::InsufficientPermissions:
+            return 403;
+
+        case ErrorCode::NotFound:
+        case ErrorCode::DocumentNotFound:
+        case ErrorCode::CollectionNotFound:
+        case ErrorCode::WorkflowNotFound:
+        case ErrorCode::NodeNotFound:
+            return 404;
+
+        case ErrorCode::AlreadyExists:
+        case ErrorCode::VersionConflict:
+            return 409;
+
+        case ErrorCode::FailedPrecondition:
+        case ErrorCode::WorkflowInactive:
+            return 412;
+
+        case ErrorCode::ResourceExhausted:
+            return 429;
+
+        case ErrorCode::Timeout:
+        case ErrorCode::ExecutionTimeout:
+        case ErrorCode::ScriptTimeout:
+            return 408;
+
+        case ErrorCode::Unavailable:
+        case ErrorCode::RunnerUnavailable:
+        case ErrorCode::NoRunnersAvailable:
+        case ErrorCode::CircuitBreakerOpen:
+            return 503;
+
+        case ErrorCode::Aborted:
+        case ErrorCode::ExecutionCancelled:
+            return 499;
+
+        default:
+            return 500;
+    }
+}
+
+Error::Error(ErrorCode code, std::string message)
+    : code_(code), message_(std::move(message)), details_(nlohmann::json::object()) {}
+
+Error::Error(ErrorCode code, std::string message, nlohmann::json details)
+    : code_(code), message_(std::move(message)), details_(std::move(details)) {}
+
+nlohmann::json Error::toJson() const {
+    nlohmann::json j;
+    j["code"] = static_cast<int>(code_);
+    j["message"] = message_;
+    if (!details_.empty()) {
+        j["details"] = details_;
+    }
+    return j;
+}
+
+std::string Error::toString() const {
+    std::ostringstream ss;
+    ss << "Error[" << static_cast<int>(code_) << "]: " << message_;
+    if (!details_.empty()) {
+        ss << " (" << details_.dump() << ")";
+    }
+    return ss.str();
+}
+
+Error Error::fromException(const std::exception& e) {
+    return Error(ErrorCode::Internal, e.what());
+}
+
+SmartBoticException::SmartBoticException(Error error)
+    : std::runtime_error(error.toString()), error_(std::move(error)) {}
+
+} // namespace smartbotic::common

+ 153 - 0
lib/common/error.hpp

@@ -0,0 +1,153 @@
+#pragma once
+
+#include <string>
+#include <stdexcept>
+#include <optional>
+#include <variant>
+#include <nlohmann/json.hpp>
+
+namespace smartbotic::common {
+
+// Error codes
+enum class ErrorCode {
+    // General errors (1xxx)
+    Unknown = 1000,
+    InvalidArgument = 1001,
+    NotFound = 1002,
+    AlreadyExists = 1003,
+    PermissionDenied = 1004,
+    Unauthenticated = 1005,
+    ResourceExhausted = 1006,
+    FailedPrecondition = 1007,
+    Aborted = 1008,
+    Internal = 1009,
+    Unavailable = 1010,
+    Timeout = 1011,
+
+    // Database errors (2xxx)
+    DatabaseError = 2000,
+    DocumentNotFound = 2001,
+    CollectionNotFound = 2002,
+    VersionConflict = 2003,
+    ValidationError = 2004,
+    QueryError = 2005,
+
+    // Auth errors (3xxx)
+    AuthError = 3000,
+    InvalidCredentials = 3001,
+    TokenExpired = 3002,
+    TokenInvalid = 3003,
+    SessionExpired = 3004,
+    InsufficientPermissions = 3005,
+
+    // Workflow errors (4xxx)
+    WorkflowError = 4000,
+    WorkflowNotFound = 4001,
+    WorkflowInactive = 4002,
+    NodeNotFound = 4003,
+    NodeExecutionFailed = 4004,
+    ExecutionTimeout = 4005,
+    ExecutionCancelled = 4006,
+    CircuitBreakerOpen = 4007,
+
+    // Runner errors (5xxx)
+    RunnerError = 5000,
+    RunnerUnavailable = 5001,
+    NoRunnersAvailable = 5002,
+    ScriptError = 5003,
+    ScriptTimeout = 5004,
+};
+
+// Convert error code to HTTP status
+int errorCodeToHttpStatus(ErrorCode code);
+
+// Error class with detailed information
+class Error {
+public:
+    Error(ErrorCode code, std::string message);
+    Error(ErrorCode code, std::string message, nlohmann::json details);
+
+    ErrorCode code() const { return code_; }
+    const std::string& message() const { return message_; }
+    const nlohmann::json& details() const { return details_; }
+
+    nlohmann::json toJson() const;
+    std::string toString() const;
+
+    static Error fromException(const std::exception& e);
+
+private:
+    ErrorCode code_;
+    std::string message_;
+    nlohmann::json details_;
+};
+
+// Exception class that wraps Error
+class SmartBoticException : public std::runtime_error {
+public:
+    explicit SmartBoticException(Error error);
+
+    const Error& error() const { return error_; }
+    ErrorCode code() const { return error_.code(); }
+
+private:
+    Error error_;
+};
+
+// Result type for operations that can fail
+template<typename T>
+class Result {
+public:
+    Result(T value) : data_(std::move(value)) {}
+    Result(Error error) : data_(std::move(error)) {}
+
+    bool ok() const { return std::holds_alternative<T>(data_); }
+    bool failed() const { return !ok(); }
+
+    const T& value() const { return std::get<T>(data_); }
+    T& value() { return std::get<T>(data_); }
+
+    const Error& error() const { return std::get<Error>(data_); }
+
+    // Throw if error
+    const T& valueOrThrow() const {
+        if (failed()) {
+            throw SmartBoticException(error());
+        }
+        return value();
+    }
+
+    T& valueOrThrow() {
+        if (failed()) {
+            throw SmartBoticException(error());
+        }
+        return value();
+    }
+
+private:
+    std::variant<T, Error> data_;
+};
+
+// Void result specialization
+template<>
+class Result<void> {
+public:
+    Result() : error_(std::nullopt) {}
+    Result(Error error) : error_(std::move(error)) {}
+
+    bool ok() const { return !error_.has_value(); }
+    bool failed() const { return error_.has_value(); }
+
+    const Error& error() const { return *error_; }
+
+    void throwIfFailed() const {
+        if (failed()) {
+            throw SmartBoticException(*error_);
+        }
+    }
+
+private:
+    std::optional<Error> error_;
+};
+
+} // namespace smartbotic::common

+ 266 - 0
lib/common/string_utils.cpp

@@ -0,0 +1,266 @@
+#include "common/string_utils.hpp"
+#include <cstdlib>
+#include <openssl/bio.h>
+#include <openssl/evp.h>
+#include <openssl/buffer.h>
+
+namespace smartbotic::common {
+
+std::string StringUtils::trim(std::string_view str) {
+    return trimRight(trimLeft(str));
+}
+
+std::string StringUtils::trimLeft(std::string_view str) {
+    auto it = std::find_if(str.begin(), str.end(),
+        [](unsigned char c) { return !std::isspace(c); });
+    return std::string(it, str.end());
+}
+
+std::string StringUtils::trimRight(std::string_view str) {
+    auto it = std::find_if(str.rbegin(), str.rend(),
+        [](unsigned char c) { return !std::isspace(c); });
+    return std::string(str.begin(), it.base());
+}
+
+std::string StringUtils::toLower(std::string_view str) {
+    std::string result(str);
+    std::transform(result.begin(), result.end(), result.begin(),
+        [](unsigned char c) { return std::tolower(c); });
+    return result;
+}
+
+std::string StringUtils::toUpper(std::string_view str) {
+    std::string result(str);
+    std::transform(result.begin(), result.end(), result.begin(),
+        [](unsigned char c) { return std::toupper(c); });
+    return result;
+}
+
+std::vector<std::string> StringUtils::split(std::string_view str, char delimiter) {
+    std::vector<std::string> result;
+    std::string current;
+    for (char c : str) {
+        if (c == delimiter) {
+            result.push_back(std::move(current));
+            current.clear();
+        } else {
+            current += c;
+        }
+    }
+    result.push_back(std::move(current));
+    return result;
+}
+
+std::vector<std::string> StringUtils::split(std::string_view str, std::string_view delimiter) {
+    std::vector<std::string> result;
+    size_t start = 0;
+    size_t end = str.find(delimiter);
+
+    while (end != std::string_view::npos) {
+        result.emplace_back(str.substr(start, end - start));
+        start = end + delimiter.length();
+        end = str.find(delimiter, start);
+    }
+    result.emplace_back(str.substr(start));
+    return result;
+}
+
+std::string StringUtils::join(const std::vector<std::string>& parts, std::string_view delimiter) {
+    if (parts.empty()) return "";
+
+    std::ostringstream ss;
+    ss << parts[0];
+    for (size_t i = 1; i < parts.size(); ++i) {
+        ss << delimiter << parts[i];
+    }
+    return ss.str();
+}
+
+bool StringUtils::startsWith(std::string_view str, std::string_view prefix) {
+    return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix;
+}
+
+bool StringUtils::endsWith(std::string_view str, std::string_view suffix) {
+    return str.size() >= suffix.size() &&
+           str.substr(str.size() - suffix.size()) == suffix;
+}
+
+bool StringUtils::contains(std::string_view str, std::string_view substr) {
+    return str.find(substr) != std::string_view::npos;
+}
+
+std::string StringUtils::replace(std::string_view str, std::string_view from, std::string_view to) {
+    std::string result(str);
+    size_t pos = result.find(from);
+    if (pos != std::string::npos) {
+        result.replace(pos, from.length(), to);
+    }
+    return result;
+}
+
+std::string StringUtils::replaceAll(std::string_view str, std::string_view from, std::string_view to) {
+    std::string result(str);
+    size_t pos = 0;
+    while ((pos = result.find(from, pos)) != std::string::npos) {
+        result.replace(pos, from.length(), to);
+        pos += to.length();
+    }
+    return result;
+}
+
+std::string StringUtils::interpolate(std::string_view tmpl,
+                                     const std::unordered_map<std::string, std::string>& vars) {
+    std::string result(tmpl);
+    std::regex pattern(R"(\$\{([^}]+)\})");
+
+    std::string output;
+    auto it = std::sregex_iterator(result.begin(), result.end(), pattern);
+    auto end = std::sregex_iterator();
+    size_t lastPos = 0;
+
+    for (; it != end; ++it) {
+        output += result.substr(lastPos, it->position() - lastPos);
+        std::string varName = (*it)[1].str();
+        auto found = vars.find(varName);
+        if (found != vars.end()) {
+            output += found->second;
+        } else {
+            output += it->str(); // Keep original if not found
+        }
+        lastPos = it->position() + it->length();
+    }
+    output += result.substr(lastPos);
+
+    return output;
+}
+
+std::string StringUtils::expandEnvVars(std::string_view str) {
+    std::string result(str);
+    std::regex pattern(R"(\$\{([^:}]+)(?::([^}]*))?\})");
+
+    std::string output;
+    auto it = std::sregex_iterator(result.begin(), result.end(), pattern);
+    auto end = std::sregex_iterator();
+    size_t lastPos = 0;
+
+    for (; it != end; ++it) {
+        output += result.substr(lastPos, it->position() - lastPos);
+        std::string varName = (*it)[1].str();
+        std::string defaultValue = (*it)[2].matched ? (*it)[2].str() : "";
+
+        const char* envValue = std::getenv(varName.c_str());
+        if (envValue) {
+            output += envValue;
+        } else {
+            output += defaultValue;
+        }
+        lastPos = it->position() + it->length();
+    }
+    output += result.substr(lastPos);
+
+    return output;
+}
+
+std::string StringUtils::urlEncode(std::string_view str) {
+    std::ostringstream escaped;
+    escaped.fill('0');
+    escaped << std::hex;
+
+    for (char c : str) {
+        if (std::isalnum(static_cast<unsigned char>(c)) ||
+            c == '-' || c == '_' || c == '.' || c == '~') {
+            escaped << c;
+        } else {
+            escaped << std::uppercase;
+            escaped << '%' << std::setw(2) << int(static_cast<unsigned char>(c));
+            escaped << std::nouppercase;
+        }
+    }
+
+    return escaped.str();
+}
+
+std::string StringUtils::urlDecode(std::string_view str) {
+    std::string result;
+    result.reserve(str.size());
+
+    for (size_t i = 0; i < str.size(); ++i) {
+        if (str[i] == '%' && i + 2 < str.size()) {
+            int hex = std::stoi(std::string(str.substr(i + 1, 2)), nullptr, 16);
+            result += static_cast<char>(hex);
+            i += 2;
+        } else if (str[i] == '+') {
+            result += ' ';
+        } else {
+            result += str[i];
+        }
+    }
+
+    return result;
+}
+
+std::string StringUtils::base64Encode(std::string_view data) {
+    BIO* b64 = BIO_new(BIO_f_base64());
+    BIO* bio = BIO_new(BIO_s_mem());
+    bio = BIO_push(b64, bio);
+
+    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
+    BIO_write(bio, data.data(), static_cast<int>(data.size()));
+    BIO_flush(bio);
+
+    BUF_MEM* bufferPtr;
+    BIO_get_mem_ptr(bio, &bufferPtr);
+
+    std::string result(bufferPtr->data, bufferPtr->length);
+    BIO_free_all(bio);
+
+    return result;
+}
+
+std::string StringUtils::base64Decode(std::string_view encoded) {
+    BIO* b64 = BIO_new(BIO_f_base64());
+    BIO* bio = BIO_new_mem_buf(encoded.data(), static_cast<int>(encoded.size()));
+    bio = BIO_push(b64, bio);
+
+    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
+
+    std::string result(encoded.size(), '\0');
+    int length = BIO_read(bio, result.data(), static_cast<int>(result.size()));
+
+    BIO_free_all(bio);
+
+    if (length > 0) {
+        result.resize(length);
+    } else {
+        result.clear();
+    }
+
+    return result;
+}
+
+std::string StringUtils::hexEncode(const std::vector<uint8_t>& data) {
+    static const char hex[] = "0123456789abcdef";
+    std::string result;
+    result.reserve(data.size() * 2);
+
+    for (uint8_t byte : data) {
+        result += hex[byte >> 4];
+        result += hex[byte & 0x0f];
+    }
+
+    return result;
+}
+
+std::vector<uint8_t> StringUtils::hexDecode(std::string_view hex) {
+    std::vector<uint8_t> result;
+    result.reserve(hex.size() / 2);
+
+    for (size_t i = 0; i + 1 < hex.size(); i += 2) {
+        uint8_t byte = static_cast<uint8_t>(std::stoi(std::string(hex.substr(i, 2)), nullptr, 16));
+        result.push_back(byte);
+    }
+
+    return result;
+}
+
+} // namespace smartbotic::common

+ 60 - 0
lib/common/string_utils.hpp

@@ -0,0 +1,60 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <string_view>
+#include <algorithm>
+#include <sstream>
+#include <regex>
+#include <unordered_map>
+#include <cstdint>
+#include <iomanip>
+
+namespace smartbotic::common {
+
+class StringUtils {
+public:
+    // Trim whitespace
+    static std::string trim(std::string_view str);
+    static std::string trimLeft(std::string_view str);
+    static std::string trimRight(std::string_view str);
+
+    // Case conversion
+    static std::string toLower(std::string_view str);
+    static std::string toUpper(std::string_view str);
+
+    // Split and join
+    static std::vector<std::string> split(std::string_view str, char delimiter);
+    static std::vector<std::string> split(std::string_view str, std::string_view delimiter);
+    static std::string join(const std::vector<std::string>& parts, std::string_view delimiter);
+
+    // String operations
+    static bool startsWith(std::string_view str, std::string_view prefix);
+    static bool endsWith(std::string_view str, std::string_view suffix);
+    static bool contains(std::string_view str, std::string_view substr);
+
+    // Replace
+    static std::string replace(std::string_view str, std::string_view from, std::string_view to);
+    static std::string replaceAll(std::string_view str, std::string_view from, std::string_view to);
+
+    // Template interpolation: replaces ${VAR} with values from map
+    static std::string interpolate(std::string_view tmpl,
+                                   const std::unordered_map<std::string, std::string>& vars);
+
+    // Environment variable expansion: replaces ${VAR:default} with env value or default
+    static std::string expandEnvVars(std::string_view str);
+
+    // URL encoding/decoding
+    static std::string urlEncode(std::string_view str);
+    static std::string urlDecode(std::string_view str);
+
+    // Base64 encoding/decoding
+    static std::string base64Encode(std::string_view data);
+    static std::string base64Decode(std::string_view encoded);
+
+    // Hex encoding/decoding
+    static std::string hexEncode(const std::vector<uint8_t>& data);
+    static std::vector<uint8_t> hexDecode(std::string_view hex);
+};
+
+} // namespace smartbotic::common

+ 104 - 0
lib/common/time_utils.cpp

@@ -0,0 +1,104 @@
+#include "common/time_utils.hpp"
+#include <iomanip>
+#include <sstream>
+#include <iostream>
+
+namespace smartbotic::common {
+
+int64_t TimeUtils::nowMs() {
+    return std::chrono::duration_cast<std::chrono::milliseconds>(
+        Clock::now().time_since_epoch()
+    ).count();
+}
+
+int64_t TimeUtils::nowSec() {
+    return std::chrono::duration_cast<std::chrono::seconds>(
+        Clock::now().time_since_epoch()
+    ).count();
+}
+
+TimeUtils::TimePoint TimeUtils::now() {
+    return Clock::now();
+}
+
+TimeUtils::TimePoint TimeUtils::fromMs(int64_t ms) {
+    return TimePoint(std::chrono::milliseconds(ms));
+}
+
+int64_t TimeUtils::toMs(TimePoint tp) {
+    return std::chrono::duration_cast<std::chrono::milliseconds>(
+        tp.time_since_epoch()
+    ).count();
+}
+
+std::string TimeUtils::toIso8601(int64_t ms) {
+    return toIso8601(fromMs(ms));
+}
+
+std::string TimeUtils::toIso8601(TimePoint tp) {
+    auto time = Clock::to_time_t(tp);
+    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+        tp.time_since_epoch()
+    ).count() % 1000;
+
+    std::tm tm{};
+    gmtime_r(&time, &tm);
+
+    std::ostringstream ss;
+    ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S")
+       << '.' << std::setfill('0') << std::setw(3) << ms << 'Z';
+
+    return ss.str();
+}
+
+int64_t TimeUtils::parseIso8601(const std::string& str) {
+    std::tm tm{};
+    int ms = 0;
+
+    std::istringstream ss(str);
+    ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
+
+    if (ss.peek() == '.') {
+        ss.ignore();
+        ss >> ms;
+    }
+
+    auto time = timegm(&tm);
+    return static_cast<int64_t>(time) * 1000 + ms;
+}
+
+bool TimeUtils::isExpired(int64_t expiryMs) {
+    return nowMs() > expiryMs;
+}
+
+bool TimeUtils::isExpired(TimePoint expiry) {
+    return now() > expiry;
+}
+
+int64_t TimeUtils::addMs(int64_t timestampMs, int64_t durationMs) {
+    return timestampMs + durationMs;
+}
+
+int64_t TimeUtils::addSec(int64_t timestampMs, int64_t durationSec) {
+    return timestampMs + (durationSec * 1000);
+}
+
+// ScopedTimer implementation
+ScopedTimer::ScopedTimer(const std::string& name)
+    : name_(name), start_(TimeUtils::now()) {}
+
+ScopedTimer::~ScopedTimer() {
+    if (!name_.empty()) {
+        std::cout << "[Timer] " << name_ << ": " << elapsedMs() << "ms" << std::endl;
+    }
+}
+
+int64_t ScopedTimer::elapsedMs() const {
+    return TimeUtils::toMs(TimeUtils::now()) - TimeUtils::toMs(start_);
+}
+
+void ScopedTimer::reset() {
+    start_ = TimeUtils::now();
+}
+
+} // namespace smartbotic::common

+ 60 - 0
lib/common/time_utils.hpp

@@ -0,0 +1,60 @@
+#pragma once
+
+#include <chrono>
+#include <string>
+#include <ctime>
+
+namespace smartbotic::common {
+
+class TimeUtils {
+public:
+    using Clock = std::chrono::system_clock;
+    using TimePoint = Clock::time_point;
+    using Duration = Clock::duration;
+
+    // Get current timestamp in milliseconds
+    static int64_t nowMs();
+
+    // Get current timestamp in seconds
+    static int64_t nowSec();
+
+    // Get current timestamp as TimePoint
+    static TimePoint now();
+
+    // Convert milliseconds to TimePoint
+    static TimePoint fromMs(int64_t ms);
+
+    // Convert TimePoint to milliseconds
+    static int64_t toMs(TimePoint tp);
+
+    // Format timestamp as ISO 8601 string
+    static std::string toIso8601(int64_t ms);
+    static std::string toIso8601(TimePoint tp);
+
+    // Parse ISO 8601 string to milliseconds
+    static int64_t parseIso8601(const std::string& str);
+
+    // Check if timestamp has expired
+    static bool isExpired(int64_t expiryMs);
+    static bool isExpired(TimePoint expiry);
+
+    // Add duration to timestamp
+    static int64_t addMs(int64_t timestampMs, int64_t durationMs);
+    static int64_t addSec(int64_t timestampMs, int64_t durationSec);
+};
+
+// RAII timer for measuring durations
+class ScopedTimer {
+public:
+    explicit ScopedTimer(const std::string& name = "");
+    ~ScopedTimer();
+
+    int64_t elapsedMs() const;
+    void reset();
+
+private:
+    std::string name_;
+    TimeUtils::TimePoint start_;
+};
+
+} // namespace smartbotic::common

+ 52 - 0
lib/common/uuid.cpp

@@ -0,0 +1,52 @@
+#include "common/uuid.hpp"
+#include <openssl/rand.h>
+#include <cstring>
+
+namespace smartbotic::common {
+
+thread_local std::mt19937_64 UUID::generator_{std::random_device{}()};
+
+std::string UUID::generate() {
+    unsigned char bytes[16];
+    RAND_bytes(bytes, 16);
+
+    // Set version 4 (random)
+    bytes[6] = (bytes[6] & 0x0f) | 0x40;
+    // Set variant (RFC 4122)
+    bytes[8] = (bytes[8] & 0x3f) | 0x80;
+
+    std::ostringstream ss;
+    ss << std::hex << std::setfill('0');
+
+    for (int i = 0; i < 16; ++i) {
+        if (i == 4 || i == 6 || i == 8 || i == 10) {
+            ss << '-';
+        }
+        ss << std::setw(2) << static_cast<int>(bytes[i]);
+    }
+
+    return ss.str();
+}
+
+std::string UUID::generatePrefixed(const std::string& prefix) {
+    return prefix + "_" + generate();
+}
+
+bool UUID::isValid(const std::string& uuid) {
+    if (uuid.length() != 36) {
+        return false;
+    }
+
+    for (size_t i = 0; i < uuid.length(); ++i) {
+        char c = uuid[i];
+        if (i == 8 || i == 13 || i == 18 || i == 23) {
+            if (c != '-') return false;
+        } else {
+            if (!std::isxdigit(c)) return false;
+        }
+    }
+
+    return true;
+}
+
+} // namespace smartbotic::common

+ 20 - 0
lib/common/uuid.hpp

@@ -0,0 +1,20 @@
+#pragma once
+
+#include <string>
+#include <random>
+#include <sstream>
+#include <iomanip>
+
+namespace smartbotic::common {
+
+class UUID {
+public:
+    static std::string generate();
+    static std::string generatePrefixed(const std::string& prefix);
+    static bool isValid(const std::string& uuid);
+
+private:
+    static thread_local std::mt19937_64 generator_;
+};
+
+} // namespace smartbotic::common

+ 166 - 0
lib/config/config_loader.cpp

@@ -0,0 +1,166 @@
+#include "config/config_loader.hpp"
+#include "common/string_utils.hpp"
+#include "logging/logger.hpp"
+#include <fstream>
+#include <regex>
+
+namespace smartbotic::config {
+
+using namespace common;
+
+Result<nlohmann::json> ConfigLoader::loadFromFile(const std::filesystem::path& path) {
+    if (!std::filesystem::exists(path)) {
+        return Error(ErrorCode::NotFound, "Config file not found: " + path.string());
+    }
+
+    std::ifstream file(path);
+    if (!file.is_open()) {
+        return Error(ErrorCode::Internal, "Failed to open config file: " + path.string());
+    }
+
+    try {
+        nlohmann::json config = nlohmann::json::parse(file);
+        return expandEnvVars(config);
+    } catch (const nlohmann::json::exception& e) {
+        return Error(ErrorCode::InvalidArgument,
+                    "Failed to parse config file: " + std::string(e.what()));
+    }
+}
+
+Result<nlohmann::json> ConfigLoader::loadFromString(const std::string& content) {
+    try {
+        nlohmann::json config = nlohmann::json::parse(content);
+        return expandEnvVars(config);
+    } catch (const nlohmann::json::exception& e) {
+        return Error(ErrorCode::InvalidArgument,
+                    "Failed to parse config: " + std::string(e.what()));
+    }
+}
+
+nlohmann::json ConfigLoader::expandEnvVars(const nlohmann::json& config) {
+    if (config.is_string()) {
+        return nlohmann::json(StringUtils::expandEnvVars(config.get<std::string>()));
+    }
+
+    if (config.is_array()) {
+        nlohmann::json result = nlohmann::json::array();
+        for (const auto& item : config) {
+            result.push_back(expandEnvVars(item));
+        }
+        return result;
+    }
+
+    if (config.is_object()) {
+        nlohmann::json result = nlohmann::json::object();
+        for (auto it = config.begin(); it != config.end(); ++it) {
+            result[it.key()] = expandEnvVars(it.value());
+        }
+        return result;
+    }
+
+    return config;
+}
+
+std::optional<nlohmann::json> ConfigLoader::get(const nlohmann::json& config,
+                                                  const std::string& path) {
+    auto parts = StringUtils::split(path, '.');
+    const nlohmann::json* current = &config;
+
+    for (const auto& part : parts) {
+        if (!current->is_object() || !current->contains(part)) {
+            return std::nullopt;
+        }
+        current = &(*current)[part];
+    }
+
+    return std::make_optional(*current);
+}
+
+nlohmann::json ConfigLoader::merge(const nlohmann::json& base, const nlohmann::json& override) {
+    nlohmann::json result = base;
+
+    if (override.is_object() && base.is_object()) {
+        for (auto it = override.begin(); it != override.end(); ++it) {
+            if (result.contains(it.key()) && result[it.key()].is_object() && it.value().is_object()) {
+                result[it.key()] = merge(result[it.key()], it.value());
+            } else {
+                result[it.key()] = it.value();
+            }
+        }
+    } else {
+        result = override;
+    }
+
+    return result;
+}
+
+Result<void> ConfigLoader::validate(const nlohmann::json& config, const nlohmann::json& schema) {
+    // Basic JSON Schema validation
+    // Note: For production, use a full JSON Schema validator library
+
+    if (schema.contains("type")) {
+        std::string expectedType = schema["type"];
+        bool valid = false;
+
+        if (expectedType == "object") valid = config.is_object();
+        else if (expectedType == "array") valid = config.is_array();
+        else if (expectedType == "string") valid = config.is_string();
+        else if (expectedType == "number") valid = config.is_number();
+        else if (expectedType == "integer") valid = config.is_number_integer();
+        else if (expectedType == "boolean") valid = config.is_boolean();
+        else if (expectedType == "null") valid = config.is_null();
+
+        if (!valid) {
+            return Error(ErrorCode::ValidationError,
+                        "Type mismatch: expected " + expectedType);
+        }
+    }
+
+    if (schema.contains("required") && config.is_object()) {
+        for (const auto& field : schema["required"]) {
+            if (!config.contains(field.get<std::string>())) {
+                return Error(ErrorCode::ValidationError,
+                            "Missing required field: " + field.get<std::string>());
+            }
+        }
+    }
+
+    if (schema.contains("properties") && config.is_object()) {
+        for (auto it = schema["properties"].begin(); it != schema["properties"].end(); ++it) {
+            if (config.contains(it.key())) {
+                auto result = validate(config[it.key()], it.value());
+                if (result.failed()) {
+                    return result;
+                }
+            }
+        }
+    }
+
+    return Result<void>();
+}
+
+// Config class implementation
+Config::Config(nlohmann::json data) : data_(std::move(data)) {}
+
+Result<Config> Config::fromFile(const std::filesystem::path& path) {
+    auto result = ConfigLoader::loadFromFile(path);
+    if (result.failed()) {
+        return result.error();
+    }
+    return Config(std::move(result.value()));
+}
+
+bool Config::has(const std::string& path) const {
+    return ConfigLoader::get(data_, path).has_value();
+}
+
+Result<void> Config::reload(const std::filesystem::path& path) {
+    auto result = ConfigLoader::loadFromFile(path);
+    if (result.failed()) {
+        return result.error();
+    }
+    data_ = std::move(result.value());
+    return Result<void>();
+}
+
+} // namespace smartbotic::config

+ 103 - 0
lib/config/config_loader.hpp

@@ -0,0 +1,103 @@
+#pragma once
+
+#include <string>
+#include <optional>
+#include <filesystem>
+#include <nlohmann/json.hpp>
+#include "common/error.hpp"
+
+namespace smartbotic::config {
+
+class ConfigLoader {
+public:
+    // Load configuration from file
+    static common::Result<nlohmann::json> loadFromFile(const std::filesystem::path& path);
+
+    // Load configuration from string
+    static common::Result<nlohmann::json> loadFromString(const std::string& content);
+
+    // Expand environment variables in JSON values (${VAR:default})
+    static nlohmann::json expandEnvVars(const nlohmann::json& config);
+
+    // Get nested value with dot notation (e.g., "database.host")
+    static std::optional<nlohmann::json> get(const nlohmann::json& config,
+                                              const std::string& path);
+
+    // Get value with default
+    template<typename T>
+    static T getOr(const nlohmann::json& config, const std::string& path, T defaultValue) {
+        auto value = get(config, path);
+        if (value.has_value()) {
+            try {
+                return value->get<T>();
+            } catch (...) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    // Merge two configurations (second overrides first)
+    static nlohmann::json merge(const nlohmann::json& base, const nlohmann::json& override);
+
+    // Validate config against schema
+    static common::Result<void> validate(const nlohmann::json& config,
+                                         const nlohmann::json& schema);
+};
+
+// Configuration holder with typed accessors
+class Config {
+public:
+    Config() = default;
+    explicit Config(nlohmann::json data);
+
+    // Load from file
+    static common::Result<Config> fromFile(const std::filesystem::path& path);
+
+    // Raw access
+    const nlohmann::json& data() const { return data_; }
+    nlohmann::json& data() { return data_; }
+
+    // Get value with path
+    template<typename T>
+    std::optional<T> get(const std::string& path) const {
+        auto value = ConfigLoader::get(data_, path);
+        if (value.has_value()) {
+            try {
+                return value->get<T>();
+            } catch (...) {
+                return std::nullopt;
+            }
+        }
+        return std::nullopt;
+    }
+
+    // Get value with default
+    template<typename T>
+    T getOr(const std::string& path, T defaultValue) const {
+        return ConfigLoader::getOr(data_, path, std::move(defaultValue));
+    }
+
+    // Get required value (throws if not found)
+    template<typename T>
+    T getRequired(const std::string& path) const {
+        auto value = get<T>(path);
+        if (!value.has_value()) {
+            throw common::SmartBoticException(
+                common::Error(common::ErrorCode::InvalidArgument,
+                             "Required config value not found: " + path));
+        }
+        return *value;
+    }
+
+    // Check if path exists
+    bool has(const std::string& path) const;
+
+    // Reload from file
+    common::Result<void> reload(const std::filesystem::path& path);
+
+private:
+    nlohmann::json data_;
+};
+
+} // namespace smartbotic::config

+ 121 - 0
lib/credentials/credential_client.cpp

@@ -0,0 +1,121 @@
+#include "credential_client.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::credentials {
+
+using namespace common;
+
+CredentialClient::CredentialClient(const CredentialClientConfig& config)
+    : config_(config) {
+
+    channel_ = grpc::CreateChannel(config_.address, grpc::InsecureChannelCredentials());
+    stub_ = proto::CredentialService::NewStub(channel_);
+
+    LOG_INFO("CredentialClient initialized with address: {}", config_.address);
+}
+
+Result<HttpAuth> CredentialClient::getHttpAuth(const std::string& credential_id,
+                                               const std::string& workflow_id) {
+    proto::GetCredentialAuthRequest request;
+    request.set_credential_id(credential_id);
+    request.set_workflow_id(workflow_id);
+
+    proto::GetCredentialAuthResponse response;
+    grpc::ClientContext context;
+
+    // Set timeout
+    auto deadline = std::chrono::system_clock::now() +
+                   std::chrono::milliseconds(config_.timeout_ms);
+    context.set_deadline(deadline);
+
+    grpc::Status status = stub_->GetCredentialAuth(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::Unavailable,
+                    "Credential service error: " + status.error_message());
+    }
+
+    if (!response.success()) {
+        return Error(ErrorCode::NotFound, response.error());
+    }
+
+    HttpAuth auth;
+    auth.header_name = response.header_name();
+    auth.header_value = response.header_value();
+
+    return auth;
+}
+
+Result<ImapData> CredentialClient::getImapCredentials(const std::string& credential_id,
+                                                       const std::string& workflow_id) {
+    proto::GetImapCredentialsRequest request;
+    request.set_credential_id(credential_id);
+    request.set_workflow_id(workflow_id);
+
+    proto::GetImapCredentialsResponse response;
+    grpc::ClientContext context;
+
+    // Set timeout
+    auto deadline = std::chrono::system_clock::now() +
+                   std::chrono::milliseconds(config_.timeout_ms);
+    context.set_deadline(deadline);
+
+    grpc::Status status = stub_->GetImapCredentials(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::Unavailable,
+                    "Credential service error: " + status.error_message());
+    }
+
+    if (!response.success()) {
+        return Error(ErrorCode::NotFound, response.error());
+    }
+
+    ImapData data;
+    data.host = response.host();
+    data.port = response.port();
+    data.username = response.username();
+    data.password = response.password();
+    data.use_ssl = response.use_ssl();
+
+    return data;
+}
+
+Result<std::vector<CredentialInfo>> CredentialClient::listCredentials(const std::string& workflow_id) {
+    proto::ListCredentialsRequest request;
+    request.set_workflow_id(workflow_id);
+
+    proto::ListCredentialsResponse response;
+    grpc::ClientContext context;
+
+    auto deadline = std::chrono::system_clock::now() +
+                   std::chrono::milliseconds(config_.timeout_ms);
+    context.set_deadline(deadline);
+
+    grpc::Status status = stub_->ListCredentials(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::Unavailable,
+                    "Credential service error: " + status.error_message());
+    }
+
+    std::vector<CredentialInfo> credentials;
+    for (const auto& cred : response.credentials()) {
+        CredentialInfo info;
+        info.id = cred.id();
+        info.name = cred.name();
+        info.type = credentialTypeFromString(cred.type());
+        info.description = cred.description();
+        credentials.push_back(info);
+    }
+
+    return credentials;
+}
+
+bool CredentialClient::isConnected() const {
+    if (!channel_) return false;
+    auto state = channel_->GetState(false);
+    return state == GRPC_CHANNEL_READY || state == GRPC_CHANNEL_IDLE;
+}
+
+} // namespace smartbotic::credentials

+ 45 - 0
lib/credentials/credential_client.hpp

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <grpcpp/grpcpp.h>
+#include "proto/credentials.grpc.pb.h"
+#include "credential_types.hpp"
+#include "common/error.hpp"
+
+namespace smartbotic::credentials {
+
+// Configuration for credential client
+struct CredentialClientConfig {
+    std::string address = "localhost:9013";  // Credential service address
+    int timeout_ms = 5000;
+};
+
+// gRPC client for credential service
+class CredentialClient {
+public:
+    explicit CredentialClient(const CredentialClientConfig& config);
+    ~CredentialClient() = default;
+
+    // Get HTTP authentication header for a credential
+    common::Result<HttpAuth> getHttpAuth(const std::string& credential_id,
+                                         const std::string& workflow_id = "");
+
+    // Get IMAP credentials
+    common::Result<ImapData> getImapCredentials(const std::string& credential_id,
+                                                 const std::string& workflow_id = "");
+
+    // List available credentials
+    common::Result<std::vector<CredentialInfo>> listCredentials(const std::string& workflow_id = "");
+
+    // Check if connected
+    bool isConnected() const;
+
+private:
+    CredentialClientConfig config_;
+    std::shared_ptr<grpc::Channel> channel_;
+    std::unique_ptr<proto::CredentialService::Stub> stub_;
+};
+
+} // namespace smartbotic::credentials

+ 531 - 0
lib/credentials/credential_store.cpp

@@ -0,0 +1,531 @@
+#include "credential_store.hpp"
+#include "common/uuid.hpp"
+#include "common/time_utils.hpp"
+#include "logging/logger.hpp"
+#include <curl/curl.h>
+
+namespace smartbotic::credentials {
+
+using namespace common;
+
+CredentialStore::CredentialStore(storage::StorageClient& storage, const CredentialStoreConfig& config)
+    : storage_(storage)
+    , config_(config) {
+
+    crypto::AesGcmConfig crypto_config;
+    crypto_config.master_key = config_.master_key;
+    crypto_config.pbkdf2_iterations = config_.pbkdf2_iterations;
+    crypto_config.use_key_derivation = true;
+
+    crypto_ = std::make_unique<crypto::AesGcm>(crypto_config);
+}
+
+Result<void> CredentialStore::initialize() {
+    // Check if collection exists by querying it
+    auto result = storage_.query(COLLECTION);
+    if (result.failed()) {
+        // Try to create the collection
+        auto create_result = storage_.createCollection(COLLECTION);
+        if (create_result.failed()) {
+            LOG_WARN("Could not create credentials collection: {}", create_result.error().message());
+            // Collection might already exist, continue
+        }
+    }
+    return Result<void>();
+}
+
+Result<std::string> CredentialStore::encryptData(const nlohmann::json& data) {
+    std::string plaintext = data.dump();
+    auto result = crypto_->encryptToBase64(plaintext);
+    if (result.failed()) {
+        return Error(ErrorCode::Internal, "Failed to encrypt credential data: " + result.error().message());
+    }
+    return result.value();
+}
+
+Result<nlohmann::json> CredentialStore::decryptData(const std::string& encrypted) {
+    auto result = crypto_->decryptFromBase64(encrypted);
+    if (result.failed()) {
+        return Error(ErrorCode::Internal, "Failed to decrypt credential data: " + result.error().message());
+    }
+    try {
+        return nlohmann::json::parse(result.value());
+    } catch (const std::exception& e) {
+        return Error(ErrorCode::Internal, "Failed to parse decrypted credential data: " + std::string(e.what()));
+    }
+}
+
+bool CredentialStore::hasWorkflowAccess(const CredentialMetadata& metadata, const std::string& workflow_id) {
+    // Empty allowed_workflows means all workflows can access
+    if (metadata.allowed_workflows.empty()) {
+        return true;
+    }
+
+    // Empty workflow_id is only allowed for admin operations
+    if (workflow_id.empty()) {
+        return true;  // Admin access
+    }
+
+    // Check if workflow is in the allowed list
+    for (const auto& allowed : metadata.allowed_workflows) {
+        if (allowed == workflow_id) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+Result<CredentialInfo> CredentialStore::create(const CreateCredentialRequest& request, const std::string& user_id) {
+    // Validate and prepare credential data based on type
+    nlohmann::json data_to_encrypt;
+    nlohmann::json public_data;  // Non-secret fields for editing
+
+    switch (request.type) {
+        case CredentialType::Basic: {
+            auto data = BasicAuthData::fromJson(request.data);
+            if (data.username.empty() || data.password.empty()) {
+                return Error(ErrorCode::InvalidArgument, "Basic auth requires username and password");
+            }
+            data_to_encrypt = data.toJson();
+            public_data = {{"username", data.username}};
+            break;
+        }
+        case CredentialType::Bearer: {
+            auto data = BearerTokenData::fromJson(request.data);
+            if (data.token.empty()) {
+                return Error(ErrorCode::InvalidArgument, "Bearer token is required");
+            }
+            data_to_encrypt = data.toJson();
+            // No public data for bearer token
+            break;
+        }
+        case CredentialType::ApiKey: {
+            auto data = ApiKeyData::fromJson(request.data);
+            if (data.key_value.empty()) {
+                return Error(ErrorCode::InvalidArgument, "API key value is required");
+            }
+            data_to_encrypt = data.toJson();
+            public_data = {{"header_name", data.header_name}};
+            break;
+        }
+        case CredentialType::OAuth2: {
+            auto data = OAuth2Data::fromJson(request.data);
+            if (data.token_url.empty() || data.client_id.empty()) {
+                return Error(ErrorCode::InvalidArgument, "OAuth2 requires token_url and client_id");
+            }
+            data_to_encrypt = data.toJson();
+            public_data = {
+                {"grant_type", data.grant_type},
+                {"client_id", data.client_id},
+                {"token_url", data.token_url},
+                {"auth_url", data.auth_url},
+                {"scope", data.scope}
+            };
+            break;
+        }
+        case CredentialType::Imap: {
+            auto data = ImapData::fromJson(request.data);
+            if (data.host.empty() || data.username.empty() || data.password.empty()) {
+                return Error(ErrorCode::InvalidArgument, "IMAP requires host, username, and password");
+            }
+            data_to_encrypt = data.toJson();
+            public_data = {
+                {"host", data.host},
+                {"port", data.port},
+                {"username", data.username},
+                {"use_ssl", data.use_ssl}
+            };
+            break;
+        }
+        default:
+            return Error(ErrorCode::InvalidArgument, "Unsupported credential type");
+    }
+
+    // Encrypt the data
+    auto encrypt_result = encryptData(data_to_encrypt);
+    if (encrypt_result.failed()) {
+        return encrypt_result.error();
+    }
+
+    // Create document - timestamps are managed by database
+    std::string id = UUID::generatePrefixed("cred");
+
+    CredentialDocument doc;
+    doc.metadata.id = id;
+    doc.metadata.name = request.name;
+    doc.metadata.description = request.description;
+    doc.metadata.type = request.type;
+    doc.metadata.created_by = user_id;
+    doc.metadata.allowed_workflows = request.allowed_workflows;
+    doc.metadata.public_data = public_data;
+    doc.encrypted_data = encrypt_result.value();
+
+    // Store in database
+    auto insert_result = storage_.insert(COLLECTION, doc.toJson(), id);
+    if (insert_result.failed()) {
+        return Error(ErrorCode::DatabaseError, "Failed to store credential: " + insert_result.error().message());
+    }
+
+    LOG_INFO("Created credential {} type={} by user {}", id, credentialTypeToString(request.type), user_id);
+
+    return CredentialInfo::fromMetadata(doc.metadata);
+}
+
+Result<CredentialInfo> CredentialStore::get(const std::string& id) {
+    auto result = storage_.get(COLLECTION, id);
+    if (result.failed()) {
+        return Error(ErrorCode::NotFound, "Credential not found: " + id);
+    }
+
+    auto doc = CredentialDocument::fromJson(result.value());
+    return CredentialInfo::fromMetadata(doc.metadata);
+}
+
+Result<void> CredentialStore::update(const std::string& id, const UpdateCredentialRequest& request) {
+    // Get existing credential
+    auto get_result = storage_.get(COLLECTION, id);
+    if (get_result.failed()) {
+        return Error(ErrorCode::NotFound, "Credential not found: " + id);
+    }
+
+    auto doc = CredentialDocument::fromJson(get_result.value());
+
+    // Update metadata - updatedAt is managed by database automatically
+    if (!request.name.empty()) {
+        doc.metadata.name = request.name;
+    }
+    doc.metadata.description = request.description;
+    doc.metadata.allowed_workflows = request.allowed_workflows;
+
+    // Update encrypted data if provided
+    if (!request.data.is_null() && !request.data.empty()) {
+        nlohmann::json data_to_encrypt;
+        nlohmann::json public_data;
+
+        switch (doc.metadata.type) {
+            case CredentialType::Basic: {
+                auto data = BasicAuthData::fromJson(request.data);
+                data_to_encrypt = data.toJson();
+                public_data = {{"username", data.username}};
+                break;
+            }
+            case CredentialType::Bearer: {
+                auto data = BearerTokenData::fromJson(request.data);
+                data_to_encrypt = data.toJson();
+                break;
+            }
+            case CredentialType::ApiKey: {
+                auto data = ApiKeyData::fromJson(request.data);
+                data_to_encrypt = data.toJson();
+                public_data = {{"header_name", data.header_name}};
+                break;
+            }
+            case CredentialType::OAuth2: {
+                auto data = OAuth2Data::fromJson(request.data);
+                data_to_encrypt = data.toJson();
+                public_data = {
+                    {"grant_type", data.grant_type},
+                    {"client_id", data.client_id},
+                    {"token_url", data.token_url},
+                    {"auth_url", data.auth_url},
+                    {"scope", data.scope}
+                };
+                break;
+            }
+            case CredentialType::Imap: {
+                auto data = ImapData::fromJson(request.data);
+                data_to_encrypt = data.toJson();
+                public_data = {
+                    {"host", data.host},
+                    {"port", data.port},
+                    {"username", data.username},
+                    {"use_ssl", data.use_ssl}
+                };
+                break;
+            }
+        }
+
+        auto encrypt_result = encryptData(data_to_encrypt);
+        if (encrypt_result.failed()) {
+            return encrypt_result.error();
+        }
+        doc.encrypted_data = encrypt_result.value();
+        if (!public_data.is_null() && !public_data.empty()) {
+            doc.metadata.public_data = public_data;
+        }
+    }
+
+    // Update in database
+    auto update_result = storage_.update(COLLECTION, id, doc.toJson());
+    if (update_result.failed()) {
+        return Error(ErrorCode::DatabaseError, "Failed to update credential: " + update_result.error().message());
+    }
+
+    LOG_INFO("Updated credential {}", id);
+    return Result<void>();
+}
+
+Result<void> CredentialStore::remove(const std::string& id) {
+    auto result = storage_.remove(COLLECTION, id);
+    if (result.failed()) {
+        return Error(ErrorCode::DatabaseError, "Failed to delete credential: " + result.error().message());
+    }
+
+    LOG_INFO("Deleted credential {}", id);
+    return Result<void>();
+}
+
+Result<std::vector<CredentialInfo>> CredentialStore::list() {
+    auto result = storage_.query(COLLECTION);
+    if (result.failed()) {
+        return Error(ErrorCode::DatabaseError, "Failed to list credentials: " + result.error().message());
+    }
+
+    std::vector<CredentialInfo> credentials;
+    for (const auto& doc_json : result.value().documents) {
+        auto doc = CredentialDocument::fromJson(doc_json);
+        credentials.push_back(CredentialInfo::fromMetadata(doc.metadata));
+    }
+
+    return credentials;
+}
+
+Result<HttpAuth> CredentialStore::getHttpAuth(const std::string& id, const std::string& workflow_id) {
+    // Get credential document
+    auto get_result = storage_.get(COLLECTION, id);
+    if (get_result.failed()) {
+        return Error(ErrorCode::NotFound, "Credential not found: " + id);
+    }
+
+    auto doc = CredentialDocument::fromJson(get_result.value());
+
+    // Check workflow access
+    if (!hasWorkflowAccess(doc.metadata, workflow_id)) {
+        return Error(ErrorCode::PermissionDenied,
+            "Workflow " + workflow_id + " does not have access to credential " + id);
+    }
+
+    // Decrypt data
+    auto decrypt_result = decryptData(doc.encrypted_data);
+    if (decrypt_result.failed()) {
+        return decrypt_result.error();
+    }
+
+    const auto& data = decrypt_result.value();
+
+    // Generate HTTP auth based on type
+    switch (doc.metadata.type) {
+        case CredentialType::Basic: {
+            auto basic = BasicAuthData::fromJson(data);
+            return basic.toHttpAuth();
+        }
+        case CredentialType::Bearer: {
+            auto bearer = BearerTokenData::fromJson(data);
+            return bearer.toHttpAuth();
+        }
+        case CredentialType::ApiKey: {
+            auto api_key = ApiKeyData::fromJson(data);
+            return api_key.toHttpAuth();
+        }
+        case CredentialType::OAuth2: {
+            auto oauth = OAuth2Data::fromJson(data);
+
+            // Check if token is expired and needs refresh
+            if (oauth.isExpired() && !oauth.refresh_token.empty()) {
+                auto refresh_result = fetchOAuth2Token(oauth);
+                if (refresh_result.ok()) {
+                    oauth = refresh_result.value();
+
+                    // Update stored token
+                    doc.encrypted_data = encryptData(oauth.toJson()).value();
+                    storage_.update(COLLECTION, id, doc.toJson());
+                }
+                // If refresh fails, try with existing token anyway
+            }
+
+            if (oauth.access_token.empty()) {
+                // Try to fetch initial token
+                auto fetch_result = fetchOAuth2Token(oauth);
+                if (fetch_result.failed()) {
+                    return fetch_result.error();
+                }
+                oauth = fetch_result.value();
+
+                // Update stored token
+                doc.encrypted_data = encryptData(oauth.toJson()).value();
+                storage_.update(COLLECTION, id, doc.toJson());
+            }
+
+            return oauth.toHttpAuth();
+        }
+        case CredentialType::Imap:
+            return Error(ErrorCode::InvalidArgument, "IMAP credentials cannot be used for HTTP authentication");
+        default:
+            return Error(ErrorCode::Internal, "Unsupported credential type");
+    }
+}
+
+Result<ImapData> CredentialStore::getImapCredentials(const std::string& id, const std::string& workflow_id) {
+    // Get document
+    auto get_result = storage_.get(COLLECTION, id);
+    if (get_result.failed()) {
+        return Error(ErrorCode::NotFound, "Credential not found: " + id);
+    }
+
+    auto doc = CredentialDocument::fromJson(get_result.value());
+
+    // Check it's an IMAP credential
+    if (doc.metadata.type != CredentialType::Imap) {
+        return Error(ErrorCode::InvalidArgument, "Credential is not an IMAP credential");
+    }
+
+    // Check workflow access
+    if (!hasWorkflowAccess(doc.metadata, workflow_id)) {
+        return Error(ErrorCode::PermissionDenied,
+            "Workflow " + workflow_id + " does not have access to credential " + id);
+    }
+
+    // Decrypt data
+    auto decrypt_result = decryptData(doc.encrypted_data);
+    if (decrypt_result.failed()) {
+        return decrypt_result.error();
+    }
+
+    return ImapData::fromJson(decrypt_result.value());
+}
+
+// Curl callback for response
+static size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
+    auto* response = static_cast<std::string*>(userdata);
+    size_t total = size * nmemb;
+    response->append(ptr, total);
+    return total;
+}
+
+Result<OAuth2Data> CredentialStore::fetchOAuth2Token(const OAuth2Data& oauth_data) {
+    CURL* curl = curl_easy_init();
+    if (!curl) {
+        return Error(ErrorCode::Internal, "Failed to initialize curl for OAuth2");
+    }
+
+    std::string response_body;
+    std::string post_data;
+
+    // Build POST data based on grant type
+    if (oauth_data.grant_type == "client_credentials") {
+        post_data = "grant_type=client_credentials";
+        post_data += "&client_id=" + oauth_data.client_id;
+        post_data += "&client_secret=" + oauth_data.client_secret;
+        if (!oauth_data.scope.empty()) {
+            post_data += "&scope=" + oauth_data.scope;
+        }
+    } else if (oauth_data.grant_type == "refresh_token" || !oauth_data.refresh_token.empty()) {
+        post_data = "grant_type=refresh_token";
+        post_data += "&refresh_token=" + oauth_data.refresh_token;
+        post_data += "&client_id=" + oauth_data.client_id;
+        post_data += "&client_secret=" + oauth_data.client_secret;
+    } else {
+        curl_easy_cleanup(curl);
+        return Error(ErrorCode::InvalidArgument, "Unsupported OAuth2 grant type: " + oauth_data.grant_type);
+    }
+
+    curl_easy_setopt(curl, CURLOPT_URL, oauth_data.token_url.c_str());
+    curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data.c_str());
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body);
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
+
+    struct curl_slist* headers = nullptr;
+    headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded");
+    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+    CURLcode res = curl_easy_perform(curl);
+
+    long http_code = 0;
+    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+
+    curl_slist_free_all(headers);
+    curl_easy_cleanup(curl);
+
+    if (res != CURLE_OK) {
+        return Error(ErrorCode::Internal, "OAuth2 token request failed: " + std::string(curl_easy_strerror(res)));
+    }
+
+    if (http_code != 200) {
+        return Error(ErrorCode::Internal, "OAuth2 token request returned " + std::to_string(http_code) + ": " + response_body);
+    }
+
+    // Parse response
+    try {
+        auto response_json = nlohmann::json::parse(response_body);
+
+        OAuth2Data result = oauth_data;
+        result.access_token = response_json.value("access_token", "");
+
+        if (response_json.contains("refresh_token")) {
+            result.refresh_token = response_json["refresh_token"].get<std::string>();
+        }
+
+        // Calculate expiration time
+        int expires_in = response_json.value("expires_in", 3600);
+        result.expires_at = TimeUtils::nowMs() + (expires_in * 1000) - 60000;  // Subtract 1 minute for safety margin
+
+        if (result.access_token.empty()) {
+            return Error(ErrorCode::Internal, "OAuth2 response missing access_token");
+        }
+
+        LOG_INFO("Successfully fetched OAuth2 token (expires in {}s)", expires_in);
+        return result;
+
+    } catch (const std::exception& e) {
+        return Error(ErrorCode::Internal, "Failed to parse OAuth2 response: " + std::string(e.what()));
+    }
+}
+
+Result<void> CredentialStore::refreshOAuth2Token(const std::string& id) {
+    // Get credential
+    auto get_result = storage_.get(COLLECTION, id);
+    if (get_result.failed()) {
+        return Error(ErrorCode::NotFound, "Credential not found: " + id);
+    }
+
+    auto doc = CredentialDocument::fromJson(get_result.value());
+
+    if (doc.metadata.type != CredentialType::OAuth2) {
+        return Error(ErrorCode::InvalidArgument, "Credential is not OAuth2 type");
+    }
+
+    // Decrypt data
+    auto decrypt_result = decryptData(doc.encrypted_data);
+    if (decrypt_result.failed()) {
+        return decrypt_result.error();
+    }
+
+    auto oauth_data = OAuth2Data::fromJson(decrypt_result.value());
+
+    // Fetch new token
+    auto fetch_result = fetchOAuth2Token(oauth_data);
+    if (fetch_result.failed()) {
+        return fetch_result.error();
+    }
+
+    // Update stored token
+    auto encrypt_result = encryptData(fetch_result.value().toJson());
+    if (encrypt_result.failed()) {
+        return encrypt_result.error();
+    }
+
+    doc.encrypted_data = encrypt_result.value();
+    // Note: updatedAt is managed by database automatically
+
+    auto update_result = storage_.update(COLLECTION, id, doc.toJson());
+    if (update_result.failed()) {
+        return Error(ErrorCode::DatabaseError, "Failed to update OAuth2 token: " + update_result.error().message());
+    }
+
+    return Result<void>();
+}
+
+} // namespace smartbotic::credentials

+ 63 - 0
lib/credentials/credential_store.hpp

@@ -0,0 +1,63 @@
+#pragma once
+
+#include <memory>
+#include <string>
+#include <vector>
+#include "credential_types.hpp"
+#include "crypto/aes_gcm.hpp"
+#include "storage/storage_client.hpp"
+
+namespace smartbotic::credentials {
+
+// Credential store configuration
+struct CredentialStoreConfig {
+    std::string master_key = "dev-key-change-in-production";
+    int pbkdf2_iterations = 100000;
+};
+
+// Credential store - manages encrypted credential storage
+class CredentialStore {
+public:
+    CredentialStore(storage::StorageClient& storage, const CredentialStoreConfig& config);
+    ~CredentialStore() = default;
+
+    // Initialize (ensure collection exists)
+    common::Result<void> initialize();
+
+    // CRUD operations
+    common::Result<CredentialInfo> create(const CreateCredentialRequest& request, const std::string& user_id);
+    common::Result<CredentialInfo> get(const std::string& id);
+    common::Result<void> update(const std::string& id, const UpdateCredentialRequest& request);
+    common::Result<void> remove(const std::string& id);
+    common::Result<std::vector<CredentialInfo>> list();
+
+    // Get decrypted credential for use (checks workflow access)
+    common::Result<HttpAuth> getHttpAuth(const std::string& id, const std::string& workflow_id = "");
+
+    // Get IMAP credentials (checks workflow access)
+    common::Result<ImapData> getImapCredentials(const std::string& id, const std::string& workflow_id = "");
+
+    // OAuth2 token refresh
+    common::Result<void> refreshOAuth2Token(const std::string& id);
+
+private:
+    // Encrypt credential data
+    common::Result<std::string> encryptData(const nlohmann::json& data);
+
+    // Decrypt credential data
+    common::Result<nlohmann::json> decryptData(const std::string& encrypted);
+
+    // Check if workflow has access to credential
+    bool hasWorkflowAccess(const CredentialMetadata& metadata, const std::string& workflow_id);
+
+    // Perform OAuth2 token fetch
+    common::Result<OAuth2Data> fetchOAuth2Token(const OAuth2Data& oauth_data);
+
+    storage::StorageClient& storage_;
+    std::unique_ptr<crypto::AesGcm> crypto_;
+    CredentialStoreConfig config_;
+
+    static constexpr const char* COLLECTION = "credentials";
+};
+
+} // namespace smartbotic::credentials

+ 327 - 0
lib/credentials/credential_types.cpp

@@ -0,0 +1,327 @@
+#include "credential_types.hpp"
+#include "common/time_utils.hpp"
+#include <stdexcept>
+
+namespace smartbotic::credentials {
+
+using namespace common;
+
+// Type conversion utilities
+std::string credentialTypeToString(CredentialType type) {
+    switch (type) {
+        case CredentialType::Basic: return "basic";
+        case CredentialType::Bearer: return "bearer";
+        case CredentialType::ApiKey: return "api_key";
+        case CredentialType::OAuth2: return "oauth2";
+        case CredentialType::Imap: return "imap";
+        default: return "unknown";
+    }
+}
+
+CredentialType credentialTypeFromString(const std::string& str) {
+    if (str == "basic") return CredentialType::Basic;
+    if (str == "bearer") return CredentialType::Bearer;
+    if (str == "api_key") return CredentialType::ApiKey;
+    if (str == "oauth2") return CredentialType::OAuth2;
+    if (str == "imap") return CredentialType::Imap;
+    throw std::invalid_argument("Unknown credential type: " + str);
+}
+
+// Base64 encoding for Basic auth
+static std::string base64Encode(const std::string& input) {
+    static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    std::string result;
+    result.reserve(((input.size() + 2) / 3) * 4);
+
+    for (size_t i = 0; i < input.size(); i += 3) {
+        uint32_t a = static_cast<uint8_t>(input[i]);
+        uint32_t b = i + 1 < input.size() ? static_cast<uint8_t>(input[i + 1]) : 0;
+        uint32_t c = i + 2 < input.size() ? static_cast<uint8_t>(input[i + 2]) : 0;
+
+        uint32_t triple = (a << 16) | (b << 8) | c;
+
+        result += table[(triple >> 18) & 0x3F];
+        result += table[(triple >> 12) & 0x3F];
+        result += (i + 1 < input.size()) ? table[(triple >> 6) & 0x3F] : '=';
+        result += (i + 2 < input.size()) ? table[triple & 0x3F] : '=';
+    }
+
+    return result;
+}
+
+// HttpAuth
+nlohmann::json HttpAuth::toJson() const {
+    return {
+        {"headerName", header_name},
+        {"headerValue", header_value}
+    };
+}
+
+// BasicAuthData
+nlohmann::json BasicAuthData::toJson() const {
+    return {
+        {"username", username},
+        {"password", password}
+    };
+}
+
+BasicAuthData BasicAuthData::fromJson(const nlohmann::json& j) {
+    BasicAuthData data;
+    data.username = j.value("username", "");
+    data.password = j.value("password", "");
+    return data;
+}
+
+HttpAuth BasicAuthData::toHttpAuth() const {
+    HttpAuth auth;
+    auth.header_name = "Authorization";
+    auth.header_value = "Basic " + base64Encode(username + ":" + password);
+    return auth;
+}
+
+// BearerTokenData
+nlohmann::json BearerTokenData::toJson() const {
+    return {
+        {"token", token}
+    };
+}
+
+BearerTokenData BearerTokenData::fromJson(const nlohmann::json& j) {
+    BearerTokenData data;
+    data.token = j.value("token", "");
+    return data;
+}
+
+HttpAuth BearerTokenData::toHttpAuth() const {
+    HttpAuth auth;
+    auth.header_name = "Authorization";
+    auth.header_value = "Bearer " + token;
+    return auth;
+}
+
+// ApiKeyData
+nlohmann::json ApiKeyData::toJson() const {
+    return {
+        {"headerName", header_name},
+        {"keyValue", key_value}
+    };
+}
+
+ApiKeyData ApiKeyData::fromJson(const nlohmann::json& j) {
+    ApiKeyData data;
+    data.header_name = j.value("headerName", j.value("header_name", "X-API-Key"));
+    data.key_value = j.value("keyValue", j.value("key_value", ""));
+    return data;
+}
+
+HttpAuth ApiKeyData::toHttpAuth() const {
+    HttpAuth auth;
+    auth.header_name = header_name;
+    auth.header_value = key_value;
+    return auth;
+}
+
+// OAuth2Data
+nlohmann::json OAuth2Data::toJson() const {
+    return {
+        {"grantType", grant_type},
+        {"clientId", client_id},
+        {"clientSecret", client_secret},
+        {"tokenUrl", token_url},
+        {"authUrl", auth_url},
+        {"scope", scope},
+        {"accessToken", access_token},
+        {"refreshToken", refresh_token},
+        {"expiresAt", expires_at}
+    };
+}
+
+OAuth2Data OAuth2Data::fromJson(const nlohmann::json& j) {
+    OAuth2Data data;
+    data.grant_type = j.value("grantType", j.value("grant_type", "client_credentials"));
+    data.client_id = j.value("clientId", j.value("client_id", ""));
+    data.client_secret = j.value("clientSecret", j.value("client_secret", ""));
+    data.token_url = j.value("tokenUrl", j.value("token_url", ""));
+    data.auth_url = j.value("authUrl", j.value("auth_url", ""));
+    data.scope = j.value("scope", "");
+    data.access_token = j.value("accessToken", j.value("access_token", ""));
+    data.refresh_token = j.value("refreshToken", j.value("refresh_token", ""));
+    data.expires_at = j.value("expiresAt", j.value("expires_at", static_cast<int64_t>(0)));
+    return data;
+}
+
+bool OAuth2Data::isExpired() const {
+    if (expires_at == 0) return false;  // No expiration set
+    return TimeUtils::nowMs() >= expires_at;
+}
+
+HttpAuth OAuth2Data::toHttpAuth() const {
+    HttpAuth auth;
+    auth.header_name = "Authorization";
+    auth.header_value = "Bearer " + access_token;
+    return auth;
+}
+
+// ImapData
+nlohmann::json ImapData::toJson() const {
+    return {
+        {"host", host},
+        {"port", port},
+        {"username", username},
+        {"password", password},
+        {"useSsl", use_ssl}
+    };
+}
+
+ImapData ImapData::fromJson(const nlohmann::json& j) {
+    ImapData data;
+    data.host = j.value("host", "");
+    data.port = j.value("port", 993);
+    data.username = j.value("username", "");
+    data.password = j.value("password", "");
+    data.use_ssl = j.value("useSsl", j.value("use_ssl", true));
+    return data;
+}
+
+// CredentialMetadata
+nlohmann::json CredentialMetadata::toJson() const {
+    // Note: id, createdAt, updatedAt are managed by database as _id, _createdAt, _updatedAt
+    nlohmann::json j = {
+        {"name", name},
+        {"description", description},
+        {"type", credentialTypeToString(type)},
+        {"createdBy", created_by},
+        {"allowedWorkflows", allowed_workflows}
+    };
+    if (!public_data.is_null() && !public_data.empty()) {
+        j["publicData"] = public_data;
+    }
+    return j;
+}
+
+CredentialMetadata CredentialMetadata::fromJson(const nlohmann::json& j) {
+    CredentialMetadata meta;
+    // Read from database metadata keys (prefixed with _)
+    meta.id = j.value("_id", j.value("id", ""));
+    meta.name = j.value("name", "");
+    meta.description = j.value("description", "");
+    meta.type = credentialTypeFromString(j.value("type", "basic"));
+    meta.created_by = j.value("createdBy", j.value("created_by", ""));
+    meta.created_at = j.value("_createdAt", j.value("createdAt", j.value("created_at", int64_t{0})));
+    meta.updated_at = j.value("_updatedAt", j.value("updatedAt", j.value("updated_at", int64_t{0})));
+    if (j.contains("allowedWorkflows") && j["allowedWorkflows"].is_array()) {
+        for (const auto& wf : j["allowedWorkflows"]) {
+            meta.allowed_workflows.push_back(wf.get<std::string>());
+        }
+    } else if (j.contains("allowed_workflows") && j["allowed_workflows"].is_array()) {
+        for (const auto& wf : j["allowed_workflows"]) {
+            meta.allowed_workflows.push_back(wf.get<std::string>());
+        }
+    }
+    if (j.contains("publicData")) {
+        meta.public_data = j["publicData"];
+    }
+    return meta;
+}
+
+// CredentialDocument
+nlohmann::json CredentialDocument::toJson() const {
+    nlohmann::json j = metadata.toJson();
+    j["encryptedData"] = encrypted_data;
+    return j;
+}
+
+CredentialDocument CredentialDocument::fromJson(const nlohmann::json& j) {
+    CredentialDocument doc;
+    doc.metadata = CredentialMetadata::fromJson(j);
+    doc.encrypted_data = j.value("encryptedData", j.value("encrypted_data", ""));
+    return doc;
+}
+
+// CredentialInfo
+nlohmann::json CredentialInfo::toJson() const {
+    nlohmann::json j = {
+        {"id", id},
+        {"name", name},
+        {"description", description},
+        {"type", credentialTypeToString(type)},
+        {"createdBy", created_by},
+        {"createdAt", created_at},
+        {"updatedAt", updated_at},
+        {"allowedWorkflows", allowed_workflows}
+    };
+    if (!public_data.is_null() && !public_data.empty()) {
+        j["publicData"] = public_data;
+    }
+    return j;
+}
+
+CredentialInfo CredentialInfo::fromMetadata(const CredentialMetadata& metadata) {
+    CredentialInfo info;
+    info.id = metadata.id;
+    info.name = metadata.name;
+    info.description = metadata.description;
+    info.type = metadata.type;
+    info.created_by = metadata.created_by;
+    info.created_at = metadata.created_at;
+    info.updated_at = metadata.updated_at;
+    info.allowed_workflows = metadata.allowed_workflows;
+    info.public_data = metadata.public_data;
+    return info;
+}
+
+// CreateCredentialRequest
+Result<CreateCredentialRequest> CreateCredentialRequest::fromJson(const nlohmann::json& j) {
+    CreateCredentialRequest req;
+
+    if (!j.contains("name") || !j["name"].is_string() || j["name"].get<std::string>().empty()) {
+        return Error(ErrorCode::InvalidArgument, "Credential name is required");
+    }
+    req.name = j["name"].get<std::string>();
+
+    if (!j.contains("type") || !j["type"].is_string()) {
+        return Error(ErrorCode::InvalidArgument, "Credential type is required");
+    }
+    try {
+        req.type = credentialTypeFromString(j["type"].get<std::string>());
+    } catch (const std::exception& e) {
+        return Error(ErrorCode::InvalidArgument, e.what());
+    }
+
+    req.description = j.value("description", "");
+
+    if (!j.contains("data") || !j["data"].is_object()) {
+        return Error(ErrorCode::InvalidArgument, "Credential data is required");
+    }
+    req.data = j["data"];
+
+    if (j.contains("allowedWorkflows") && j["allowedWorkflows"].is_array()) {
+        for (const auto& wf : j["allowedWorkflows"]) {
+            req.allowed_workflows.push_back(wf.get<std::string>());
+        }
+    }
+
+    return req;
+}
+
+// UpdateCredentialRequest
+Result<UpdateCredentialRequest> UpdateCredentialRequest::fromJson(const nlohmann::json& j) {
+    UpdateCredentialRequest req;
+
+    req.name = j.value("name", "");
+    req.description = j.value("description", "");
+
+    if (j.contains("data") && j["data"].is_object()) {
+        req.data = j["data"];
+    }
+
+    if (j.contains("allowedWorkflows") && j["allowedWorkflows"].is_array()) {
+        for (const auto& wf : j["allowedWorkflows"]) {
+            req.allowed_workflows.push_back(wf.get<std::string>());
+        }
+    }
+
+    return req;
+}
+
+} // namespace smartbotic::credentials

+ 156 - 0
lib/credentials/credential_types.hpp

@@ -0,0 +1,156 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <nlohmann/json.hpp>
+#include "common/error.hpp"
+
+namespace smartbotic::credentials {
+
+// Credential types supported
+enum class CredentialType {
+    Basic,      // Basic Auth: username + password
+    Bearer,     // Bearer Token: token
+    ApiKey,     // API Key: header name + key value
+    OAuth2,     // OAuth2: client credentials with token refresh
+    Imap        // IMAP: host + port + username + password + SSL
+};
+
+std::string credentialTypeToString(CredentialType type);
+CredentialType credentialTypeFromString(const std::string& str);
+
+// HTTP authentication header result (for applying to requests)
+struct HttpAuth {
+    std::string header_name;   // e.g., "Authorization"
+    std::string header_value;  // e.g., "Basic dXNlcjpwYXNz" or "Bearer token123"
+
+    nlohmann::json toJson() const;
+};
+
+// Basic authentication data (stored encrypted)
+struct BasicAuthData {
+    std::string username;
+    std::string password;
+
+    nlohmann::json toJson() const;
+    static BasicAuthData fromJson(const nlohmann::json& j);
+
+    // Generate HTTP auth header
+    HttpAuth toHttpAuth() const;
+};
+
+// Bearer token data (stored encrypted)
+struct BearerTokenData {
+    std::string token;
+
+    nlohmann::json toJson() const;
+    static BearerTokenData fromJson(const nlohmann::json& j);
+
+    HttpAuth toHttpAuth() const;
+};
+
+// API Key data (stored encrypted)
+struct ApiKeyData {
+    std::string header_name;  // e.g., "X-API-Key" or "Authorization"
+    std::string key_value;
+
+    nlohmann::json toJson() const;
+    static ApiKeyData fromJson(const nlohmann::json& j);
+
+    HttpAuth toHttpAuth() const;
+};
+
+// OAuth2 data (stored encrypted)
+struct OAuth2Data {
+    std::string grant_type;       // "client_credentials", "password", etc.
+    std::string client_id;
+    std::string client_secret;
+    std::string token_url;
+    std::string auth_url;         // For authorization code flow
+    std::string scope;
+    std::string access_token;     // Cached access token
+    std::string refresh_token;    // For token refresh
+    int64_t expires_at = 0;       // Access token expiration timestamp
+
+    nlohmann::json toJson() const;
+    static OAuth2Data fromJson(const nlohmann::json& j);
+
+    bool isExpired() const;
+    HttpAuth toHttpAuth() const;
+};
+
+// IMAP data (stored encrypted)
+struct ImapData {
+    std::string host;
+    int port = 993;               // Default IMAPS port
+    std::string username;
+    std::string password;
+    bool use_ssl = true;          // Use IMAPS (SSL/TLS)
+
+    nlohmann::json toJson() const;
+    static ImapData fromJson(const nlohmann::json& j);
+};
+
+// Credential metadata (not encrypted, stored directly)
+struct CredentialMetadata {
+    std::string id;
+    std::string name;
+    std::string description;
+    CredentialType type;
+    std::string created_by;
+    int64_t created_at = 0;
+    int64_t updated_at = 0;
+    std::vector<std::string> allowed_workflows;  // Empty = all workflows
+    nlohmann::json public_data;  // Non-secret fields for editing (e.g., host, username)
+
+    nlohmann::json toJson() const;
+    static CredentialMetadata fromJson(const nlohmann::json& j);
+};
+
+// Full credential document (for storage)
+struct CredentialDocument {
+    CredentialMetadata metadata;
+    std::string encrypted_data;  // Base64 encoded encrypted credential data
+
+    nlohmann::json toJson() const;
+    static CredentialDocument fromJson(const nlohmann::json& j);
+};
+
+// Credential for API responses (without secrets)
+struct CredentialInfo {
+    std::string id;
+    std::string name;
+    std::string description;
+    CredentialType type;
+    std::string created_by;
+    int64_t created_at = 0;
+    int64_t updated_at = 0;
+    std::vector<std::string> allowed_workflows;
+    nlohmann::json public_data;  // Non-secret fields for editing
+
+    nlohmann::json toJson() const;
+    static CredentialInfo fromMetadata(const CredentialMetadata& metadata);
+};
+
+// Create credential request (from API)
+struct CreateCredentialRequest {
+    std::string name;
+    std::string description;
+    CredentialType type;
+    nlohmann::json data;  // Type-specific data (Basic, Bearer, ApiKey, OAuth2)
+    std::vector<std::string> allowed_workflows;
+
+    static common::Result<CreateCredentialRequest> fromJson(const nlohmann::json& j);
+};
+
+// Update credential request (from API)
+struct UpdateCredentialRequest {
+    std::string name;
+    std::string description;
+    nlohmann::json data;  // Type-specific data (optional - if empty, keep existing)
+    std::vector<std::string> allowed_workflows;
+
+    static common::Result<UpdateCredentialRequest> fromJson(const nlohmann::json& j);
+};
+
+} // namespace smartbotic::credentials

+ 379 - 0
lib/crypto/aes_gcm.cpp

@@ -0,0 +1,379 @@
+#include "aes_gcm.hpp"
+#include <openssl/evp.h>
+#include <openssl/rand.h>
+#include <openssl/err.h>
+#include <nlohmann/json.hpp>
+#include <cstring>
+#include <stdexcept>
+
+namespace smartbotic::crypto {
+
+using namespace common;
+
+// Constants
+constexpr size_t AES_256_KEY_SIZE = 32;
+constexpr size_t GCM_IV_SIZE = 12;
+constexpr size_t GCM_TAG_SIZE = 16;
+
+// Base64 encoding table
+static const char base64_chars[] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    "abcdefghijklmnopqrstuvwxyz"
+    "0123456789+/";
+
+std::string base64::encode(const std::vector<uint8_t>& data) {
+    return encode(data.data(), data.size());
+}
+
+std::string base64::encode(const uint8_t* data, size_t len) {
+    std::string result;
+    result.reserve(((len + 2) / 3) * 4);
+
+    for (size_t i = 0; i < len; i += 3) {
+        uint32_t octet_a = i < len ? data[i] : 0;
+        uint32_t octet_b = i + 1 < len ? data[i + 1] : 0;
+        uint32_t octet_c = i + 2 < len ? data[i + 2] : 0;
+
+        uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c;
+
+        result += base64_chars[(triple >> 18) & 0x3F];
+        result += base64_chars[(triple >> 12) & 0x3F];
+        result += (i + 1 < len) ? base64_chars[(triple >> 6) & 0x3F] : '=';
+        result += (i + 2 < len) ? base64_chars[triple & 0x3F] : '=';
+    }
+
+    return result;
+}
+
+Result<std::vector<uint8_t>> base64::decode(const std::string& encoded) {
+    if (encoded.empty()) {
+        return std::vector<uint8_t>{};
+    }
+
+    // Build decoding table
+    static int decode_table[256] = {-1};
+    static bool initialized = false;
+    if (!initialized) {
+        for (int i = 0; i < 256; ++i) decode_table[i] = -1;
+        for (int i = 0; i < 64; ++i) {
+            decode_table[static_cast<unsigned char>(base64_chars[i])] = i;
+        }
+        initialized = true;
+    }
+
+    size_t len = encoded.length();
+    if (len % 4 != 0) {
+        return Error(ErrorCode::InvalidArgument, "Invalid base64 length");
+    }
+
+    // Calculate output size
+    size_t out_len = (len / 4) * 3;
+    if (len >= 1 && encoded[len - 1] == '=') out_len--;
+    if (len >= 2 && encoded[len - 2] == '=') out_len--;
+
+    std::vector<uint8_t> result(out_len);
+
+    size_t j = 0;
+    for (size_t i = 0; i < len; i += 4) {
+        int a = decode_table[static_cast<unsigned char>(encoded[i])];
+        int b = decode_table[static_cast<unsigned char>(encoded[i + 1])];
+        int c = encoded[i + 2] == '=' ? 0 : decode_table[static_cast<unsigned char>(encoded[i + 2])];
+        int d = encoded[i + 3] == '=' ? 0 : decode_table[static_cast<unsigned char>(encoded[i + 3])];
+
+        if (a == -1 || b == -1 || (encoded[i + 2] != '=' && c == -1) || (encoded[i + 3] != '=' && d == -1)) {
+            return Error(ErrorCode::InvalidArgument, "Invalid base64 character");
+        }
+
+        uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d;
+
+        if (j < out_len) result[j++] = (triple >> 16) & 0xFF;
+        if (j < out_len) result[j++] = (triple >> 8) & 0xFF;
+        if (j < out_len) result[j++] = triple & 0xFF;
+    }
+
+    return result;
+}
+
+std::string EncryptedData::toJson() const {
+    nlohmann::json j;
+    j["ciphertext"] = base64::encode(ciphertext);
+    j["iv"] = base64::encode(iv);
+    j["auth_tag"] = base64::encode(auth_tag);
+    return j.dump();
+}
+
+Result<EncryptedData> EncryptedData::fromJson(const std::string& json) {
+    try {
+        auto j = nlohmann::json::parse(json);
+
+        EncryptedData data;
+
+        auto ct_result = base64::decode(j.value("ciphertext", ""));
+        if (ct_result.failed()) return ct_result.error();
+        data.ciphertext = ct_result.value();
+
+        auto iv_result = base64::decode(j.value("iv", ""));
+        if (iv_result.failed()) return iv_result.error();
+        data.iv = iv_result.value();
+
+        auto tag_result = base64::decode(j.value("auth_tag", ""));
+        if (tag_result.failed()) return tag_result.error();
+        data.auth_tag = tag_result.value();
+
+        return data;
+    } catch (const std::exception& e) {
+        return Error(ErrorCode::InvalidArgument, "Failed to parse encrypted data JSON: " + std::string(e.what()));
+    }
+}
+
+std::string EncryptedData::toBase64() const {
+    // Format: iv || auth_tag || ciphertext, all concatenated and base64 encoded
+    std::vector<uint8_t> combined;
+    combined.reserve(iv.size() + auth_tag.size() + ciphertext.size());
+    combined.insert(combined.end(), iv.begin(), iv.end());
+    combined.insert(combined.end(), auth_tag.begin(), auth_tag.end());
+    combined.insert(combined.end(), ciphertext.begin(), ciphertext.end());
+    return base64::encode(combined);
+}
+
+Result<EncryptedData> EncryptedData::fromBase64(const std::string& encoded) {
+    auto decode_result = base64::decode(encoded);
+    if (decode_result.failed()) {
+        return decode_result.error();
+    }
+
+    const auto& data = decode_result.value();
+    if (data.size() < GCM_IV_SIZE + GCM_TAG_SIZE) {
+        return Error(ErrorCode::InvalidArgument, "Encrypted data too short");
+    }
+
+    EncryptedData result;
+    result.iv.assign(data.begin(), data.begin() + GCM_IV_SIZE);
+    result.auth_tag.assign(data.begin() + GCM_IV_SIZE, data.begin() + GCM_IV_SIZE + GCM_TAG_SIZE);
+    result.ciphertext.assign(data.begin() + GCM_IV_SIZE + GCM_TAG_SIZE, data.end());
+
+    return result;
+}
+
+AesGcm::AesGcm(const AesGcmConfig& config) : config_(config) {
+    if (config_.master_key.empty()) {
+        throw std::invalid_argument("Master key cannot be empty");
+    }
+
+    // Pre-derive key if not using per-encryption salt
+    if (!config_.use_key_derivation) {
+        // Use key directly (must be 32 bytes)
+        if (config_.master_key.size() < AES_256_KEY_SIZE) {
+            // Pad with zeros if too short (not recommended for production)
+            derived_key_.resize(AES_256_KEY_SIZE, 0);
+            std::memcpy(derived_key_.data(), config_.master_key.data(),
+                       std::min(config_.master_key.size(), AES_256_KEY_SIZE));
+        } else {
+            derived_key_.assign(config_.master_key.begin(),
+                               config_.master_key.begin() + AES_256_KEY_SIZE);
+        }
+    }
+}
+
+std::vector<uint8_t> AesGcm::deriveKey(const std::vector<uint8_t>& salt) {
+    std::vector<uint8_t> key(AES_256_KEY_SIZE);
+
+    if (PKCS5_PBKDF2_HMAC(
+            config_.master_key.c_str(),
+            static_cast<int>(config_.master_key.length()),
+            salt.data(),
+            static_cast<int>(salt.size()),
+            config_.pbkdf2_iterations,
+            EVP_sha256(),
+            static_cast<int>(key.size()),
+            key.data()) != 1) {
+        throw std::runtime_error("PBKDF2 key derivation failed");
+    }
+
+    return key;
+}
+
+std::vector<uint8_t> AesGcm::generateRandomIv() {
+    std::vector<uint8_t> iv(GCM_IV_SIZE);
+    if (RAND_bytes(iv.data(), static_cast<int>(iv.size())) != 1) {
+        throw std::runtime_error("Failed to generate random IV");
+    }
+    return iv;
+}
+
+std::string AesGcm::generateRandomKey() {
+    std::vector<uint8_t> key(AES_256_KEY_SIZE);
+    if (RAND_bytes(key.data(), static_cast<int>(key.size())) != 1) {
+        throw std::runtime_error("Failed to generate random key");
+    }
+    return base64::encode(key);
+}
+
+Result<EncryptedData> AesGcm::encrypt(const std::string& plaintext) {
+    return encrypt(std::vector<uint8_t>(plaintext.begin(), plaintext.end()));
+}
+
+Result<EncryptedData> AesGcm::encrypt(const std::vector<uint8_t>& plaintext) {
+    EncryptedData result;
+
+    // Generate random IV
+    result.iv = generateRandomIv();
+
+    // Get or derive key
+    std::vector<uint8_t> key;
+    if (config_.use_key_derivation) {
+        // Use IV as salt for key derivation (provides unique key per encryption)
+        key = deriveKey(result.iv);
+    } else {
+        key = derived_key_;
+    }
+
+    // Create cipher context
+    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) {
+        return Error(ErrorCode::Internal, "Failed to create cipher context");
+    }
+
+    // Cleanup helper
+    struct CtxGuard {
+        EVP_CIPHER_CTX* ctx;
+        ~CtxGuard() { EVP_CIPHER_CTX_free(ctx); }
+    } guard{ctx};
+
+    // Initialize encryption
+    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) {
+        return Error(ErrorCode::Internal, "Failed to initialize encryption");
+    }
+
+    // Set IV length
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, GCM_IV_SIZE, nullptr) != 1) {
+        return Error(ErrorCode::Internal, "Failed to set IV length");
+    }
+
+    // Set key and IV
+    if (EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), result.iv.data()) != 1) {
+        return Error(ErrorCode::Internal, "Failed to set key and IV");
+    }
+
+    // Encrypt
+    result.ciphertext.resize(plaintext.size() + EVP_MAX_BLOCK_LENGTH);
+    int out_len = 0;
+    int total_len = 0;
+
+    if (EVP_EncryptUpdate(ctx, result.ciphertext.data(), &out_len,
+                          plaintext.data(), static_cast<int>(plaintext.size())) != 1) {
+        return Error(ErrorCode::Internal, "Encryption failed");
+    }
+    total_len = out_len;
+
+    // Finalize
+    if (EVP_EncryptFinal_ex(ctx, result.ciphertext.data() + total_len, &out_len) != 1) {
+        return Error(ErrorCode::Internal, "Encryption finalization failed");
+    }
+    total_len += out_len;
+    result.ciphertext.resize(total_len);
+
+    // Get authentication tag
+    result.auth_tag.resize(GCM_TAG_SIZE);
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, GCM_TAG_SIZE, result.auth_tag.data()) != 1) {
+        return Error(ErrorCode::Internal, "Failed to get authentication tag");
+    }
+
+    return result;
+}
+
+Result<std::string> AesGcm::decrypt(const EncryptedData& data) {
+    auto result = decryptBytes(data);
+    if (result.failed()) {
+        return result.error();
+    }
+    return std::string(result.value().begin(), result.value().end());
+}
+
+Result<std::vector<uint8_t>> AesGcm::decryptBytes(const EncryptedData& data) {
+    if (data.iv.size() != GCM_IV_SIZE) {
+        return Error(ErrorCode::InvalidArgument, "Invalid IV size");
+    }
+    if (data.auth_tag.size() != GCM_TAG_SIZE) {
+        return Error(ErrorCode::InvalidArgument, "Invalid auth tag size");
+    }
+
+    // Get or derive key
+    std::vector<uint8_t> key;
+    if (config_.use_key_derivation) {
+        key = deriveKey(data.iv);
+    } else {
+        key = derived_key_;
+    }
+
+    // Create cipher context
+    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) {
+        return Error(ErrorCode::Internal, "Failed to create cipher context");
+    }
+
+    struct CtxGuard {
+        EVP_CIPHER_CTX* ctx;
+        ~CtxGuard() { EVP_CIPHER_CTX_free(ctx); }
+    } guard{ctx};
+
+    // Initialize decryption
+    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) {
+        return Error(ErrorCode::Internal, "Failed to initialize decryption");
+    }
+
+    // Set IV length
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, GCM_IV_SIZE, nullptr) != 1) {
+        return Error(ErrorCode::Internal, "Failed to set IV length");
+    }
+
+    // Set key and IV
+    if (EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), data.iv.data()) != 1) {
+        return Error(ErrorCode::Internal, "Failed to set key and IV");
+    }
+
+    // Set authentication tag
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, GCM_TAG_SIZE,
+                            const_cast<uint8_t*>(data.auth_tag.data())) != 1) {
+        return Error(ErrorCode::Internal, "Failed to set authentication tag");
+    }
+
+    // Decrypt
+    std::vector<uint8_t> plaintext(data.ciphertext.size() + EVP_MAX_BLOCK_LENGTH);
+    int out_len = 0;
+    int total_len = 0;
+
+    if (EVP_DecryptUpdate(ctx, plaintext.data(), &out_len,
+                          data.ciphertext.data(), static_cast<int>(data.ciphertext.size())) != 1) {
+        return Error(ErrorCode::Internal, "Decryption failed");
+    }
+    total_len = out_len;
+
+    // Finalize and verify tag
+    if (EVP_DecryptFinal_ex(ctx, plaintext.data() + total_len, &out_len) != 1) {
+        // Tag verification failed
+        return Error(ErrorCode::InvalidArgument, "Authentication failed - data may be corrupted or tampered");
+    }
+    total_len += out_len;
+    plaintext.resize(total_len);
+
+    return plaintext;
+}
+
+Result<std::string> AesGcm::encryptToBase64(const std::string& plaintext) {
+    auto result = encrypt(plaintext);
+    if (result.failed()) {
+        return result.error();
+    }
+    return result.value().toBase64();
+}
+
+Result<std::string> AesGcm::decryptFromBase64(const std::string& encoded) {
+    auto data_result = EncryptedData::fromBase64(encoded);
+    if (data_result.failed()) {
+        return data_result.error();
+    }
+    return decrypt(data_result.value());
+}
+
+} // namespace smartbotic::crypto

+ 70 - 0
lib/crypto/aes_gcm.hpp

@@ -0,0 +1,70 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include "common/error.hpp"
+
+namespace smartbotic::crypto {
+
+// AES-256-GCM encryption result
+struct EncryptedData {
+    std::vector<uint8_t> ciphertext;
+    std::vector<uint8_t> iv;          // 12 bytes
+    std::vector<uint8_t> auth_tag;    // 16 bytes
+
+    // Serialize to base64 encoded JSON for storage
+    std::string toJson() const;
+    static common::Result<EncryptedData> fromJson(const std::string& json);
+
+    // Convenience for single-field storage
+    std::string toBase64() const;
+    static common::Result<EncryptedData> fromBase64(const std::string& encoded);
+};
+
+// AES-256-GCM encryption configuration
+struct AesGcmConfig {
+    std::string master_key;           // Raw key or passphrase
+    int pbkdf2_iterations = 100000;   // Key derivation iterations
+    bool use_key_derivation = true;   // If false, master_key is used directly
+};
+
+// AES-256-GCM authenticated encryption
+class AesGcm {
+public:
+    explicit AesGcm(const AesGcmConfig& config);
+    ~AesGcm() = default;
+
+    // Encrypt plaintext
+    common::Result<EncryptedData> encrypt(const std::string& plaintext);
+    common::Result<EncryptedData> encrypt(const std::vector<uint8_t>& plaintext);
+
+    // Decrypt ciphertext
+    common::Result<std::string> decrypt(const EncryptedData& data);
+    common::Result<std::vector<uint8_t>> decryptBytes(const EncryptedData& data);
+
+    // Convenience methods for string-based storage
+    common::Result<std::string> encryptToBase64(const std::string& plaintext);
+    common::Result<std::string> decryptFromBase64(const std::string& encoded);
+
+    // Generate random bytes for IV
+    static std::vector<uint8_t> generateRandomIv();
+
+    // Generate random key (32 bytes for AES-256)
+    static std::string generateRandomKey();
+
+private:
+    // Derive encryption key from master key using PBKDF2
+    std::vector<uint8_t> deriveKey(const std::vector<uint8_t>& salt);
+
+    AesGcmConfig config_;
+    std::vector<uint8_t> derived_key_;  // Cached derived key (if not using salt per encryption)
+};
+
+// Base64 encoding/decoding utilities
+namespace base64 {
+    std::string encode(const std::vector<uint8_t>& data);
+    std::string encode(const uint8_t* data, size_t len);
+    common::Result<std::vector<uint8_t>> decode(const std::string& encoded);
+}
+
+} // namespace smartbotic::crypto

+ 85 - 0
lib/logging/logger.cpp

@@ -0,0 +1,85 @@
+#include "logging/logger.hpp"
+#include <spdlog/sinks/basic_file_sink.h>
+
+namespace smartbotic::logging {
+
+std::shared_ptr<spdlog::logger> Logger::defaultLogger_ = nullptr;
+bool Logger::initialized_ = false;
+
+void Logger::init(const LogConfig& config) {
+    if (initialized_) {
+        return;
+    }
+
+    std::vector<spdlog::sink_ptr> sinks;
+
+    // Console sink
+    if (config.console) {
+        auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
+        console_sink->set_level(static_cast<spdlog::level::level_enum>(config.level));
+        sinks.push_back(console_sink);
+    }
+
+    // File sink
+    if (!config.file_path.empty()) {
+        auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+            config.file_path, config.max_file_size, config.max_files);
+        file_sink->set_level(static_cast<spdlog::level::level_enum>(config.level));
+        sinks.push_back(file_sink);
+    }
+
+    // Create logger
+    defaultLogger_ = std::make_shared<spdlog::logger>(config.name, sinks.begin(), sinks.end());
+    defaultLogger_->set_level(static_cast<spdlog::level::level_enum>(config.level));
+    defaultLogger_->set_pattern(config.pattern);
+
+    // Register as default
+    spdlog::set_default_logger(defaultLogger_);
+    spdlog::flush_every(std::chrono::seconds(3));
+
+    initialized_ = true;
+}
+
+void Logger::shutdown() {
+    if (initialized_) {
+        spdlog::shutdown();
+        defaultLogger_.reset();
+        initialized_ = false;
+    }
+}
+
+std::shared_ptr<spdlog::logger> Logger::get(const std::string& name) {
+    if (!initialized_) {
+        // Initialize with defaults if not already initialized
+        init(LogConfig{});
+    }
+
+    if (name.empty()) {
+        return defaultLogger_;
+    }
+
+    auto logger = spdlog::get(name);
+    if (!logger) {
+        // Create a new logger with same sinks as default
+        logger = defaultLogger_->clone(name);
+        spdlog::register_logger(logger);
+    }
+    return logger;
+}
+
+void Logger::setLevel(LogLevel level) {
+    if (defaultLogger_) {
+        defaultLogger_->set_level(static_cast<spdlog::level::level_enum>(level));
+    }
+}
+
+void Logger::flush() {
+    if (defaultLogger_) {
+        defaultLogger_->flush();
+    }
+}
+
+ScopedLogger::ScopedLogger(const std::string& component)
+    : logger_(Logger::get(component)) {}
+
+} // namespace smartbotic::logging

+ 124 - 0
lib/logging/logger.hpp

@@ -0,0 +1,124 @@
+#pragma once
+
+#include <string>
+#include <memory>
+#include <spdlog/spdlog.h>
+#include <spdlog/sinks/stdout_color_sinks.h>
+#include <spdlog/sinks/rotating_file_sink.h>
+
+namespace smartbotic::logging {
+
+enum class LogLevel {
+    Trace = spdlog::level::trace,
+    Debug = spdlog::level::debug,
+    Info = spdlog::level::info,
+    Warn = spdlog::level::warn,
+    Error = spdlog::level::err,
+    Critical = spdlog::level::critical,
+    Off = spdlog::level::off
+};
+
+struct LogConfig {
+    std::string name = "smartbotic";
+    LogLevel level = LogLevel::Info;
+    std::string pattern = "[%Y-%m-%d %H:%M:%S.%e] [%n] [%^%l%$] [%t] %v";
+    bool console = true;
+    std::string file_path;
+    size_t max_file_size = 10 * 1024 * 1024;  // 10MB
+    size_t max_files = 5;
+};
+
+class Logger {
+public:
+    static void init(const LogConfig& config);
+    static void shutdown();
+
+    static std::shared_ptr<spdlog::logger> get(const std::string& name = "");
+
+    // Convenience methods
+    template<typename... Args>
+    static void trace(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        get()->trace(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    static void debug(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        get()->debug(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    static void info(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        get()->info(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    static void warn(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        get()->warn(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    static void error(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        get()->error(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    static void critical(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        get()->critical(fmt, std::forward<Args>(args)...);
+    }
+
+    static void setLevel(LogLevel level);
+    static void flush();
+
+private:
+    static std::shared_ptr<spdlog::logger> defaultLogger_;
+    static bool initialized_;
+};
+
+// Scoped logger for component-specific logging
+class ScopedLogger {
+public:
+    explicit ScopedLogger(const std::string& component);
+
+    template<typename... Args>
+    void trace(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        logger_->trace(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    void debug(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        logger_->debug(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    void info(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        logger_->info(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    void warn(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        logger_->warn(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    void error(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        logger_->error(fmt, std::forward<Args>(args)...);
+    }
+
+    template<typename... Args>
+    void critical(spdlog::format_string_t<Args...> fmt, Args&&... args) {
+        logger_->critical(fmt, std::forward<Args>(args)...);
+    }
+
+private:
+    std::shared_ptr<spdlog::logger> logger_;
+};
+
+} // namespace smartbotic::logging
+
+// Convenience macros
+#define LOG_TRACE(...) smartbotic::logging::Logger::trace(__VA_ARGS__)
+#define LOG_DEBUG(...) smartbotic::logging::Logger::debug(__VA_ARGS__)
+#define LOG_INFO(...) smartbotic::logging::Logger::info(__VA_ARGS__)
+#define LOG_WARN(...) smartbotic::logging::Logger::warn(__VA_ARGS__)
+#define LOG_ERROR(...) smartbotic::logging::Logger::error(__VA_ARGS__)
+#define LOG_CRITICAL(...) smartbotic::logging::Logger::critical(__VA_ARGS__)

+ 343 - 0
lib/storage/storage_client.cpp

@@ -0,0 +1,343 @@
+#include "storage/storage_client.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::storage {
+
+using namespace common;
+
+StorageClient::StorageClient(const StorageClientConfig& config)
+    : config_(config) {
+    channel_ = grpc::CreateChannel(config_.address, grpc::InsecureChannelCredentials());
+    stub_ = proto::StorageService::NewStub(channel_);
+    LOG_DEBUG("Storage client created for {}", config_.address);
+}
+
+Result<nlohmann::json> StorageClient::get(const std::string& collection, const std::string& id) {
+    proto::GetRequest request;
+    request.set_collection(collection);
+    request.set_id(id);
+
+    proto::Document response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->Get(&context, request, &response);
+
+    if (!status.ok()) {
+        if (status.error_code() == grpc::StatusCode::NOT_FOUND) {
+            return Error(ErrorCode::DocumentNotFound, status.error_message());
+        }
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    nlohmann::json result = nlohmann::json::parse(response.data());
+    result["_id"] = response.id();
+    result["_version"] = response.version();
+    result["_createdAt"] = response.created_at();
+    result["_updatedAt"] = response.updated_at();
+
+    return result;
+}
+
+Result<std::string> StorageClient::insert(const std::string& collection,
+                                          const nlohmann::json& data,
+                                          const std::string& id,
+                                          int64_t ttl_ms) {
+    proto::InsertRequest request;
+    request.set_collection(collection);
+    request.set_id(id);
+    request.set_data(data.dump());
+    request.set_ttl_ms(ttl_ms);
+
+    proto::MutationResponse response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->Insert(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    if (!response.success()) {
+        return Error(static_cast<ErrorCode>(response.error().code()),
+                    response.error().message());
+    }
+
+    return response.id();
+}
+
+Result<int64_t> StorageClient::update(const std::string& collection,
+                                      const std::string& id,
+                                      const nlohmann::json& data,
+                                      int64_t expected_version,
+                                      bool partial) {
+    proto::UpdateRequest request;
+    request.set_collection(collection);
+    request.set_id(id);
+    request.set_data(data.dump());
+    request.set_expected_version(expected_version);
+    request.set_partial(partial);
+
+    proto::MutationResponse response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->Update(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    if (!response.success()) {
+        return Error(static_cast<ErrorCode>(response.error().code()),
+                    response.error().message());
+    }
+
+    return response.version();
+}
+
+Result<void> StorageClient::remove(const std::string& collection,
+                                   const std::string& id,
+                                   int64_t expected_version) {
+    proto::DeleteRequest request;
+    request.set_collection(collection);
+    request.set_id(id);
+    request.set_expected_version(expected_version);
+
+    proto::MutationResponse response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->Delete(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    if (!response.success()) {
+        return Error(static_cast<ErrorCode>(response.error().code()),
+                    response.error().message());
+    }
+
+    return Result<void>();
+}
+
+Result<QueryResult> StorageClient::query(const std::string& collection) {
+    return query(collection, QueryOptions{});
+}
+
+Result<QueryResult> StorageClient::query(const std::string& collection,
+                                         const QueryOptions& options) {
+    proto::QueryRequest request;
+    request.set_collection(collection);
+
+    // Add filters
+    for (const auto& [field, value] : options.filters) {
+        auto* filter = request.add_filters();
+        filter->set_field(field);
+        filter->set_op(proto::FILTER_OP_EQ);
+        filter->set_value(value.dump());
+    }
+
+    // Add sorts
+    for (const auto& [field, ascending] : options.sorts) {
+        auto* sort = request.add_sorts();
+        sort->set_field(field);
+        sort->set_direction(ascending ? proto::SORT_DIRECTION_ASC : proto::SORT_DIRECTION_DESC);
+    }
+
+    // Add projection
+    for (const auto& field : options.fields) {
+        request.add_fields(field);
+    }
+
+    // Pagination
+    auto* pagination = request.mutable_pagination();
+    pagination->set_page(options.page);
+    pagination->set_page_size(options.page_size);
+
+    proto::QueryResponse response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->Query(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    QueryResult result;
+    result.total_count = response.pagination().total_count();
+    result.has_more = response.pagination().has_more();
+
+    for (const auto& doc : response.documents()) {
+        nlohmann::json j = nlohmann::json::parse(doc.data());
+        j["_id"] = doc.id();
+        j["_version"] = doc.version();
+        j["_createdAt"] = doc.created_at();
+        j["_updatedAt"] = doc.updated_at();
+        result.documents.push_back(std::move(j));
+    }
+
+    return result;
+}
+
+Result<void> StorageClient::createCollection(const std::string& name,
+                                             const nlohmann::json& schema,
+                                             int64_t default_ttl_ms,
+                                             const VersioningOptions* versioning) {
+    proto::CreateCollectionRequest request;
+    request.set_name(name);
+    if (!schema.is_null()) {
+        request.set_schema(schema.dump());
+    }
+    request.set_default_ttl_ms(default_ttl_ms);
+
+    // Set versioning configuration if provided
+    if (versioning) {
+        auto* ver_config = request.mutable_versioning();
+        ver_config->set_enabled(versioning->enabled);
+        ver_config->set_max_versions(versioning->max_versions);
+        ver_config->set_version_ttl_ms(versioning->version_ttl_ms);
+        ver_config->set_keep_on_delete(versioning->keep_on_delete);
+    }
+
+    proto::Empty response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->CreateCollection(&context, request, &response);
+
+    if (!status.ok()) {
+        if (status.error_code() == grpc::StatusCode::ALREADY_EXISTS) {
+            return Error(ErrorCode::AlreadyExists, status.error_message());
+        }
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    return Result<void>();
+}
+
+Result<void> StorageClient::dropCollection(const std::string& name) {
+    proto::DropCollectionRequest request;
+    request.set_name(name);
+
+    proto::Empty response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->DropCollection(&context, request, &response);
+
+    if (!status.ok()) {
+        if (status.error_code() == grpc::StatusCode::NOT_FOUND) {
+            return Error(ErrorCode::CollectionNotFound, status.error_message());
+        }
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    return Result<void>();
+}
+
+std::vector<std::string> StorageClient::listCollections() {
+    proto::ListCollectionsRequest request;
+    proto::ListCollectionsResponse response;
+
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->ListCollections(&context, request, &response);
+
+    if (!status.ok()) {
+        return {};
+    }
+
+    std::vector<std::string> result;
+    for (const auto& name : response.collections()) {
+        result.push_back(name);
+    }
+    return result;
+}
+
+bool StorageClient::isConnected() const {
+    return channel_->GetState(false) == GRPC_CHANNEL_READY;
+}
+
+Result<nlohmann::json> StorageClient::getVersion(const std::string& collection,
+                                                 const std::string& id,
+                                                 int64_t version) {
+    proto::GetVersionRequest request;
+    request.set_collection(collection);
+    request.set_id(id);
+    request.set_version(version);
+
+    proto::Document response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->GetVersion(&context, request, &response);
+
+    if (!status.ok()) {
+        if (status.error_code() == grpc::StatusCode::NOT_FOUND) {
+            return Error(ErrorCode::DocumentNotFound, status.error_message());
+        }
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    nlohmann::json result = nlohmann::json::parse(response.data());
+    result["_id"] = response.id();
+    result["_version"] = response.version();
+    result["_createdAt"] = response.created_at();
+    result["_updatedAt"] = response.updated_at();
+
+    return result;
+}
+
+Result<VersionListResult> StorageClient::listVersions(const std::string& collection,
+                                                      const std::string& id,
+                                                      int32_t limit,
+                                                      int32_t offset) {
+    proto::ListVersionsRequest request;
+    request.set_collection(collection);
+    request.set_id(id);
+    request.set_limit(limit);
+    request.set_offset(offset);
+
+    proto::ListVersionsResponse response;
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() +
+                        std::chrono::milliseconds(config_.timeout_ms));
+
+    auto status = stub_->ListVersions(&context, request, &response);
+
+    if (!status.ok()) {
+        return Error(ErrorCode::DatabaseError, status.error_message());
+    }
+
+    VersionListResult result;
+    result.total_count = response.total_count();
+    result.has_more = response.has_more();
+
+    for (const auto& ver : response.versions()) {
+        DocumentVersionInfo info;
+        info.id = ver.id();
+        info.document_id = ver.document_id();
+        info.version = ver.version();
+        info.data = nlohmann::json::parse(ver.data());
+        info.created_at = ver.created_at();
+        result.versions.push_back(std::move(info));
+    }
+
+    return result;
+}
+
+} // namespace smartbotic::storage

+ 114 - 0
lib/storage/storage_client.hpp

@@ -0,0 +1,114 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <grpcpp/grpcpp.h>
+#include "proto/storage.grpc.pb.h"
+#include "common/error.hpp"
+
+namespace smartbotic::storage {
+
+// Storage client configuration
+struct StorageClientConfig {
+    std::string address = "localhost:9001";
+    int timeout_ms = 5000;
+};
+
+// Query options - defined outside class to avoid default initializer issues
+struct QueryOptions {
+    std::vector<std::pair<std::string, nlohmann::json>> filters;
+    std::vector<std::pair<std::string, bool>> sorts;  // field, ascending
+    std::vector<std::string> fields;
+    int32_t page = 1;
+    int32_t page_size = 100;
+};
+
+// Query result
+struct QueryResult {
+    std::vector<nlohmann::json> documents;
+    int64_t total_count = 0;
+    bool has_more = false;
+};
+
+// Versioning configuration for storage client
+struct VersioningOptions {
+    bool enabled = false;
+    int32_t max_versions = 0;
+    int64_t version_ttl_ms = 0;
+    bool keep_on_delete = false;
+};
+
+// Document version info
+struct DocumentVersionInfo {
+    std::string id;
+    std::string document_id;
+    int64_t version = 0;
+    nlohmann::json data;
+    int64_t created_at = 0;
+};
+
+// Version list result
+struct VersionListResult {
+    std::vector<DocumentVersionInfo> versions;
+    int64_t total_count = 0;
+    bool has_more = false;
+};
+
+// RAII storage client for gRPC communication with database service
+class StorageClient {
+public:
+    explicit StorageClient(const StorageClientConfig& config);
+
+    // Document operations
+    common::Result<nlohmann::json> get(const std::string& collection, const std::string& id);
+
+    common::Result<std::string> insert(const std::string& collection,
+                                       const nlohmann::json& data,
+                                       const std::string& id = "",
+                                       int64_t ttl_ms = 0);
+
+    common::Result<int64_t> update(const std::string& collection,
+                                   const std::string& id,
+                                   const nlohmann::json& data,
+                                   int64_t expected_version = 0,
+                                   bool partial = false);
+
+    common::Result<void> remove(const std::string& collection,
+                                const std::string& id,
+                                int64_t expected_version = 0);
+
+    // Query operations - overloaded versions
+    common::Result<QueryResult> query(const std::string& collection);
+    common::Result<QueryResult> query(const std::string& collection, const QueryOptions& options);
+
+    // Collection operations
+    common::Result<void> createCollection(const std::string& name,
+                                          const nlohmann::json& schema = nlohmann::json{},
+                                          int64_t default_ttl_ms = 0,
+                                          const VersioningOptions* versioning = nullptr);
+
+    common::Result<void> dropCollection(const std::string& name);
+
+    std::vector<std::string> listCollections();
+
+    // Version operations
+    common::Result<nlohmann::json> getVersion(const std::string& collection,
+                                              const std::string& id,
+                                              int64_t version);
+
+    common::Result<VersionListResult> listVersions(const std::string& collection,
+                                                   const std::string& id,
+                                                   int32_t limit = 100,
+                                                   int32_t offset = 0);
+
+    // Connection status
+    bool isConnected() const;
+
+private:
+    StorageClientConfig config_;
+    std::shared_ptr<grpc::Channel> channel_;
+    std::unique_ptr<proto::StorageService::Stub> stub_;
+};
+
+} // namespace smartbotic::storage

+ 373 - 0
nodes/core/http-request.js

@@ -0,0 +1,373 @@
+/**
+ * @node http-request
+ * @name HTTP Request
+ * @category http
+ * @version 2.0.0
+ * @description Make HTTP requests to external APIs with binary file download support
+ * @icon globe
+ */
+
+const configSchema = {
+  type: 'object',
+  properties: {
+    method: {
+      type: 'string',
+      title: 'Method',
+      enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
+      default: 'GET'
+    },
+    url: {
+      type: 'string',
+      title: 'URL',
+      description: 'The URL to request. Supports {{variable}} interpolation.'
+    },
+    authType: {
+      type: 'string',
+      title: 'Authentication',
+      enum: ['none', 'credential'],
+      default: 'none',
+      description: 'Authentication method to use'
+    },
+    credentialId: {
+      type: 'string',
+      title: 'Credential',
+      description: 'Select a stored credential for authentication'
+    },
+    headers: {
+      type: 'object',
+      title: 'Headers',
+      additionalProperties: { type: 'string' }
+    },
+    body: {
+      type: 'string',
+      title: 'Body',
+      description: 'Request body (for POST/PUT/PATCH)'
+    },
+    timeout: {
+      type: 'number',
+      title: 'Timeout (ms)',
+      default: 30000
+    },
+    followRedirects: {
+      type: 'boolean',
+      title: 'Follow Redirects',
+      default: true
+    },
+    responseMode: {
+      type: 'string',
+      title: 'Response Mode',
+      enum: ['auto', 'json', 'text', 'binary'],
+      default: 'auto',
+      description: 'How to handle response body. Binary mode will Base64 encode the response.'
+    },
+    expectedContentType: {
+      type: 'string',
+      title: 'Expected Content-Type',
+      description: 'Expected response content-type (e.g., application/pdf). Fails if mismatch.'
+    },
+    storeDownload: {
+      type: 'boolean',
+      title: 'Store Download',
+      description: 'Store downloaded file in database (only for binary mode)',
+      default: false
+    },
+    downloadCollection: {
+      type: 'string',
+      title: 'Storage Collection',
+      default: 'downloads',
+      description: 'Collection name for stored downloads'
+    },
+    downloadTtlHours: {
+      type: 'number',
+      title: 'TTL (hours)',
+      default: 24,
+      description: 'Auto-delete stored file after hours (0 = never)'
+    }
+  },
+  required: ['url']
+};
+
+const inputSchema = {
+  type: 'object',
+  properties: {
+    data: {
+      type: 'any',
+      description: 'Input data available for interpolation'
+    }
+  }
+};
+
+const outputSchema = {
+  type: 'object',
+  properties: {
+    statusCode: { type: 'number' },
+    headers: { type: 'object' },
+    body: { type: 'any' },
+    ok: { type: 'boolean' },
+    file: {
+      type: 'object',
+      description: 'Binary file object (only when responseMode is binary)',
+      properties: {
+        type: { type: 'string', const: 'binary' },
+        data: { type: 'string', description: 'Base64-encoded content' },
+        mimeType: { type: 'string' },
+        filename: { type: 'string' },
+        size: { type: 'number' }
+      }
+    },
+    storage: {
+      type: 'object',
+      description: 'Storage reference (only when storeDownload is true)',
+      properties: {
+        collection: { type: 'string' },
+        id: { type: 'string' }
+      }
+    }
+  }
+};
+
+function interpolate(template, data) {
+  if (typeof template !== 'string') return template;
+  return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
+    const keys = key.trim().split('.');
+    let value = data;
+    for (const k of keys) {
+      if (value && typeof value === 'object' && k in value) {
+        value = value[k];
+      } else {
+        return match;
+      }
+    }
+    return value !== undefined ? String(value) : match;
+  });
+}
+
+/**
+ * Extract filename from Content-Disposition header or URL
+ */
+function extractFilename(headers, url) {
+  // Try Content-Disposition header first
+  const contentDisposition = headers['content-disposition'];
+  if (contentDisposition) {
+    // Try to match filename*= (RFC 5987) or filename=
+    const filenameStarMatch = contentDisposition.match(/filename\*=(?:UTF-8''|utf-8'')([^;\s]+)/i);
+    if (filenameStarMatch) {
+      try {
+        return decodeURIComponent(filenameStarMatch[1]);
+      } catch (e) {
+        // Fall through
+      }
+    }
+
+    const filenameMatch = contentDisposition.match(/filename=["']?([^"';\s]+)["']?/i);
+    if (filenameMatch) {
+      return filenameMatch[1];
+    }
+  }
+
+  // Extract from URL path
+  try {
+    const urlPath = url.split('?')[0];
+    const pathParts = urlPath.split('/');
+    const lastPart = pathParts[pathParts.length - 1];
+    if (lastPart && lastPart.includes('.')) {
+      return decodeURIComponent(lastPart);
+    }
+  } catch (e) {
+    // Ignore URL parsing errors
+  }
+
+  return 'download';
+}
+
+/**
+ * Determine if response should be treated as binary based on Content-Type
+ */
+function isBinaryContentType(contentType) {
+  if (!contentType) return false;
+
+  const binaryTypes = [
+    'application/octet-stream',
+    'application/pdf',
+    'application/zip',
+    'application/gzip',
+    'application/x-tar',
+    'application/x-rar-compressed',
+    'application/x-7z-compressed',
+    'application/msword',
+    'application/vnd.openxmlformats',
+    'application/vnd.ms-',
+    'image/',
+    'audio/',
+    'video/',
+    'font/'
+  ];
+
+  const lowerType = contentType.toLowerCase();
+  return binaryTypes.some(type => lowerType.startsWith(type) || lowerType.includes(type));
+}
+
+async function execute(config, input, context) {
+  const data = input.data || input;
+
+  // Interpolate URL
+  const url = interpolate(config.url, data);
+
+  // Interpolate headers
+  const headers = {};
+  if (config.headers) {
+    for (const [key, value] of Object.entries(config.headers)) {
+      headers[key] = interpolate(value, data);
+    }
+  }
+
+  // Apply credential authentication if configured
+  if (config.authType === 'credential' && config.credentialId) {
+    const credentialId = interpolate(config.credentialId, data);
+    const auth = smartbotic.credentials.get(credentialId);
+    if (!auth.success) {
+      throw new Error(`Failed to load credential: ${auth.error}`);
+    }
+    headers[auth.headerName] = auth.headerValue;
+  }
+
+  // Set default content-type for POST/PUT/PATCH
+  if (['POST', 'PUT', 'PATCH'].includes(config.method) && !headers['Content-Type']) {
+    headers['Content-Type'] = 'application/json';
+  }
+
+  // Interpolate body
+  let body = config.body;
+  if (body) {
+    body = interpolate(body, data);
+    // Try to parse as JSON and re-stringify
+    if (typeof body === 'string') {
+      try {
+        const parsed = JSON.parse(body);
+        body = JSON.stringify(parsed);
+      } catch (e) {
+        // Keep as string
+      }
+    }
+  }
+
+  smartbotic.log.info(`HTTP ${config.method} ${url}`);
+
+  try {
+    const response = await smartbotic.http.request({
+      method: config.method,
+      url: url,
+      headers: headers,
+      body: body,
+      timeout: config.timeout,
+      followRedirects: config.followRedirects
+    });
+
+    const responseHeaders = response.headers || {};
+    const contentType = responseHeaders['content-type'] || '';
+
+    // Validate expected content-type if specified
+    if (config.expectedContentType) {
+      const expectedType = config.expectedContentType.toLowerCase();
+      const actualType = contentType.toLowerCase();
+      if (!actualType.includes(expectedType)) {
+        throw new Error(`Content-Type mismatch: expected "${config.expectedContentType}" but got "${contentType}"`);
+      }
+    }
+
+    // Determine response mode
+    let responseMode = config.responseMode || 'auto';
+    if (responseMode === 'auto') {
+      if (isBinaryContentType(contentType)) {
+        responseMode = 'binary';
+      } else if (contentType.includes('application/json')) {
+        responseMode = 'json';
+      } else {
+        responseMode = 'text';
+      }
+    }
+
+    const result = {
+      statusCode: response.status,
+      headers: responseHeaders,
+      ok: response.status >= 200 && response.status < 300
+    };
+
+    // Handle response based on mode
+    if (responseMode === 'binary') {
+      // Get the response data - it's already a string from HTTP
+      const rawData = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+
+      // Base64 encode for binary mode
+      const base64Data = smartbotic.utils.base64Encode(rawData);
+      const filename = extractFilename(responseHeaders, url);
+      const mimeType = contentType.split(';')[0].trim() || 'application/octet-stream';
+
+      const fileObject = {
+        type: 'binary',
+        data: base64Data,
+        mimeType: mimeType,
+        filename: filename,
+        size: rawData.length
+      };
+
+      result.file = fileObject;
+      result.body = null; // Don't include raw body for binary
+
+      // Store in database if configured
+      if (config.storeDownload) {
+        const collection = config.downloadCollection || 'downloads';
+        const ttlHours = config.downloadTtlHours || 0;
+        const ttlMs = ttlHours > 0 ? ttlHours * 60 * 60 * 1000 : 0;
+
+        const checksum = smartbotic.utils.sha256(rawData);
+
+        const storageDoc = {
+          filename: filename,
+          mimeType: mimeType,
+          size: rawData.length,
+          sourceUrl: url,
+          checksum: checksum,
+          downloadedAt: Date.now(),
+          data: base64Data
+        };
+
+        const insertResult = smartbotic.storage.insert(collection, storageDoc, null, ttlMs);
+        if (!insertResult.success) {
+          smartbotic.log.error(`Failed to store download: ${insertResult.error}`);
+        } else {
+          result.storage = {
+            collection: collection,
+            id: insertResult.id
+          };
+          smartbotic.log.info(`Stored download in ${collection}/${insertResult.id}`);
+        }
+      }
+    } else if (responseMode === 'json') {
+      // Try to parse as JSON
+      if (typeof response.data === 'string') {
+        try {
+          result.body = JSON.parse(response.data);
+        } catch (e) {
+          result.body = response.data;
+        }
+      } else {
+        result.body = response.data;
+      }
+    } else {
+      // Text mode
+      result.body = response.data;
+    }
+
+    if (!result.ok) {
+      smartbotic.log.warn(`HTTP request returned ${response.status}`);
+    }
+
+    return result;
+  } catch (error) {
+    smartbotic.log.error(`HTTP request failed: ${error.message}`);
+    throw error;
+  }
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };

+ 268 - 0
nodes/core/if-condition.js

@@ -0,0 +1,268 @@
+/**
+ * @node if-condition
+ * @name IF Condition
+ * @category flow-control
+ * @version 2.0.0
+ * @description Conditional branching with TRUE/FALSE output paths
+ * @icon git-branch
+ */
+
+// Define multiple outputs for branching
+const outputs = [
+  { name: 'true', displayName: 'True', type: 'any', color: '#22c55e' },
+  { name: 'false', displayName: 'False', type: 'any', color: '#ef4444' }
+];
+
+const configSchema = {
+  type: 'object',
+  properties: {
+    conditions: {
+      type: 'array',
+      title: 'Conditions',
+      items: {
+        type: 'object',
+        properties: {
+          field: {
+            type: 'string',
+            title: 'Field',
+            description: 'Path to field (e.g., data.status)'
+          },
+          operator: {
+            type: 'string',
+            title: 'Operator',
+            enum: ['equals', 'not_equals', 'greater_than', 'less_than', 'greater_than_or_equal', 'less_than_or_equal', 'contains', 'not_contains', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty', 'is_true', 'is_false', 'is_null', 'is_not_null', 'exists', 'regex']
+          },
+          value: {
+            type: 'string',
+            title: 'Value',
+            description: 'Value to compare against'
+          }
+        }
+      }
+    },
+    combineWith: {
+      type: 'string',
+      title: 'Combine Conditions',
+      enum: ['and', 'or'],
+      default: 'and'
+    }
+  }
+};
+
+const inputSchema = {
+  type: 'object',
+  properties: {
+    data: { type: 'any' }
+  }
+};
+
+const outputSchema = {
+  type: 'object',
+  properties: {
+    result: { type: 'boolean' },
+    matchedConditions: { type: 'array' },
+    data: { type: 'any' },
+    _activeBranch: { type: 'string', description: 'Active output branch (true or false)' }
+  }
+};
+
+function getFieldValue(data, path) {
+  if (!path) return data;
+
+  const keys = path.split('.');
+  let value = data;
+
+  for (const key of keys) {
+    if (value === null || value === undefined) return undefined;
+
+    // Handle array .length property
+    if (key === 'length' && Array.isArray(value)) {
+      value = value.length;
+      continue;
+    }
+
+    // Handle array index access like [0] or body[0]
+    const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
+    if (arrayMatch) {
+      const [, objKey, index] = arrayMatch;
+      if (objKey && typeof value === 'object' && objKey in value) {
+        value = value[objKey];
+      }
+      if (Array.isArray(value)) {
+        value = value[parseInt(index, 10)];
+        continue;
+      }
+      return undefined;
+    }
+
+    // Handle pure numeric index for arrays
+    if (/^\d+$/.test(key) && Array.isArray(value)) {
+      value = value[parseInt(key, 10)];
+      continue;
+    }
+
+    if (typeof value === 'object' && key in value) {
+      value = value[key];
+    } else {
+      return undefined;
+    }
+  }
+
+  return value;
+}
+
+function evaluateCondition(data, condition) {
+  let fieldValue;
+  const field = condition.field;
+
+  // If field is a pure number (from expression evaluation), use it directly
+  if (/^\d+$/.test(field) || /^\d+\.\d+$/.test(field)) {
+    fieldValue = parseFloat(field);
+  } else {
+    fieldValue = getFieldValue(data, field);
+  }
+
+  const compareValue = condition.value;
+
+  switch (condition.operator) {
+    case 'equals':
+      return fieldValue == compareValue;
+
+    case 'not_equals':
+      return fieldValue != compareValue;
+
+    case 'greater_than':
+      return Number(fieldValue) > Number(compareValue);
+
+    case 'less_than':
+      return Number(fieldValue) < Number(compareValue);
+
+    case 'greater_than_or_equal':
+      return Number(fieldValue) >= Number(compareValue);
+
+    case 'less_than_or_equal':
+      return Number(fieldValue) <= Number(compareValue);
+
+    case 'contains':
+      if (Array.isArray(fieldValue)) {
+        return fieldValue.includes(compareValue);
+      }
+      return String(fieldValue).includes(compareValue);
+
+    case 'not_contains':
+      if (Array.isArray(fieldValue)) {
+        return !fieldValue.includes(compareValue);
+      }
+      return !String(fieldValue).includes(compareValue);
+
+    case 'starts_with':
+      return String(fieldValue).startsWith(compareValue);
+
+    case 'ends_with':
+      return String(fieldValue).endsWith(compareValue);
+
+    case 'is_empty':
+      if (fieldValue === null || fieldValue === undefined) return true;
+      if (Array.isArray(fieldValue)) return fieldValue.length === 0;
+      if (typeof fieldValue === 'string') return fieldValue.length === 0;
+      if (typeof fieldValue === 'object') return Object.keys(fieldValue).length === 0;
+      return false;
+
+    case 'is_not_empty':
+      if (fieldValue === null || fieldValue === undefined) return false;
+      if (Array.isArray(fieldValue)) return fieldValue.length > 0;
+      if (typeof fieldValue === 'string') return fieldValue.length > 0;
+      if (typeof fieldValue === 'object') return Object.keys(fieldValue).length > 0;
+      return true;
+
+    case 'is_true':
+      return fieldValue === true || fieldValue === 'true' || fieldValue === 1;
+
+    case 'is_false':
+      return fieldValue === false || fieldValue === 'false' || fieldValue === 0;
+
+    case 'is_null':
+      return fieldValue === null || fieldValue === undefined;
+
+    case 'is_not_null':
+      return fieldValue !== null && fieldValue !== undefined;
+
+    case 'exists':
+      return fieldValue !== undefined;
+
+    case 'regex':
+      try {
+        const regex = new RegExp(compareValue);
+        return regex.test(String(fieldValue));
+      } catch (e) {
+        return false;
+      }
+
+    default:
+      return false;
+  }
+}
+
+async function execute(config, input, context) {
+  // Use the entire input object for condition evaluation
+  // This allows access to loop variables (currentItem, currentIndex) at the top level
+  // as well as nested data from previous nodes
+  const data = input;
+  const conditions = config.conditions || [];
+  const combineWith = config.combineWith || 'and';
+
+  // Debug: log the input keys to understand the structure
+  smartbotic.log.info('IF condition input keys: ' + Object.keys(input || {}).join(', '));
+
+  if (conditions.length === 0) {
+    // No conditions = always true
+    smartbotic.log.info('IF condition: No conditions defined, defaulting to true');
+    return {
+      result: true,
+      matchedConditions: [],
+      data: data,
+      _activeBranch: 'true'
+    };
+  }
+
+  const results = [];
+  const matchedConditions = [];
+
+  for (let i = 0; i < conditions.length; i++) {
+    const condition = conditions[i];
+    const passed = evaluateCondition(data, condition);
+    results.push(passed);
+
+    if (passed) {
+      matchedConditions.push({
+        index: i,
+        field: condition.field,
+        operator: condition.operator,
+        value: condition.value
+      });
+    }
+  }
+
+  let finalResult;
+  if (combineWith === 'and') {
+    finalResult = results.every(r => r);
+  } else {
+    finalResult = results.some(r => r);
+  }
+
+  const activeBranch = finalResult ? 'true' : 'false';
+
+  smartbotic.log.info(`IF condition evaluated to ${finalResult} (${matchedConditions.length}/${conditions.length} conditions matched) -> branch: ${activeBranch}`);
+
+  // Return data on the active branch key for conditional execution
+  return {
+    result: finalResult,
+    matchedConditions: matchedConditions,
+    data: data,
+    _activeBranch: activeBranch,
+    // Put the data under the active branch name for easy downstream access
+    [activeBranch]: data
+  };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, outputs, execute };

+ 182 - 0
nodes/core/loop.js

@@ -0,0 +1,182 @@
+/**
+ * @node loop
+ * @name Loop
+ * @category flow-control
+ * @version 1.0.0
+ * @description Iterate over arrays - executes connected nodes for each item
+ * @icon repeat
+ */
+
+// Define multiple outputs for loop body and done
+const outputs = [
+  { name: 'loop', displayName: 'Loop Body', type: 'any', color: '#3b82f6' },
+  { name: 'done', displayName: 'Done', type: 'any', color: '#22c55e' }
+];
+
+const configSchema = {
+  type: 'object',
+  properties: {
+    inputField: {
+      type: 'string',
+      title: 'Input Array Field',
+      description: 'Path to the array field to iterate over (e.g., data.items)',
+      default: 'data'
+    },
+    outputField: {
+      type: 'string',
+      title: 'Output Field Name',
+      description: 'Name for the collected results array',
+      default: 'results'
+    },
+    itemVariableName: {
+      type: 'string',
+      title: 'Item Variable Name',
+      description: 'Name for the current item in each iteration',
+      default: 'item'
+    },
+    indexVariableName: {
+      type: 'string',
+      title: 'Index Variable Name',
+      description: 'Name for the current index in each iteration',
+      default: 'index'
+    },
+    continueOnError: {
+      type: 'boolean',
+      title: 'Continue On Error',
+      description: 'Continue iterating even if one item fails',
+      default: true
+    },
+    maxIterations: {
+      type: 'number',
+      title: 'Max Iterations',
+      description: 'Maximum number of iterations (0 = unlimited)',
+      default: 0
+    }
+  }
+};
+
+const inputSchema = {
+  type: 'object',
+  properties: {
+    data: { type: 'any' }
+  }
+};
+
+const outputSchema = {
+  type: 'object',
+  properties: {
+    _isLoop: { type: 'boolean', description: 'Loop marker for engine' },
+    _items: { type: 'array', description: 'Array of items to iterate' },
+    _outputField: { type: 'string', description: 'Name for collected results' },
+    _itemVariable: { type: 'string', description: 'Variable name for current item' },
+    _indexVariable: { type: 'string', description: 'Variable name for current index' },
+    _continueOnError: { type: 'boolean', description: 'Continue on error flag' },
+    _activeBranch: { type: 'string', description: 'Active branch (loop or done)' },
+    currentItem: { type: 'any', description: 'Current item being processed' },
+    currentIndex: { type: 'number', description: 'Current iteration index' },
+    totalItems: { type: 'number', description: 'Total number of items' },
+    data: { type: 'any', description: 'Original input data' }
+  }
+};
+
+function getFieldValue(data, path) {
+  if (!path || path === 'data') return data;
+
+  const keys = path.split('.');
+  let value = data;
+
+  // Handle "data.field" paths by skipping "data" prefix
+  const startIndex = keys[0] === 'data' ? 1 : 0;
+
+  for (let i = startIndex; i < keys.length; i++) {
+    const key = keys[i];
+    if (value === null || value === undefined) return undefined;
+    if (typeof value === 'object' && key in value) {
+      value = value[key];
+    } else {
+      return undefined;
+    }
+  }
+
+  return value;
+}
+
+async function execute(config, input, context) {
+  const data = input.data || input;
+  const inputField = config.inputField || 'data';
+  const outputField = config.outputField || 'results';
+  const itemVariable = config.itemVariableName || 'item';
+  const indexVariable = config.indexVariableName || 'index';
+  const continueOnError = config.continueOnError !== false;
+  const maxIterations = config.maxIterations || 0;
+
+  // Get the array to iterate
+  let items = getFieldValue(data, inputField);
+
+  // Handle case where input itself is the array
+  if (!Array.isArray(items) && Array.isArray(data)) {
+    items = data;
+  }
+
+  // Validate input
+  if (!Array.isArray(items)) {
+    smartbotic.log.warn(`Loop node: Input at "${inputField}" is not an array, converting to single-item array`);
+    items = items !== undefined ? [items] : [];
+  }
+
+  // Apply max iterations limit
+  if (maxIterations > 0 && items.length > maxIterations) {
+    smartbotic.log.warn(`Loop node: Limiting iterations from ${items.length} to ${maxIterations}`);
+    items = items.slice(0, maxIterations);
+  }
+
+  const totalItems = items.length;
+
+  if (totalItems === 0) {
+    // Empty array - go directly to done
+    smartbotic.log.info('Loop node: Empty array, skipping to done');
+    return {
+      _isLoop: false,
+      _activeBranch: 'done',
+      [outputField]: [],
+      data: data,
+      totalItems: 0,
+      done: {
+        [outputField]: [],
+        data: data,
+        totalItems: 0
+      }
+    };
+  }
+
+  smartbotic.log.info(`Loop node: Starting iteration over ${totalItems} items`);
+
+  // Return loop control data for the engine
+  return {
+    _isLoop: true,
+    _items: items,
+    _outputField: outputField,
+    _itemVariable: itemVariable,
+    _indexVariable: indexVariable,
+    _continueOnError: continueOnError,
+    _activeBranch: 'loop',
+
+    // For first iteration (engine will update these)
+    currentItem: items[0],
+    currentIndex: 0,
+    totalItems: totalItems,
+    data: data,
+
+    // Loop output includes current item info
+    loop: {
+      [itemVariable]: items[0],
+      [indexVariable]: 0,
+      totalItems: totalItems,
+      isFirst: true,
+      isLast: totalItems === 1,
+      data: items[0]  // Current item, not original input
+    }
+  };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, outputs, execute };

+ 90 - 0
nodes/core/wait.js

@@ -0,0 +1,90 @@
+/**
+ * @node wait
+ * @name Wait
+ * @category flow-control
+ * @version 1.0.0
+ * @description Pause workflow execution for a specified duration
+ * @icon clock
+ */
+
+const configSchema = {
+  type: 'object',
+  properties: {
+    duration: {
+      type: 'number',
+      title: 'Duration',
+      description: 'How long to wait',
+      default: 1,
+      minimum: 0
+    },
+    unit: {
+      type: 'string',
+      title: 'Unit',
+      description: 'Time unit for the duration',
+      enum: ['milliseconds', 'seconds', 'minutes'],
+      default: 'seconds'
+    }
+  },
+  required: ['duration', 'unit']
+};
+
+const inputSchema = {
+  type: 'object',
+  properties: {
+    data: { type: 'any', description: 'Input data (passed through to output)' }
+  }
+};
+
+const outputSchema = {
+  type: 'object',
+  properties: {
+    data: { type: 'any', description: 'Input data passed through' },
+    waitedMs: { type: 'number', description: 'Actual wait time in milliseconds' }
+  }
+};
+
+const outputs = [
+  { name: 'main', displayName: 'Output', type: 'any', color: '#8b5cf6' }
+];
+
+async function execute(config, input, context) {
+  const duration = config.duration || 1;
+  const unit = config.unit || 'seconds';
+
+  // Convert to milliseconds
+  let waitMs = duration;
+  switch (unit) {
+    case 'seconds':
+      waitMs = duration * 1000;
+      break;
+    case 'minutes':
+      waitMs = duration * 60 * 1000;
+      break;
+    case 'milliseconds':
+    default:
+      waitMs = duration;
+      break;
+  }
+
+  // Cap at 5 minutes (300000ms) to prevent abuse
+  const maxWait = 300000;
+  if (waitMs > maxWait) {
+    smartbotic.log.warn(`Wait duration ${waitMs}ms exceeds maximum, capping at ${maxWait}ms`);
+    waitMs = maxWait;
+  }
+
+  smartbotic.log.info(`Wait node: Waiting for ${duration} ${unit} (${waitMs}ms)`);
+
+  // Perform the wait
+  smartbotic.utils.sleep(waitMs);
+
+  smartbotic.log.info('Wait node: Wait completed');
+
+  // Pass through input data
+  return {
+    data: input.data || input,
+    waitedMs: waitMs
+  };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, outputs, execute };

+ 121 - 0
nodes/imap/imap-fetch.js

@@ -0,0 +1,121 @@
+/**
+ * @node imap-fetch
+ * @name IMAP Fetch Email
+ * @category email
+ * @version 1.0.1
+ * @description Fetch the full content of an email from an IMAP server.
+ * @icon download
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        credentialId: {
+            type: 'string',
+            title: 'IMAP Credential',
+            description: 'Select the IMAP credential to use for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'imap' }
+            }
+        },
+        mailbox: {
+            type: 'string',
+            title: 'Mailbox',
+            description: 'The mailbox containing the email',
+            default: 'INBOX'
+        },
+        uid: {
+            type: 'string',
+            title: 'Email UID',
+            description: 'The unique identifier of the email to fetch (can be provided from input)'
+        },
+        includeRaw: {
+            type: 'boolean',
+            title: 'Include Raw',
+            description: 'Include the raw email content in the output',
+            default: false
+        }
+    },
+    required: ['credentialId']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        uid: {
+            type: 'string',
+            description: 'Email UID to fetch'
+        },
+        mailbox: {
+            type: 'string',
+            description: 'Override mailbox from input'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        uid: { type: 'string' },
+        subject: { type: 'string' },
+        from: { type: 'string' },
+        to: { type: 'string' },
+        date: { type: 'string' },
+        body: { type: 'string' },
+        raw: { type: 'string' }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Email Content', type: 'object', color: '#8b5cf6' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const credentialId = config.credentialId;
+        const mailbox = input.mailbox || config.mailbox || 'INBOX';
+        const uid = input.uid || config.uid;
+        const includeRaw = config.includeRaw || false;
+
+        if (!credentialId) {
+            throw new Error('IMAP credential is required');
+        }
+
+        if (!uid) {
+            throw new Error('Email UID is required');
+        }
+
+        // Fetch the email
+        const fetchResult = smartbotic.imap.fetch({
+            credentialId,
+            mailbox,
+            uid
+        });
+
+        if (!fetchResult.success) {
+            throw new Error(`IMAP fetch failed: ${fetchResult.error}`);
+        }
+
+        const result = {
+            uid: fetchResult.uid,
+            subject: fetchResult.subject,
+            from: fetchResult.from,
+            to: fetchResult.to,
+            date: fetchResult.date,
+            body: fetchResult.body,
+            mailbox
+        };
+
+        if (includeRaw) {
+            result.raw = fetchResult.raw;
+        }
+
+        return result;
+    }
+};

+ 164 - 0
nodes/imap/imap-modify.js

@@ -0,0 +1,164 @@
+/**
+ * @node imap-modify
+ * @name IMAP Modify Email
+ * @category email
+ * @version 1.0.1
+ * @description Modify emails on an IMAP server - mark as read/unread, flag, delete, or move.
+ * @icon edit
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        credentialId: {
+            type: 'string',
+            title: 'IMAP Credential',
+            description: 'Select the IMAP credential to use for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'imap' }
+            }
+        },
+        mailbox: {
+            type: 'string',
+            title: 'Mailbox',
+            description: 'The mailbox containing the email',
+            default: 'INBOX'
+        },
+        uid: {
+            type: 'string',
+            title: 'Email UID',
+            description: 'The unique identifier of the email to modify (can be provided from input)'
+        },
+        action: {
+            type: 'string',
+            title: 'Action',
+            description: 'The action to perform on the email',
+            enum: ['markRead', 'markUnread', 'flag', 'unflag', 'delete', 'move'],
+            default: 'markRead'
+        },
+        targetMailbox: {
+            type: 'string',
+            title: 'Target Mailbox',
+            description: 'The target mailbox for move action (e.g., Archive, Trash)'
+        }
+    },
+    required: ['credentialId', 'action']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        uid: {
+            type: 'string',
+            description: 'Email UID to modify'
+        },
+        uids: {
+            type: 'array',
+            items: { type: 'string' },
+            description: 'Array of email UIDs to modify (batch operation)'
+        },
+        mailbox: {
+            type: 'string',
+            description: 'Override mailbox from input'
+        },
+        action: {
+            type: 'string',
+            description: 'Override action from input'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        success: { type: 'boolean' },
+        action: { type: 'string' },
+        processed: { type: 'integer' },
+        results: { type: 'array' }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Result', type: 'object', color: '#f59e0b' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const credentialId = config.credentialId;
+        const mailbox = input.mailbox || config.mailbox || 'INBOX';
+        const action = input.action || config.action;
+        const targetMailbox = config.targetMailbox;
+
+        if (!credentialId) {
+            throw new Error('IMAP credential is required');
+        }
+
+        if (!action) {
+            throw new Error('Action is required');
+        }
+
+        // Support both single UID and array of UIDs
+        let uids = [];
+        if (input.uids && Array.isArray(input.uids)) {
+            uids = input.uids;
+        } else if (input.uid) {
+            uids = [input.uid];
+        } else if (config.uid) {
+            uids = [config.uid];
+        }
+
+        if (uids.length === 0) {
+            throw new Error('At least one email UID is required');
+        }
+
+        // Validate move action has target
+        if (action === 'move' && !targetMailbox) {
+            throw new Error('Target mailbox is required for move action');
+        }
+
+        const results = [];
+        let successCount = 0;
+
+        // Process each UID
+        for (const uid of uids) {
+            const modifyOptions = {
+                credentialId,
+                mailbox,
+                uid,
+                action
+            };
+
+            if (action === 'move') {
+                modifyOptions.targetMailbox = targetMailbox;
+            }
+
+            const modifyResult = smartbotic.imap.modify(modifyOptions);
+
+            results.push({
+                uid,
+                success: modifyResult.success,
+                error: modifyResult.error
+            });
+
+            if (modifyResult.success) {
+                successCount++;
+            }
+        }
+
+        return {
+            success: successCount === uids.length,
+            action,
+            mailbox,
+            processed: uids.length,
+            succeeded: successCount,
+            failed: uids.length - successCount,
+            results
+        };
+    }
+};

+ 190 - 0
nodes/imap/imap-search.js

@@ -0,0 +1,190 @@
+/**
+ * @node imap-search
+ * @name IMAP Search
+ * @category email
+ * @version 1.0.1
+ * @description Search and list emails from an IMAP server with various filters.
+ * @icon search
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        credentialId: {
+            type: 'string',
+            title: 'IMAP Credential',
+            description: 'Select the IMAP credential to use for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'imap' }
+            }
+        },
+        mailbox: {
+            type: 'string',
+            title: 'Mailbox',
+            description: 'The mailbox to search (e.g., INBOX, Sent, Drafts)',
+            default: 'INBOX'
+        },
+        criteria: {
+            type: 'string',
+            title: 'Search Criteria',
+            description: 'IMAP search criteria (e.g., ALL, UNSEEN, FROM user@example.com)',
+            default: 'ALL'
+        },
+        filterUnread: {
+            type: 'boolean',
+            title: 'Only Unread',
+            description: 'Add UNSEEN filter to search criteria',
+            default: false
+        },
+        filterFrom: {
+            type: 'string',
+            title: 'Filter by From',
+            description: 'Filter emails from this address (optional)'
+        },
+        filterSubject: {
+            type: 'string',
+            title: 'Filter by Subject',
+            description: 'Filter emails containing this text in subject (optional)'
+        },
+        filterSince: {
+            type: 'string',
+            title: 'Since Date',
+            description: 'Filter emails since this date (DD-Mon-YYYY format, e.g., 01-Jan-2024)'
+        },
+        limit: {
+            type: 'integer',
+            title: 'Limit',
+            description: 'Maximum number of emails to return',
+            default: 50,
+            minimum: 1,
+            maximum: 1000
+        },
+        fetchContent: {
+            type: 'boolean',
+            title: 'Fetch Content',
+            description: 'Fetch full email content for each result',
+            default: false
+        }
+    },
+    required: ['credentialId']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        criteria: {
+            type: 'string',
+            description: 'Override search criteria from input'
+        },
+        mailbox: {
+            type: 'string',
+            description: 'Override mailbox from input'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        uids: {
+            type: 'array',
+            items: { type: 'string' },
+            description: 'Array of message UIDs'
+        },
+        count: {
+            type: 'integer',
+            description: 'Number of emails found'
+        },
+        emails: {
+            type: 'array',
+            description: 'Array of email objects (if fetchContent is enabled)'
+        }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Search Results', type: 'object', color: '#3b82f6' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const credentialId = config.credentialId;
+        const mailbox = input.mailbox || config.mailbox || 'INBOX';
+        const limit = config.limit || 50;
+        const fetchContent = config.fetchContent || false;
+
+        if (!credentialId) {
+            throw new Error('IMAP credential is required');
+        }
+
+        // Build search criteria
+        let criteria = input.criteria || config.criteria || 'ALL';
+
+        if (config.filterUnread && criteria.indexOf('UNSEEN') === -1) {
+            criteria = criteria === 'ALL' ? 'UNSEEN' : `${criteria} UNSEEN`;
+        }
+
+        if (config.filterFrom) {
+            criteria += ` FROM "${config.filterFrom}"`;
+        }
+
+        if (config.filterSubject) {
+            criteria += ` SUBJECT "${config.filterSubject}"`;
+        }
+
+        if (config.filterSince) {
+            criteria += ` SINCE "${config.filterSince}"`;
+        }
+
+        // Perform search
+        const searchResult = smartbotic.imap.search({
+            credentialId,
+            mailbox,
+            criteria,
+            limit
+        });
+
+        if (!searchResult.success) {
+            throw new Error(`IMAP search failed: ${searchResult.error}`);
+        }
+
+        const result = {
+            uids: searchResult.uids,
+            count: searchResult.count,
+            mailbox,
+            criteria
+        };
+
+        // Optionally fetch content for each email
+        if (fetchContent && searchResult.uids.length > 0) {
+            result.emails = [];
+
+            for (const uid of searchResult.uids) {
+                const fetchResult = smartbotic.imap.fetch({
+                    credentialId,
+                    mailbox,
+                    uid
+                });
+
+                if (fetchResult.success) {
+                    result.emails.push({
+                        uid: fetchResult.uid,
+                        subject: fetchResult.subject,
+                        from: fetchResult.from,
+                        to: fetchResult.to,
+                        date: fetchResult.date,
+                        body: fetchResult.body
+                    });
+                }
+            }
+        }
+
+        return result;
+    }
+};

+ 148 - 0
nodes/imap/imap-trigger.js

@@ -0,0 +1,148 @@
+/**
+ * @node imap-trigger
+ * @name IMAP Email Trigger
+ * @category email
+ * @version 1.0.1
+ * @description Triggers when new emails are received. Polls the IMAP server for new messages.
+ * @trigger
+ * @icon mail
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        credentialId: {
+            type: 'string',
+            title: 'IMAP Credential',
+            description: 'Select the IMAP credential to use for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'imap' }
+            }
+        },
+        mailbox: {
+            type: 'string',
+            title: 'Mailbox',
+            description: 'The mailbox to monitor (e.g., INBOX)',
+            default: 'INBOX'
+        },
+        filterUnread: {
+            type: 'boolean',
+            title: 'Only Unread',
+            description: 'Only trigger on unread (unseen) emails',
+            default: true
+        },
+        filterFrom: {
+            type: 'string',
+            title: 'Filter by From',
+            description: 'Only trigger on emails from this address (optional)'
+        },
+        filterSubject: {
+            type: 'string',
+            title: 'Filter by Subject',
+            description: 'Only trigger on emails containing this text in subject (optional)'
+        },
+        markAsRead: {
+            type: 'boolean',
+            title: 'Mark as Read',
+            description: 'Mark emails as read after processing',
+            default: true
+        },
+        limit: {
+            type: 'integer',
+            title: 'Max Emails',
+            description: 'Maximum number of emails to process per trigger',
+            default: 10,
+            minimum: 1,
+            maximum: 100
+        }
+    },
+    required: ['credentialId']
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Email Received', type: 'object', color: '#22c55e' }
+];
+
+module.exports = {
+    configSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const {
+            credentialId,
+            mailbox = 'INBOX',
+            filterUnread = true,
+            filterFrom,
+            filterSubject,
+            markAsRead = true,
+            limit = 10
+        } = config;
+
+        if (!credentialId) {
+            throw new Error('IMAP credential is required');
+        }
+
+        // Build search criteria
+        let criteria = filterUnread ? 'UNSEEN' : 'ALL';
+
+        if (filterFrom) {
+            criteria += ` FROM "${filterFrom}"`;
+        }
+
+        if (filterSubject) {
+            criteria += ` SUBJECT "${filterSubject}"`;
+        }
+
+        // Search for emails
+        const searchResult = smartbotic.imap.search({
+            credentialId,
+            mailbox,
+            criteria,
+            limit
+        });
+
+        if (!searchResult.success) {
+            throw new Error(`IMAP search failed: ${searchResult.error}`);
+        }
+
+        const emails = [];
+
+        // Fetch each email
+        for (const uid of searchResult.uids) {
+            const fetchResult = smartbotic.imap.fetch({
+                credentialId,
+                mailbox,
+                uid
+            });
+
+            if (fetchResult.success) {
+                emails.push({
+                    uid: fetchResult.uid,
+                    subject: fetchResult.subject,
+                    from: fetchResult.from,
+                    to: fetchResult.to,
+                    date: fetchResult.date,
+                    body: fetchResult.body
+                });
+
+                // Mark as read if configured
+                if (markAsRead) {
+                    smartbotic.imap.modify({
+                        credentialId,
+                        mailbox,
+                        uid,
+                        action: 'markRead'
+                    });
+                }
+            }
+        }
+
+        return {
+            emails,
+            count: emails.length,
+            mailbox,
+            timestamp: Date.now()
+        };
+    }
+};

+ 106 - 0
nodes/ocr/ocr-delete-job.js

@@ -0,0 +1,106 @@
+/**
+ * @node ocr-delete-job
+ * @name OCR Delete Job
+ * @category ocr
+ * @version 1.0.1
+ * @description Delete an OCR job
+ * @icon trash-2
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        },
+        jobId: {
+            type: 'string',
+            title: 'Job ID',
+            description: 'The ID of the job to delete (can be provided from input)'
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        jobId: {
+            type: 'string',
+            description: 'Job ID to delete (overrides config)'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        jobId: { type: 'string' },
+        deleted: { type: 'boolean' }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Deleted', type: 'object', color: '#ef4444' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+        const jobId = input.jobId || config.jobId;
+
+        if (!jobId) {
+            throw new Error('Job ID is required');
+        }
+
+        const headers = {};
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+        }
+
+        smartbotic.log.info(`OCR Delete Job: ${jobId}`);
+
+        const response = smartbotic.http.request({
+            method: 'DELETE',
+            url: `${baseUrl}/api/jobs/${encodeURIComponent(jobId)}`,
+            headers: headers,
+            timeout: 30000
+        });
+
+        if (response.status === 404) {
+            throw new Error(`Job not found: ${jobId}`);
+        }
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        return {
+            jobId: jobId,
+            deleted: true
+        };
+    }
+};

+ 102 - 0
nodes/ocr/ocr-job-stats.js

@@ -0,0 +1,102 @@
+/**
+ * @node ocr-job-stats
+ * @name OCR Job Stats
+ * @category ocr
+ * @version 1.0.1
+ * @description Get OCR job queue statistics
+ * @icon bar-chart-2
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {}
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        stats: {
+            type: 'object',
+            properties: {
+                pending: { type: 'number' },
+                processing: { type: 'number' },
+                completed: { type: 'number' },
+                failed: { type: 'number' }
+            }
+        }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Stats', type: 'object', color: '#f59e0b' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+
+        // Build headers
+        const headers = {};
+
+        smartbotic.log.info(`OCR Job Stats - credentialId: ${config.credentialId || 'NOT SET'}`);
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            smartbotic.log.info(`Credential lookup result: ${JSON.stringify(auth)}`);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+            smartbotic.log.info(`Auth header set: ${auth.headerName}`);
+        } else {
+            smartbotic.log.warn('No credential selected');
+        }
+
+        smartbotic.log.info(`OCR Get Job Stats from ${baseUrl}`);
+
+        const response = smartbotic.http.request({
+            method: 'GET',
+            url: `${baseUrl}/api/jobs/stats`,
+            headers: headers,
+            timeout: 30000
+        });
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
+
+        return {
+            stats: data.stats || data
+        };
+    }
+};

+ 126 - 0
nodes/ocr/ocr-job-status.js

@@ -0,0 +1,126 @@
+/**
+ * @node ocr-job-status
+ * @name OCR Job Status
+ * @category ocr
+ * @version 1.0.1
+ * @description Get the status and results of an OCR job
+ * @icon clipboard-check
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        },
+        jobId: {
+            type: 'string',
+            title: 'Job ID',
+            description: 'The ID of the job to check (can be provided from input)'
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        jobId: {
+            type: 'string',
+            description: 'Job ID to check (overrides config)'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        job: {
+            type: 'object',
+            properties: {
+                id: { type: 'string' },
+                status: { type: 'string' },
+                filename: { type: 'string' },
+                created_at: { type: 'string' },
+                completed_at: { type: 'string' }
+            }
+        },
+        result: {
+            type: 'object',
+            properties: {
+                text: { type: 'string' }
+            }
+        },
+        usage: { type: 'object' },
+        timings: { type: 'object' }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Job Status', type: 'object', color: '#3b82f6' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+        const jobId = input.jobId || config.jobId;
+
+        if (!jobId) {
+            throw new Error('Job ID is required');
+        }
+
+        const headers = {};
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+        }
+
+        smartbotic.log.info(`OCR Get Job Status: ${jobId}`);
+
+        const response = smartbotic.http.request({
+            method: 'GET',
+            url: `${baseUrl}/api/jobs/${encodeURIComponent(jobId)}`,
+            headers: headers,
+            timeout: 30000
+        });
+
+        if (response.status === 404) {
+            throw new Error(`Job not found: ${jobId}`);
+        }
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
+
+        return {
+            job: data.job,
+            result: data.result,
+            usage: data.usage,
+            timings: data.timings
+        };
+    }
+};

+ 100 - 0
nodes/ocr/ocr-limitations.js

@@ -0,0 +1,100 @@
+/**
+ * @node ocr-limitations
+ * @name OCR Limitations
+ * @category ocr
+ * @version 1.0.1
+ * @description Get user limitations and quotas for OCR API
+ * @icon info
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {}
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        limitations: {
+            type: 'object',
+            properties: {
+                allowedFileTypes: {
+                    type: 'array',
+                    items: { type: 'string' }
+                },
+                maxFileSizeMb: { type: 'number' },
+                maxPdfPages: { type: 'number' },
+                maxConcurrentJobs: { type: 'number' },
+                dailyLimit: { type: 'number' },
+                monthlyLimit: { type: 'number' }
+            }
+        }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Limitations', type: 'object', color: '#14b8a6' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+
+        const headers = {};
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+        }
+
+        smartbotic.log.info(`OCR Get Limitations from ${baseUrl}`);
+
+        const response = smartbotic.http.request({
+            method: 'GET',
+            url: `${baseUrl}/api/ocr/limitations`,
+            headers: headers,
+            timeout: 30000
+        });
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
+
+        return {
+            limitations: data.limitations || data
+        };
+    }
+};

+ 140 - 0
nodes/ocr/ocr-list-jobs.js

@@ -0,0 +1,140 @@
+/**
+ * @node ocr-list-jobs
+ * @name OCR List Jobs
+ * @category ocr
+ * @version 1.0.1
+ * @description List OCR jobs with optional status filter
+ * @icon list
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        },
+        statusFilter: {
+            type: 'string',
+            title: 'Status Filter',
+            enum: ['', 'pending', 'processing', 'completed', 'failed'],
+            default: '',
+            description: 'Filter jobs by status (empty for all)'
+        },
+        limit: {
+            type: 'number',
+            title: 'Limit',
+            default: 50,
+            description: 'Maximum number of jobs to return'
+        },
+        offset: {
+            type: 'number',
+            title: 'Offset',
+            default: 0,
+            description: 'Number of jobs to skip'
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        statusFilter: {
+            type: 'string',
+            description: 'Override status filter from input'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        jobs: {
+            type: 'array',
+            items: {
+                type: 'object',
+                properties: {
+                    id: { type: 'string' },
+                    status: { type: 'string' },
+                    filename: { type: 'string' },
+                    created_at: { type: 'string' }
+                }
+            }
+        },
+        total: { type: 'number' }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Jobs List', type: 'object', color: '#6366f1' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+        const statusFilter = input.statusFilter || config.statusFilter || '';
+        const limit = config.limit || 50;
+        const offset = config.offset || 0;
+
+        const headers = {};
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+        }
+
+        const params = [];
+        if (statusFilter) {
+            params.push(`status=${encodeURIComponent(statusFilter)}`);
+        }
+        if (limit) {
+            params.push(`limit=${limit}`);
+        }
+        if (offset) {
+            params.push(`offset=${offset}`);
+        }
+
+        const queryString = params.length > 0 ? '?' + params.join('&') : '';
+
+        smartbotic.log.info(`OCR List Jobs${statusFilter ? ` (status: ${statusFilter})` : ''}`);
+
+        const response = smartbotic.http.request({
+            method: 'GET',
+            url: `${baseUrl}/api/jobs${queryString}`,
+            headers: headers,
+            timeout: 30000
+        });
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
+
+        return {
+            jobs: data.jobs || [],
+            total: data.total || (data.jobs ? data.jobs.length : 0)
+        };
+    }
+};

+ 114 - 0
nodes/ocr/ocr-retry-job.js

@@ -0,0 +1,114 @@
+/**
+ * @node ocr-retry-job
+ * @name OCR Retry Job
+ * @category ocr
+ * @version 1.0.1
+ * @description Retry a failed OCR job
+ * @icon refresh-cw
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        },
+        jobId: {
+            type: 'string',
+            title: 'Job ID',
+            description: 'The ID of the job to retry (can be provided from input)'
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        jobId: {
+            type: 'string',
+            description: 'Job ID to retry (overrides config)'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        job: {
+            type: 'object',
+            properties: {
+                id: { type: 'string' },
+                status: { type: 'string' },
+                filename: { type: 'string' },
+                created_at: { type: 'string' }
+            }
+        }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Retried', type: 'object', color: '#8b5cf6' }
+];
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+        const jobId = input.jobId || config.jobId;
+
+        if (!jobId) {
+            throw new Error('Job ID is required');
+        }
+
+        const headers = {};
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+        }
+
+        smartbotic.log.info(`OCR Retry Job: ${jobId}`);
+
+        const response = smartbotic.http.request({
+            method: 'POST',
+            url: `${baseUrl}/api/jobs/${encodeURIComponent(jobId)}/retry`,
+            headers: headers,
+            timeout: 30000
+        });
+
+        if (response.status === 404) {
+            throw new Error(`Job not found: ${jobId}`);
+        }
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
+
+        return {
+            job: data.job || data
+        };
+    }
+};

+ 223 - 0
nodes/ocr/ocr-upload.js

@@ -0,0 +1,223 @@
+/**
+ * @node ocr-upload
+ * @name OCR Upload
+ * @category ocr
+ * @version 1.0.1
+ * @description Upload a file for OCR processing via pdftoimageapi
+ * @icon file-scan
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        baseUrl: {
+            type: 'string',
+            title: 'API Base URL',
+            default: 'http://localhost:3000',
+            description: 'Base URL of the OCR API'
+        },
+        credentialId: {
+            type: 'string',
+            title: 'Credential',
+            description: 'Bearer or API Key credential for authentication',
+            dynamicOptions: {
+                source: 'credentials',
+                filter: { type: 'api_key' }
+            }
+        },
+        processingMode: {
+            type: 'string',
+            title: 'Processing Mode',
+            enum: ['auto', 'single', 'batch', 'smart'],
+            default: 'auto',
+            description: 'How to process the uploaded file'
+        },
+        batchSize: {
+            type: 'number',
+            title: 'Batch Size',
+            default: 5,
+            description: 'Number of pages per batch (for batch mode)'
+        },
+        targetWidth: {
+            type: 'number',
+            title: 'Target Width',
+            description: 'Target width in pixels for image conversion'
+        },
+        groupId: {
+            type: 'string',
+            title: 'Group ID',
+            description: 'Optional group ID to associate jobs'
+        }
+    },
+    required: ['baseUrl']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        file: {
+            type: 'object',
+            description: 'Binary file object with type, data (base64), mimeType, filename, size'
+        },
+        storage: {
+            type: 'object',
+            description: 'Storage reference to retrieve file from database',
+            properties: {
+                collection: { type: 'string' },
+                id: { type: 'string' }
+            }
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        job: {
+            type: 'object',
+            properties: {
+                id: { type: 'string' },
+                status: { type: 'string' },
+                filename: { type: 'string' },
+                created_at: { type: 'string' }
+            }
+        }
+    }
+};
+
+const outputs = [
+    { name: 'main', displayName: 'Job Created', type: 'object', color: '#10b981' }
+];
+
+function isBinaryInput(input) {
+    return input && input.type === 'binary' && typeof input.data === 'string';
+}
+
+function generateBoundary() {
+    return '----SmartBoticBoundary' + smartbotic.utils.uuid().replace(/-/g, '');
+}
+
+function buildMultipartBody(boundary, file, options) {
+    const crlf = '\r\n';
+    let body = '';
+
+    body += `--${boundary}${crlf}`;
+    body += `Content-Disposition: form-data; name="file"; filename="${file.filename}"${crlf}`;
+    body += `Content-Type: ${file.mimeType}${crlf}`;
+    body += `Content-Transfer-Encoding: base64${crlf}`;
+    body += crlf;
+    body += file.data;
+    body += crlf;
+
+    if (options.processingMode && options.processingMode !== 'auto') {
+        body += `--${boundary}${crlf}`;
+        body += `Content-Disposition: form-data; name="processing_mode"${crlf}`;
+        body += crlf;
+        body += options.processingMode;
+        body += crlf;
+    }
+
+    if (options.batchSize) {
+        body += `--${boundary}${crlf}`;
+        body += `Content-Disposition: form-data; name="batch_size"${crlf}`;
+        body += crlf;
+        body += String(options.batchSize);
+        body += crlf;
+    }
+
+    if (options.targetWidth) {
+        body += `--${boundary}${crlf}`;
+        body += `Content-Disposition: form-data; name="target_width"${crlf}`;
+        body += crlf;
+        body += String(options.targetWidth);
+        body += crlf;
+    }
+
+    if (options.groupId) {
+        body += `--${boundary}${crlf}`;
+        body += `Content-Disposition: form-data; name="group_id"${crlf}`;
+        body += crlf;
+        body += options.groupId;
+        body += crlf;
+    }
+
+    body += `--${boundary}--${crlf}`;
+
+    return body;
+}
+
+module.exports = {
+    configSchema,
+    inputSchema,
+    outputSchema,
+    outputs,
+
+    async execute(config, input, context) {
+        const baseUrl = (config.baseUrl || 'http://localhost:3000').replace(/\/$/, '');
+        let file = null;
+
+        if (isBinaryInput(input.file)) {
+            file = input.file;
+        } else if (isBinaryInput(input)) {
+            file = input;
+        } else if (input.storage && input.storage.collection && input.storage.id) {
+            const storageResult = smartbotic.storage.get(input.storage.collection, input.storage.id);
+            if (!storageResult.found) {
+                throw new Error(`File not found in storage: ${input.storage.collection}/${input.storage.id}`);
+            }
+            const doc = storageResult.document;
+            file = {
+                type: 'binary',
+                data: doc.data,
+                mimeType: doc.mimeType,
+                filename: doc.filename,
+                size: doc.size
+            };
+        }
+
+        if (!file) {
+            throw new Error('No file provided. Provide either a binary file object or a storage reference.');
+        }
+
+        const headers = {};
+
+        if (config.credentialId) {
+            const auth = smartbotic.credentials.get(config.credentialId);
+            if (!auth.success) {
+                throw new Error(`Failed to load credential: ${auth.error}`);
+            }
+            headers[auth.headerName] = auth.headerValue;
+        }
+
+        const boundary = generateBoundary();
+        headers['Content-Type'] = `multipart/form-data; boundary=${boundary}`;
+
+        const body = buildMultipartBody(boundary, file, {
+            processingMode: config.processingMode,
+            batchSize: config.batchSize,
+            targetWidth: config.targetWidth,
+            groupId: config.groupId
+        });
+
+        smartbotic.log.info(`OCR Upload: ${file.filename} (${file.size} bytes)`);
+
+        const response = smartbotic.http.request({
+            method: 'POST',
+            url: `${baseUrl}/api/ocr/upload`,
+            headers: headers,
+            body: body,
+            timeout: 120000
+        });
+
+        if (response.status < 200 || response.status >= 300) {
+            const errorMsg = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
+            if (response.status === 401 && !config.credentialId) { throw new Error('Authentication required - please select a credential in node settings'); } throw new Error(`OCR API error (${response.status}): ${errorMsg}`);
+        }
+
+        const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
+
+        return {
+            job: data.job || data
+        };
+    }
+};

+ 69 - 0
nodes/test/utils-test.js

@@ -0,0 +1,69 @@
+/**
+ * @node utils-test
+ * @name Utils Test
+ * @description Test node for smartbotic.utils API
+ * @category development
+ * @version 1.0.0
+ * @trigger false
+ */
+
+const configSchema = {};
+const inputSchema = {};
+const outputSchema = {};
+
+async function execute(config, input, context) {
+    const results = {};
+
+    // Test uuid()
+    results.uuid = smartbotic.utils.uuid();
+    smartbotic.log.info("Generated UUID: " + results.uuid);
+
+    // Test interpolate()
+    const template = "Hello {{name}}, your order #{{order.id}} for {{order.items.0}} is ready!";
+    const data = {
+        name: "John",
+        order: {
+            id: 12345,
+            items: ["Widget", "Gadget"]
+        }
+    };
+    results.interpolated = smartbotic.utils.interpolate(template, data);
+    smartbotic.log.info("Interpolated: " + results.interpolated);
+
+    // Test deepClone()
+    const original = { a: 1, b: { c: 2 } };
+    const cloned = smartbotic.utils.deepClone(original);
+    cloned.b.c = 999;  // Modify clone
+    results.deepClone = {
+        original: original,
+        cloned: cloned,
+        independentCheck: original.b.c === 2  // Should be true if deep cloned properly
+    };
+    smartbotic.log.info("Deep clone check: original.b.c = " + original.b.c + ", cloned.b.c = " + cloned.b.c);
+
+    // Test isEmpty()
+    results.isEmpty = {
+        emptyString: smartbotic.utils.isEmpty(""),
+        nonEmptyString: smartbotic.utils.isEmpty("hello"),
+        emptyArray: smartbotic.utils.isEmpty([]),
+        nonEmptyArray: smartbotic.utils.isEmpty([1, 2]),
+        emptyObject: smartbotic.utils.isEmpty({}),
+        nonEmptyObject: smartbotic.utils.isEmpty({ a: 1 }),
+        nullValue: smartbotic.utils.isEmpty(null),
+        undefinedValue: smartbotic.utils.isEmpty(undefined)
+    };
+    smartbotic.log.info("isEmpty tests: " + JSON.stringify(results.isEmpty));
+
+    // Test pick()
+    const obj = { a: 1, b: 2, c: 3, d: 4 };
+    results.pick = smartbotic.utils.pick(obj, ["a", "c"]);
+    smartbotic.log.info("pick result: " + JSON.stringify(results.pick));
+
+    // Test omit()
+    results.omit = smartbotic.utils.omit(obj, ["b", "d"]);
+    smartbotic.log.info("omit result: " + JSON.stringify(results.omit));
+
+    return results;
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };

+ 45 - 0
nodes/triggers/click-trigger.js

@@ -0,0 +1,45 @@
+/**
+ * @node click-trigger
+ * @name Click Trigger
+ * @category triggers
+ * @version 1.0.0
+ * @description Manual execution trigger - starts workflow when clicked
+ * @trigger
+ * @icon play
+ */
+
+const configSchema = {
+  type: 'object',
+  properties: {
+    label: {
+      type: 'string',
+      title: 'Button Label',
+      default: 'Execute'
+    }
+  }
+};
+
+const inputSchema = {
+  type: 'object',
+  properties: {}
+};
+
+const outputSchema = {
+  type: 'object',
+  properties: {
+    timestamp: { type: 'number' },
+    triggeredBy: { type: 'string' }
+  }
+};
+
+async function execute(config, input, context) {
+  smartbotic.log.info('Click trigger activated');
+
+  return {
+    timestamp: Date.now(),
+    triggeredBy: 'manual',
+    executionId: context.executionId || smartbotic.utils.uuid()
+  };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };

+ 49 - 0
proto/common.proto

@@ -0,0 +1,49 @@
+syntax = "proto3";
+
+package smartbotic.proto;
+
+option cc_enable_arenas = true;
+
+// Common timestamp message
+message Timestamp {
+    int64 milliseconds = 1;
+}
+
+// Error details
+message Error {
+    int32 code = 1;
+    string message = 2;
+    string details = 3;  // JSON string for additional info
+}
+
+// Pagination request
+message PaginationRequest {
+    int32 page = 1;
+    int32 page_size = 2;
+    string cursor = 3;  // Alternative to offset pagination
+}
+
+// Pagination response
+message PaginationResponse {
+    int32 total_count = 1;
+    int32 page = 2;
+    int32 page_size = 3;
+    bool has_more = 4;
+    string next_cursor = 5;
+}
+
+// Generic key-value pair
+message KeyValue {
+    string key = 1;
+    string value = 2;
+}
+
+// Request metadata
+message RequestMeta {
+    string request_id = 1;
+    string user_id = 2;
+    map<string, string> headers = 3;
+}
+
+// Empty message for void returns
+message Empty {}

+ 66 - 0
proto/credentials.proto

@@ -0,0 +1,66 @@
+syntax = "proto3";
+
+package smartbotic.proto;
+
+option cc_enable_arenas = true;
+
+// Credential service for runners to fetch authentication headers
+service CredentialService {
+    // Get HTTP authentication header for a credential
+    rpc GetCredentialAuth(GetCredentialAuthRequest) returns (GetCredentialAuthResponse);
+
+    // Get IMAP credentials for a credential
+    rpc GetImapCredentials(GetImapCredentialsRequest) returns (GetImapCredentialsResponse);
+
+    // List available credentials (metadata only)
+    rpc ListCredentials(ListCredentialsRequest) returns (ListCredentialsResponse);
+}
+
+// Request to get credential auth header
+message GetCredentialAuthRequest {
+    string credential_id = 1;
+    string workflow_id = 2;  // For access control verification
+}
+
+// Response with auth header
+message GetCredentialAuthResponse {
+    bool success = 1;
+    string header_name = 2;   // e.g., "Authorization"
+    string header_value = 3;  // e.g., "Bearer token123"
+    string error = 4;         // Error message if success is false
+}
+
+// Credential info (metadata only, no secrets)
+message CredentialInfo {
+    string id = 1;
+    string name = 2;
+    string type = 3;  // "basic", "bearer", "api_key", "oauth2", "imap"
+    string description = 4;
+}
+
+// Request to get IMAP credentials
+message GetImapCredentialsRequest {
+    string credential_id = 1;
+    string workflow_id = 2;  // For access control verification
+}
+
+// Response with IMAP credentials
+message GetImapCredentialsResponse {
+    bool success = 1;
+    string host = 2;
+    int32 port = 3;
+    string username = 4;
+    string password = 5;
+    bool use_ssl = 6;
+    string error = 7;  // Error message if success is false
+}
+
+// Request to list credentials
+message ListCredentialsRequest {
+    string workflow_id = 1;  // Optional: filter by workflow access
+}
+
+// Response with credential list
+message ListCredentialsResponse {
+    repeated CredentialInfo credentials = 1;
+}

+ 298 - 0
proto/runner.proto

@@ -0,0 +1,298 @@
+syntax = "proto3";
+
+package smartbotic.proto;
+
+import "common.proto";
+import "workflow.proto";
+
+option cc_enable_arenas = true;
+
+// Runner status
+enum RunnerStatus {
+    RUNNER_STATUS_UNSPECIFIED = 0;
+    RUNNER_STATUS_ONLINE = 1;
+    RUNNER_STATUS_BUSY = 2;
+    RUNNER_STATUS_OFFLINE = 3;
+    RUNNER_STATUS_DRAINING = 4;  // Not accepting new work
+}
+
+// Runner metrics
+message RunnerMetrics {
+    int32 active_executions = 1;
+    int32 max_executions = 2;
+    int64 memory_used_bytes = 3;
+    int64 memory_total_bytes = 4;
+    double cpu_percent = 5;
+    int64 total_executions = 6;
+    int64 failed_executions = 7;
+    double avg_execution_time_ms = 8;
+}
+
+// Runner capabilities
+message RunnerCapabilities {
+    repeated string node_types = 1;  // Supported node types
+    int32 max_memory_per_script_mb = 2;
+    int32 max_execution_timeout_sec = 3;
+}
+
+// Runner registration
+message Runner {
+    string id = 1;
+    string address = 2;  // host:port
+    RunnerStatus status = 3;
+    int64 registered_at = 4;
+    int64 last_heartbeat = 5;
+    RunnerMetrics metrics = 6;
+    RunnerCapabilities capabilities = 7;
+    map<string, string> labels = 8;
+}
+
+// Register runner request
+message RegisterRunnerRequest {
+    string runner_id = 1;
+    string address = 2;
+    RunnerCapabilities capabilities = 3;
+    map<string, string> labels = 4;
+}
+
+// Register runner response
+message RegisterRunnerResponse {
+    bool success = 1;
+    string message = 2;
+}
+
+// Heartbeat request
+message HeartbeatRequest {
+    string runner_id = 1;
+    RunnerStatus status = 2;
+    RunnerMetrics metrics = 3;
+}
+
+// Heartbeat response
+message HeartbeatResponse {
+    bool success = 1;
+    repeated string pending_cancellations = 2;  // Execution IDs to cancel
+}
+
+// Unregister runner request
+message UnregisterRunnerRequest {
+    string runner_id = 1;
+    string reason = 2;
+}
+
+// Node definition
+message NodeDefinition {
+    string id = 1;
+    string name = 2;
+    string category = 3;
+    string version = 4;
+    string description = 5;
+    string icon = 6;
+    string config_schema = 7;  // JSON Schema
+    string input_schema = 8;   // JSON Schema
+    string output_schema = 9;  // JSON Schema
+    repeated NodeInput inputs = 10;
+    repeated NodeOutput outputs = 11;
+    bool is_trigger = 12;
+}
+
+message NodeInput {
+    string name = 1;
+    string display_name = 2;
+    string type = 3;
+    bool required = 4;
+}
+
+message NodeOutput {
+    string name = 1;
+    string display_name = 2;
+    string type = 3;
+    string color = 4;  // Optional color for visual representation
+}
+
+// List nodes request
+message ListNodesRequest {
+    string category = 1;  // Optional filter
+}
+
+// List nodes response
+message ListNodesResponse {
+    repeated NodeDefinition nodes = 1;
+}
+
+// Reload node request
+message ReloadNodeRequest {
+    string node_id = 1;
+}
+
+// Reload node response
+message ReloadNodeResponse {
+    bool success = 1;
+    NodeDefinition node = 2;
+    Error error = 3;
+}
+
+// Get node code request
+message GetNodeCodeRequest {
+    string node_id = 1;
+}
+
+// Get node code response
+message GetNodeCodeResponse {
+    bool success = 1;
+    string code = 2;
+    string file_path = 3;
+    Error error = 4;
+}
+
+// Save node code request
+message SaveNodeCodeRequest {
+    string node_id = 1;
+    string code = 2;
+}
+
+// Save node code response
+message SaveNodeCodeResponse {
+    bool success = 1;
+    NodeDefinition node = 2;  // Updated node definition
+    Error error = 3;
+}
+
+// Create node request
+message CreateNodeRequest {
+    string id = 1;
+    string name = 2;
+    string category = 3;
+    string code = 4;
+}
+
+// Create node response
+message CreateNodeResponse {
+    bool success = 1;
+    NodeDefinition node = 2;
+    Error error = 3;
+}
+
+// Delete node request
+message DeleteNodeRequest {
+    string node_id = 1;
+}
+
+// Delete node response
+message DeleteNodeResponse {
+    bool success = 1;
+    Error error = 2;
+}
+
+// Execute node request (for testing)
+message ExecuteNodeRequest {
+    string node_type = 1;
+    string config = 2;  // JSON string
+    string input = 3;   // JSON string
+    string context = 4; // JSON string - execution context
+}
+
+// Execute node response
+message ExecuteNodeResponse {
+    bool success = 1;
+    string output = 2;  // JSON string
+    string error = 3;
+    int64 execution_time_ms = 4;
+}
+
+// Runner service - called by WebServer to execute workflows
+service RunnerService {
+    // Execute a workflow
+    rpc ExecuteWorkflow(ExecuteWorkflowRequest) returns (ExecuteWorkflowResponse);
+
+    // Cancel an execution
+    rpc CancelExecution(CancelExecutionRequest) returns (Empty);
+
+    // Node management
+    rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);
+    rpc ReloadNode(ReloadNodeRequest) returns (ReloadNodeResponse);
+    rpc GetNodeCode(GetNodeCodeRequest) returns (GetNodeCodeResponse);
+    rpc SaveNodeCode(SaveNodeCodeRequest) returns (SaveNodeCodeResponse);
+    rpc CreateNode(CreateNodeRequest) returns (CreateNodeResponse);
+    rpc DeleteNode(DeleteNodeRequest) returns (DeleteNodeResponse);
+
+    // Execute single node (for testing)
+    rpc ExecuteNode(ExecuteNodeRequest) returns (ExecuteNodeResponse);
+}
+
+// Runner registration service - runners call this on WebServer
+service RunnerRegistrationService {
+    rpc Register(RegisterRunnerRequest) returns (RegisterRunnerResponse);
+    rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
+    rpc Unregister(UnregisterRunnerRequest) returns (Empty);
+}
+
+// ============================================================================
+// Node Sync Service - WebServer exposes this for runners to fetch nodes
+// ============================================================================
+
+// Full node definition with code (for sync)
+message NodeDefinitionWithCode {
+    string id = 1;
+    string name = 2;
+    string category = 3;
+    string version = 4;
+    string description = 5;
+    string icon = 6;
+    string code = 7;                 // Full JavaScript source
+    string config_schema = 8;        // JSON Schema
+    string input_schema = 9;         // JSON Schema
+    string output_schema = 10;       // JSON Schema
+    repeated NodeInput inputs = 11;
+    repeated NodeOutput outputs = 12;
+    bool is_trigger = 13;
+    string owner_id = 14;
+    int64 created_at = 15;
+    int64 updated_at = 16;
+}
+
+// Get all nodes request
+message GetAllNodesRequest {
+    // Empty - returns all nodes
+}
+
+// Get all nodes response
+message GetAllNodesResponse {
+    repeated NodeDefinitionWithCode nodes = 1;
+}
+
+// Get single node request
+message GetNodeRequest {
+    string node_id = 1;
+}
+
+// Subscribe to node changes request
+message SubscribeToNodeChangesRequest {
+    string runner_id = 1;  // For tracking subscriptions
+}
+
+// Node change event
+message NodeChangeEvent {
+    enum ChangeType {
+        CHANGE_TYPE_UNSPECIFIED = 0;
+        CHANGE_TYPE_CREATED = 1;
+        CHANGE_TYPE_UPDATED = 2;
+        CHANGE_TYPE_DELETED = 3;
+    }
+    ChangeType type = 1;
+    string node_id = 2;
+    NodeDefinitionWithCode node = 3;  // Present for CREATED/UPDATED
+    int64 timestamp = 4;
+}
+
+// Node sync service - WebServer hosts this for runners to sync nodes
+service NodeSyncService {
+    // Get all node definitions
+    rpc GetAllNodes(GetAllNodesRequest) returns (GetAllNodesResponse);
+
+    // Get a single node by ID
+    rpc GetNode(GetNodeRequest) returns (NodeDefinitionWithCode);
+
+    // Subscribe to node changes (server-side streaming)
+    rpc SubscribeToNodeChanges(SubscribeToNodeChangesRequest) returns (stream NodeChangeEvent);
+}

+ 204 - 0
proto/storage.proto

@@ -0,0 +1,204 @@
+syntax = "proto3";
+
+package smartbotic.proto;
+
+import "common.proto";
+
+option cc_enable_arenas = true;
+
+// Document with metadata
+message Document {
+    string id = 1;
+    string collection = 2;
+    string data = 3;  // JSON string
+    int64 version = 4;
+    int64 created_at = 5;
+    int64 updated_at = 6;
+    int64 expires_at = 7;  // 0 = no expiry
+}
+
+// Query filter operation
+enum FilterOp {
+    FILTER_OP_UNSPECIFIED = 0;
+    FILTER_OP_EQ = 1;      // Equal
+    FILTER_OP_NE = 2;      // Not equal
+    FILTER_OP_GT = 3;      // Greater than
+    FILTER_OP_GTE = 4;     // Greater than or equal
+    FILTER_OP_LT = 5;      // Less than
+    FILTER_OP_LTE = 6;     // Less than or equal
+    FILTER_OP_IN = 7;      // In array
+    FILTER_OP_NIN = 8;     // Not in array
+    FILTER_OP_CONTAINS = 9; // Contains substring
+    FILTER_OP_REGEX = 10;  // Regular expression match
+    FILTER_OP_EXISTS = 11; // Field exists
+}
+
+// Single filter condition
+message Filter {
+    string field = 1;
+    FilterOp op = 2;
+    string value = 3;  // JSON encoded value
+}
+
+// Sort direction
+enum SortDirection {
+    SORT_DIRECTION_UNSPECIFIED = 0;
+    SORT_DIRECTION_ASC = 1;
+    SORT_DIRECTION_DESC = 2;
+}
+
+// Sort specification
+message Sort {
+    string field = 1;
+    SortDirection direction = 2;
+}
+
+// Query request
+message QueryRequest {
+    string collection = 1;
+    repeated Filter filters = 2;
+    repeated Sort sorts = 3;
+    PaginationRequest pagination = 4;
+    repeated string fields = 5;  // Projection - empty means all fields
+}
+
+// Query response
+message QueryResponse {
+    repeated Document documents = 1;
+    PaginationResponse pagination = 2;
+}
+
+// Get document request
+message GetRequest {
+    string collection = 1;
+    string id = 2;
+}
+
+// Insert request
+message InsertRequest {
+    string collection = 1;
+    string id = 2;  // Optional - generated if empty
+    string data = 3;  // JSON string
+    int64 ttl_ms = 4;  // Time to live in milliseconds (0 = no expiry)
+}
+
+// Update request
+message UpdateRequest {
+    string collection = 1;
+    string id = 2;
+    string data = 3;  // JSON string - full document or partial update
+    int64 expected_version = 4;  // Optimistic locking (0 = no check)
+    bool partial = 5;  // If true, merge with existing document
+}
+
+// Delete request
+message DeleteRequest {
+    string collection = 1;
+    string id = 2;
+    int64 expected_version = 3;  // Optimistic locking (0 = no check)
+}
+
+// Batch operations
+message BatchInsertRequest {
+    string collection = 1;
+    repeated InsertRequest documents = 2;
+}
+
+message BatchDeleteRequest {
+    string collection = 1;
+    repeated string ids = 2;
+}
+
+// Versioning configuration
+message VersioningConfig {
+    bool enabled = 1;
+    int32 max_versions = 2;      // 0 = unlimited
+    int64 version_ttl_ms = 3;    // 0 = no expiry
+    bool keep_on_delete = 4;     // Archive deleted documents
+}
+
+// Collection management
+message CreateCollectionRequest {
+    string name = 1;
+    string schema = 2;  // JSON Schema for validation (optional)
+    int64 default_ttl_ms = 3;  // Default TTL for documents
+    VersioningConfig versioning = 4;  // Versioning configuration
+}
+
+message DropCollectionRequest {
+    string name = 1;
+}
+
+message ListCollectionsRequest {}
+
+message ListCollectionsResponse {
+    repeated string collections = 1;
+}
+
+// Mutation response
+message MutationResponse {
+    string id = 1;
+    int64 version = 2;
+    bool success = 3;
+    Error error = 4;
+}
+
+message BatchMutationResponse {
+    repeated MutationResponse results = 1;
+    int32 success_count = 2;
+    int32 failure_count = 3;
+}
+
+// Document version for version history
+message DocumentVersion {
+    string id = 1;              // "{documentId}__v{version}"
+    string document_id = 2;
+    int64 version = 3;
+    string data = 4;            // JSON string
+    int64 created_at = 5;
+    int64 expires_at = 6;
+}
+
+// Get specific document version
+message GetVersionRequest {
+    string collection = 1;
+    string id = 2;
+    int64 version = 3;
+}
+
+// List versions for a document
+message ListVersionsRequest {
+    string collection = 1;
+    string id = 2;
+    int32 limit = 3;
+    int32 offset = 4;
+}
+
+message ListVersionsResponse {
+    repeated DocumentVersion versions = 1;
+    int64 total_count = 2;
+    bool has_more = 3;
+}
+
+// Storage service definition
+service StorageService {
+    // Document operations
+    rpc Get(GetRequest) returns (Document);
+    rpc Query(QueryRequest) returns (QueryResponse);
+    rpc Insert(InsertRequest) returns (MutationResponse);
+    rpc Update(UpdateRequest) returns (MutationResponse);
+    rpc Delete(DeleteRequest) returns (MutationResponse);
+
+    // Batch operations
+    rpc BatchInsert(BatchInsertRequest) returns (BatchMutationResponse);
+    rpc BatchDelete(BatchDeleteRequest) returns (BatchMutationResponse);
+
+    // Collection management
+    rpc CreateCollection(CreateCollectionRequest) returns (Empty);
+    rpc DropCollection(DropCollectionRequest) returns (Empty);
+    rpc ListCollections(ListCollectionsRequest) returns (ListCollectionsResponse);
+
+    // Version operations
+    rpc GetVersion(GetVersionRequest) returns (Document);
+    rpc ListVersions(ListVersionsRequest) returns (ListVersionsResponse);
+}

+ 156 - 0
proto/workflow.proto

@@ -0,0 +1,156 @@
+syntax = "proto3";
+
+package smartbotic.proto;
+
+import "common.proto";
+
+option cc_enable_arenas = true;
+
+// Node position in editor
+message Position {
+    double x = 1;
+    double y = 2;
+}
+
+// Workflow node definition
+message WorkflowNode {
+    string id = 1;
+    string type = 2;
+    string name = 3;
+    string config = 4;  // JSON string
+    Position position = 5;
+    bool disabled = 6;
+    map<string, string> metadata = 7;
+}
+
+// Connection between nodes
+message Connection {
+    string source_node_id = 1;
+    string source_output = 2;
+    string target_node_id = 3;
+    string target_input = 4;
+}
+
+// Workflow settings
+message WorkflowSettings {
+    int32 timeout_ms = 1;
+    bool retry_on_fail = 2;
+    int32 max_retries = 3;
+    int32 retry_delay_ms = 4;
+    string error_policy = 5;  // "stop", "continue", "retry"
+}
+
+// Workflow definition
+message Workflow {
+    string id = 1;
+    string name = 2;
+    string description = 3;
+    string group_id = 4;
+    string owner_id = 5;
+    bool active = 6;
+    repeated WorkflowNode nodes = 7;
+    repeated Connection connections = 8;
+    WorkflowSettings settings = 9;
+    int64 version = 10;
+    int64 created_at = 11;
+    int64 updated_at = 12;
+    map<string, string> tags = 13;
+}
+
+// Workflow group
+message WorkflowGroup {
+    string id = 1;
+    string name = 2;
+    string description = 3;
+    string parent_id = 4;
+    string owner_id = 5;
+    int64 created_at = 6;
+    int64 updated_at = 7;
+}
+
+// Execution status
+enum ExecutionStatus {
+    EXECUTION_STATUS_UNSPECIFIED = 0;
+    EXECUTION_STATUS_PENDING = 1;
+    EXECUTION_STATUS_RUNNING = 2;
+    EXECUTION_STATUS_COMPLETED = 3;
+    EXECUTION_STATUS_FAILED = 4;
+    EXECUTION_STATUS_CANCELLED = 5;
+    EXECUTION_STATUS_WAITING = 6;  // Waiting for external trigger
+}
+
+// Node execution result
+message NodeExecution {
+    string node_id = 1;
+    ExecutionStatus status = 2;
+    int64 started_at = 3;
+    int64 finished_at = 4;
+    string input = 5;  // JSON string
+    string output = 6;  // JSON string
+    string error = 7;
+    int32 retry_count = 8;
+}
+
+// Workflow execution
+message Execution {
+    string id = 1;
+    string workflow_id = 2;
+    string workflow_name = 3;
+    string runner_id = 4;
+    ExecutionStatus status = 5;
+    string trigger_type = 6;  // "manual", "webhook", "schedule", "node"
+    string trigger_data = 7;  // JSON string
+    int64 started_at = 8;
+    int64 finished_at = 9;
+    repeated NodeExecution node_executions = 10;
+    string error = 11;
+    map<string, string> context = 12;
+    string workflow_snapshot = 13;  // JSON string: {nodes, connections} at execution time
+}
+
+// Execute workflow request
+message ExecuteWorkflowRequest {
+    string workflow_id = 1;
+    string trigger_type = 2;
+    string trigger_data = 3;  // JSON string
+    bool wait_for_completion = 4;
+    int32 timeout_ms = 5;
+}
+
+// Execute workflow response
+message ExecuteWorkflowResponse {
+    string execution_id = 1;
+    ExecutionStatus status = 2;
+    string result = 3;  // JSON string (if wait_for_completion)
+    Error error = 4;
+}
+
+// Cancel execution request
+message CancelExecutionRequest {
+    string execution_id = 1;
+    string reason = 2;
+}
+
+// Retry execution request
+message RetryExecutionRequest {
+    string execution_id = 1;
+    string from_node_id = 2;  // Optional: retry from specific node
+}
+
+// Get execution request
+message GetExecutionRequest {
+    string execution_id = 1;
+}
+
+// List executions request
+message ListExecutionsRequest {
+    string workflow_id = 1;  // Optional filter
+    repeated ExecutionStatus statuses = 2;  // Optional filter
+    PaginationRequest pagination = 3;
+}
+
+// List executions response
+message ListExecutionsResponse {
+    repeated Execution executions = 1;
+    PaginationResponse pagination = 2;
+}

+ 602 - 0
src/database/database_service.cpp

@@ -0,0 +1,602 @@
+#include "database_service.hpp"
+#include "logging/logger.hpp"
+#include "common/time_utils.hpp"
+#include <grpcpp/health_check_service_interface.h>
+
+namespace smartbotic::database {
+
+using namespace common;
+
+// DatabaseServiceImpl implementation
+DatabaseServiceImpl::DatabaseServiceImpl(MemoryStore& store)
+    : store_(store) {}
+
+grpc::Status DatabaseServiceImpl::Get(grpc::ServerContext* context,
+                                      const proto::GetRequest* request,
+                                      proto::Document* response) {
+    auto result = store_.get(request->collection(), request->id());
+    if (result.failed()) {
+        return grpc::Status(grpc::StatusCode::NOT_FOUND, result.error().message());
+    }
+
+    documentToProto(result.value(), response);
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::Query(grpc::ServerContext* context,
+                                        const proto::QueryRequest* request,
+                                        proto::QueryResponse* response) {
+    struct Query q(request->collection());
+
+    // Build filters
+    for (const auto& f : request->filters()) {
+        Filter filter;
+        filter.field = f.field();
+        filter.op = protoToFilterOp(f.op());
+        filter.value = nlohmann::json::parse(f.value());
+        q.filters.push_back(std::move(filter));
+    }
+
+    // Build sorts
+    for (const auto& s : request->sorts()) {
+        Sort sort;
+        sort.field = s.field();
+        sort.direction = s.direction() == proto::SORT_DIRECTION_DESC ?
+                        SortDirection::Desc : SortDirection::Asc;
+        q.sorts.push_back(std::move(sort));
+    }
+
+    // Pagination
+    if (request->has_pagination()) {
+        q.offset = (request->pagination().page() - 1) * request->pagination().page_size();
+        q.limit = request->pagination().page_size();
+    }
+
+    // Projection
+    for (const auto& field : request->fields()) {
+        q.fields.push_back(field);
+    }
+
+    auto result = store_.query(q);
+
+    for (const auto& doc : result.documents) {
+        documentToProto(doc, response->add_documents());
+    }
+
+    auto* pagination = response->mutable_pagination();
+    pagination->set_total_count(result.total_count);
+    pagination->set_has_more(result.has_more);
+
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::Insert(grpc::ServerContext* context,
+                                         const proto::InsertRequest* request,
+                                         proto::MutationResponse* response) {
+    Document doc;
+    doc.id = request->id();
+    doc.collection = request->collection();
+    doc.data = nlohmann::json::parse(request->data());
+
+    if (request->ttl_ms() > 0) {
+        doc.expires_at = TimeUtils::nowMs() + request->ttl_ms();
+    }
+
+    auto result = store_.insert(request->collection(), std::move(doc));
+    if (result.failed()) {
+        response->set_success(false);
+        auto* error = response->mutable_error();
+        error->set_code(static_cast<int32_t>(result.error().code()));
+        error->set_message(result.error().message());
+        return grpc::Status::OK;
+    }
+
+    response->set_id(result.value().id);
+    response->set_version(result.value().version);
+    response->set_success(true);
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::Update(grpc::ServerContext* context,
+                                         const proto::UpdateRequest* request,
+                                         proto::MutationResponse* response) {
+    auto data = nlohmann::json::parse(request->data());
+    auto result = store_.update(request->collection(), request->id(), data,
+                               request->expected_version(), request->partial());
+
+    if (result.failed()) {
+        response->set_success(false);
+        auto* error = response->mutable_error();
+        error->set_code(static_cast<int32_t>(result.error().code()));
+        error->set_message(result.error().message());
+        return grpc::Status::OK;
+    }
+
+    response->set_id(result.value().id);
+    response->set_version(result.value().version);
+    response->set_success(true);
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::Delete(grpc::ServerContext* context,
+                                         const proto::DeleteRequest* request,
+                                         proto::MutationResponse* response) {
+    auto result = store_.remove(request->collection(), request->id(),
+                               request->expected_version());
+
+    if (result.failed()) {
+        response->set_success(false);
+        auto* error = response->mutable_error();
+        error->set_code(static_cast<int32_t>(result.error().code()));
+        error->set_message(result.error().message());
+        return grpc::Status::OK;
+    }
+
+    response->set_id(request->id());
+    response->set_success(true);
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::BatchInsert(grpc::ServerContext* context,
+                                              const proto::BatchInsertRequest* request,
+                                              proto::BatchMutationResponse* response) {
+    int32_t success_count = 0;
+    int32_t failure_count = 0;
+
+    for (const auto& req : request->documents()) {
+        Document doc;
+        doc.id = req.id();
+        doc.collection = request->collection();
+        doc.data = nlohmann::json::parse(req.data());
+
+        if (req.ttl_ms() > 0) {
+            doc.expires_at = TimeUtils::nowMs() + req.ttl_ms();
+        }
+
+        auto result = store_.insert(request->collection(), std::move(doc));
+        auto* mutation_result = response->add_results();
+
+        if (result.ok()) {
+            mutation_result->set_id(result.value().id);
+            mutation_result->set_version(result.value().version);
+            mutation_result->set_success(true);
+            ++success_count;
+        } else {
+            mutation_result->set_success(false);
+            auto* error = mutation_result->mutable_error();
+            error->set_code(static_cast<int32_t>(result.error().code()));
+            error->set_message(result.error().message());
+            ++failure_count;
+        }
+    }
+
+    response->set_success_count(success_count);
+    response->set_failure_count(failure_count);
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::BatchDelete(grpc::ServerContext* context,
+                                              const proto::BatchDeleteRequest* request,
+                                              proto::BatchMutationResponse* response) {
+    int32_t success_count = 0;
+    int32_t failure_count = 0;
+
+    for (const auto& id : request->ids()) {
+        auto result = store_.remove(request->collection(), id, 0);
+        auto* mutation_result = response->add_results();
+        mutation_result->set_id(id);
+
+        if (result.ok()) {
+            mutation_result->set_success(true);
+            ++success_count;
+        } else {
+            mutation_result->set_success(false);
+            auto* error = mutation_result->mutable_error();
+            error->set_code(static_cast<int32_t>(result.error().code()));
+            error->set_message(result.error().message());
+            ++failure_count;
+        }
+    }
+
+    response->set_success_count(success_count);
+    response->set_failure_count(failure_count);
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::CreateCollection(grpc::ServerContext* context,
+                                                    const proto::CreateCollectionRequest* request,
+                                                    proto::Empty* response) {
+    CollectionConfig config(request->name());
+    config.default_ttl_ms = request->default_ttl_ms();
+
+    if (!request->schema().empty()) {
+        config.schema = nlohmann::json::parse(request->schema());
+    }
+
+    // Handle versioning configuration
+    if (request->has_versioning()) {
+        VersioningConfig versioning;
+        versioning.enabled = request->versioning().enabled();
+        versioning.max_versions = request->versioning().max_versions();
+        versioning.version_ttl_ms = request->versioning().version_ttl_ms();
+        versioning.keep_on_delete = request->versioning().keep_on_delete();
+        config.versioning = versioning;
+    }
+
+    auto result = store_.createCollection(config);
+    if (result.failed()) {
+        return grpc::Status(grpc::StatusCode::ALREADY_EXISTS, result.error().message());
+    }
+
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::DropCollection(grpc::ServerContext* context,
+                                                  const proto::DropCollectionRequest* request,
+                                                  proto::Empty* response) {
+    auto result = store_.dropCollection(request->name());
+    if (result.failed()) {
+        return grpc::Status(grpc::StatusCode::NOT_FOUND, result.error().message());
+    }
+
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::ListCollections(grpc::ServerContext* context,
+                                                   const proto::ListCollectionsRequest* request,
+                                                   proto::ListCollectionsResponse* response) {
+    auto collections = store_.listCollections();
+    for (const auto& name : collections) {
+        response->add_collections(name);
+    }
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::GetVersion(grpc::ServerContext* context,
+                                             const proto::GetVersionRequest* request,
+                                             proto::Document* response) {
+    auto result = store_.getVersion(request->collection(), request->id(), request->version());
+    if (result.failed()) {
+        return grpc::Status(grpc::StatusCode::NOT_FOUND, result.error().message());
+    }
+
+    const auto& ver = result.value();
+    response->set_id(ver.document_id);
+    response->set_collection(request->collection());
+    response->set_data(ver.data.dump());
+    response->set_version(ver.version);
+    response->set_created_at(ver.created_at);
+    response->set_updated_at(ver.created_at);
+    response->set_expires_at(ver.expires_at);
+
+    return grpc::Status::OK;
+}
+
+grpc::Status DatabaseServiceImpl::ListVersions(grpc::ServerContext* context,
+                                               const proto::ListVersionsRequest* request,
+                                               proto::ListVersionsResponse* response) {
+    int32_t limit = request->limit() > 0 ? request->limit() : 100;
+    int32_t offset = request->offset();
+
+    auto result = store_.listVersions(request->collection(), request->id(), limit, offset);
+
+    for (const auto& doc : result.documents) {
+        auto ver = DocumentVersion::fromJson(doc.data);
+        auto* proto_ver = response->add_versions();
+        documentVersionToProto(ver, proto_ver);
+    }
+
+    response->set_total_count(result.total_count);
+    response->set_has_more(result.has_more);
+
+    return grpc::Status::OK;
+}
+
+void DatabaseServiceImpl::documentToProto(const Document& doc, proto::Document* proto) {
+    proto->set_id(doc.id);
+    proto->set_collection(doc.collection);
+    proto->set_data(doc.data.dump());
+    proto->set_version(doc.version);
+    proto->set_created_at(doc.created_at);
+    proto->set_updated_at(doc.updated_at);
+    proto->set_expires_at(doc.expires_at);
+}
+
+void DatabaseServiceImpl::documentVersionToProto(const DocumentVersion& ver, proto::DocumentVersion* proto) {
+    proto->set_id(ver.id);
+    proto->set_document_id(ver.document_id);
+    proto->set_version(ver.version);
+    proto->set_data(ver.data.dump());
+    proto->set_created_at(ver.created_at);
+    proto->set_expires_at(ver.expires_at);
+}
+
+FilterOp DatabaseServiceImpl::protoToFilterOp(proto::FilterOp op) {
+    switch (op) {
+        case proto::FILTER_OP_EQ: return FilterOp::Eq;
+        case proto::FILTER_OP_NE: return FilterOp::Ne;
+        case proto::FILTER_OP_GT: return FilterOp::Gt;
+        case proto::FILTER_OP_GTE: return FilterOp::Gte;
+        case proto::FILTER_OP_LT: return FilterOp::Lt;
+        case proto::FILTER_OP_LTE: return FilterOp::Lte;
+        case proto::FILTER_OP_IN: return FilterOp::In;
+        case proto::FILTER_OP_NIN: return FilterOp::Nin;
+        case proto::FILTER_OP_CONTAINS: return FilterOp::Contains;
+        case proto::FILTER_OP_REGEX: return FilterOp::Regex;
+        case proto::FILTER_OP_EXISTS: return FilterOp::Exists;
+        default: return FilterOp::Eq;
+    }
+}
+
+// DatabaseService implementation
+DatabaseService::DatabaseService(const Config& config)
+    : config_(config) {
+
+    // Setup WAL
+    WAL::Config wal_config;
+    wal_config.directory = std::filesystem::path(config_.data_directory) / "wal";
+    wal_config.sync_interval_ms = config_.wal.sync_interval_ms;
+    wal_config.enabled = true;
+    wal_ = std::make_unique<WAL>(wal_config);
+
+    // Setup snapshot manager
+    SnapshotManager::Config snapshot_config;
+    snapshot_config.directory = std::filesystem::path(config_.data_directory) / "snapshots";
+    snapshot_config.interval_sec = config_.snapshot.interval_sec;
+    snapshot_config.enabled = true;
+    snapshot_manager_ = std::make_unique<SnapshotManager>(snapshot_config);
+
+    // Setup mutation callback for WAL
+    store_.setMutationCallback([this](const WalEntry& entry) {
+        wal_->append(entry);
+    });
+}
+
+DatabaseService::~DatabaseService() {
+    stop();
+}
+
+DatabaseService::Config DatabaseService::loadConfig(const std::filesystem::path& path) {
+    Config config;
+
+    auto result = config::Config::fromFile(path);
+    if (result.ok()) {
+        auto& cfg = result.value();
+        config.grpc_port = cfg.getOr<int>("grpc_port", 9001);
+        config.data_directory = cfg.getOr<std::string>("data_directory", "./data/database");
+        config.wal.sync_interval_ms = cfg.getOr<int>("persistence.wal_sync_interval_ms", 100);
+        config.snapshot.interval_sec = cfg.getOr<int>("persistence.snapshot_interval_sec", 3600);
+    }
+
+    return config;
+}
+
+void DatabaseService::start() {
+    if (running_) {
+        return;
+    }
+
+    LOG_INFO("Starting database service...");
+
+    // Create data directory
+    std::filesystem::create_directories(config_.data_directory);
+
+    // Recovery from persistence
+    recoveryFromPersistence();
+
+    // Create predefined collections
+    createPredefinedCollections();
+
+    // Start WAL
+    wal_->start();
+
+    // Start gRPC server
+    service_impl_ = std::make_unique<DatabaseServiceImpl>(store_);
+
+    // Disable health check to avoid async completion queue issues
+    // grpc::EnableDefaultHealthCheckService(true);
+    grpc::ServerBuilder builder;
+    builder.AddListeningPort("0.0.0.0:" + std::to_string(config_.grpc_port),
+                            grpc::InsecureServerCredentials());
+    builder.RegisterService(service_impl_.get());
+
+    server_ = builder.BuildAndStart();
+    LOG_INFO("Database gRPC server listening on port {}", config_.grpc_port);
+
+    running_ = true;
+
+    // Start background threads
+    snapshot_thread_ = std::thread(&DatabaseService::snapshotLoop, this);
+    ttl_thread_ = std::thread(&DatabaseService::ttlLoop, this);
+}
+
+void DatabaseService::stop() {
+    if (!running_) {
+        return;
+    }
+
+    LOG_INFO("Stopping database service...");
+
+    // Signal shutdown to background threads
+    {
+        std::lock_guard<std::mutex> lock(shutdown_mutex_);
+        running_ = false;
+    }
+    shutdown_cv_.notify_all();
+
+    // Stop background threads (they will wake up immediately now)
+    if (snapshot_thread_.joinable()) {
+        snapshot_thread_.join();
+    }
+    if (ttl_thread_.joinable()) {
+        ttl_thread_.join();
+    }
+
+    // Final snapshot before shutdown
+    LOG_INFO("Creating final snapshot before shutdown...");
+    createSnapshot();
+
+    // Stop WAL
+    wal_->stop();
+
+    // Stop gRPC server
+    if (server_) {
+        server_->Shutdown();
+    }
+
+    LOG_INFO("Database service stopped");
+}
+
+void DatabaseService::createPredefinedCollections() {
+    // Users collection
+    store_.createCollection(CollectionConfig("users"));
+
+    // Workflows collection
+    store_.createCollection(CollectionConfig("workflows"));
+
+    // Workflow groups collection
+    store_.createCollection(CollectionConfig("workflow_groups"));
+
+    // Executions collection with 7-day TTL
+    store_.createCollection(
+        CollectionConfig("executions").withTTL(7 * 24 * 60 * 60 * 1000));
+
+    // Sessions collection with 1-day TTL
+    store_.createCollection(
+        CollectionConfig("sessions").withTTL(24 * 60 * 60 * 1000));
+
+    // API keys collection
+    store_.createCollection(CollectionConfig("api_keys"));
+
+    // Credentials collection
+    store_.createCollection(CollectionConfig("credentials"));
+
+    // Runners collection
+    store_.createCollection(CollectionConfig("runners"));
+
+    // Nodes collection - stores node definitions
+    store_.createCollection(CollectionConfig("nodes"));
+
+    LOG_INFO("Created predefined collections");
+}
+
+void DatabaseService::createSnapshot() {
+    auto snapshot = store_.createSnapshot();
+    auto result = snapshot_manager_->save(snapshot);
+    if (result.ok()) {
+        // Truncate WAL after successful snapshot
+        wal_->truncate(snapshot.value("sequence", 0));
+    }
+}
+
+void DatabaseService::recoveryFromPersistence() {
+    LOG_INFO("Starting recovery from persistence...");
+
+    // Try to load latest snapshot
+    auto snapshot_result = snapshot_manager_->loadLatest();
+    if (snapshot_result.ok()) {
+        store_.loadSnapshot(snapshot_result.value());
+        LOG_INFO("Loaded snapshot");
+    }
+
+    // Replay WAL entries after snapshot
+    int64_t last_sequence = snapshot_manager_->getLatestSequence();
+    wal_->replay([this, last_sequence](const WalEntry& entry) {
+        if (entry.sequence <= last_sequence) {
+            return;  // Skip entries already in snapshot
+        }
+
+        // Apply WAL entry
+        switch (entry.type) {
+            case WalEntryType::Insert: {
+                auto doc = Document::fromJson(entry.data);
+                store_.getCollection(entry.collection)->loadFromSnapshot({doc});
+                break;
+            }
+            case WalEntryType::Update: {
+                auto doc = Document::fromJson(entry.data);
+                store_.getCollection(entry.collection)->loadFromSnapshot({doc});
+                break;
+            }
+            case WalEntryType::Delete: {
+                auto* col = store_.getCollection(entry.collection);
+                if (col) {
+                    col->remove(entry.document_id, 0);
+                }
+                break;
+            }
+            case WalEntryType::CreateCollection: {
+                CollectionConfig config(entry.collection);
+                if (entry.data.contains("default_ttl_ms")) {
+                    config.default_ttl_ms = entry.data["default_ttl_ms"];
+                }
+                if (entry.data.contains("versioning")) {
+                    config.versioning = VersioningConfig::fromJson(entry.data["versioning"]);
+                }
+                store_.createCollection(config);
+                break;
+            }
+            case WalEntryType::DropCollection: {
+                store_.dropCollection(entry.collection);
+                break;
+            }
+            case WalEntryType::StoreVersion: {
+                // Version entries are stored in-memory, no special recovery needed
+                // They are reconstructed from document updates during normal operation
+                break;
+            }
+            case WalEntryType::DeleteVersion: {
+                // Handled similarly to StoreVersion
+                break;
+            }
+        }
+    });
+
+    LOG_INFO("Recovery complete");
+}
+
+void DatabaseService::snapshotLoop() {
+    while (running_) {
+        // Wait for shutdown signal or timeout
+        {
+            std::unique_lock<std::mutex> lock(shutdown_mutex_);
+            if (shutdown_cv_.wait_for(lock,
+                    std::chrono::seconds(config_.snapshot.interval_sec),
+                    [this] { return !running_.load(); })) {
+                // Shutdown signaled, exit loop
+                break;
+            }
+        }
+
+        if (!running_) break;
+
+        // Check if WAL is large enough to trigger snapshot
+        if (wal_->getSize() >= config_.snapshot.wal_size_trigger) {
+            createSnapshot();
+        }
+    }
+}
+
+void DatabaseService::ttlLoop() {
+    constexpr int ttl_check_interval_sec = 60;  // Check every minute
+
+    while (running_) {
+        // Wait for shutdown signal or timeout
+        {
+            std::unique_lock<std::mutex> lock(shutdown_mutex_);
+            if (shutdown_cv_.wait_for(lock,
+                    std::chrono::seconds(ttl_check_interval_sec),
+                    [this] { return !running_.load(); })) {
+                // Shutdown signaled, exit loop
+                break;
+            }
+        }
+
+        if (!running_) break;
+
+        store_.expireAllDocuments();
+        store_.expireAllVersions();
+    }
+}
+
+} // namespace smartbotic::database

+ 131 - 0
src/database/database_service.hpp

@@ -0,0 +1,131 @@
+#pragma once
+
+#include <memory>
+#include <thread>
+#include <atomic>
+#include <mutex>
+#include <condition_variable>
+#include <grpcpp/grpcpp.h>
+#include "proto/storage.grpc.pb.h"
+#include "memory_store.hpp"
+#include "persistence/wal.hpp"
+#include "persistence/snapshot.hpp"
+#include "config/config_loader.hpp"
+
+namespace smartbotic::database {
+
+// gRPC Storage Service implementation
+class DatabaseServiceImpl final : public proto::StorageService::Service {
+public:
+    explicit DatabaseServiceImpl(MemoryStore& store);
+
+    grpc::Status Get(grpc::ServerContext* context,
+                    const proto::GetRequest* request,
+                    proto::Document* response) override;
+
+    grpc::Status Query(grpc::ServerContext* context,
+                      const proto::QueryRequest* request,
+                      proto::QueryResponse* response) override;
+
+    grpc::Status Insert(grpc::ServerContext* context,
+                       const proto::InsertRequest* request,
+                       proto::MutationResponse* response) override;
+
+    grpc::Status Update(grpc::ServerContext* context,
+                       const proto::UpdateRequest* request,
+                       proto::MutationResponse* response) override;
+
+    grpc::Status Delete(grpc::ServerContext* context,
+                       const proto::DeleteRequest* request,
+                       proto::MutationResponse* response) override;
+
+    grpc::Status BatchInsert(grpc::ServerContext* context,
+                            const proto::BatchInsertRequest* request,
+                            proto::BatchMutationResponse* response) override;
+
+    grpc::Status BatchDelete(grpc::ServerContext* context,
+                            const proto::BatchDeleteRequest* request,
+                            proto::BatchMutationResponse* response) override;
+
+    grpc::Status CreateCollection(grpc::ServerContext* context,
+                                  const proto::CreateCollectionRequest* request,
+                                  proto::Empty* response) override;
+
+    grpc::Status DropCollection(grpc::ServerContext* context,
+                                const proto::DropCollectionRequest* request,
+                                proto::Empty* response) override;
+
+    grpc::Status ListCollections(grpc::ServerContext* context,
+                                 const proto::ListCollectionsRequest* request,
+                                 proto::ListCollectionsResponse* response) override;
+
+    // Version operations
+    grpc::Status GetVersion(grpc::ServerContext* context,
+                           const proto::GetVersionRequest* request,
+                           proto::Document* response) override;
+
+    grpc::Status ListVersions(grpc::ServerContext* context,
+                             const proto::ListVersionsRequest* request,
+                             proto::ListVersionsResponse* response) override;
+
+private:
+    void documentToProto(const Document& doc, proto::Document* proto);
+    void documentVersionToProto(const DocumentVersion& ver, proto::DocumentVersion* proto);
+    FilterOp protoToFilterOp(proto::FilterOp op);
+
+    MemoryStore& store_;
+};
+
+// Main database service
+class DatabaseService {
+public:
+    struct Config {
+        int grpc_port = 9001;
+        std::string data_directory = "./data/database";
+        WAL::Config wal;
+        SnapshotManager::Config snapshot;
+    };
+
+    explicit DatabaseService(const Config& config);
+    ~DatabaseService();
+
+    // Load config from file
+    static Config loadConfig(const std::filesystem::path& path);
+
+    // Start service
+    void start();
+
+    // Stop service
+    void stop();
+
+    // Get memory store for direct access
+    MemoryStore& store() { return store_; }
+
+    // Create predefined collections
+    void createPredefinedCollections();
+
+    // Force snapshot
+    void createSnapshot();
+
+private:
+    void recoveryFromPersistence();
+    void snapshotLoop();
+    void ttlLoop();
+
+    Config config_;
+    MemoryStore store_;
+    std::unique_ptr<WAL> wal_;
+    std::unique_ptr<SnapshotManager> snapshot_manager_;
+    std::unique_ptr<DatabaseServiceImpl> service_impl_;
+    std::unique_ptr<grpc::Server> server_;
+
+    std::thread snapshot_thread_;
+    std::thread ttl_thread_;
+    std::atomic<bool> running_{false};
+
+    // Shutdown signaling
+    std::mutex shutdown_mutex_;
+    std::condition_variable shutdown_cv_;
+};
+
+} // namespace smartbotic::database

+ 365 - 0
src/database/document.hpp

@@ -0,0 +1,365 @@
+#pragma once
+
+#include <string>
+#include <optional>
+#include <vector>
+#include <nlohmann/json.hpp>
+#include "common/uuid.hpp"
+#include "common/time_utils.hpp"
+
+namespace smartbotic::database {
+
+// Document structure
+struct Document {
+    std::string id;
+    std::string collection;
+    nlohmann::json data;
+    int64_t version = 1;
+    int64_t created_at = 0;
+    int64_t updated_at = 0;
+    int64_t expires_at = 0;  // 0 = no expiry
+
+    Document() = default;
+    Document(std::string id, std::string collection, nlohmann::json data)
+        : id(std::move(id))
+        , collection(std::move(collection))
+        , data(std::move(data)) {
+        auto now = common::TimeUtils::nowMs();
+        created_at = now;
+        updated_at = now;
+    }
+
+    bool isExpired() const {
+        return expires_at > 0 && common::TimeUtils::nowMs() > expires_at;
+    }
+
+    nlohmann::json toJson() const {
+        nlohmann::json j;
+        j["id"] = id;
+        j["collection"] = collection;
+        j["data"] = data;
+        j["version"] = version;
+        j["createdAt"] = created_at;
+        j["updatedAt"] = updated_at;
+        if (expires_at > 0) {
+            j["expiresAt"] = expires_at;
+        }
+        return j;
+    }
+
+    static Document fromJson(const nlohmann::json& j) {
+        Document doc;
+        doc.id = j.value("id", "");
+        doc.collection = j.value("collection", "");
+        doc.data = j.value("data", nlohmann::json::object());
+        doc.version = j.value("version", 1);
+        doc.created_at = j.value("createdAt", int64_t{0});
+        doc.updated_at = j.value("updatedAt", int64_t{0});
+        doc.expires_at = j.value("expiresAt", int64_t{0});
+        return doc;
+    }
+};
+
+// Filter operations
+enum class FilterOp {
+    Eq,       // Equal
+    Ne,       // Not equal
+    Gt,       // Greater than
+    Gte,      // Greater than or equal
+    Lt,       // Less than
+    Lte,      // Less than or equal
+    In,       // In array
+    Nin,      // Not in array
+    Contains, // Contains substring
+    Regex,    // Regular expression
+    Exists    // Field exists
+};
+
+// Single filter condition
+struct Filter {
+    std::string field;
+    FilterOp op = FilterOp::Eq;
+    nlohmann::json value;
+
+    Filter() = default;
+    Filter(std::string field, FilterOp op, nlohmann::json value)
+        : field(std::move(field)), op(op), value(std::move(value)) {}
+
+    // Convenience constructors
+    static Filter eq(const std::string& field, const nlohmann::json& value) {
+        return Filter(field, FilterOp::Eq, value);
+    }
+    static Filter ne(const std::string& field, const nlohmann::json& value) {
+        return Filter(field, FilterOp::Ne, value);
+    }
+    static Filter gt(const std::string& field, const nlohmann::json& value) {
+        return Filter(field, FilterOp::Gt, value);
+    }
+    static Filter gte(const std::string& field, const nlohmann::json& value) {
+        return Filter(field, FilterOp::Gte, value);
+    }
+    static Filter lt(const std::string& field, const nlohmann::json& value) {
+        return Filter(field, FilterOp::Lt, value);
+    }
+    static Filter lte(const std::string& field, const nlohmann::json& value) {
+        return Filter(field, FilterOp::Lte, value);
+    }
+    static Filter in(const std::string& field, const nlohmann::json& values) {
+        return Filter(field, FilterOp::In, values);
+    }
+    static Filter contains(const std::string& field, const std::string& substr) {
+        return Filter(field, FilterOp::Contains, substr);
+    }
+    static Filter regex(const std::string& field, const std::string& pattern) {
+        return Filter(field, FilterOp::Regex, pattern);
+    }
+    static Filter exists(const std::string& field, bool exists = true) {
+        return Filter(field, FilterOp::Exists, exists);
+    }
+};
+
+// Sort direction
+enum class SortDirection {
+    Asc,
+    Desc
+};
+
+// Sort specification
+struct Sort {
+    std::string field;
+    SortDirection direction = SortDirection::Asc;
+
+    Sort() = default;
+    Sort(std::string field, SortDirection direction = SortDirection::Asc)
+        : field(std::move(field)), direction(direction) {}
+
+    static Sort asc(const std::string& field) {
+        return Sort(field, SortDirection::Asc);
+    }
+    static Sort desc(const std::string& field) {
+        return Sort(field, SortDirection::Desc);
+    }
+};
+
+// Query request
+struct Query {
+    std::string collection;
+    std::vector<Filter> filters;
+    std::vector<Sort> sorts;
+    std::vector<std::string> fields;  // Projection
+    int32_t offset = 0;
+    int32_t limit = 100;
+
+    Query() = default;
+    explicit Query(std::string collection)
+        : collection(std::move(collection)) {}
+
+    Query& where(Filter filter) {
+        filters.push_back(std::move(filter));
+        return *this;
+    }
+
+    Query& orderBy(Sort sort) {
+        sorts.push_back(std::move(sort));
+        return *this;
+    }
+
+    Query& select(std::vector<std::string> fieldList) {
+        fields = std::move(fieldList);
+        return *this;
+    }
+
+    Query& skip(int32_t n) {
+        offset = n;
+        return *this;
+    }
+
+    Query& take(int32_t n) {
+        limit = n;
+        return *this;
+    }
+};
+
+// Query result
+struct QueryResult {
+    std::vector<Document> documents;
+    int64_t total_count = 0;
+    bool has_more = false;
+};
+
+// Versioning configuration for collections
+struct VersioningConfig {
+    bool enabled = false;
+    int32_t max_versions = 0;          // 0 = unlimited
+    int64_t version_ttl_ms = 0;        // 0 = no expiry
+    bool keep_on_delete = false;       // Archive deleted documents
+
+    VersioningConfig() = default;
+
+    VersioningConfig& withMaxVersions(int32_t max) {
+        max_versions = max;
+        return *this;
+    }
+
+    VersioningConfig& withTTL(int64_t ttl_ms) {
+        version_ttl_ms = ttl_ms;
+        return *this;
+    }
+
+    VersioningConfig& keepDeleted(bool keep = true) {
+        keep_on_delete = keep;
+        return *this;
+    }
+
+    nlohmann::json toJson() const {
+        return {
+            {"enabled", enabled},
+            {"maxVersions", max_versions},
+            {"versionTtlMs", version_ttl_ms},
+            {"keepOnDelete", keep_on_delete}
+        };
+    }
+
+    static VersioningConfig fromJson(const nlohmann::json& j) {
+        VersioningConfig config;
+        config.enabled = j.value("enabled", false);
+        config.max_versions = j.value("maxVersions", 0);
+        config.version_ttl_ms = j.value("versionTtlMs", 0);
+        config.keep_on_delete = j.value("keepOnDelete", false);
+        return config;
+    }
+
+    static VersioningConfig create(int32_t max_versions = 50) {
+        VersioningConfig config;
+        config.enabled = true;
+        config.max_versions = max_versions;
+        return config;
+    }
+};
+
+// Document version entry for version history
+struct DocumentVersion {
+    std::string id;              // "{documentId}__v{version}"
+    std::string document_id;
+    int64_t version = 0;
+    nlohmann::json data;
+    int64_t created_at = 0;
+    int64_t expires_at = 0;      // For TTL cleanup
+
+    DocumentVersion() = default;
+    DocumentVersion(const Document& doc)
+        : document_id(doc.id)
+        , version(doc.version)
+        , data(doc.data)
+        , created_at(common::TimeUtils::nowMs()) {
+        id = document_id + "__v" + std::to_string(version);
+    }
+
+    bool isExpired() const {
+        return expires_at > 0 && common::TimeUtils::nowMs() > expires_at;
+    }
+
+    nlohmann::json toJson() const {
+        return {
+            {"id", id},
+            {"documentId", document_id},
+            {"version", version},
+            {"data", data},
+            {"createdAt", created_at},
+            {"expiresAt", expires_at}
+        };
+    }
+
+    static DocumentVersion fromJson(const nlohmann::json& j) {
+        DocumentVersion ver;
+        ver.id = j.value("id", "");
+        ver.document_id = j.value("documentId", "");
+        ver.version = j.value("version", 0);
+        ver.data = j.value("data", nlohmann::json::object());
+        ver.created_at = j.value("createdAt", int64_t{0});
+        ver.expires_at = j.value("expiresAt", int64_t{0});
+        return ver;
+    }
+
+    Document toDocument() const {
+        Document doc;
+        doc.id = id;
+        doc.data = toJson();
+        doc.created_at = created_at;
+        doc.updated_at = created_at;
+        doc.expires_at = expires_at;
+        return doc;
+    }
+};
+
+// Collection configuration
+struct CollectionConfig {
+    std::string name;
+    std::optional<nlohmann::json> schema;  // JSON Schema for validation
+    int64_t default_ttl_ms = 0;  // Default TTL for documents
+    std::optional<VersioningConfig> versioning;  // Versioning configuration
+
+    CollectionConfig() = default;
+    explicit CollectionConfig(std::string name)
+        : name(std::move(name)) {}
+
+    CollectionConfig& withSchema(nlohmann::json s) {
+        schema = std::move(s);
+        return *this;
+    }
+
+    CollectionConfig& withTTL(int64_t ttl_ms) {
+        default_ttl_ms = ttl_ms;
+        return *this;
+    }
+
+    CollectionConfig& withVersioning(VersioningConfig config) {
+        versioning = std::move(config);
+        return *this;
+    }
+};
+
+// WAL entry types
+enum class WalEntryType {
+    Insert,
+    Update,
+    Delete,
+    CreateCollection,
+    DropCollection,
+    StoreVersion,      // Archive a document version
+    DeleteVersion      // Delete a specific version
+};
+
+// WAL entry for persistence
+struct WalEntry {
+    int64_t sequence = 0;
+    int64_t timestamp = 0;
+    WalEntryType type;
+    std::string collection;
+    std::string document_id;
+    nlohmann::json data;
+
+    nlohmann::json toJson() const {
+        nlohmann::json j;
+        j["seq"] = sequence;
+        j["ts"] = timestamp;
+        j["type"] = static_cast<int>(type);
+        j["col"] = collection;
+        j["id"] = document_id;
+        j["data"] = data;
+        return j;
+    }
+
+    static WalEntry fromJson(const nlohmann::json& j) {
+        WalEntry entry;
+        entry.sequence = j.value("seq", 0);
+        entry.timestamp = j.value("ts", 0);
+        entry.type = static_cast<WalEntryType>(j.value("type", 0));
+        entry.collection = j.value("col", "");
+        entry.document_id = j.value("id", "");
+        entry.data = j.value("data", nlohmann::json::object());
+        return entry;
+    }
+};
+
+} // namespace smartbotic::database

+ 64 - 0
src/database/main.cpp

@@ -0,0 +1,64 @@
+#include <csignal>
+#include <iostream>
+#include "database_service.hpp"
+#include "logging/logger.hpp"
+#include "config/config_loader.hpp"
+
+using namespace smartbotic;
+
+std::atomic<bool> g_shutdown{false};
+
+void signalHandler(int signal) {
+    LOG_INFO("Received signal {}, shutting down...", signal);
+    g_shutdown = true;
+}
+
+int main(int argc, char* argv[]) {
+    // Initialize logging
+    logging::LogConfig log_config;
+    log_config.name = "database";
+    log_config.level = logging::LogLevel::Info;
+    logging::Logger::init(log_config);
+
+    LOG_INFO("SmartBotic Database Service starting...");
+
+    // Load configuration
+    std::filesystem::path config_path = "./config/database.json";
+    if (argc > 1) {
+        config_path = argv[1];
+    }
+
+    database::DatabaseService::Config config;
+    if (std::filesystem::exists(config_path)) {
+        config = database::DatabaseService::loadConfig(config_path);
+        LOG_INFO("Loaded configuration from {}", config_path.string());
+    } else {
+        LOG_WARN("Config file not found at {}, using defaults", config_path.string());
+    }
+
+    // Setup signal handlers
+    std::signal(SIGINT, signalHandler);
+    std::signal(SIGTERM, signalHandler);
+
+    // Create and start service
+    database::DatabaseService service(config);
+
+    try {
+        service.start();
+
+        // Wait for shutdown signal
+        while (!g_shutdown) {
+            std::this_thread::sleep_for(std::chrono::milliseconds(100));
+        }
+
+        service.stop();
+    } catch (const std::exception& e) {
+        LOG_ERROR("Fatal error: {}", e.what());
+        return 1;
+    }
+
+    LOG_INFO("SmartBotic Database Service stopped");
+    logging::Logger::shutdown();
+
+    return 0;
+}

+ 830 - 0
src/database/memory_store.cpp

@@ -0,0 +1,830 @@
+#include "memory_store.hpp"
+#include "common/uuid.hpp"
+#include "common/time_utils.hpp"
+#include "logging/logger.hpp"
+#include <algorithm>
+
+namespace smartbotic::database {
+
+using namespace common;
+
+// Collection implementation
+Collection::Collection(const CollectionConfig& config)
+    : config_(config) {}
+
+Result<Document> Collection::get(const std::string& id) const {
+    std::shared_lock lock(mutex_);
+
+    auto it = documents_.find(id);
+    if (it == documents_.end()) {
+        return Error(ErrorCode::DocumentNotFound,
+                    "Document not found: " + id);
+    }
+
+    if (it->second.isExpired()) {
+        return Error(ErrorCode::DocumentNotFound,
+                    "Document expired: " + id);
+    }
+
+    return it->second;
+}
+
+Result<Document> Collection::insert(Document doc) {
+    if (doc.id.empty()) {
+        doc.id = UUID::generate();
+    }
+
+    if (!config_.schema || validateSchema(doc.data)) {
+        std::unique_lock lock(mutex_);
+
+        if (documents_.contains(doc.id)) {
+            return Error(ErrorCode::AlreadyExists,
+                        "Document already exists: " + doc.id);
+        }
+
+        auto now = TimeUtils::nowMs();
+        doc.created_at = now;
+        doc.updated_at = now;
+        doc.version = 1;
+        doc.collection = config_.name;
+
+        if (config_.default_ttl_ms > 0 && doc.expires_at == 0) {
+            doc.expires_at = now + config_.default_ttl_ms;
+        }
+
+        documents_[doc.id] = doc;
+        return doc;
+    }
+
+    return Error(ErrorCode::ValidationError, "Document failed schema validation");
+}
+
+Result<Document> Collection::update(const std::string& id, const nlohmann::json& data,
+                                    int64_t expected_version, bool partial) {
+    std::unique_lock lock(mutex_);
+
+    auto it = documents_.find(id);
+    if (it == documents_.end()) {
+        return Error(ErrorCode::DocumentNotFound,
+                    "Document not found: " + id);
+    }
+
+    if (it->second.isExpired()) {
+        documents_.erase(it);
+        return Error(ErrorCode::DocumentNotFound,
+                    "Document expired: " + id);
+    }
+
+    if (expected_version > 0 && it->second.version != expected_version) {
+        return Error(ErrorCode::VersionConflict,
+                    "Version conflict: expected " + std::to_string(expected_version) +
+                    ", got " + std::to_string(it->second.version));
+    }
+
+    // Store version before updating if versioning is enabled
+    if (config_.versioning && config_.versioning->enabled) {
+        storeVersionInternal(it->second);
+    }
+
+    nlohmann::json new_data;
+    if (partial) {
+        new_data = it->second.data;
+        new_data.merge_patch(data);
+    } else {
+        new_data = data;
+    }
+
+    if (config_.schema && !validateSchema(new_data)) {
+        return Error(ErrorCode::ValidationError, "Document failed schema validation");
+    }
+
+    it->second.data = std::move(new_data);
+    it->second.version++;
+    it->second.updated_at = TimeUtils::nowMs();
+
+    return it->second;
+}
+
+void Collection::storeVersionInternal(const Document& doc) {
+    // Note: caller must hold mutex_
+    DocumentVersion ver(doc);
+
+    // Set expiration if configured
+    if (config_.versioning && config_.versioning->version_ttl_ms > 0) {
+        ver.expires_at = TimeUtils::nowMs() + config_.versioning->version_ttl_ms;
+    }
+
+    versions_[doc.id].push_back(ver);
+    cleanupOldVersions(doc.id);
+}
+
+void Collection::storeVersion(const Document& doc) {
+    std::unique_lock lock(mutex_);
+    storeVersionInternal(doc);
+}
+
+void Collection::storeDeletedVersion(const Document& doc) {
+    if (!config_.versioning || !config_.versioning->enabled || !config_.versioning->keep_on_delete) {
+        return;
+    }
+    storeVersion(doc);
+}
+
+void Collection::cleanupOldVersions(const std::string& id) {
+    // Note: caller must hold mutex_
+    if (!config_.versioning || config_.versioning->max_versions <= 0) {
+        return;
+    }
+
+    auto& vers = versions_[id];
+    int32_t max_versions = config_.versioning->max_versions;
+
+    while (static_cast<int32_t>(vers.size()) > max_versions) {
+        vers.erase(vers.begin());  // Remove oldest
+    }
+}
+
+Result<DocumentVersion> Collection::getVersion(const std::string& id, int64_t version) {
+    std::shared_lock lock(mutex_);
+
+    auto it = versions_.find(id);
+    if (it == versions_.end()) {
+        return Error(ErrorCode::DocumentNotFound,
+                    "No versions found for document: " + id);
+    }
+
+    for (const auto& ver : it->second) {
+        if (ver.version == version) {
+            if (ver.isExpired()) {
+                return Error(ErrorCode::DocumentNotFound,
+                            "Version expired: " + id + " v" + std::to_string(version));
+            }
+            return ver;
+        }
+    }
+
+    return Error(ErrorCode::DocumentNotFound,
+                "Version not found: " + id + " v" + std::to_string(version));
+}
+
+QueryResult Collection::listVersions(const std::string& id, int32_t limit, int32_t offset) {
+    std::shared_lock lock(mutex_);
+
+    QueryResult result;
+
+    auto it = versions_.find(id);
+    if (it == versions_.end()) {
+        return result;
+    }
+
+    // Filter out expired versions and collect non-expired ones
+    std::vector<DocumentVersion> valid_versions;
+    for (const auto& ver : it->second) {
+        if (!ver.isExpired()) {
+            valid_versions.push_back(ver);
+        }
+    }
+
+    // Sort by version descending (newest first)
+    std::sort(valid_versions.begin(), valid_versions.end(),
+              [](const DocumentVersion& a, const DocumentVersion& b) {
+                  return a.version > b.version;
+              });
+
+    result.total_count = static_cast<int64_t>(valid_versions.size());
+
+    // Apply pagination
+    int32_t start = std::min(offset, static_cast<int32_t>(valid_versions.size()));
+    int32_t end = std::min(offset + limit, static_cast<int32_t>(valid_versions.size()));
+
+    result.has_more = end < static_cast<int32_t>(valid_versions.size());
+
+    for (int32_t i = start; i < end; ++i) {
+        result.documents.push_back(valid_versions[i].toDocument());
+    }
+
+    return result;
+}
+
+int64_t Collection::expireVersions() {
+    std::unique_lock lock(mutex_);
+
+    auto now = TimeUtils::nowMs();
+    int64_t expired = 0;
+
+    for (auto& [doc_id, vers] : versions_) {
+        for (auto it = vers.begin(); it != vers.end();) {
+            if (it->expires_at > 0 && it->expires_at < now) {
+                it = vers.erase(it);
+                ++expired;
+            } else {
+                ++it;
+            }
+        }
+    }
+
+    return expired;
+}
+
+Result<void> Collection::remove(const std::string& id, int64_t expected_version) {
+    std::unique_lock lock(mutex_);
+
+    auto it = documents_.find(id);
+    if (it == documents_.end()) {
+        return Error(ErrorCode::DocumentNotFound,
+                    "Document not found: " + id);
+    }
+
+    if (expected_version > 0 && it->second.version != expected_version) {
+        return Error(ErrorCode::VersionConflict,
+                    "Version conflict: expected " + std::to_string(expected_version) +
+                    ", got " + std::to_string(it->second.version));
+    }
+
+    // Store final version if keep_on_delete is enabled
+    if (config_.versioning && config_.versioning->enabled && config_.versioning->keep_on_delete) {
+        storeVersionInternal(it->second);
+    }
+
+    documents_.erase(it);
+    return Result<void>();
+}
+
+QueryResult Collection::query(const Query& q) const {
+    std::shared_lock lock(mutex_);
+
+    std::vector<Document> results;
+    results.reserve(documents_.size());
+
+    for (const auto& [id, doc] : documents_) {
+        if (!doc.isExpired() && matchesAllFilters(doc, q.filters)) {
+            results.push_back(doc);
+        }
+    }
+
+    QueryResult result;
+    result.total_count = static_cast<int64_t>(results.size());
+
+    // Sort
+    if (!q.sorts.empty()) {
+        sortDocuments(results, q.sorts);
+    }
+
+    // Pagination
+    int32_t start = std::min(q.offset, static_cast<int32_t>(results.size()));
+    int32_t end = std::min(q.offset + q.limit, static_cast<int32_t>(results.size()));
+
+    result.has_more = end < static_cast<int32_t>(results.size());
+
+    // Apply offset and limit
+    for (int32_t i = start; i < end; ++i) {
+        if (!q.fields.empty()) {
+            result.documents.push_back(applyProjection(results[i], q.fields));
+        } else {
+            result.documents.push_back(results[i]);
+        }
+    }
+
+    return result;
+}
+
+int64_t Collection::count(const std::vector<Filter>& filters) const {
+    std::shared_lock lock(mutex_);
+
+    if (filters.empty()) {
+        return static_cast<int64_t>(documents_.size());
+    }
+
+    int64_t cnt = 0;
+    for (const auto& [id, doc] : documents_) {
+        if (!doc.isExpired() && matchesAllFilters(doc, filters)) {
+            ++cnt;
+        }
+    }
+    return cnt;
+}
+
+bool Collection::exists(const std::string& id) const {
+    std::shared_lock lock(mutex_);
+    auto it = documents_.find(id);
+    return it != documents_.end() && !it->second.isExpired();
+}
+
+std::vector<Result<Document>> Collection::insertMany(std::vector<Document> docs) {
+    std::vector<Result<Document>> results;
+    results.reserve(docs.size());
+
+    for (auto& doc : docs) {
+        results.push_back(insert(std::move(doc)));
+    }
+
+    return results;
+}
+
+int64_t Collection::removeMany(const std::vector<std::string>& ids) {
+    std::unique_lock lock(mutex_);
+
+    int64_t count = 0;
+    for (const auto& id : ids) {
+        if (documents_.erase(id) > 0) {
+            ++count;
+        }
+    }
+    return count;
+}
+
+int64_t Collection::size() const {
+    std::shared_lock lock(mutex_);
+    return static_cast<int64_t>(documents_.size());
+}
+
+int64_t Collection::expireDocuments() {
+    std::unique_lock lock(mutex_);
+
+    auto now = TimeUtils::nowMs();
+    int64_t expired = 0;
+
+    for (auto it = documents_.begin(); it != documents_.end();) {
+        if (it->second.expires_at > 0 && it->second.expires_at < now) {
+            it = documents_.erase(it);
+            ++expired;
+        } else {
+            ++it;
+        }
+    }
+
+    return expired;
+}
+
+std::vector<Document> Collection::getAllDocuments() const {
+    std::shared_lock lock(mutex_);
+
+    std::vector<Document> docs;
+    docs.reserve(documents_.size());
+
+    for (const auto& [id, doc] : documents_) {
+        docs.push_back(doc);
+    }
+
+    return docs;
+}
+
+void Collection::loadFromSnapshot(const std::vector<Document>& docs) {
+    std::unique_lock lock(mutex_);
+
+    documents_.clear();
+    for (const auto& doc : docs) {
+        documents_[doc.id] = doc;
+    }
+}
+
+bool Collection::matchesFilter(const Document& doc, const Filter& filter) const {
+    auto value = getFieldValue(doc.data, filter.field);
+
+    switch (filter.op) {
+        case FilterOp::Eq:
+            return value == filter.value;
+
+        case FilterOp::Ne:
+            return value != filter.value;
+
+        case FilterOp::Gt:
+            return value > filter.value;
+
+        case FilterOp::Gte:
+            return value >= filter.value;
+
+        case FilterOp::Lt:
+            return value < filter.value;
+
+        case FilterOp::Lte:
+            return value <= filter.value;
+
+        case FilterOp::In:
+            if (filter.value.is_array()) {
+                for (const auto& v : filter.value) {
+                    if (value == v) return true;
+                }
+            }
+            return false;
+
+        case FilterOp::Nin:
+            if (filter.value.is_array()) {
+                for (const auto& v : filter.value) {
+                    if (value == v) return false;
+                }
+            }
+            return true;
+
+        case FilterOp::Contains:
+            if (value.is_string() && filter.value.is_string()) {
+                return value.get<std::string>().find(filter.value.get<std::string>()) != std::string::npos;
+            }
+            return false;
+
+        case FilterOp::Regex:
+            if (value.is_string() && filter.value.is_string()) {
+                try {
+                    std::regex re(filter.value.get<std::string>());
+                    return std::regex_search(value.get<std::string>(), re);
+                } catch (...) {
+                    return false;
+                }
+            }
+            return false;
+
+        case FilterOp::Exists:
+            return !value.is_null() == filter.value.get<bool>();
+    }
+
+    return false;
+}
+
+bool Collection::matchesAllFilters(const Document& doc, const std::vector<Filter>& filters) const {
+    for (const auto& filter : filters) {
+        if (!matchesFilter(doc, filter)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+nlohmann::json Collection::getFieldValue(const nlohmann::json& data, const std::string& field) const {
+    // Support nested fields with dot notation
+    const nlohmann::json* current = &data;
+
+    size_t start = 0;
+    size_t pos;
+    while ((pos = field.find('.', start)) != std::string::npos) {
+        std::string part = field.substr(start, pos - start);
+        if (!current->is_object() || !current->contains(part)) {
+            return nlohmann::json(nullptr);
+        }
+        current = &(*current)[part];
+        start = pos + 1;
+    }
+
+    std::string last_part = field.substr(start);
+    if (!current->is_object() || !current->contains(last_part)) {
+        return nlohmann::json(nullptr);
+    }
+
+    return (*current)[last_part];
+}
+
+bool Collection::validateSchema(const nlohmann::json& data) const {
+    // Basic validation - in production, use a JSON Schema validator
+    if (!config_.schema) return true;
+
+    const auto& schema = *config_.schema;
+
+    // Check required fields
+    if (schema.contains("required") && schema["required"].is_array()) {
+        for (const auto& field : schema["required"]) {
+            if (!data.contains(field.get<std::string>())) {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+void Collection::sortDocuments(std::vector<Document>& docs, const std::vector<Sort>& sorts) const {
+    std::sort(docs.begin(), docs.end(), [this, &sorts](const Document& a, const Document& b) {
+        for (const auto& sort : sorts) {
+            auto va = getFieldValue(a.data, sort.field);
+            auto vb = getFieldValue(b.data, sort.field);
+
+            if (va == vb) continue;
+
+            bool less = va < vb;
+            if (sort.direction == SortDirection::Desc) {
+                less = !less;
+            }
+            return less;
+        }
+        return false;
+    });
+}
+
+Document Collection::applyProjection(const Document& doc, const std::vector<std::string>& fields) const {
+    Document result = doc;
+    nlohmann::json projected = nlohmann::json::object();
+
+    for (const auto& field : fields) {
+        auto value = getFieldValue(doc.data, field);
+        if (!value.is_null()) {
+            projected[field] = value;
+        }
+    }
+
+    result.data = projected;
+    return result;
+}
+
+// MemoryStore implementation
+MemoryStore::MemoryStore() = default;
+MemoryStore::~MemoryStore() = default;
+
+void MemoryStore::setMutationCallback(MutationCallback callback) {
+    mutation_callback_ = std::move(callback);
+}
+
+Result<void> MemoryStore::createCollection(const CollectionConfig& config) {
+    std::unique_lock lock(mutex_);
+
+    if (collections_.contains(config.name)) {
+        return Error(ErrorCode::AlreadyExists,
+                    "Collection already exists: " + config.name);
+    }
+
+    collections_[config.name] = std::make_unique<Collection>(config);
+
+    WalEntry entry;
+    entry.type = WalEntryType::CreateCollection;
+    entry.collection = config.name;
+    entry.data = {
+        {"name", config.name},
+        {"default_ttl_ms", config.default_ttl_ms}
+    };
+    if (config.schema) {
+        entry.data["schema"] = *config.schema;
+    }
+    if (config.versioning) {
+        entry.data["versioning"] = config.versioning->toJson();
+    }
+    notifyMutation(std::move(entry));
+
+    return Result<void>();
+}
+
+Result<void> MemoryStore::dropCollection(const std::string& name) {
+    std::unique_lock lock(mutex_);
+
+    if (!collections_.contains(name)) {
+        return Error(ErrorCode::CollectionNotFound,
+                    "Collection not found: " + name);
+    }
+
+    collections_.erase(name);
+
+    WalEntry entry;
+    entry.type = WalEntryType::DropCollection;
+    entry.collection = name;
+    notifyMutation(std::move(entry));
+
+    return Result<void>();
+}
+
+bool MemoryStore::hasCollection(const std::string& name) const {
+    std::shared_lock lock(mutex_);
+    return collections_.contains(name);
+}
+
+std::vector<std::string> MemoryStore::listCollections() const {
+    std::shared_lock lock(mutex_);
+
+    std::vector<std::string> names;
+    names.reserve(collections_.size());
+
+    for (const auto& [name, _] : collections_) {
+        names.push_back(name);
+    }
+
+    return names;
+}
+
+Collection* MemoryStore::getCollection(const std::string& name) {
+    std::shared_lock lock(mutex_);
+    auto it = collections_.find(name);
+    return it != collections_.end() ? it->second.get() : nullptr;
+}
+
+const Collection* MemoryStore::getCollection(const std::string& name) const {
+    std::shared_lock lock(mutex_);
+    auto it = collections_.find(name);
+    return it != collections_.end() ? it->second.get() : nullptr;
+}
+
+Result<Document> MemoryStore::get(const std::string& collection, const std::string& id) {
+    auto* col = getCollection(collection);
+    if (!col) {
+        return Error(ErrorCode::CollectionNotFound,
+                    "Collection not found: " + collection);
+    }
+    return col->get(id);
+}
+
+Result<Document> MemoryStore::insert(const std::string& collection, Document doc) {
+    auto* col = getCollection(collection);
+    if (!col) {
+        return Error(ErrorCode::CollectionNotFound,
+                    "Collection not found: " + collection);
+    }
+
+    auto result = col->insert(std::move(doc));
+    if (result.ok()) {
+        WalEntry entry;
+        entry.type = WalEntryType::Insert;
+        entry.collection = collection;
+        entry.document_id = result.value().id;
+        entry.data = result.value().toJson();
+        notifyMutation(std::move(entry));
+    }
+    return result;
+}
+
+Result<Document> MemoryStore::update(const std::string& collection, const std::string& id,
+                                     const nlohmann::json& data, int64_t expected_version,
+                                     bool partial) {
+    auto* col = getCollection(collection);
+    if (!col) {
+        return Error(ErrorCode::CollectionNotFound,
+                    "Collection not found: " + collection);
+    }
+
+    auto result = col->update(id, data, expected_version, partial);
+    if (result.ok()) {
+        WalEntry entry;
+        entry.type = WalEntryType::Update;
+        entry.collection = collection;
+        entry.document_id = id;
+        entry.data = result.value().toJson();
+        notifyMutation(std::move(entry));
+    }
+    return result;
+}
+
+Result<void> MemoryStore::remove(const std::string& collection, const std::string& id,
+                                  int64_t expected_version) {
+    auto* col = getCollection(collection);
+    if (!col) {
+        return Error(ErrorCode::CollectionNotFound,
+                    "Collection not found: " + collection);
+    }
+
+    auto result = col->remove(id, expected_version);
+    if (result.ok()) {
+        WalEntry entry;
+        entry.type = WalEntryType::Delete;
+        entry.collection = collection;
+        entry.document_id = id;
+        notifyMutation(std::move(entry));
+    }
+    return result;
+}
+
+QueryResult MemoryStore::query(const Query& q) {
+    auto* col = getCollection(q.collection);
+    if (!col) {
+        return QueryResult{};
+    }
+    return col->query(q);
+}
+
+void MemoryStore::expireAllDocuments() {
+    std::shared_lock lock(mutex_);
+
+    for (auto& [name, col] : collections_) {
+        auto expired = col->expireDocuments();
+        if (expired > 0) {
+            LOG_DEBUG("Expired {} documents from collection {}", expired, name);
+        }
+    }
+}
+
+void MemoryStore::expireAllVersions() {
+    std::shared_lock lock(mutex_);
+
+    for (auto& [name, col] : collections_) {
+        auto expired = col->expireVersions();
+        if (expired > 0) {
+            LOG_DEBUG("Expired {} versions from collection {}", expired, name);
+        }
+    }
+}
+
+Result<DocumentVersion> MemoryStore::getVersion(const std::string& collection,
+                                                const std::string& id,
+                                                int64_t version) {
+    auto* col = getCollection(collection);
+    if (!col) {
+        return Error(ErrorCode::CollectionNotFound,
+                    "Collection not found: " + collection);
+    }
+    return col->getVersion(id, version);
+}
+
+QueryResult MemoryStore::listVersions(const std::string& collection,
+                                      const std::string& id,
+                                      int32_t limit,
+                                      int32_t offset) {
+    auto* col = getCollection(collection);
+    if (!col) {
+        return QueryResult{};
+    }
+    return col->listVersions(id, limit, offset);
+}
+
+nlohmann::json MemoryStore::createSnapshot() const {
+    std::shared_lock lock(mutex_);
+
+    nlohmann::json snapshot;
+    snapshot["version"] = 1;
+    snapshot["timestamp"] = TimeUtils::nowMs();
+    snapshot["sequence"] = sequence_.load();
+    snapshot["collections"] = nlohmann::json::object();
+
+    for (const auto& [name, col] : collections_) {
+        nlohmann::json col_data;
+        col_data["config"] = {
+            {"name", col->config().name},
+            {"default_ttl_ms", col->config().default_ttl_ms}
+        };
+        if (col->config().schema) {
+            col_data["config"]["schema"] = *col->config().schema;
+        }
+        if (col->config().versioning) {
+            col_data["config"]["versioning"] = col->config().versioning->toJson();
+        }
+
+        col_data["documents"] = nlohmann::json::array();
+        for (const auto& doc : col->getAllDocuments()) {
+            col_data["documents"].push_back(doc.toJson());
+        }
+
+        snapshot["collections"][name] = col_data;
+    }
+
+    return snapshot;
+}
+
+void MemoryStore::loadSnapshot(const nlohmann::json& snapshot) {
+    std::unique_lock lock(mutex_);
+
+    collections_.clear();
+
+    if (snapshot.contains("sequence")) {
+        sequence_ = snapshot["sequence"].get<int64_t>();
+    }
+
+    if (snapshot.contains("collections")) {
+        for (auto it = snapshot["collections"].begin(); it != snapshot["collections"].end(); ++it) {
+            const auto& col_data = it.value();
+
+            CollectionConfig config;
+            config.name = it.key();
+            if (col_data.contains("config")) {
+                config.default_ttl_ms = col_data["config"].value("default_ttl_ms", 0);
+                if (col_data["config"].contains("schema")) {
+                    config.schema = col_data["config"]["schema"];
+                }
+                if (col_data["config"].contains("versioning")) {
+                    config.versioning = VersioningConfig::fromJson(col_data["config"]["versioning"]);
+                }
+            }
+
+            collections_[config.name] = std::make_unique<Collection>(config);
+
+            if (col_data.contains("documents")) {
+                std::vector<Document> docs;
+                for (const auto& doc_json : col_data["documents"]) {
+                    docs.push_back(Document::fromJson(doc_json));
+                }
+                collections_[config.name]->loadFromSnapshot(docs);
+            }
+        }
+    }
+}
+
+MemoryStore::Stats MemoryStore::getStats() const {
+    std::shared_lock lock(mutex_);
+
+    Stats stats;
+    stats.collection_count = collections_.size();
+
+    for (const auto& [name, col] : collections_) {
+        stats.total_documents += col->size();
+    }
+
+    // Rough memory estimate
+    stats.memory_estimate_bytes = stats.total_documents * 1024;  // ~1KB per document estimate
+
+    return stats;
+}
+
+void MemoryStore::notifyMutation(WalEntry entry) {
+    entry.sequence = nextSequence();
+    entry.timestamp = TimeUtils::nowMs();
+
+    if (mutation_callback_) {
+        mutation_callback_(entry);
+    }
+}
+
+int64_t MemoryStore::nextSequence() {
+    return ++sequence_;
+}
+
+} // namespace smartbotic::database

+ 136 - 0
src/database/memory_store.hpp

@@ -0,0 +1,136 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <shared_mutex>
+#include <functional>
+#include <regex>
+#include <atomic>
+#include <nlohmann/json.hpp>
+#include "document.hpp"
+#include "common/error.hpp"
+
+namespace smartbotic::database {
+
+// Collection - stores documents with thread-safe access
+class Collection {
+public:
+    explicit Collection(const CollectionConfig& config);
+
+    // Document operations
+    common::Result<Document> get(const std::string& id) const;
+    common::Result<Document> insert(Document doc);
+    common::Result<Document> update(const std::string& id, const nlohmann::json& data,
+                                    int64_t expected_version = 0, bool partial = false);
+    common::Result<void> remove(const std::string& id, int64_t expected_version = 0);
+
+    // Query operations
+    QueryResult query(const Query& q) const;
+    int64_t count(const std::vector<Filter>& filters = {}) const;
+    bool exists(const std::string& id) const;
+
+    // Bulk operations
+    std::vector<common::Result<Document>> insertMany(std::vector<Document> docs);
+    int64_t removeMany(const std::vector<std::string>& ids);
+
+    // Collection info
+    const std::string& name() const { return config_.name; }
+    const CollectionConfig& config() const { return config_; }
+    int64_t size() const;
+
+    // TTL expiration
+    int64_t expireDocuments();
+
+    // Snapshot support
+    std::vector<Document> getAllDocuments() const;
+    void loadFromSnapshot(const std::vector<Document>& docs);
+
+    // Version operations
+    common::Result<DocumentVersion> getVersion(const std::string& id, int64_t version);
+    QueryResult listVersions(const std::string& id, int32_t limit = 100, int32_t offset = 0);
+    int64_t expireVersions();
+    void storeVersion(const Document& doc);
+    void storeDeletedVersion(const Document& doc);
+
+private:
+    void storeVersionInternal(const Document& doc);
+    void cleanupOldVersions(const std::string& id);
+    bool matchesFilter(const Document& doc, const Filter& filter) const;
+    bool matchesAllFilters(const Document& doc, const std::vector<Filter>& filters) const;
+    nlohmann::json getFieldValue(const nlohmann::json& data, const std::string& field) const;
+    bool validateSchema(const nlohmann::json& data) const;
+    void sortDocuments(std::vector<Document>& docs, const std::vector<Sort>& sorts) const;
+    Document applyProjection(const Document& doc, const std::vector<std::string>& fields) const;
+
+    CollectionConfig config_;
+    std::unordered_map<std::string, Document> documents_;
+    std::unordered_map<std::string, std::vector<DocumentVersion>> versions_;  // doc_id -> versions
+    mutable std::shared_mutex mutex_;
+};
+
+// MemoryStore - manages multiple collections
+class MemoryStore {
+public:
+    using MutationCallback = std::function<void(const WalEntry&)>;
+
+    MemoryStore();
+    ~MemoryStore();
+
+    // Set mutation callback for WAL
+    void setMutationCallback(MutationCallback callback);
+
+    // Collection management
+    common::Result<void> createCollection(const CollectionConfig& config);
+    common::Result<void> dropCollection(const std::string& name);
+    bool hasCollection(const std::string& name) const;
+    std::vector<std::string> listCollections() const;
+    Collection* getCollection(const std::string& name);
+    const Collection* getCollection(const std::string& name) const;
+
+    // Document operations (convenience wrappers)
+    common::Result<Document> get(const std::string& collection, const std::string& id);
+    common::Result<Document> insert(const std::string& collection, Document doc);
+    common::Result<Document> update(const std::string& collection, const std::string& id,
+                                    const nlohmann::json& data, int64_t expected_version = 0,
+                                    bool partial = false);
+    common::Result<void> remove(const std::string& collection, const std::string& id,
+                                int64_t expected_version = 0);
+    QueryResult query(const Query& q);
+
+    // TTL expiration (run periodically)
+    void expireAllDocuments();
+    void expireAllVersions();
+
+    // Version operations
+    common::Result<DocumentVersion> getVersion(const std::string& collection,
+                                               const std::string& id,
+                                               int64_t version);
+    QueryResult listVersions(const std::string& collection,
+                            const std::string& id,
+                            int32_t limit = 100,
+                            int32_t offset = 0);
+
+    // Snapshot support
+    nlohmann::json createSnapshot() const;
+    void loadSnapshot(const nlohmann::json& snapshot);
+
+    // Stats
+    struct Stats {
+        size_t collection_count = 0;
+        size_t total_documents = 0;
+        size_t memory_estimate_bytes = 0;
+    };
+    Stats getStats() const;
+
+private:
+    void notifyMutation(WalEntry entry);
+    int64_t nextSequence();
+
+    std::unordered_map<std::string, std::unique_ptr<Collection>> collections_;
+    mutable std::shared_mutex mutex_;
+    MutationCallback mutation_callback_;
+    std::atomic<int64_t> sequence_{0};
+};
+
+} // namespace smartbotic::database

+ 159 - 0
src/database/persistence/snapshot.cpp

@@ -0,0 +1,159 @@
+#include "snapshot.hpp"
+#include "common/time_utils.hpp"
+#include "logging/logger.hpp"
+#include <fstream>
+#include <algorithm>
+
+namespace smartbotic::database {
+
+using namespace common;
+
+SnapshotManager::SnapshotManager(const Config& config)
+    : config_(config) {
+    if (config_.enabled && !config_.directory.empty()) {
+        std::filesystem::create_directories(config_.directory);
+    }
+}
+
+Result<std::filesystem::path> SnapshotManager::save(const nlohmann::json& data) {
+    if (!config_.enabled) {
+        return Error(ErrorCode::FailedPrecondition, "Snapshots disabled");
+    }
+
+    auto path = generatePath();
+
+    try {
+        std::ofstream file(path, std::ios::binary);
+        if (!file.is_open()) {
+            return Error(ErrorCode::Internal, "Failed to create snapshot file: " + path.string());
+        }
+
+        // Write with pretty formatting for debugging, or dump() for compact
+        file << data.dump();
+        file.close();
+
+        LOG_INFO("Created snapshot: {}", path.string());
+
+        // Update latest sequence
+        if (data.contains("sequence")) {
+            latest_sequence_ = data["sequence"].get<int64_t>();
+        }
+
+        // Cleanup old snapshots
+        cleanup();
+
+        return path;
+    } catch (const std::exception& e) {
+        return Error(ErrorCode::Internal, "Failed to save snapshot: " + std::string(e.what()));
+    }
+}
+
+Result<nlohmann::json> SnapshotManager::loadLatest() {
+    auto path = getLatestPath();
+    if (!path) {
+        return Error(ErrorCode::NotFound, "No snapshot found");
+    }
+    return load(*path);
+}
+
+Result<nlohmann::json> SnapshotManager::load(const std::filesystem::path& path) {
+    if (!std::filesystem::exists(path)) {
+        return Error(ErrorCode::NotFound, "Snapshot not found: " + path.string());
+    }
+
+    try {
+        std::ifstream file(path, std::ios::binary);
+        if (!file.is_open()) {
+            return Error(ErrorCode::Internal, "Failed to open snapshot: " + path.string());
+        }
+
+        nlohmann::json data = nlohmann::json::parse(file);
+
+        if (data.contains("sequence")) {
+            latest_sequence_ = data["sequence"].get<int64_t>();
+        }
+
+        LOG_INFO("Loaded snapshot: {}", path.string());
+        return data;
+    } catch (const std::exception& e) {
+        return Error(ErrorCode::Internal, "Failed to load snapshot: " + std::string(e.what()));
+    }
+}
+
+std::optional<std::filesystem::path> SnapshotManager::getLatestPath() const {
+    auto snapshots = getSnapshots();
+    if (snapshots.empty()) {
+        return std::nullopt;
+    }
+
+    // Sort by modification time (newest first)
+    std::sort(snapshots.begin(), snapshots.end(),
+        [](const auto& a, const auto& b) {
+            return std::filesystem::last_write_time(a) > std::filesystem::last_write_time(b);
+        });
+
+    return snapshots[0];
+}
+
+int64_t SnapshotManager::getLatestSequence() const {
+    if (latest_sequence_ > 0) {
+        return latest_sequence_;
+    }
+
+    auto path = getLatestPath();
+    if (!path) {
+        return 0;
+    }
+
+    try {
+        std::ifstream file(*path);
+        nlohmann::json data = nlohmann::json::parse(file);
+        latest_sequence_ = data.value("sequence", 0);
+        return latest_sequence_;
+    } catch (...) {
+        return 0;
+    }
+}
+
+void SnapshotManager::cleanup() {
+    auto snapshots = getSnapshots();
+    if (snapshots.size() <= static_cast<size_t>(config_.max_snapshots)) {
+        return;
+    }
+
+    // Sort by modification time (oldest first)
+    std::sort(snapshots.begin(), snapshots.end(),
+        [](const auto& a, const auto& b) {
+            return std::filesystem::last_write_time(a) < std::filesystem::last_write_time(b);
+        });
+
+    // Remove oldest snapshots
+    size_t to_remove = snapshots.size() - config_.max_snapshots;
+    for (size_t i = 0; i < to_remove; ++i) {
+        std::filesystem::remove(snapshots[i]);
+        LOG_INFO("Removed old snapshot: {}", snapshots[i].string());
+    }
+}
+
+std::vector<std::filesystem::path> SnapshotManager::getSnapshots() const {
+    std::vector<std::filesystem::path> snapshots;
+
+    if (!std::filesystem::exists(config_.directory)) {
+        return snapshots;
+    }
+
+    for (const auto& entry : std::filesystem::directory_iterator(config_.directory)) {
+        if (entry.is_regular_file() && entry.path().extension() == ".snapshot") {
+            snapshots.push_back(entry.path());
+        }
+    }
+
+    return snapshots;
+}
+
+std::filesystem::path SnapshotManager::generatePath() const {
+    auto timestamp = TimeUtils::nowMs();
+    return config_.directory / ("snapshot_" + std::to_string(timestamp) + ".snapshot");
+}
+
+} // namespace smartbotic::database

+ 53 - 0
src/database/persistence/snapshot.hpp

@@ -0,0 +1,53 @@
+#pragma once
+
+#include <string>
+#include <filesystem>
+#include <nlohmann/json.hpp>
+#include "common/error.hpp"
+
+namespace smartbotic::database {
+
+class MemoryStore;
+
+// Snapshot manager for full state persistence
+class SnapshotManager {
+public:
+    struct Config {
+        std::filesystem::path directory;
+        int64_t interval_sec = 3600;  // Snapshot every hour
+        size_t wal_size_trigger = 100 * 1024 * 1024;  // Snapshot at 100MB WAL
+        int32_t max_snapshots = 5;  // Keep last N snapshots
+        bool enabled = true;
+    };
+
+    explicit SnapshotManager(const Config& config);
+
+    // Save snapshot
+    common::Result<std::filesystem::path> save(const nlohmann::json& data);
+
+    // Load latest snapshot
+    common::Result<nlohmann::json> loadLatest();
+
+    // Load specific snapshot
+    common::Result<nlohmann::json> load(const std::filesystem::path& path);
+
+    // Get latest snapshot path
+    std::optional<std::filesystem::path> getLatestPath() const;
+
+    // Get sequence number from latest snapshot
+    int64_t getLatestSequence() const;
+
+    // Cleanup old snapshots
+    void cleanup();
+
+    // Get all snapshot paths
+    std::vector<std::filesystem::path> getSnapshots() const;
+
+private:
+    std::filesystem::path generatePath() const;
+
+    Config config_;
+    mutable int64_t latest_sequence_ = 0;
+};
+
+} // namespace smartbotic::database

+ 243 - 0
src/database/persistence/wal.cpp

@@ -0,0 +1,243 @@
+#include "wal.hpp"
+#include "logging/logger.hpp"
+#include <algorithm>
+
+namespace smartbotic::database {
+
+using namespace common;
+
+WAL::WAL(const Config& config)
+    : config_(config) {
+    if (config_.enabled && !config_.directory.empty()) {
+        std::filesystem::create_directories(config_.directory);
+    }
+}
+
+WAL::~WAL() {
+    stop();
+}
+
+void WAL::start() {
+    if (!config_.enabled || running_) {
+        return;
+    }
+
+    running_ = true;
+    sync_thread_ = std::thread(&WAL::syncLoop, this);
+    LOG_INFO("WAL started, directory: {}", config_.directory.string());
+}
+
+void WAL::stop() {
+    if (!running_) {
+        return;
+    }
+
+    running_ = false;
+    cv_.notify_all();
+
+    if (sync_thread_.joinable()) {
+        sync_thread_.join();
+    }
+
+    std::lock_guard lock(mutex_);
+    if (current_file_.is_open()) {
+        current_file_.flush();
+        current_file_.close();
+    }
+
+    LOG_INFO("WAL stopped");
+}
+
+Result<void> WAL::append(const WalEntry& entry) {
+    if (!config_.enabled) {
+        return Result<void>();
+    }
+
+    std::lock_guard lock(mutex_);
+
+    pending_entries_.push(entry);
+    current_sequence_ = std::max(current_sequence_, entry.sequence);
+
+    cv_.notify_one();
+
+    return Result<void>();
+}
+
+Result<void> WAL::replay(std::function<void(const WalEntry&)> callback) {
+    if (!config_.enabled) {
+        return Result<void>();
+    }
+
+    auto files = getWalFiles();
+    std::sort(files.begin(), files.end());
+
+    int64_t replayed = 0;
+
+    for (const auto& file_path : files) {
+        std::ifstream file(file_path);
+        if (!file.is_open()) {
+            LOG_WARN("Failed to open WAL file: {}", file_path.string());
+            continue;
+        }
+
+        std::string line;
+        while (std::getline(file, line)) {
+            if (line.empty()) continue;
+
+            try {
+                auto j = nlohmann::json::parse(line);
+                auto entry = WalEntry::fromJson(j);
+                callback(entry);
+                current_sequence_ = std::max(current_sequence_, entry.sequence);
+                ++replayed;
+            } catch (const std::exception& e) {
+                LOG_WARN("Failed to parse WAL entry: {}", e.what());
+            }
+        }
+    }
+
+    LOG_INFO("Replayed {} WAL entries, current sequence: {}", replayed, current_sequence_);
+    return Result<void>();
+}
+
+Result<void> WAL::truncate(int64_t up_to_sequence) {
+    if (!config_.enabled) {
+        return Result<void>();
+    }
+
+    std::lock_guard lock(mutex_);
+
+    // Close current file
+    if (current_file_.is_open()) {
+        current_file_.close();
+    }
+
+    // Remove old WAL files
+    auto files = getWalFiles();
+    for (const auto& file_path : files) {
+        // Parse sequence from filename
+        auto filename = file_path.filename().string();
+        if (filename.starts_with("wal_")) {
+            try {
+                auto seq_str = filename.substr(4, filename.find('.') - 4);
+                int64_t file_seq = std::stoll(seq_str);
+                if (file_seq <= up_to_sequence) {
+                    std::filesystem::remove(file_path);
+                    LOG_DEBUG("Removed WAL file: {}", file_path.string());
+                }
+            } catch (...) {
+                // Ignore parse errors
+            }
+        }
+    }
+
+    // Reset state
+    current_file_path_.clear();
+    current_file_size_ = 0;
+
+    return Result<void>();
+}
+
+size_t WAL::getSize() const {
+    if (!config_.enabled) {
+        return 0;
+    }
+
+    size_t total = 0;
+    auto files = getWalFiles();
+    for (const auto& file : files) {
+        total += std::filesystem::file_size(file);
+    }
+    return total;
+}
+
+void WAL::flush() {
+    std::lock_guard lock(mutex_);
+    if (current_file_.is_open()) {
+        current_file_.flush();
+    }
+}
+
+void WAL::syncLoop() {
+    while (running_) {
+        std::vector<WalEntry> entries_to_write;
+
+        {
+            std::unique_lock lock(mutex_);
+            cv_.wait_for(lock, std::chrono::milliseconds(config_.sync_interval_ms),
+                        [this] { return !pending_entries_.empty() || !running_; });
+
+            while (!pending_entries_.empty()) {
+                entries_to_write.push_back(std::move(pending_entries_.front()));
+                pending_entries_.pop();
+            }
+        }
+
+        if (entries_to_write.empty()) {
+            continue;
+        }
+
+        // Write entries outside the lock
+        {
+            std::lock_guard lock(mutex_);
+
+            // Rotate if needed
+            rotateIfNeeded();
+
+            // Ensure file is open
+            if (!current_file_.is_open()) {
+                current_file_path_ = getWalFilePath(current_sequence_);
+                current_file_.open(current_file_path_, std::ios::app | std::ios::binary);
+                if (!current_file_.is_open()) {
+                    LOG_ERROR("Failed to open WAL file: {}", current_file_path_.string());
+                    continue;
+                }
+            }
+
+            for (const auto& entry : entries_to_write) {
+                std::string line = entry.toJson().dump() + "\n";
+                current_file_ << line;
+                current_file_size_ += line.size();
+            }
+
+            current_file_.flush();
+        }
+    }
+}
+
+void WAL::rotateIfNeeded() {
+    if (current_file_size_ >= config_.max_file_size) {
+        if (current_file_.is_open()) {
+            current_file_.close();
+        }
+        current_file_path_ = getWalFilePath(current_sequence_);
+        current_file_.open(current_file_path_, std::ios::app | std::ios::binary);
+        current_file_size_ = 0;
+        LOG_INFO("Rotated WAL file: {}", current_file_path_.string());
+    }
+}
+
+std::filesystem::path WAL::getWalFilePath(int64_t sequence) const {
+    return config_.directory / ("wal_" + std::to_string(sequence) + ".log");
+}
+
+std::vector<std::filesystem::path> WAL::getWalFiles() const {
+    std::vector<std::filesystem::path> files;
+
+    if (!std::filesystem::exists(config_.directory)) {
+        return files;
+    }
+
+    for (const auto& entry : std::filesystem::directory_iterator(config_.directory)) {
+        if (entry.is_regular_file() && entry.path().extension() == ".log") {
+            auto filename = entry.path().filename().string();
+            if (filename.starts_with("wal_")) {
+                files.push_back(entry.path());
+            }
+        }
+    }
+
+    return files;
+}
+
+} // namespace smartbotic::database

+ 71 - 0
src/database/persistence/wal.hpp

@@ -0,0 +1,71 @@
+#pragma once
+
+#include <string>
+#include <fstream>
+#include <mutex>
+#include <filesystem>
+#include <functional>
+#include <thread>
+#include <atomic>
+#include <condition_variable>
+#include <queue>
+#include "../document.hpp"
+#include "common/error.hpp"
+
+namespace smartbotic::database {
+
+// Write-Ahead Log for durability
+class WAL {
+public:
+    struct Config {
+        std::filesystem::path directory;
+        int64_t sync_interval_ms = 100;
+        size_t max_file_size = 100 * 1024 * 1024;  // 100MB per file
+        bool enabled = true;
+    };
+
+    explicit WAL(const Config& config);
+    ~WAL();
+
+    // Start/stop background sync
+    void start();
+    void stop();
+
+    // Write entry to log
+    common::Result<void> append(const WalEntry& entry);
+
+    // Replay log entries (used during recovery)
+    common::Result<void> replay(std::function<void(const WalEntry&)> callback);
+
+    // Get current sequence number
+    int64_t getCurrentSequence() const { return current_sequence_; }
+
+    // Truncate WAL after snapshot
+    common::Result<void> truncate(int64_t up_to_sequence);
+
+    // Get WAL size in bytes
+    size_t getSize() const;
+
+    // Force flush
+    void flush();
+
+private:
+    void syncLoop();
+    void rotateIfNeeded();
+    std::filesystem::path getWalFilePath(int64_t sequence) const;
+    std::vector<std::filesystem::path> getWalFiles() const;
+
+    Config config_;
+    std::ofstream current_file_;
+    std::filesystem::path current_file_path_;
+    int64_t current_sequence_ = 0;
+    size_t current_file_size_ = 0;
+
+    std::mutex mutex_;
+    std::queue<WalEntry> pending_entries_;
+    std::condition_variable cv_;
+    std::thread sync_thread_;
+    std::atomic<bool> running_{false};
+};
+
+} // namespace smartbotic::database

+ 142 - 0
src/runner/collection_permissions.cpp

@@ -0,0 +1,142 @@
+#include "collection_permissions.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::runner {
+
+// System collections that are always protected - no workflow access allowed
+const std::unordered_set<std::string> CollectionPermissions::SYSTEM_COLLECTIONS = {
+    "users",
+    "sessions",
+    "api_keys",
+    "credentials",
+    "collection_permissions"
+};
+
+std::string collectionAccessToString(CollectionAccess access) {
+    switch (access) {
+        case CollectionAccess::None: return "none";
+        case CollectionAccess::ReadOnly: return "read-only";
+        case CollectionAccess::ReadWrite: return "read-write";
+        default: return "none";
+    }
+}
+
+CollectionAccess collectionAccessFromString(const std::string& str) {
+    if (str == "read-only") return CollectionAccess::ReadOnly;
+    if (str == "read-write") return CollectionAccess::ReadWrite;
+    return CollectionAccess::None;
+}
+
+CollectionPermissions::CollectionPermissions(storage::StorageClient& storage)
+    : storage_(storage) {
+}
+
+void CollectionPermissions::loadPermissions() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    permissions_.clear();
+
+    // Set default permissions for known collections
+    // These can be overridden by database settings
+    permissions_["workflows"] = CollectionAccess::ReadWrite;
+    permissions_["workflow_groups"] = CollectionAccess::ReadWrite;
+    permissions_["executions"] = CollectionAccess::ReadOnly;
+    permissions_["runners"] = CollectionAccess::ReadOnly;
+    permissions_["nodes"] = CollectionAccess::ReadOnly;
+
+    // Load permissions from database
+    auto result = storage_.get("collection_permissions", "settings");
+    if (result.ok()) {
+        const auto& data = result.value();
+
+        // Load default access
+        if (data.contains("defaultAccess")) {
+            default_access_ = collectionAccessFromString(data["defaultAccess"].get<std::string>());
+        }
+
+        // Load per-collection permissions
+        if (data.contains("permissions") && data["permissions"].is_object()) {
+            for (auto& [collection, config] : data["permissions"].items()) {
+                if (config.is_object() && config.contains("access")) {
+                    auto access = collectionAccessFromString(config["access"].get<std::string>());
+                    permissions_[collection] = access;
+                }
+            }
+        }
+
+        LOG_INFO("Loaded collection permissions for {} collections", permissions_.size());
+    } else {
+        LOG_INFO("No collection permissions found in database, using defaults");
+    }
+}
+
+bool CollectionPermissions::canRead(const std::string& collection) const {
+    // System collections are never accessible
+    if (isSystemCollection(collection)) {
+        return false;
+    }
+
+    std::lock_guard<std::mutex> lock(mutex_);
+    auto it = permissions_.find(collection);
+    if (it != permissions_.end()) {
+        return it->second == CollectionAccess::ReadOnly ||
+               it->second == CollectionAccess::ReadWrite;
+    }
+    // Use default access for unlisted collections
+    return default_access_ == CollectionAccess::ReadOnly ||
+           default_access_ == CollectionAccess::ReadWrite;
+}
+
+bool CollectionPermissions::canWrite(const std::string& collection) const {
+    // System collections are never accessible
+    if (isSystemCollection(collection)) {
+        return false;
+    }
+
+    std::lock_guard<std::mutex> lock(mutex_);
+    auto it = permissions_.find(collection);
+    if (it != permissions_.end()) {
+        return it->second == CollectionAccess::ReadWrite;
+    }
+    // Use default access for unlisted collections
+    return default_access_ == CollectionAccess::ReadWrite;
+}
+
+CollectionAccess CollectionPermissions::getAccess(const std::string& collection) const {
+    // System collections are never accessible
+    if (isSystemCollection(collection)) {
+        return CollectionAccess::None;
+    }
+
+    std::lock_guard<std::mutex> lock(mutex_);
+    auto it = permissions_.find(collection);
+    if (it != permissions_.end()) {
+        return it->second;
+    }
+    return default_access_;
+}
+
+bool CollectionPermissions::isSystemCollection(const std::string& collection) const {
+    return SYSTEM_COLLECTIONS.contains(collection);
+}
+
+std::unordered_map<std::string, CollectionAccess> CollectionPermissions::getPermissions() const {
+    std::lock_guard<std::mutex> lock(mutex_);
+    return permissions_;
+}
+
+CollectionAccess CollectionPermissions::getDefaultAccess() const {
+    std::lock_guard<std::mutex> lock(mutex_);
+    return default_access_;
+}
+
+void CollectionPermissions::setPermission(const std::string& collection, CollectionAccess access) {
+    std::lock_guard<std::mutex> lock(mutex_);
+    permissions_[collection] = access;
+}
+
+void CollectionPermissions::setDefaultAccess(CollectionAccess access) {
+    std::lock_guard<std::mutex> lock(mutex_);
+    default_access_ = access;
+}
+
+} // namespace smartbotic::runner

+ 66 - 0
src/runner/collection_permissions.hpp

@@ -0,0 +1,66 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <mutex>
+#include "storage/storage_client.hpp"
+
+namespace smartbotic::runner {
+
+// Collection access levels
+enum class CollectionAccess {
+    None,       // No access allowed
+    ReadOnly,   // Read operations only
+    ReadWrite   // Full access
+};
+
+// Convert access level to string
+std::string collectionAccessToString(CollectionAccess access);
+
+// Convert string to access level
+CollectionAccess collectionAccessFromString(const std::string& str);
+
+// Manages collection access permissions for workflows
+class CollectionPermissions {
+public:
+    explicit CollectionPermissions(storage::StorageClient& storage);
+
+    // Load permissions from database
+    void loadPermissions();
+
+    // Check if a collection can be read
+    bool canRead(const std::string& collection) const;
+
+    // Check if a collection can be written to
+    bool canWrite(const std::string& collection) const;
+
+    // Get the access level for a collection
+    CollectionAccess getAccess(const std::string& collection) const;
+
+    // Check if a collection is a system collection (always protected)
+    bool isSystemCollection(const std::string& collection) const;
+
+    // Get all permission entries
+    std::unordered_map<std::string, CollectionAccess> getPermissions() const;
+
+    // Get default access level for unlisted collections
+    CollectionAccess getDefaultAccess() const;
+
+    // Set permission for a collection (for runtime updates)
+    void setPermission(const std::string& collection, CollectionAccess access);
+
+    // Set default access level
+    void setDefaultAccess(CollectionAccess access);
+
+private:
+    storage::StorageClient& storage_;
+    std::unordered_map<std::string, CollectionAccess> permissions_;
+    CollectionAccess default_access_ = CollectionAccess::None;
+    mutable std::mutex mutex_;
+
+    // System collections that are always protected (no workflow access)
+    static const std::unordered_set<std::string> SYSTEM_COLLECTIONS;
+};
+
+} // namespace smartbotic::runner

+ 2048 - 0
src/runner/engine/script_engine.cpp

@@ -0,0 +1,2048 @@
+#include "script_engine.hpp"
+#include "common/uuid.hpp"
+#include "logging/logger.hpp"
+#include "runner/imap/imap_client.hpp"
+#include <quickjs.h>
+#include <chrono>
+#include <thread>
+#include <curl/curl.h>
+#include <sstream>
+#include <iomanip>
+#include <algorithm>
+#include <regex>
+#include <openssl/sha.h>
+#include <openssl/evp.h>
+#include <openssl/buffer.h>
+
+namespace smartbotic::runner::engine {
+
+using namespace common;
+
+// Base64 encoding helper
+static std::string base64Encode(const std::string& input) {
+    BIO* bio = BIO_new(BIO_s_mem());
+    BIO* b64 = BIO_new(BIO_f_base64());
+    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+    bio = BIO_push(b64, bio);
+
+    BIO_write(bio, input.data(), static_cast<int>(input.size()));
+    BIO_flush(bio);
+
+    BUF_MEM* bufferPtr;
+    BIO_get_mem_ptr(bio, &bufferPtr);
+
+    std::string result(bufferPtr->data, bufferPtr->length);
+    BIO_free_all(bio);
+
+    return result;
+}
+
+// Base64 decoding helper
+static std::string base64Decode(const std::string& input) {
+    BIO* bio = BIO_new_mem_buf(input.data(), static_cast<int>(input.size()));
+    BIO* b64 = BIO_new(BIO_f_base64());
+    BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+    bio = BIO_push(b64, bio);
+
+    std::string result(input.size(), '\0');
+    int decoded_length = BIO_read(bio, result.data(), static_cast<int>(input.size()));
+
+    BIO_free_all(bio);
+
+    if (decoded_length > 0) {
+        result.resize(decoded_length);
+    } else {
+        result.clear();
+    }
+
+    return result;
+}
+
+// SHA256 hash helper - returns hex string
+static std::string sha256Hash(const std::string& input) {
+    unsigned char hash[SHA256_DIGEST_LENGTH];
+    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
+
+    EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr);
+    EVP_DigestUpdate(ctx, input.data(), input.size());
+    EVP_DigestFinal_ex(ctx, hash, nullptr);
+    EVP_MD_CTX_free(ctx);
+
+    // Convert to hex string
+    std::ostringstream ss;
+    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
+        ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(hash[i]);
+    }
+
+    return ss.str();
+}
+
+// Helper structure for curl response
+struct CurlResponse {
+    std::string body;
+    std::string headers;
+    long status_code = 0;
+};
+
+// Curl write callback for response body
+static size_t curlWriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
+    auto* response = static_cast<std::string*>(userdata);
+    size_t total = size * nmemb;
+    response->append(ptr, total);
+    return total;
+}
+
+// Curl header callback
+static size_t curlHeaderCallback(char* buffer, size_t size, size_t nitems, void* userdata) {
+    auto* headers = static_cast<std::string*>(userdata);
+    size_t total = size * nitems;
+    headers->append(buffer, total);
+    return total;
+}
+
+// Parse raw headers into JSON object
+static nlohmann::json parseHeaders(const std::string& raw_headers) {
+    nlohmann::json headers = nlohmann::json::object();
+    std::istringstream stream(raw_headers);
+    std::string line;
+
+    while (std::getline(stream, line)) {
+        // Remove \r if present
+        if (!line.empty() && line.back() == '\r') {
+            line.pop_back();
+        }
+        // Skip empty lines and status line
+        if (line.empty() || line.find("HTTP/") == 0) {
+            continue;
+        }
+        // Find colon separator
+        size_t colon = line.find(':');
+        if (colon != std::string::npos) {
+            std::string key = line.substr(0, colon);
+            std::string value = line.substr(colon + 1);
+            // Trim leading whitespace from value
+            size_t start = value.find_first_not_of(" \t");
+            if (start != std::string::npos) {
+                value = value.substr(start);
+            }
+            // Convert header name to lowercase for consistency
+            std::transform(key.begin(), key.end(), key.begin(), ::tolower);
+            headers[key] = value;
+        }
+    }
+    return headers;
+}
+
+// Perform HTTP request using curl
+static CurlResponse performHttpRequest(
+    const std::string& method,
+    const std::string& url,
+    const nlohmann::json& headers,
+    const std::string& body,
+    long timeout_ms,
+    bool follow_redirects
+) {
+    CurlResponse response;
+
+    CURL* curl = curl_easy_init();
+    if (!curl) {
+        throw std::runtime_error("Failed to initialize curl");
+    }
+
+    std::string response_body;
+    std::string response_headers;
+
+    // Set URL
+    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+
+    // Set method
+    if (method == "POST") {
+        curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    } else if (method == "PUT") {
+        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
+    } else if (method == "PATCH") {
+        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
+    } else if (method == "DELETE") {
+        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
+    } else if (method == "HEAD") {
+        curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
+    } else if (method == "OPTIONS") {
+        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");
+    }
+    // GET is default
+
+    // Set headers
+    struct curl_slist* header_list = nullptr;
+    if (headers.is_object()) {
+        for (auto& [key, value] : headers.items()) {
+            if (value.is_string()) {
+                std::string header = key + ": " + value.get<std::string>();
+                header_list = curl_slist_append(header_list, header.c_str());
+            }
+        }
+    }
+    if (header_list) {
+        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
+    }
+
+    // Set body
+    if (!body.empty() && (method == "POST" || method == "PUT" || method == "PATCH")) {
+        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
+        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
+    }
+
+    // Set timeout
+    if (timeout_ms > 0) {
+        curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
+    }
+
+    // Set redirect following
+    if (follow_redirects) {
+        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+        curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L);
+    }
+
+    // Set callbacks
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body);
+    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curlHeaderCallback);
+    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers);
+
+    // Perform request
+    CURLcode res = curl_easy_perform(curl);
+
+    if (res != CURLE_OK) {
+        std::string error_msg = curl_easy_strerror(res);
+        if (header_list) curl_slist_free_all(header_list);
+        curl_easy_cleanup(curl);
+        throw std::runtime_error("HTTP request failed: " + error_msg);
+    }
+
+    // Get status code
+    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.status_code);
+
+    response.body = std::move(response_body);
+    response.headers = std::move(response_headers);
+
+    if (header_list) curl_slist_free_all(header_list);
+    curl_easy_cleanup(curl);
+
+    return response;
+}
+
+// JavaScript HTTP request function
+static JSValue js_http_request(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    if (argc < 1 || !JS_IsObject(argv[0])) {
+        return JS_ThrowTypeError(ctx, "http.request requires an options object");
+    }
+
+    JSValue options = argv[0];
+
+    // Get method (default GET)
+    std::string method = "GET";
+    JSValue method_val = JS_GetPropertyStr(ctx, options, "method");
+    if (!JS_IsUndefined(method_val)) {
+        const char* method_str = JS_ToCString(ctx, method_val);
+        if (method_str) {
+            method = method_str;
+            JS_FreeCString(ctx, method_str);
+        }
+    }
+    JS_FreeValue(ctx, method_val);
+
+    // Get URL (required)
+    std::string url;
+    JSValue url_val = JS_GetPropertyStr(ctx, options, "url");
+    if (JS_IsUndefined(url_val)) {
+        JS_FreeValue(ctx, url_val);
+        return JS_ThrowTypeError(ctx, "http.request requires a url");
+    }
+    const char* url_str = JS_ToCString(ctx, url_val);
+    if (url_str) {
+        url = url_str;
+        JS_FreeCString(ctx, url_str);
+    }
+    JS_FreeValue(ctx, url_val);
+
+    // Get headers (optional)
+    nlohmann::json headers = nlohmann::json::object();
+    JSValue headers_val = JS_GetPropertyStr(ctx, options, "headers");
+    if (JS_IsObject(headers_val)) {
+        JSValue json_str = JS_JSONStringify(ctx, headers_val, JS_UNDEFINED, JS_UNDEFINED);
+        const char* str = JS_ToCString(ctx, json_str);
+        if (str) {
+            try {
+                headers = nlohmann::json::parse(str);
+            } catch (...) {}
+            JS_FreeCString(ctx, str);
+        }
+        JS_FreeValue(ctx, json_str);
+    }
+    JS_FreeValue(ctx, headers_val);
+
+    // Get body (optional)
+    std::string body;
+    JSValue body_val = JS_GetPropertyStr(ctx, options, "body");
+    if (!JS_IsUndefined(body_val) && !JS_IsNull(body_val)) {
+        if (JS_IsString(body_val)) {
+            const char* body_str = JS_ToCString(ctx, body_val);
+            if (body_str) {
+                body = body_str;
+                JS_FreeCString(ctx, body_str);
+            }
+        } else if (JS_IsObject(body_val)) {
+            // Stringify object body
+            JSValue json_str = JS_JSONStringify(ctx, body_val, JS_UNDEFINED, JS_UNDEFINED);
+            const char* str = JS_ToCString(ctx, json_str);
+            if (str) {
+                body = str;
+                JS_FreeCString(ctx, str);
+            }
+            JS_FreeValue(ctx, json_str);
+        }
+    }
+    JS_FreeValue(ctx, body_val);
+
+    // Get timeout (default 30000ms)
+    long timeout_ms = 30000;
+    JSValue timeout_val = JS_GetPropertyStr(ctx, options, "timeout");
+    if (JS_IsNumber(timeout_val)) {
+        double t;
+        JS_ToFloat64(ctx, &t, timeout_val);
+        timeout_ms = static_cast<long>(t);
+    }
+    JS_FreeValue(ctx, timeout_val);
+
+    // Get followRedirects (default true)
+    bool follow_redirects = true;
+    JSValue follow_val = JS_GetPropertyStr(ctx, options, "followRedirects");
+    if (JS_IsBool(follow_val)) {
+        follow_redirects = JS_ToBool(ctx, follow_val);
+    }
+    JS_FreeValue(ctx, follow_val);
+
+    // Perform the request
+    try {
+        CurlResponse response = performHttpRequest(method, url, headers, body, timeout_ms, follow_redirects);
+
+        // Create response object
+        JSValue result = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, result, "status", JS_NewInt32(ctx, static_cast<int32_t>(response.status_code)));
+
+        // Parse response headers
+        nlohmann::json resp_headers = parseHeaders(response.headers);
+        std::string headers_json = resp_headers.dump();
+        JSValue headers_obj = JS_ParseJSON(ctx, headers_json.c_str(), headers_json.size(), "<headers>");
+        JS_SetPropertyStr(ctx, result, "headers", headers_obj);
+
+        // Try to parse body as JSON, fall back to string
+        std::string content_type;
+        if (resp_headers.contains("content-type")) {
+            content_type = resp_headers["content-type"].get<std::string>();
+        }
+
+        if (content_type.find("application/json") != std::string::npos && !response.body.empty()) {
+            JSValue data = JS_ParseJSON(ctx, response.body.c_str(), response.body.size(), "<body>");
+            if (JS_IsException(data)) {
+                // JSON parse failed, use as string
+                JS_FreeValue(ctx, JS_GetException(ctx));
+                JS_SetPropertyStr(ctx, result, "data", JS_NewString(ctx, response.body.c_str()));
+            } else {
+                JS_SetPropertyStr(ctx, result, "data", data);
+            }
+        } else {
+            JS_SetPropertyStr(ctx, result, "data", JS_NewString(ctx, response.body.c_str()));
+        }
+
+        return result;
+
+    } catch (const std::exception& e) {
+        return JS_ThrowInternalError(ctx, "%s", e.what());
+    }
+}
+
+// Interrupt handler returns int (0 to continue, non-zero to stop)
+static int interruptHandler(JSRuntime* rt, void* opaque) {
+    auto* engine = static_cast<ScriptEngine*>(opaque);
+    return engine->isCancelled() ? 1 : 0;
+}
+
+ScriptEngine::ScriptEngine(const ScriptEngineConfig& config)
+    : config_(config) {
+    initRuntime();
+}
+
+ScriptEngine::~ScriptEngine() {
+    destroyRuntime();
+}
+
+void ScriptEngine::initRuntime() {
+    runtime_ = JS_NewRuntime();
+    if (!runtime_) {
+        throw std::runtime_error("Failed to create QuickJS runtime");
+    }
+
+    // Set memory limit
+    JS_SetMemoryLimit(runtime_, static_cast<size_t>(config_.max_memory_mb) * 1024 * 1024);
+
+    // Set stack size
+    JS_SetMaxStackSize(runtime_, static_cast<size_t>(config_.stack_size_kb) * 1024);
+
+    // Set interrupt handler for cancellation support
+    JS_SetInterruptHandler(runtime_, interruptHandler, this);
+
+    // Create context
+    context_ = JS_NewContext(runtime_);
+    if (!context_) {
+        JS_FreeRuntime(runtime_);
+        runtime_ = nullptr;
+        throw std::runtime_error("Failed to create QuickJS context");
+    }
+
+    // Set up global objects and APIs
+    setupBuiltinAPIs();
+}
+
+void ScriptEngine::destroyRuntime() {
+    if (context_) {
+        JS_FreeContext(context_);
+        context_ = nullptr;
+    }
+    if (runtime_) {
+        JS_FreeRuntime(runtime_);
+        runtime_ = nullptr;
+    }
+}
+
+void ScriptEngine::setupBuiltinAPIs() {
+    if (!context_) return;
+
+    JSContext* ctx = context_;
+    JSValue global = JS_GetGlobalObject(ctx);
+
+    // Create smartbotic namespace object
+    JSValue smartbotic = JS_NewObject(ctx);
+
+    // smartbotic.log
+    JSValue log = JS_NewObject(ctx);
+    JS_SetPropertyStr(ctx, log, "info", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc > 0) {
+            const char* str = JS_ToCString(ctx, argv[0]);
+            if (str) {
+                LOG_INFO("[JS] {}", str);
+                JS_FreeCString(ctx, str);
+            }
+        }
+        return JS_UNDEFINED;
+    }, "info", 1));
+
+    JS_SetPropertyStr(ctx, log, "warn", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc > 0) {
+            const char* str = JS_ToCString(ctx, argv[0]);
+            if (str) {
+                LOG_WARN("[JS] {}", str);
+                JS_FreeCString(ctx, str);
+            }
+        }
+        return JS_UNDEFINED;
+    }, "warn", 1));
+
+    JS_SetPropertyStr(ctx, log, "error", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc > 0) {
+            const char* str = JS_ToCString(ctx, argv[0]);
+            if (str) {
+                LOG_ERROR("[JS] {}", str);
+                JS_FreeCString(ctx, str);
+            }
+        }
+        return JS_UNDEFINED;
+    }, "error", 1));
+
+    JS_SetPropertyStr(ctx, smartbotic, "log", log);
+
+    // smartbotic.utils
+    JSValue utils = JS_NewObject(ctx);
+
+    // utils.uuid() - Generate a UUID
+    JS_SetPropertyStr(ctx, utils, "uuid", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        std::string uuid = common::UUID::generate();
+        return JS_NewString(ctx, uuid.c_str());
+    }, "uuid", 0));
+
+    // utils.sleep(ms) - Pause execution for specified milliseconds
+    JS_SetPropertyStr(ctx, utils, "sleep", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "utils.sleep requires a number of milliseconds");
+        }
+
+        int64_t ms = 0;
+        if (JS_ToInt64(ctx, &ms, argv[0]) != 0) {
+            return JS_ThrowTypeError(ctx, "utils.sleep requires a valid number");
+        }
+
+        if (ms < 0) {
+            ms = 0;
+        }
+        if (ms > 300000) {  // Cap at 5 minutes to prevent abuse
+            ms = 300000;
+        }
+
+        std::this_thread::sleep_for(std::chrono::milliseconds(ms));
+        return JS_UNDEFINED;
+    }, "sleep", 1));
+
+    // utils.interpolate(template, data) - Replace {{key}} placeholders with values from data
+    JS_SetPropertyStr(ctx, utils, "interpolate", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 2) {
+            return JS_ThrowTypeError(ctx, "utils.interpolate requires a template string and data object");
+        }
+
+        // Get template string
+        const char* tpl_cstr = JS_ToCString(ctx, argv[0]);
+        if (!tpl_cstr) {
+            return JS_ThrowTypeError(ctx, "First argument must be a string");
+        }
+        std::string tpl(tpl_cstr);
+        JS_FreeCString(ctx, tpl_cstr);
+
+        // Get data object
+        if (!JS_IsObject(argv[1])) {
+            return JS_ThrowTypeError(ctx, "Second argument must be an object");
+        }
+
+        // Parse data object to JSON for easier access
+        JSValue json_str = JS_JSONStringify(ctx, argv[1], JS_UNDEFINED, JS_UNDEFINED);
+        const char* data_cstr = JS_ToCString(ctx, json_str);
+        nlohmann::json data;
+        if (data_cstr) {
+            try {
+                data = nlohmann::json::parse(data_cstr);
+            } catch (...) {
+                data = nlohmann::json::object();
+            }
+            JS_FreeCString(ctx, data_cstr);
+        }
+        JS_FreeValue(ctx, json_str);
+
+        // Replace {{key}} patterns with values from data
+        // Also supports {{key.nested}} for nested access
+        std::regex pattern(R"(\{\{([^}]+)\}\})");
+        std::string result;
+        std::smatch match;
+        std::string temp = tpl;
+
+        while (std::regex_search(temp, match, pattern)) {
+            // Add text before the match
+            result += match.prefix().str();
+
+            // Get the key (without {{ and }})
+            std::string key = match[1].str();
+
+            // Trim whitespace from key
+            size_t start = key.find_first_not_of(" \t");
+            size_t end = key.find_last_not_of(" \t");
+            if (start != std::string::npos) {
+                key = key.substr(start, end - start + 1);
+            }
+
+            // Handle nested keys (e.g., "user.name")
+            nlohmann::json* current = &data;
+            std::istringstream key_stream(key);
+            std::string part;
+            bool found = true;
+
+            while (std::getline(key_stream, part, '.')) {
+                if (current->is_object() && current->contains(part)) {
+                    current = &(*current)[part];
+                } else if (current->is_array()) {
+                    try {
+                        size_t idx = std::stoul(part);
+                        if (idx < current->size()) {
+                            current = &(*current)[idx];
+                        } else {
+                            found = false;
+                            break;
+                        }
+                    } catch (...) {
+                        found = false;
+                        break;
+                    }
+                } else {
+                    found = false;
+                    break;
+                }
+            }
+
+            if (found && current) {
+                if (current->is_string()) {
+                    result += current->get<std::string>();
+                } else if (current->is_number()) {
+                    if (current->is_number_integer()) {
+                        result += std::to_string(current->get<int64_t>());
+                    } else {
+                        result += std::to_string(current->get<double>());
+                    }
+                } else if (current->is_boolean()) {
+                    result += current->get<bool>() ? "true" : "false";
+                } else if (current->is_null()) {
+                    result += "null";
+                } else {
+                    // For objects/arrays, use JSON representation
+                    result += current->dump();
+                }
+            } else {
+                // Keep original placeholder if key not found
+                result += match[0].str();
+            }
+
+            // Continue with the rest of the string
+            temp = match.suffix().str();
+        }
+
+        // Add remaining text after last match
+        result += temp;
+
+        return JS_NewString(ctx, result.c_str());
+    }, "interpolate", 2));
+
+    // utils.deepClone(obj) - Deep clone an object
+    JS_SetPropertyStr(ctx, utils, "deepClone", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_UNDEFINED;
+        }
+
+        // Use JSON stringify/parse for deep clone
+        JSValue json_str = JS_JSONStringify(ctx, argv[0], JS_UNDEFINED, JS_UNDEFINED);
+        if (JS_IsException(json_str)) {
+            return JS_ThrowTypeError(ctx, "Cannot deep clone: object is not JSON serializable");
+        }
+
+        const char* str = JS_ToCString(ctx, json_str);
+        if (!str) {
+            JS_FreeValue(ctx, json_str);
+            return JS_ThrowTypeError(ctx, "Failed to stringify object");
+        }
+
+        JSValue result = JS_ParseJSON(ctx, str, strlen(str), "<clone>");
+        JS_FreeCString(ctx, str);
+        JS_FreeValue(ctx, json_str);
+
+        return result;
+    }, "deepClone", 1));
+
+    // utils.isEmpty(value) - Check if value is empty (null, undefined, empty string, empty array, empty object)
+    JS_SetPropertyStr(ctx, utils, "isEmpty", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_TRUE;
+        }
+
+        JSValue val = argv[0];
+
+        // Check for null/undefined
+        if (JS_IsNull(val) || JS_IsUndefined(val)) {
+            return JS_TRUE;
+        }
+
+        // Check for empty string
+        if (JS_IsString(val)) {
+            const char* str = JS_ToCString(ctx, val);
+            bool empty = !str || strlen(str) == 0;
+            JS_FreeCString(ctx, str);
+            return empty ? JS_TRUE : JS_FALSE;
+        }
+
+        // Check for empty array
+        if (JS_IsArray(ctx, val)) {
+            JSValue length = JS_GetPropertyStr(ctx, val, "length");
+            int64_t len = 0;
+            JS_ToInt64(ctx, &len, length);
+            JS_FreeValue(ctx, length);
+            return len == 0 ? JS_TRUE : JS_FALSE;
+        }
+
+        // Check for empty object
+        if (JS_IsObject(val)) {
+            JSPropertyEnum* props;
+            uint32_t count;
+            if (JS_GetOwnPropertyNames(ctx, &props, &count, val, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
+                for (uint32_t i = 0; i < count; i++) {
+                    JS_FreeAtom(ctx, props[i].atom);
+                }
+                js_free(ctx, props);
+                return count == 0 ? JS_TRUE : JS_FALSE;
+            }
+        }
+
+        return JS_FALSE;
+    }, "isEmpty", 1));
+
+    // utils.pick(obj, keys) - Pick specific keys from an object
+    JS_SetPropertyStr(ctx, utils, "pick", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 2 || !JS_IsObject(argv[0]) || !JS_IsArray(ctx, argv[1])) {
+            return JS_ThrowTypeError(ctx, "utils.pick requires an object and an array of keys");
+        }
+
+        JSValue result = JS_NewObject(ctx);
+        JSValue keys = argv[1];
+        JSValue length = JS_GetPropertyStr(ctx, keys, "length");
+        int64_t len = 0;
+        JS_ToInt64(ctx, &len, length);
+        JS_FreeValue(ctx, length);
+
+        for (int64_t i = 0; i < len; i++) {
+            JSValue key_val = JS_GetPropertyUint32(ctx, keys, i);
+            const char* key = JS_ToCString(ctx, key_val);
+            if (key) {
+                JSValue prop = JS_GetPropertyStr(ctx, argv[0], key);
+                if (!JS_IsUndefined(prop)) {
+                    JS_SetPropertyStr(ctx, result, key, prop);
+                } else {
+                    JS_FreeValue(ctx, prop);
+                }
+                JS_FreeCString(ctx, key);
+            }
+            JS_FreeValue(ctx, key_val);
+        }
+
+        return result;
+    }, "pick", 2));
+
+    // utils.omit(obj, keys) - Omit specific keys from an object
+    JS_SetPropertyStr(ctx, utils, "omit", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 2 || !JS_IsObject(argv[0]) || !JS_IsArray(ctx, argv[1])) {
+            return JS_ThrowTypeError(ctx, "utils.omit requires an object and an array of keys");
+        }
+
+        // Get keys to omit
+        JSValue omit_keys = argv[1];
+        JSValue omit_length = JS_GetPropertyStr(ctx, omit_keys, "length");
+        int64_t omit_len = 0;
+        JS_ToInt64(ctx, &omit_len, omit_length);
+        JS_FreeValue(ctx, omit_length);
+
+        std::vector<std::string> keys_to_omit;
+        for (int64_t i = 0; i < omit_len; i++) {
+            JSValue key_val = JS_GetPropertyUint32(ctx, omit_keys, i);
+            const char* key = JS_ToCString(ctx, key_val);
+            if (key) {
+                keys_to_omit.push_back(key);
+                JS_FreeCString(ctx, key);
+            }
+            JS_FreeValue(ctx, key_val);
+        }
+
+        // Clone object without omitted keys
+        JSValue result = JS_NewObject(ctx);
+        JSPropertyEnum* props;
+        uint32_t count;
+        if (JS_GetOwnPropertyNames(ctx, &props, &count, argv[0], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
+            for (uint32_t i = 0; i < count; i++) {
+                const char* prop_name = JS_AtomToCString(ctx, props[i].atom);
+                if (prop_name) {
+                    bool should_omit = std::find(keys_to_omit.begin(), keys_to_omit.end(), prop_name) != keys_to_omit.end();
+                    if (!should_omit) {
+                        JSValue prop = JS_GetProperty(ctx, argv[0], props[i].atom);
+                        JS_SetPropertyStr(ctx, result, prop_name, prop);
+                    }
+                    JS_FreeCString(ctx, prop_name);
+                }
+                JS_FreeAtom(ctx, props[i].atom);
+            }
+            js_free(ctx, props);
+        }
+
+        return result;
+    }, "omit", 2));
+
+    // utils.base64Encode(data) - Encode string to Base64
+    JS_SetPropertyStr(ctx, utils, "base64Encode", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "utils.base64Encode requires a string argument");
+        }
+
+        const char* input_str = JS_ToCString(ctx, argv[0]);
+        if (!input_str) {
+            return JS_ThrowTypeError(ctx, "Argument must be a string");
+        }
+
+        std::string input(input_str);
+        JS_FreeCString(ctx, input_str);
+
+        std::string encoded = base64Encode(input);
+        return JS_NewString(ctx, encoded.c_str());
+    }, "base64Encode", 1));
+
+    // utils.base64Decode(base64) - Decode Base64 to string
+    JS_SetPropertyStr(ctx, utils, "base64Decode", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "utils.base64Decode requires a string argument");
+        }
+
+        const char* input_str = JS_ToCString(ctx, argv[0]);
+        if (!input_str) {
+            return JS_ThrowTypeError(ctx, "Argument must be a string");
+        }
+
+        std::string input(input_str);
+        JS_FreeCString(ctx, input_str);
+
+        std::string decoded = base64Decode(input);
+        return JS_NewString(ctx, decoded.c_str());
+    }, "base64Decode", 1));
+
+    // utils.sha256(data) - Calculate SHA256 hash (hex string)
+    JS_SetPropertyStr(ctx, utils, "sha256", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "utils.sha256 requires a string argument");
+        }
+
+        const char* input_str = JS_ToCString(ctx, argv[0]);
+        if (!input_str) {
+            return JS_ThrowTypeError(ctx, "Argument must be a string");
+        }
+
+        std::string input(input_str);
+        JS_FreeCString(ctx, input_str);
+
+        std::string hash = sha256Hash(input);
+        return JS_NewString(ctx, hash.c_str());
+    }, "sha256", 1));
+
+    JS_SetPropertyStr(ctx, smartbotic, "utils", utils);
+
+    // smartbotic.http
+    JSValue http = JS_NewObject(ctx);
+    JS_SetPropertyStr(ctx, http, "request", JS_NewCFunction(ctx, js_http_request, "request", 1));
+
+    // Convenience methods
+    JS_SetPropertyStr(ctx, http, "get", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "http.get requires a URL");
+        }
+        JSValue options = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, options, "method", JS_NewString(ctx, "GET"));
+        if (JS_IsString(argv[0])) {
+            JS_SetPropertyStr(ctx, options, "url", JS_DupValue(ctx, argv[0]));
+        } else if (JS_IsObject(argv[0])) {
+            // Copy properties from options object
+            JSValue url = JS_GetPropertyStr(ctx, argv[0], "url");
+            JS_SetPropertyStr(ctx, options, "url", url);
+            JSValue headers = JS_GetPropertyStr(ctx, argv[0], "headers");
+            if (!JS_IsUndefined(headers)) {
+                JS_SetPropertyStr(ctx, options, "headers", headers);
+            } else {
+                JS_FreeValue(ctx, headers);
+            }
+            JSValue timeout = JS_GetPropertyStr(ctx, argv[0], "timeout");
+            if (!JS_IsUndefined(timeout)) {
+                JS_SetPropertyStr(ctx, options, "timeout", timeout);
+            } else {
+                JS_FreeValue(ctx, timeout);
+            }
+        }
+        JSValue args[1] = { options };
+        JSValue result = js_http_request(ctx, JS_UNDEFINED, 1, args);
+        JS_FreeValue(ctx, options);
+        return result;
+    }, "get", 1));
+
+    JS_SetPropertyStr(ctx, http, "post", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "http.post requires a URL or options object");
+        }
+        JSValue options = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, options, "method", JS_NewString(ctx, "POST"));
+        if (JS_IsString(argv[0])) {
+            JS_SetPropertyStr(ctx, options, "url", JS_DupValue(ctx, argv[0]));
+            if (argc > 1) {
+                JS_SetPropertyStr(ctx, options, "body", JS_DupValue(ctx, argv[1]));
+            }
+        } else if (JS_IsObject(argv[0])) {
+            JSValue url = JS_GetPropertyStr(ctx, argv[0], "url");
+            JS_SetPropertyStr(ctx, options, "url", url);
+            JSValue headers = JS_GetPropertyStr(ctx, argv[0], "headers");
+            if (!JS_IsUndefined(headers)) {
+                JS_SetPropertyStr(ctx, options, "headers", headers);
+            } else {
+                JS_FreeValue(ctx, headers);
+            }
+            JSValue body = JS_GetPropertyStr(ctx, argv[0], "body");
+            if (!JS_IsUndefined(body)) {
+                JS_SetPropertyStr(ctx, options, "body", body);
+            } else {
+                JS_FreeValue(ctx, body);
+            }
+            JSValue timeout = JS_GetPropertyStr(ctx, argv[0], "timeout");
+            if (!JS_IsUndefined(timeout)) {
+                JS_SetPropertyStr(ctx, options, "timeout", timeout);
+            } else {
+                JS_FreeValue(ctx, timeout);
+            }
+        }
+        JSValue args[1] = { options };
+        JSValue result = js_http_request(ctx, JS_UNDEFINED, 1, args);
+        JS_FreeValue(ctx, options);
+        return result;
+    }, "post", 2));
+
+    JS_SetPropertyStr(ctx, smartbotic, "http", http);
+
+    JS_SetPropertyStr(ctx, global, "smartbotic", smartbotic);
+
+    // Console for compatibility
+    JS_SetPropertyStr(ctx, global, "console", JS_DupValue(ctx, log));
+
+    JS_FreeValue(ctx, global);
+}
+
+// Helper to get ScriptContext from QuickJS context opaque data
+static const ScriptContext* getScriptContext(JSContext* ctx) {
+    return static_cast<const ScriptContext*>(JS_GetContextOpaque(ctx));
+}
+
+// Helper to create error result in JS
+static JSValue createJsError(JSContext* ctx, const std::string& message) {
+    return JS_ThrowInternalError(ctx, "%s", message.c_str());
+}
+
+// Helper to create success result object
+static JSValue createJsResult(JSContext* ctx, bool success, const nlohmann::json& data = nlohmann::json{}) {
+    JSValue result = JS_NewObject(ctx);
+    JS_SetPropertyStr(ctx, result, "success", success ? JS_TRUE : JS_FALSE);
+    if (!data.is_null()) {
+        std::string data_str = data.dump();
+        JSValue data_val = JS_ParseJSON(ctx, data_str.c_str(), data_str.size(), "<data>");
+        JS_SetPropertyStr(ctx, result, "data", data_val);
+    }
+    return result;
+}
+
+// storage.get(collection, id) - Get document by ID
+static JSValue js_storage_get(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->storage_get_doc) {
+        return createJsError(ctx, "Storage API not available");
+    }
+
+    if (argc < 2) {
+        return JS_ThrowTypeError(ctx, "storage.get requires collection and id arguments");
+    }
+
+    const char* collection = JS_ToCString(ctx, argv[0]);
+    const char* id = JS_ToCString(ctx, argv[1]);
+
+    if (!collection || !id) {
+        if (collection) JS_FreeCString(ctx, collection);
+        if (id) JS_FreeCString(ctx, id);
+        return JS_ThrowTypeError(ctx, "collection and id must be strings");
+    }
+
+    std::string coll_str(collection);
+    std::string id_str(id);
+    JS_FreeCString(ctx, collection);
+    JS_FreeCString(ctx, id);
+
+    auto result = script_ctx->storage_get_doc(coll_str, id_str);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        JS_SetPropertyStr(ctx, response, "found", JS_TRUE);
+        std::string doc_str = result.value().dump();
+        JSValue doc_val = JS_ParseJSON(ctx, doc_str.c_str(), doc_str.size(), "<document>");
+        JS_SetPropertyStr(ctx, response, "document", doc_val);
+    } else {
+        JS_SetPropertyStr(ctx, response, "found", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "document", JS_NULL);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+    JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
+    JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, id_str.c_str()));
+
+    return response;
+}
+
+// storage.insert(collection, data, id?, ttlMs?) - Insert new document
+static JSValue js_storage_insert(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->storage_insert) {
+        return createJsError(ctx, "Storage API not available");
+    }
+
+    if (argc < 2) {
+        return JS_ThrowTypeError(ctx, "storage.insert requires collection and data arguments");
+    }
+
+    const char* collection = JS_ToCString(ctx, argv[0]);
+    if (!collection) {
+        return JS_ThrowTypeError(ctx, "collection must be a string");
+    }
+    std::string coll_str(collection);
+    JS_FreeCString(ctx, collection);
+
+    // Parse data
+    JSValue json_str = JS_JSONStringify(ctx, argv[1], JS_UNDEFINED, JS_UNDEFINED);
+    const char* data_cstr = JS_ToCString(ctx, json_str);
+    nlohmann::json data;
+    if (data_cstr) {
+        try {
+            data = nlohmann::json::parse(data_cstr);
+        } catch (...) {
+            JS_FreeCString(ctx, data_cstr);
+            JS_FreeValue(ctx, json_str);
+            return JS_ThrowTypeError(ctx, "data must be a valid JSON object");
+        }
+        JS_FreeCString(ctx, data_cstr);
+    }
+    JS_FreeValue(ctx, json_str);
+
+    // Optional id
+    std::string id_str;
+    if (argc > 2 && !JS_IsUndefined(argv[2]) && !JS_IsNull(argv[2])) {
+        const char* id = JS_ToCString(ctx, argv[2]);
+        if (id) {
+            id_str = id;
+            JS_FreeCString(ctx, id);
+        }
+    }
+
+    // Optional TTL
+    int64_t ttl_ms = 0;
+    if (argc > 3 && JS_IsNumber(argv[3])) {
+        double ttl;
+        JS_ToFloat64(ctx, &ttl, argv[3]);
+        ttl_ms = static_cast<int64_t>(ttl);
+    }
+
+    auto result = script_ctx->storage_insert(coll_str, data, id_str, ttl_ms);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
+        JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, result.value().c_str()));
+    } else {
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+    JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
+
+    return response;
+}
+
+// storage.update(collection, id, data, expectedVersion?, partial?) - Update document
+static JSValue js_storage_update(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->storage_update) {
+        return createJsError(ctx, "Storage API not available");
+    }
+
+    if (argc < 3) {
+        return JS_ThrowTypeError(ctx, "storage.update requires collection, id, and data arguments");
+    }
+
+    const char* collection = JS_ToCString(ctx, argv[0]);
+    const char* id = JS_ToCString(ctx, argv[1]);
+
+    if (!collection || !id) {
+        if (collection) JS_FreeCString(ctx, collection);
+        if (id) JS_FreeCString(ctx, id);
+        return JS_ThrowTypeError(ctx, "collection and id must be strings");
+    }
+    std::string coll_str(collection);
+    std::string id_str(id);
+    JS_FreeCString(ctx, collection);
+    JS_FreeCString(ctx, id);
+
+    // Parse data
+    JSValue json_str = JS_JSONStringify(ctx, argv[2], JS_UNDEFINED, JS_UNDEFINED);
+    const char* data_cstr = JS_ToCString(ctx, json_str);
+    nlohmann::json data;
+    if (data_cstr) {
+        try {
+            data = nlohmann::json::parse(data_cstr);
+        } catch (...) {
+            JS_FreeCString(ctx, data_cstr);
+            JS_FreeValue(ctx, json_str);
+            return JS_ThrowTypeError(ctx, "data must be a valid JSON object");
+        }
+        JS_FreeCString(ctx, data_cstr);
+    }
+    JS_FreeValue(ctx, json_str);
+
+    // Optional expectedVersion
+    int64_t expected_version = 0;
+    if (argc > 3 && JS_IsNumber(argv[3])) {
+        double ver;
+        JS_ToFloat64(ctx, &ver, argv[3]);
+        expected_version = static_cast<int64_t>(ver);
+    }
+
+    // Optional partial
+    bool partial = false;
+    if (argc > 4 && JS_IsBool(argv[4])) {
+        partial = JS_ToBool(ctx, argv[4]);
+    }
+
+    auto result = script_ctx->storage_update(coll_str, id_str, data, expected_version, partial);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
+        JS_SetPropertyStr(ctx, response, "version", JS_NewInt64(ctx, result.value()));
+    } else {
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+    JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
+    JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, id_str.c_str()));
+
+    return response;
+}
+
+// storage.delete(collection, id, expectedVersion?) - Delete document
+static JSValue js_storage_delete(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->storage_delete) {
+        return createJsError(ctx, "Storage API not available");
+    }
+
+    if (argc < 2) {
+        return JS_ThrowTypeError(ctx, "storage.delete requires collection and id arguments");
+    }
+
+    const char* collection = JS_ToCString(ctx, argv[0]);
+    const char* id = JS_ToCString(ctx, argv[1]);
+
+    if (!collection || !id) {
+        if (collection) JS_FreeCString(ctx, collection);
+        if (id) JS_FreeCString(ctx, id);
+        return JS_ThrowTypeError(ctx, "collection and id must be strings");
+    }
+    std::string coll_str(collection);
+    std::string id_str(id);
+    JS_FreeCString(ctx, collection);
+    JS_FreeCString(ctx, id);
+
+    // Optional expectedVersion
+    int64_t expected_version = 0;
+    if (argc > 2 && JS_IsNumber(argv[2])) {
+        double ver;
+        JS_ToFloat64(ctx, &ver, argv[2]);
+        expected_version = static_cast<int64_t>(ver);
+    }
+
+    auto result = script_ctx->storage_delete(coll_str, id_str, expected_version);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
+    } else {
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+    JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
+    JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, id_str.c_str()));
+
+    return response;
+}
+
+// storage.query(collection, options?) - Query documents
+static JSValue js_storage_query(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->storage_query) {
+        return createJsError(ctx, "Storage API not available");
+    }
+
+    if (argc < 1) {
+        return JS_ThrowTypeError(ctx, "storage.query requires collection argument");
+    }
+
+    const char* collection = JS_ToCString(ctx, argv[0]);
+    if (!collection) {
+        return JS_ThrowTypeError(ctx, "collection must be a string");
+    }
+    std::string coll_str(collection);
+    JS_FreeCString(ctx, collection);
+
+    // Parse options
+    StorageQueryOptions options;
+    if (argc > 1 && JS_IsObject(argv[1])) {
+        JSValue opts = argv[1];
+
+        // Parse filters
+        JSValue filters_val = JS_GetPropertyStr(ctx, opts, "filters");
+        if (JS_IsArray(ctx, filters_val)) {
+            JSValue length = JS_GetPropertyStr(ctx, filters_val, "length");
+            int64_t len = 0;
+            JS_ToInt64(ctx, &len, length);
+            JS_FreeValue(ctx, length);
+
+            for (int64_t i = 0; i < len; i++) {
+                JSValue filter = JS_GetPropertyUint32(ctx, filters_val, i);
+                if (JS_IsObject(filter)) {
+                    JSValue field_val = JS_GetPropertyStr(ctx, filter, "field");
+                    JSValue value_val = JS_GetPropertyStr(ctx, filter, "value");
+
+                    const char* field = JS_ToCString(ctx, field_val);
+                    if (field) {
+                        // Get value as JSON
+                        JSValue val_str = JS_JSONStringify(ctx, value_val, JS_UNDEFINED, JS_UNDEFINED);
+                        const char* val_cstr = JS_ToCString(ctx, val_str);
+                        if (val_cstr) {
+                            try {
+                                nlohmann::json val = nlohmann::json::parse(val_cstr);
+                                options.filters.push_back({field, val});
+                            } catch (...) {}
+                            JS_FreeCString(ctx, val_cstr);
+                        }
+                        JS_FreeValue(ctx, val_str);
+                        JS_FreeCString(ctx, field);
+                    }
+                    JS_FreeValue(ctx, field_val);
+                    JS_FreeValue(ctx, value_val);
+                }
+                JS_FreeValue(ctx, filter);
+            }
+        }
+        JS_FreeValue(ctx, filters_val);
+
+        // Parse sorts
+        JSValue sorts_val = JS_GetPropertyStr(ctx, opts, "sorts");
+        if (JS_IsArray(ctx, sorts_val)) {
+            JSValue length = JS_GetPropertyStr(ctx, sorts_val, "length");
+            int64_t len = 0;
+            JS_ToInt64(ctx, &len, length);
+            JS_FreeValue(ctx, length);
+
+            for (int64_t i = 0; i < len; i++) {
+                JSValue sort = JS_GetPropertyUint32(ctx, sorts_val, i);
+                if (JS_IsObject(sort)) {
+                    JSValue field_val = JS_GetPropertyStr(ctx, sort, "field");
+                    JSValue asc_val = JS_GetPropertyStr(ctx, sort, "ascending");
+
+                    const char* field = JS_ToCString(ctx, field_val);
+                    if (field) {
+                        bool asc = JS_IsUndefined(asc_val) ? true : JS_ToBool(ctx, asc_val);
+                        options.sorts.push_back({field, asc});
+                        JS_FreeCString(ctx, field);
+                    }
+                    JS_FreeValue(ctx, field_val);
+                    JS_FreeValue(ctx, asc_val);
+                }
+                JS_FreeValue(ctx, sort);
+            }
+        }
+        JS_FreeValue(ctx, sorts_val);
+
+        // Parse fields (projection)
+        JSValue fields_val = JS_GetPropertyStr(ctx, opts, "fields");
+        if (JS_IsArray(ctx, fields_val)) {
+            JSValue length = JS_GetPropertyStr(ctx, fields_val, "length");
+            int64_t len = 0;
+            JS_ToInt64(ctx, &len, length);
+            JS_FreeValue(ctx, length);
+
+            for (int64_t i = 0; i < len; i++) {
+                JSValue field = JS_GetPropertyUint32(ctx, fields_val, i);
+                const char* field_str = JS_ToCString(ctx, field);
+                if (field_str) {
+                    options.fields.push_back(field_str);
+                    JS_FreeCString(ctx, field_str);
+                }
+                JS_FreeValue(ctx, field);
+            }
+        }
+        JS_FreeValue(ctx, fields_val);
+
+        // Parse page
+        JSValue page_val = JS_GetPropertyStr(ctx, opts, "page");
+        if (JS_IsNumber(page_val)) {
+            double page;
+            JS_ToFloat64(ctx, &page, page_val);
+            options.page = static_cast<int32_t>(page);
+        }
+        JS_FreeValue(ctx, page_val);
+
+        // Parse pageSize
+        JSValue page_size_val = JS_GetPropertyStr(ctx, opts, "pageSize");
+        if (JS_IsNumber(page_size_val)) {
+            double page_size;
+            JS_ToFloat64(ctx, &page_size, page_size_val);
+            options.page_size = static_cast<int32_t>(page_size);
+        }
+        JS_FreeValue(ctx, page_size_val);
+    }
+
+    auto result = script_ctx->storage_query(coll_str, options);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        const auto& query_result = result.value();
+
+        // Convert documents to JS array
+        JSValue docs_array = JS_NewArray(ctx);
+        for (size_t i = 0; i < query_result.documents.size(); i++) {
+            std::string doc_str = query_result.documents[i].dump();
+            JSValue doc_val = JS_ParseJSON(ctx, doc_str.c_str(), doc_str.size(), "<document>");
+            JS_SetPropertyUint32(ctx, docs_array, i, doc_val);
+        }
+        JS_SetPropertyStr(ctx, response, "documents", docs_array);
+        JS_SetPropertyStr(ctx, response, "totalCount", JS_NewInt64(ctx, query_result.total_count));
+        JS_SetPropertyStr(ctx, response, "hasMore", query_result.has_more ? JS_TRUE : JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "page", JS_NewInt32(ctx, options.page));
+        JS_SetPropertyStr(ctx, response, "pageSize", JS_NewInt32(ctx, options.page_size));
+    } else {
+        JS_SetPropertyStr(ctx, response, "documents", JS_NewArray(ctx));
+        JS_SetPropertyStr(ctx, response, "totalCount", JS_NewInt32(ctx, 0));
+        JS_SetPropertyStr(ctx, response, "hasMore", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+    JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
+
+    return response;
+}
+
+// storage.listCollections(includeSystem?) - List accessible collections
+static JSValue js_storage_list_collections(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->storage_list_collections) {
+        return createJsError(ctx, "Storage API not available");
+    }
+
+    bool include_system = false;
+    if (argc > 0 && JS_IsBool(argv[0])) {
+        include_system = JS_ToBool(ctx, argv[0]);
+    }
+
+    auto result = script_ctx->storage_list_collections(include_system);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        const auto& collections = result.value();
+
+        JSValue coll_array = JS_NewArray(ctx);
+        for (size_t i = 0; i < collections.size(); i++) {
+            JS_SetPropertyUint32(ctx, coll_array, i, JS_NewString(ctx, collections[i].c_str()));
+        }
+        JS_SetPropertyStr(ctx, response, "collections", coll_array);
+        JS_SetPropertyStr(ctx, response, "count", JS_NewInt64(ctx, collections.size()));
+    } else {
+        JS_SetPropertyStr(ctx, response, "collections", JS_NewArray(ctx));
+        JS_SetPropertyStr(ctx, response, "count", JS_NewInt32(ctx, 0));
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+
+    return response;
+}
+
+// Setup storage API on smartbotic namespace
+static void setupStorageAPI(JSContext* ctx, JSValue smartbotic) {
+    JSValue storage = JS_NewObject(ctx);
+
+    JS_SetPropertyStr(ctx, storage, "get", JS_NewCFunction(ctx, js_storage_get, "get", 2));
+    JS_SetPropertyStr(ctx, storage, "insert", JS_NewCFunction(ctx, js_storage_insert, "insert", 4));
+    JS_SetPropertyStr(ctx, storage, "update", JS_NewCFunction(ctx, js_storage_update, "update", 5));
+    JS_SetPropertyStr(ctx, storage, "delete", JS_NewCFunction(ctx, js_storage_delete, "delete", 3));
+    JS_SetPropertyStr(ctx, storage, "query", JS_NewCFunction(ctx, js_storage_query, "query", 2));
+    JS_SetPropertyStr(ctx, storage, "listCollections", JS_NewCFunction(ctx, js_storage_list_collections, "listCollections", 1));
+
+    JS_SetPropertyStr(ctx, smartbotic, "storage", storage);
+}
+
+// credentials.get(id) - Get credential authentication header
+static JSValue js_credentials_get(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->credentials_get) {
+        // Return error object if credentials API not available
+        JSValue response = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "Credentials API not available"));
+        return response;
+    }
+
+    if (argc < 1) {
+        JSValue response = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "credentials.get requires credential ID argument"));
+        return response;
+    }
+
+    const char* id = JS_ToCString(ctx, argv[0]);
+    if (!id) {
+        JSValue response = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "Credential ID must be a string"));
+        return response;
+    }
+    std::string id_str(id);
+    JS_FreeCString(ctx, id);
+
+    auto result = script_ctx->credentials_get(id_str);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
+        JS_SetPropertyStr(ctx, response, "headerName", JS_NewString(ctx, result.value().header_name.c_str()));
+        JS_SetPropertyStr(ctx, response, "headerValue", JS_NewString(ctx, result.value().header_value.c_str()));
+    } else {
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+
+    return response;
+}
+
+// credentials.getImap(id) - Get IMAP credentials
+static JSValue js_credentials_get_imap(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->credentials_get_imap) {
+        JSValue response = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "IMAP Credentials API not available"));
+        return response;
+    }
+
+    if (argc < 1) {
+        JSValue response = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "credentials.getImap requires credential ID argument"));
+        return response;
+    }
+
+    const char* id = JS_ToCString(ctx, argv[0]);
+    if (!id) {
+        JSValue response = JS_NewObject(ctx);
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "Credential ID must be a string"));
+        return response;
+    }
+    std::string id_str(id);
+    JS_FreeCString(ctx, id);
+
+    auto result = script_ctx->credentials_get_imap(id_str);
+
+    JSValue response = JS_NewObject(ctx);
+    if (result.ok()) {
+        JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
+        JS_SetPropertyStr(ctx, response, "host", JS_NewString(ctx, result.value().host.c_str()));
+        JS_SetPropertyStr(ctx, response, "port", JS_NewInt32(ctx, result.value().port));
+        JS_SetPropertyStr(ctx, response, "username", JS_NewString(ctx, result.value().username.c_str()));
+        JS_SetPropertyStr(ctx, response, "password", JS_NewString(ctx, result.value().password.c_str()));
+        JS_SetPropertyStr(ctx, response, "useSsl", result.value().use_ssl ? JS_TRUE : JS_FALSE);
+    } else {
+        JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
+    }
+
+    return response;
+}
+
+// IMAP operations now use the imap::ImapClient class
+
+// imap.search(options) - Search emails
+static JSValue js_imap_search(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->credentials_get_imap) {
+        return JS_ThrowInternalError(ctx, "IMAP credentials API not available");
+    }
+
+    if (argc < 1 || !JS_IsObject(argv[0])) {
+        return JS_ThrowTypeError(ctx, "imap.search requires an options object");
+    }
+
+    JSValue options = argv[0];
+
+    // Get credential ID
+    JSValue cred_val = JS_GetPropertyStr(ctx, options, "credentialId");
+    if (JS_IsUndefined(cred_val)) {
+        JS_FreeValue(ctx, cred_val);
+        return JS_ThrowTypeError(ctx, "imap.search requires credentialId");
+    }
+    const char* cred_id = JS_ToCString(ctx, cred_val);
+    std::string credential_id(cred_id ? cred_id : "");
+    JS_FreeCString(ctx, cred_id);
+    JS_FreeValue(ctx, cred_val);
+
+    // Get mailbox (default INBOX)
+    std::string mailbox = "INBOX";
+    JSValue mailbox_val = JS_GetPropertyStr(ctx, options, "mailbox");
+    if (!JS_IsUndefined(mailbox_val)) {
+        const char* mb = JS_ToCString(ctx, mailbox_val);
+        if (mb) { mailbox = mb; JS_FreeCString(ctx, mb); }
+    }
+    JS_FreeValue(ctx, mailbox_val);
+
+    // Get search criteria (default ALL)
+    std::string criteria = "ALL";
+    JSValue criteria_val = JS_GetPropertyStr(ctx, options, "criteria");
+    if (!JS_IsUndefined(criteria_val)) {
+        const char* cr = JS_ToCString(ctx, criteria_val);
+        if (cr) { criteria = cr; JS_FreeCString(ctx, cr); }
+    }
+    JS_FreeValue(ctx, criteria_val);
+
+    // Get limit
+    int limit = 50;
+    JSValue limit_val = JS_GetPropertyStr(ctx, options, "limit");
+    if (JS_IsNumber(limit_val)) {
+        int32_t l;
+        JS_ToInt32(ctx, &l, limit_val);
+        limit = l;
+    }
+    JS_FreeValue(ctx, limit_val);
+
+    // Get credentials
+    auto cred_result = script_ctx->credentials_get_imap(credential_id);
+    if (cred_result.failed()) {
+        return JS_ThrowInternalError(ctx, "Failed to get IMAP credentials: %s",
+                                     cred_result.error().message().c_str());
+    }
+
+    const auto& cred = cred_result.value();
+
+    // Create ImapClient and perform search
+    imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
+    imap::ImapClient client(imap_creds);
+
+    auto search_result = client.search(mailbox, criteria, limit);
+    if (search_result.failed()) {
+        return JS_ThrowInternalError(ctx, "IMAP search failed: %s",
+                                     search_result.error().message().c_str());
+    }
+
+    const auto& search_data = search_result.value();
+
+    // Build JS result
+    JSValue result = JS_NewObject(ctx);
+    JSValue uids = JS_NewArray(ctx);
+
+    for (int i = 0; i < search_data.count; i++) {
+        JS_SetPropertyUint32(ctx, uids, i, JS_NewString(ctx, search_data.uids[i].c_str()));
+    }
+
+    JS_SetPropertyStr(ctx, result, "success", JS_TRUE);
+    JS_SetPropertyStr(ctx, result, "uids", uids);
+    JS_SetPropertyStr(ctx, result, "count", JS_NewInt32(ctx, search_data.count));
+
+    return result;
+}
+
+// imap.fetch(options) - Fetch email content
+static JSValue js_imap_fetch(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->credentials_get_imap) {
+        return JS_ThrowInternalError(ctx, "IMAP credentials API not available");
+    }
+
+    if (argc < 1 || !JS_IsObject(argv[0])) {
+        return JS_ThrowTypeError(ctx, "imap.fetch requires an options object");
+    }
+
+    JSValue options = argv[0];
+
+    // Get credential ID
+    JSValue cred_val = JS_GetPropertyStr(ctx, options, "credentialId");
+    if (JS_IsUndefined(cred_val)) {
+        JS_FreeValue(ctx, cred_val);
+        return JS_ThrowTypeError(ctx, "imap.fetch requires credentialId");
+    }
+    const char* cred_id = JS_ToCString(ctx, cred_val);
+    std::string credential_id(cred_id ? cred_id : "");
+    JS_FreeCString(ctx, cred_id);
+    JS_FreeValue(ctx, cred_val);
+
+    // Get UID (required)
+    JSValue uid_val = JS_GetPropertyStr(ctx, options, "uid");
+    if (JS_IsUndefined(uid_val)) {
+        JS_FreeValue(ctx, uid_val);
+        return JS_ThrowTypeError(ctx, "imap.fetch requires uid");
+    }
+    const char* uid_str = JS_ToCString(ctx, uid_val);
+    std::string uid(uid_str ? uid_str : "");
+    JS_FreeCString(ctx, uid_str);
+    JS_FreeValue(ctx, uid_val);
+
+    // Get mailbox (default INBOX)
+    std::string mailbox = "INBOX";
+    JSValue mailbox_val = JS_GetPropertyStr(ctx, options, "mailbox");
+    if (!JS_IsUndefined(mailbox_val)) {
+        const char* mb = JS_ToCString(ctx, mailbox_val);
+        if (mb) { mailbox = mb; JS_FreeCString(ctx, mb); }
+    }
+    JS_FreeValue(ctx, mailbox_val);
+
+    // Get credentials
+    auto cred_result = script_ctx->credentials_get_imap(credential_id);
+    if (cred_result.failed()) {
+        return JS_ThrowInternalError(ctx, "Failed to get IMAP credentials: %s",
+                                     cred_result.error().message().c_str());
+    }
+
+    const auto& cred = cred_result.value();
+
+    // Create ImapClient and perform fetch
+    imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
+    imap::ImapClient client(imap_creds);
+
+    auto fetch_result = client.fetch(mailbox, uid);
+    if (fetch_result.failed()) {
+        return JS_ThrowInternalError(ctx, "IMAP fetch failed: %s",
+                                     fetch_result.error().message().c_str());
+    }
+
+    const auto& email = fetch_result.value();
+
+    // Build JS result
+    JSValue result = JS_NewObject(ctx);
+    JS_SetPropertyStr(ctx, result, "success", JS_TRUE);
+    JS_SetPropertyStr(ctx, result, "uid", JS_NewString(ctx, email.uid.c_str()));
+    JS_SetPropertyStr(ctx, result, "subject", JS_NewString(ctx, email.subject.c_str()));
+    JS_SetPropertyStr(ctx, result, "from", JS_NewString(ctx, email.from.c_str()));
+    JS_SetPropertyStr(ctx, result, "to", JS_NewString(ctx, email.to.c_str()));
+    JS_SetPropertyStr(ctx, result, "date", JS_NewString(ctx, email.date.c_str()));
+    JS_SetPropertyStr(ctx, result, "body", JS_NewString(ctx, email.body.c_str()));
+    JS_SetPropertyStr(ctx, result, "raw", JS_NewString(ctx, email.raw.c_str()));
+
+    return result;
+}
+
+// imap.modify(options) - Modify email flags
+static JSValue js_imap_modify(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
+    const ScriptContext* script_ctx = getScriptContext(ctx);
+    if (!script_ctx || !script_ctx->credentials_get_imap) {
+        return JS_ThrowInternalError(ctx, "IMAP credentials API not available");
+    }
+
+    if (argc < 1 || !JS_IsObject(argv[0])) {
+        return JS_ThrowTypeError(ctx, "imap.modify requires an options object");
+    }
+
+    JSValue options = argv[0];
+
+    // Get credential ID
+    JSValue cred_val = JS_GetPropertyStr(ctx, options, "credentialId");
+    if (JS_IsUndefined(cred_val)) {
+        JS_FreeValue(ctx, cred_val);
+        return JS_ThrowTypeError(ctx, "imap.modify requires credentialId");
+    }
+    const char* cred_id = JS_ToCString(ctx, cred_val);
+    std::string credential_id(cred_id ? cred_id : "");
+    JS_FreeCString(ctx, cred_id);
+    JS_FreeValue(ctx, cred_val);
+
+    // Get UID (required)
+    JSValue uid_val = JS_GetPropertyStr(ctx, options, "uid");
+    if (JS_IsUndefined(uid_val)) {
+        JS_FreeValue(ctx, uid_val);
+        return JS_ThrowTypeError(ctx, "imap.modify requires uid");
+    }
+    const char* uid_str = JS_ToCString(ctx, uid_val);
+    std::string uid(uid_str ? uid_str : "");
+    JS_FreeCString(ctx, uid_str);
+    JS_FreeValue(ctx, uid_val);
+
+    // Get mailbox (default INBOX)
+    std::string mailbox = "INBOX";
+    JSValue mailbox_val = JS_GetPropertyStr(ctx, options, "mailbox");
+    if (!JS_IsUndefined(mailbox_val)) {
+        const char* mb = JS_ToCString(ctx, mailbox_val);
+        if (mb) { mailbox = mb; JS_FreeCString(ctx, mb); }
+    }
+    JS_FreeValue(ctx, mailbox_val);
+
+    // Get action (required)
+    JSValue action_val = JS_GetPropertyStr(ctx, options, "action");
+    if (JS_IsUndefined(action_val)) {
+        JS_FreeValue(ctx, action_val);
+        return JS_ThrowTypeError(ctx, "imap.modify requires action");
+    }
+    const char* action_str = JS_ToCString(ctx, action_val);
+    std::string action(action_str ? action_str : "");
+    JS_FreeCString(ctx, action_str);
+    JS_FreeValue(ctx, action_val);
+
+    // Get credentials
+    auto cred_result = script_ctx->credentials_get_imap(credential_id);
+    if (cred_result.failed()) {
+        return JS_ThrowInternalError(ctx, "Failed to get IMAP credentials: %s",
+                                     cred_result.error().message().c_str());
+    }
+
+    const auto& cred = cred_result.value();
+
+    // Create ImapClient
+    imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
+    imap::ImapClient client(imap_creds);
+
+    // Handle move action separately
+    if (action == "move") {
+        JSValue target_val = JS_GetPropertyStr(ctx, options, "targetMailbox");
+        if (JS_IsUndefined(target_val)) {
+            JS_FreeValue(ctx, target_val);
+            return JS_ThrowTypeError(ctx, "imap.modify with 'move' action requires targetMailbox");
+        }
+        const char* target_str = JS_ToCString(ctx, target_val);
+        std::string target_mailbox(target_str ? target_str : "");
+        JS_FreeCString(ctx, target_str);
+        JS_FreeValue(ctx, target_val);
+
+        auto move_result = client.move(mailbox, uid, target_mailbox);
+        JSValue result = JS_NewObject(ctx);
+        if (move_result.failed()) {
+            JS_SetPropertyStr(ctx, result, "success", JS_FALSE);
+            JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, move_result.error().message().c_str()));
+        } else {
+            const auto& modify_data = move_result.value();
+            JS_SetPropertyStr(ctx, result, "success", modify_data.success ? JS_TRUE : JS_FALSE);
+            if (!modify_data.success) {
+                JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, modify_data.error.c_str()));
+            } else {
+                JS_SetPropertyStr(ctx, result, "action", JS_NewString(ctx, action.c_str()));
+                JS_SetPropertyStr(ctx, result, "uid", JS_NewString(ctx, uid.c_str()));
+            }
+        }
+        return result;
+    }
+
+    // Handle other modify actions
+    auto modify_result = client.modify(mailbox, uid, action);
+
+    JSValue result = JS_NewObject(ctx);
+    if (modify_result.failed()) {
+        JS_SetPropertyStr(ctx, result, "success", JS_FALSE);
+        JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, modify_result.error().message().c_str()));
+    } else {
+        const auto& modify_data = modify_result.value();
+        JS_SetPropertyStr(ctx, result, "success", modify_data.success ? JS_TRUE : JS_FALSE);
+        if (!modify_data.success) {
+            JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, modify_data.error.c_str()));
+        } else {
+            JS_SetPropertyStr(ctx, result, "action", JS_NewString(ctx, action.c_str()));
+            JS_SetPropertyStr(ctx, result, "uid", JS_NewString(ctx, uid.c_str()));
+        }
+    }
+
+    return result;
+}
+
+// Setup IMAP API on smartbotic namespace
+static void setupImapAPI(JSContext* ctx, JSValue smartbotic) {
+    JSValue imap = JS_NewObject(ctx);
+
+    JS_SetPropertyStr(ctx, imap, "search", JS_NewCFunction(ctx, js_imap_search, "search", 1));
+    JS_SetPropertyStr(ctx, imap, "fetch", JS_NewCFunction(ctx, js_imap_fetch, "fetch", 1));
+    JS_SetPropertyStr(ctx, imap, "modify", JS_NewCFunction(ctx, js_imap_modify, "modify", 1));
+
+    JS_SetPropertyStr(ctx, smartbotic, "imap", imap);
+}
+
+// Setup credentials API on smartbotic namespace
+static void setupCredentialsAPI(JSContext* ctx, JSValue smartbotic) {
+    JSValue credentials = JS_NewObject(ctx);
+
+    JS_SetPropertyStr(ctx, credentials, "get", JS_NewCFunction(ctx, js_credentials_get, "get", 1));
+    JS_SetPropertyStr(ctx, credentials, "getImap", JS_NewCFunction(ctx, js_credentials_get_imap, "getImap", 1));
+
+    JS_SetPropertyStr(ctx, smartbotic, "credentials", credentials);
+}
+
+ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContext& context) {
+    ScriptResult result;
+    result.success = true;
+
+    // Reset context to ensure it's created on this thread
+    // QuickJS contexts should be used only on the thread that created them
+    destroyRuntime();
+    initRuntime();
+
+    if (!context_ || !runtime_) {
+        result.error = "Runtime not initialized";
+        result.success = false;
+        return result;
+    }
+
+    cancelled_.store(false);
+
+    // Store ScriptContext pointer for storage API callbacks
+    JS_SetContextOpaque(context_, const_cast<ScriptContext*>(&context));
+
+    JSValue global = JS_GetGlobalObject(context_);
+
+    // Set up storage, credentials, and IMAP APIs on smartbotic namespace
+    JSValue smartbotic = JS_GetPropertyStr(context_, global, "smartbotic");
+    if (JS_IsObject(smartbotic)) {
+        setupStorageAPI(context_, smartbotic);
+        setupCredentialsAPI(context_, smartbotic);
+        setupImapAPI(context_, smartbotic);
+    }
+    JS_FreeValue(context_, smartbotic);
+
+    // Set up module.exports for CommonJS compatibility
+    JSValue module_obj = JS_NewObject(context_);
+    JSValue exports_obj = JS_NewObject(context_);
+    JS_SetPropertyStr(context_, module_obj, "exports", exports_obj);  // module owns exports
+    JS_SetPropertyStr(context_, global, "module", module_obj);
+    // Also set exports directly for shorthand access
+    JSValue exports_ref = JS_GetPropertyStr(context_, module_obj, "exports");
+    JS_SetPropertyStr(context_, global, "exports", exports_ref);
+
+    // Set input data (default to empty object if null)
+    std::string input_json = context.input.is_null() ? "{}" : context.input.dump();
+    JSValue input_val = JS_ParseJSON(context_, input_json.c_str(), input_json.size(), "<input>");
+    if (JS_IsException(input_val)) {
+        LOG_ERROR("Failed to parse input JSON: {}", input_json);
+        input_val = JS_NewObject(context_);
+    }
+    JS_SetPropertyStr(context_, global, "input", input_val);
+
+    // Set config data (default to empty object if null)
+    std::string config_json = context.config.is_null() ? "{}" : context.config.dump();
+    JSValue config_val = JS_ParseJSON(context_, config_json.c_str(), config_json.size(), "<config>");
+    if (JS_IsException(config_val)) {
+        LOG_ERROR("Failed to parse config JSON: {}", config_json);
+        config_val = JS_NewObject(context_);
+    }
+    JS_SetPropertyStr(context_, global, "config", config_val);
+
+    // Create context object for the execute function
+    nlohmann::json ctx_json = {
+        {"executionId", context.execution_id},
+        {"nodeId", context.node_id}
+    };
+    std::string ctx_str = ctx_json.dump();
+    JSValue ctx_val = JS_ParseJSON(context_, ctx_str.c_str(), ctx_str.size(), "<context>");
+    if (JS_IsException(ctx_val)) {
+        LOG_ERROR("Failed to parse context JSON");
+        ctx_val = JS_NewObject(context_);
+    }
+    JS_SetPropertyStr(context_, global, "context", ctx_val);
+
+    // Execute the script (loads module.exports with execute function)
+    LOG_DEBUG("Executing script: {} bytes", script.size());
+
+    JSValue val = JS_Eval(context_, script.c_str(), script.size(),
+                          "<script>", JS_EVAL_TYPE_GLOBAL);
+
+    if (JS_IsException(val)) {
+        JSValue exception = JS_GetException(context_);
+
+        // Try to get error message
+        const char* msg = JS_ToCString(context_, exception);
+        std::string error_msg = msg ? msg : "Unknown error";
+        JS_FreeCString(context_, msg);
+
+        // Try to get stack trace
+        if (JS_IsObject(exception)) {
+            JSValue stack = JS_GetPropertyStr(context_, exception, "stack");
+            if (!JS_IsUndefined(stack)) {
+                const char* stack_str = JS_ToCString(context_, stack);
+                if (stack_str) {
+                    error_msg += "\nStack: " + std::string(stack_str);
+                    JS_FreeCString(context_, stack_str);
+                }
+            }
+            JS_FreeValue(context_, stack);
+        }
+
+        result.error = "Script error: " + error_msg;
+        LOG_ERROR("Script exception: {}", result.error);
+        JS_FreeValue(context_, exception);
+        result.success = false;
+        JS_FreeValue(context_, val);
+        JS_FreeValue(context_, global);
+        return result;
+    }
+    LOG_DEBUG("Script loaded successfully");
+
+    JS_FreeValue(context_, val);
+
+    // Get the execute function from module.exports
+    JSValue module_val = JS_GetPropertyStr(context_, global, "module");
+    JSValue exports_val = JS_GetPropertyStr(context_, module_val, "exports");
+    JSValue execute_fn = JS_GetPropertyStr(context_, exports_val, "execute");
+
+    if (!JS_IsFunction(context_, execute_fn)) {
+        result.error = "Node does not export an execute function";
+        LOG_ERROR("execute is not a function. JS_VALUE_GET_TAG = {}", JS_VALUE_GET_TAG(execute_fn));
+        result.success = false;
+        JS_FreeValue(context_, execute_fn);
+        JS_FreeValue(context_, exports_val);
+        JS_FreeValue(context_, module_val);
+        JS_FreeValue(context_, global);
+        return result;
+    }
+    LOG_DEBUG("Found execute function");
+
+    // Get config and input values that were set earlier
+    JSValue config_arg = JS_GetPropertyStr(context_, global, "config");
+    JSValue input_arg = JS_GetPropertyStr(context_, global, "input");
+    JSValue ctx_arg = JS_GetPropertyStr(context_, global, "context");
+
+    // Call execute(config, input, context)
+    JSValue args[3] = { config_arg, input_arg, ctx_arg };
+    JSValue exec_result = JS_Call(context_, execute_fn, JS_UNDEFINED, 3, args);
+
+    if (JS_IsException(exec_result)) {
+        JSValue exception = JS_GetException(context_);
+        JSValue exception_str = JS_ToString(context_, exception);
+        const char* str = JS_ToCString(context_, exception_str);
+        result.error = str ? std::string("Execute error: ") + str : "Execute function threw an exception";
+        LOG_ERROR("Execute exception: {}", result.error);
+        JS_FreeCString(context_, str);
+        JS_FreeValue(context_, exception_str);
+        JS_FreeValue(context_, exception);
+        result.success = false;
+    } else {
+        LOG_DEBUG("Execute call returned successfully");
+        // Process any pending async jobs (for async functions)
+        JSContext* ctx2;
+        int err;
+        while ((err = JS_ExecutePendingJob(JS_GetRuntime(context_), &ctx2)) > 0) {
+            if (cancelled_.load()) {
+                result.error = "Execution cancelled";
+                result.success = false;
+                break;
+            }
+        }
+
+        if (err < 0) {
+            JSValue exception = JS_GetException(context_);
+            const char* str = JS_ToCString(context_, exception);
+            result.error = str ? str : "Async execution error";
+            JS_FreeCString(context_, str);
+            JS_FreeValue(context_, exception);
+            result.success = false;
+        }
+
+        // Check if result is a promise and get its value
+        if (result.success && JS_IsObject(exec_result)) {
+            JSPromiseStateEnum state = JS_PromiseState(context_, exec_result);
+            if (state == JS_PROMISE_REJECTED) {
+                JSValue promise_result = JS_PromiseResult(context_, exec_result);
+                const char* str = JS_ToCString(context_, promise_result);
+                result.error = str ? str : "Promise rejected";
+                JS_FreeCString(context_, str);
+                JS_FreeValue(context_, promise_result);
+                result.success = false;
+            } else if (state == JS_PROMISE_FULFILLED) {
+                JSValue promise_result = JS_PromiseResult(context_, exec_result);
+                JSValue json_str = JS_JSONStringify(context_, promise_result, JS_UNDEFINED, JS_UNDEFINED);
+                const char* str = JS_ToCString(context_, json_str);
+                if (str) {
+                    try {
+                        result.output = nlohmann::json::parse(str);
+                    } catch (...) {
+                        result.output = nullptr;
+                    }
+                    JS_FreeCString(context_, str);
+                }
+                JS_FreeValue(context_, json_str);
+                JS_FreeValue(context_, promise_result);
+            } else if (state == JS_PROMISE_PENDING) {
+                // Not a promise, or promise still pending - try to stringify directly
+                JSValue json_str = JS_JSONStringify(context_, exec_result, JS_UNDEFINED, JS_UNDEFINED);
+                const char* str = JS_ToCString(context_, json_str);
+                if (str) {
+                    try {
+                        result.output = nlohmann::json::parse(str);
+                    } catch (...) {
+                        result.output = nullptr;
+                    }
+                    JS_FreeCString(context_, str);
+                }
+                JS_FreeValue(context_, json_str);
+            }
+        } else if (result.success) {
+            // Non-object result (number, string, etc.)
+            JSValue json_str = JS_JSONStringify(context_, exec_result, JS_UNDEFINED, JS_UNDEFINED);
+            const char* str = JS_ToCString(context_, json_str);
+            if (str) {
+                try {
+                    result.output = nlohmann::json::parse(str);
+                } catch (...) {
+                    result.output = nullptr;
+                }
+                JS_FreeCString(context_, str);
+            }
+            JS_FreeValue(context_, json_str);
+        }
+    }
+
+    JS_FreeValue(context_, exec_result);
+    JS_FreeValue(context_, config_arg);
+    JS_FreeValue(context_, input_arg);
+    JS_FreeValue(context_, ctx_arg);
+    JS_FreeValue(context_, execute_fn);
+    JS_FreeValue(context_, exports_val);
+    JS_FreeValue(context_, module_val);
+    JS_FreeValue(context_, global);
+
+    return result;
+}
+
+void ScriptEngine::cancel() {
+    cancelled_.store(true);
+}
+
+bool ScriptEngine::isCancelled() const {
+    return cancelled_.load();
+}
+
+void ScriptEngine::reset() {
+    destroyRuntime();
+    initRuntime();
+}
+
+// ScriptEnginePool implementation
+ScriptEnginePool::ScriptEnginePool(size_t pool_size, const ScriptEngineConfig& config)
+    : config_(config), stop_(false) {
+    for (size_t i = 0; i < pool_size; ++i) {
+        engines_.push_back(std::make_unique<ScriptEngine>(config));
+        available_.push_back(engines_.back().get());
+    }
+}
+
+ScriptEnginePool::~ScriptEnginePool() {
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        stop_ = true;
+    }
+    cv_.notify_all();
+}
+
+ScriptEngine* ScriptEnginePool::acquire() {
+    std::unique_lock<std::mutex> lock(mutex_);
+    cv_.wait(lock, [this] { return !available_.empty() || stop_; });
+
+    if (stop_ || available_.empty()) {
+        return nullptr;
+    }
+
+    ScriptEngine* engine = available_.back();
+    available_.pop_back();
+    return engine;
+}
+
+void ScriptEnginePool::release(ScriptEngine* engine) {
+    if (!engine) return;
+
+    // Note: We don't reset here because execute() resets at the start
+    // to ensure the context is created on the executing thread
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        available_.push_back(engine);
+    }
+    cv_.notify_one();
+}
+
+size_t ScriptEnginePool::getAvailableCount() const {
+    std::lock_guard<std::mutex> lock(mutex_);
+    return available_.size();
+}
+
+size_t ScriptEngine::getMemoryUsage() const {
+    // QuickJS memory info would be retrieved here
+    // For now return 0 as placeholder
+    return 0;
+}
+
+} // namespace smartbotic::runner::engine

+ 153 - 0
src/runner/engine/script_engine.hpp

@@ -0,0 +1,153 @@
+#pragma once
+
+#include <string>
+#include <memory>
+#include <vector>
+#include <functional>
+#include <atomic>
+#include <mutex>
+#include <condition_variable>
+#include <nlohmann/json.hpp>
+#include "common/error.hpp"
+
+// Forward declare QuickJS types
+struct JSRuntime;
+struct JSContext;
+
+namespace smartbotic::runner::engine {
+
+// Query options for storage queries
+struct StorageQueryOptions {
+    std::vector<std::pair<std::string, nlohmann::json>> filters;
+    std::vector<std::pair<std::string, bool>> sorts;  // field, ascending
+    std::vector<std::string> fields;
+    int32_t page = 1;
+    int32_t page_size = 100;
+};
+
+// Query result from storage
+struct StorageQueryResult {
+    std::vector<nlohmann::json> documents;
+    int64_t total_count = 0;
+    bool has_more = false;
+};
+
+// Credential auth result for JS API
+struct CredentialAuth {
+    std::string header_name;
+    std::string header_value;
+};
+
+// IMAP credential data for JS API
+struct ImapCredential {
+    std::string host;
+    int port = 993;
+    std::string username;
+    std::string password;
+    bool use_ssl = true;
+};
+
+// Script execution context
+struct ScriptContext {
+    std::string execution_id;
+    std::string node_id;
+    std::string workflow_id;
+    nlohmann::json input;
+    nlohmann::json config;
+    std::function<void(const std::string&, const std::string&)> log_handler;
+
+    // Storage operations - existing
+    std::function<common::Result<nlohmann::json>(const std::string&)> storage_get;
+    std::function<common::Result<void>(const std::string&, const nlohmann::json&)> storage_set;
+
+    // Storage operations - new full API
+    std::function<common::Result<nlohmann::json>(const std::string&, const std::string&)> storage_get_doc;
+    std::function<common::Result<std::string>(const std::string&, const nlohmann::json&, const std::string&, int64_t)> storage_insert;
+    std::function<common::Result<int64_t>(const std::string&, const std::string&, const nlohmann::json&, int64_t, bool)> storage_update;
+    std::function<common::Result<void>(const std::string&, const std::string&, int64_t)> storage_delete;
+    std::function<common::Result<StorageQueryResult>(const std::string&, const StorageQueryOptions&)> storage_query;
+    std::function<common::Result<std::vector<std::string>>(bool)> storage_list_collections;
+
+    // Credential operations
+    std::function<common::Result<CredentialAuth>(const std::string&)> credentials_get;
+    std::function<common::Result<ImapCredential>(const std::string&)> credentials_get_imap;
+};
+
+// Script execution result
+struct ScriptResult {
+    bool success = false;
+    nlohmann::json output;
+    std::string error;
+    int64_t execution_time_ms = 0;
+};
+
+// Script engine configuration
+struct ScriptEngineConfig {
+    size_t max_memory_mb = 64;
+    size_t stack_size_kb = 256;
+    int64_t timeout_ms = 60000;
+};
+
+// QuickJS-based JavaScript execution engine
+class ScriptEngine {
+public:
+    explicit ScriptEngine(const ScriptEngineConfig& config = {});
+    ~ScriptEngine();
+
+    // Execute JavaScript code
+    ScriptResult execute(const std::string& code, const ScriptContext& context);
+
+    // Request cancellation of current execution
+    void cancel();
+
+    // Check if cancellation was requested
+    bool isCancelled() const;
+
+    // Check if engine is busy
+    bool isBusy() const { return busy_.load(); }
+
+    // Reset the engine (recreate runtime)
+    void reset();
+
+    // Get memory usage
+    size_t getMemoryUsage() const;
+
+private:
+    void initRuntime();
+    void destroyRuntime();
+    void setupBuiltinAPIs();
+
+    ScriptEngineConfig config_;
+    JSRuntime* runtime_ = nullptr;
+    JSContext* context_ = nullptr;
+    std::atomic<bool> busy_{false};
+    std::atomic<bool> cancelled_{false};
+    std::mutex mutex_;
+};
+
+// Pool of script engines for concurrent execution
+class ScriptEnginePool {
+public:
+    explicit ScriptEnginePool(size_t pool_size, const ScriptEngineConfig& config = {});
+    ~ScriptEnginePool();
+
+    // Acquire an engine from the pool (blocks if none available)
+    ScriptEngine* acquire();
+
+    // Release an engine back to the pool
+    void release(ScriptEngine* engine);
+
+    // Get pool statistics
+    size_t getAvailableCount() const;
+    size_t getTotalCount() const { return engines_.size(); }
+
+private:
+    std::vector<std::unique_ptr<ScriptEngine>> engines_;
+    std::vector<ScriptEngine*> available_;
+    mutable std::mutex mutex_;
+    std::condition_variable cv_;
+    ScriptEngineConfig config_;
+    bool stop_ = false;
+};
+
+} // namespace smartbotic::runner::engine

+ 121 - 0
src/runner/error_handler.cpp

@@ -0,0 +1,121 @@
+#include "error_handler.hpp"
+#include "logging/logger.hpp"
+#include <algorithm>
+#include <cmath>
+
+namespace smartbotic::runner {
+
+ErrorHandler::ErrorHandler(const ErrorHandlerConfig& config)
+    : config_(config) {}
+
+int ErrorHandler::getRetryDelay(int attempt, RetryStrategy strategy) const {
+    if (strategy == RetryStrategy::None) {
+        return 0;
+    }
+
+    if (strategy == RetryStrategy::Fixed) {
+        return config_.base_delay_ms;
+    }
+
+    // Exponential backoff
+    double delay = config_.base_delay_ms * std::pow(config_.exponential_factor, attempt);
+    return std::min(static_cast<int>(delay), config_.max_delay_ms);
+}
+
+bool ErrorHandler::shouldRetry(int attempt) const {
+    return attempt < config_.max_retries;
+}
+
+bool ErrorHandler::isCircuitOpen(const std::string& node_type) {
+    std::lock_guard<std::mutex> lock(mutex_);
+
+    auto it = circuits_.find(node_type);
+    if (it == circuits_.end()) {
+        return false;
+    }
+
+    auto& circuit = it->second;
+    checkCircuitReset(node_type);
+
+    if (circuit.state == CircuitState::Open) {
+        return true;
+    }
+
+    return false;
+}
+
+void ErrorHandler::recordSuccess(const std::string& node_type) {
+    std::lock_guard<std::mutex> lock(mutex_);
+
+    auto& circuit = circuits_[node_type];
+
+    if (circuit.state == CircuitState::HalfOpen) {
+        ++circuit.success_count;
+        if (circuit.success_count >= CircuitBreaker::success_threshold) {
+            circuit.state = CircuitState::Closed;
+            circuit.failure_count = 0;
+            circuit.success_count = 0;
+            LOG_INFO("Circuit closed for node type: {}", node_type);
+        }
+    } else if (circuit.state == CircuitState::Closed) {
+        circuit.failure_count = 0;
+    }
+}
+
+void ErrorHandler::recordFailure(const std::string& node_type) {
+    std::lock_guard<std::mutex> lock(mutex_);
+
+    auto& circuit = circuits_[node_type];
+    circuit.last_failure = std::chrono::steady_clock::now();
+
+    if (circuit.state == CircuitState::HalfOpen) {
+        // Failure in half-open state opens circuit again
+        circuit.state = CircuitState::Open;
+        circuit.opened_at = std::chrono::steady_clock::now();
+        circuit.success_count = 0;
+        LOG_WARN("Circuit re-opened for node type: {}", node_type);
+    } else if (circuit.state == CircuitState::Closed) {
+        ++circuit.failure_count;
+        if (circuit.failure_count >= CircuitBreaker::failure_threshold) {
+            circuit.state = CircuitState::Open;
+            circuit.opened_at = std::chrono::steady_clock::now();
+            LOG_WARN("Circuit opened for node type: {} after {} failures",
+                    node_type, circuit.failure_count);
+        }
+    }
+}
+
+CircuitState ErrorHandler::getCircuitState(const std::string& node_type) {
+    std::lock_guard<std::mutex> lock(mutex_);
+
+    auto it = circuits_.find(node_type);
+    if (it == circuits_.end()) {
+        return CircuitState::Closed;
+    }
+
+    checkCircuitReset(node_type);
+    return it->second.state;
+}
+
+void ErrorHandler::checkCircuitReset(const std::string& node_type) {
+    auto it = circuits_.find(node_type);
+    if (it == circuits_.end()) {
+        return;
+    }
+
+    auto& circuit = it->second;
+
+    if (circuit.state == CircuitState::Open) {
+        auto now = std::chrono::steady_clock::now();
+        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
+            now - circuit.opened_at).count();
+
+        if (elapsed >= CircuitBreaker::reset_timeout_sec) {
+            circuit.state = CircuitState::HalfOpen;
+            circuit.success_count = 0;
+            LOG_INFO("Circuit half-open for node type: {}", node_type);
+        }
+    }
+}
+
+} // namespace smartbotic::runner

+ 71 - 0
src/runner/error_handler.hpp

@@ -0,0 +1,71 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <chrono>
+#include <mutex>
+
+namespace smartbotic::runner {
+
+// Retry strategy
+enum class RetryStrategy {
+    None,
+    Fixed,
+    Exponential
+};
+
+// Error handler configuration
+struct ErrorHandlerConfig {
+    RetryStrategy default_strategy = RetryStrategy::Exponential;
+    int max_retries = 3;
+    int base_delay_ms = 1000;
+    int max_delay_ms = 30000;
+    double exponential_factor = 2.0;
+};
+
+// Circuit breaker state
+enum class CircuitState {
+    Closed,    // Normal operation
+    Open,      // Failing, reject requests
+    HalfOpen   // Testing if recovered
+};
+
+// Circuit breaker for a node type
+struct CircuitBreaker {
+    CircuitState state = CircuitState::Closed;
+    int failure_count = 0;
+    int success_count = 0;
+    std::chrono::steady_clock::time_point last_failure;
+    std::chrono::steady_clock::time_point opened_at;
+
+    static constexpr int failure_threshold = 5;
+    static constexpr int success_threshold = 3;
+    static constexpr int reset_timeout_sec = 30;
+};
+
+// Error handler with retry and circuit breaker support
+class ErrorHandler {
+public:
+    explicit ErrorHandler(const ErrorHandlerConfig& config = {});
+
+    // Calculate delay for retry attempt
+    int getRetryDelay(int attempt, RetryStrategy strategy = RetryStrategy::Exponential) const;
+
+    // Check if should retry
+    bool shouldRetry(int attempt) const;
+
+    // Circuit breaker operations
+    bool isCircuitOpen(const std::string& node_type);
+    void recordSuccess(const std::string& node_type);
+    void recordFailure(const std::string& node_type);
+    CircuitState getCircuitState(const std::string& node_type);
+
+private:
+    void checkCircuitReset(const std::string& node_type);
+
+    ErrorHandlerConfig config_;
+    std::unordered_map<std::string, CircuitBreaker> circuits_;
+    std::mutex mutex_;
+};
+
+} // namespace smartbotic::runner

+ 313 - 0
src/runner/imap/imap_client.cpp

@@ -0,0 +1,313 @@
+#include "imap_client.hpp"
+#include <curl/curl.h>
+#include <sstream>
+#include <algorithm>
+#include <logging/logger.hpp>
+
+namespace smartbotic::runner::imap {
+
+// Curl write callback
+static size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
+    auto* response = static_cast<std::string*>(userdata);
+    size_t total = size * nmemb;
+    response->append(ptr, total);
+    return total;
+}
+
+ImapClient::ImapClient(const ImapCredentials& credentials)
+    : credentials_(credentials) {
+}
+
+ImapClient::~ImapClient() = default;
+
+std::string ImapClient::buildUrl(const std::string& path) const {
+    std::string scheme = credentials_.use_ssl ? "imaps" : "imap";
+    return scheme + "://" + credentials_.host + ":" +
+           std::to_string(credentials_.port) + "/" + path;
+}
+
+std::string ImapClient::escapeMailbox(const std::string& mailbox) const {
+    // URL-encode the mailbox name
+    CURL* curl = curl_easy_init();
+    if (!curl) return mailbox;
+
+    char* encoded = curl_easy_escape(curl, mailbox.c_str(), static_cast<int>(mailbox.length()));
+    std::string result(encoded ? encoded : mailbox);
+    curl_free(encoded);
+    curl_easy_cleanup(curl);
+    return result;
+}
+
+ImapClient::ImapResponse ImapClient::performCommand(
+    const std::string& url_path,
+    const std::string& custom_request
+) {
+    ImapResponse response;
+
+    CURL* curl = curl_easy_init();
+    if (!curl) {
+        response.error = "Failed to initialize curl";
+        return response;
+    }
+
+    std::string url = buildUrl(url_path);
+
+    LOG_DEBUG("IMAP request - URL: {}, command: '{}'", url, custom_request);
+
+    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curl, CURLOPT_USERNAME, credentials_.username.c_str());
+    curl_easy_setopt(curl, CURLOPT_PASSWORD, credentials_.password.c_str());
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.data);
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
+    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
+
+    if (credentials_.use_ssl) {
+        curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL);
+        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
+        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
+    }
+
+    if (!custom_request.empty()) {
+        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, custom_request.c_str());
+    }
+
+    CURLcode res = curl_easy_perform(curl);
+
+    if (res != CURLE_OK) {
+        response.error = curl_easy_strerror(res);
+        LOG_WARN("IMAP curl error: {}", response.error);
+    }
+
+    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.status_code);
+    curl_easy_cleanup(curl);
+
+    LOG_DEBUG("IMAP response - status: {}, data length: {}",
+              response.status_code, response.data.length());
+
+    return response;
+}
+
+common::Result<SearchResult> ImapClient::search(
+    const std::string& mailbox,
+    const std::string& criteria,
+    int limit
+) {
+    LOG_INFO("IMAP search - mailbox: '{}', criteria: '{}', limit: {}",
+             mailbox, criteria, limit);
+
+    // For IMAP SEARCH, we use CUSTOMREQUEST
+    std::string url_path = escapeMailbox(mailbox);
+    std::string search_cmd = "SEARCH " + criteria;
+
+    auto response = performCommand(url_path, search_cmd);
+
+    if (!response.error.empty()) {
+        return common::Error(common::ErrorCode::Internal,
+            "IMAP search failed: " + response.error);
+    }
+
+    // Parse search results (format: "* SEARCH 1 2 3 4 5")
+    SearchResult result;
+    std::istringstream stream(response.data);
+    std::string line;
+
+    while (std::getline(stream, line) && result.count < limit) {
+        // Find lines starting with "* SEARCH"
+        if (line.find("* SEARCH") == 0) {
+            std::istringstream uid_stream(line.substr(8));
+            std::string uid;
+            while (uid_stream >> uid && result.count < limit) {
+                // Filter out non-numeric tokens
+                if (!uid.empty() && std::all_of(uid.begin(), uid.end(), ::isdigit)) {
+                    result.uids.push_back(uid);
+                    result.count++;
+                }
+            }
+        }
+    }
+
+    LOG_INFO("IMAP search found {} messages", result.count);
+    return result;
+}
+
+common::Result<EmailContent> ImapClient::fetch(
+    const std::string& mailbox,
+    const std::string& uid
+) {
+    LOG_INFO("IMAP fetch - mailbox: '{}', uid: '{}'", mailbox, uid);
+
+    // Select mailbox and fetch the message
+    std::string url_path = escapeMailbox(mailbox) + ";UID=" + uid;
+
+    auto response = performCommand(url_path);
+
+    if (!response.error.empty()) {
+        return common::Error(common::ErrorCode::Internal,
+            "IMAP fetch failed: " + response.error);
+    }
+
+    EmailContent content;
+    content.uid = uid;
+    content.raw = response.data;
+
+    // Parse headers from the raw email
+    std::istringstream stream(response.data);
+    std::string line;
+    bool in_headers = true;
+    std::ostringstream body_stream;
+
+    while (std::getline(stream, line)) {
+        if (in_headers) {
+            if (line.empty() || line == "\r") {
+                in_headers = false;
+                continue;
+            }
+
+            // Parse common headers
+            if (line.find("Subject:") == 0) {
+                content.subject = line.substr(8);
+                // Trim whitespace
+                content.subject.erase(0, content.subject.find_first_not_of(" \t"));
+            } else if (line.find("From:") == 0) {
+                content.from = line.substr(5);
+                content.from.erase(0, content.from.find_first_not_of(" \t"));
+            } else if (line.find("To:") == 0) {
+                content.to = line.substr(3);
+                content.to.erase(0, content.to.find_first_not_of(" \t"));
+            } else if (line.find("Date:") == 0) {
+                content.date = line.substr(5);
+                content.date.erase(0, content.date.find_first_not_of(" \t"));
+            }
+        } else {
+            body_stream << line << "\n";
+        }
+    }
+
+    content.body = body_stream.str();
+
+    LOG_INFO("IMAP fetch complete - subject: '{}'", content.subject);
+    return content;
+}
+
+common::Result<ModifyResult> ImapClient::modify(
+    const std::string& mailbox,
+    const std::string& uid,
+    const std::string& action
+) {
+    LOG_INFO("IMAP modify - mailbox: '{}', uid: '{}', action: '{}'",
+             mailbox, uid, action);
+
+    std::string url_path = escapeMailbox(mailbox) + ";UID=" + uid;
+    std::string command;
+
+    if (action == "markRead") {
+        command = "STORE " + uid + " +FLAGS (\\Seen)";
+    } else if (action == "markUnread") {
+        command = "STORE " + uid + " -FLAGS (\\Seen)";
+    } else if (action == "flag") {
+        command = "STORE " + uid + " +FLAGS (\\Flagged)";
+    } else if (action == "unflag") {
+        command = "STORE " + uid + " -FLAGS (\\Flagged)";
+    } else if (action == "delete") {
+        command = "STORE " + uid + " +FLAGS (\\Deleted)";
+    } else {
+        return common::Error(common::ErrorCode::InvalidArgument,
+            "Unknown action: " + action);
+    }
+
+    auto response = performCommand(url_path, command);
+
+    ModifyResult result;
+    if (!response.error.empty()) {
+        result.success = false;
+        result.error = response.error;
+    } else {
+        result.success = true;
+    }
+
+    return result;
+}
+
+common::Result<ModifyResult> ImapClient::move(
+    const std::string& mailbox,
+    const std::string& uid,
+    const std::string& target_mailbox
+) {
+    LOG_INFO("IMAP move - from: '{}', uid: '{}', to: '{}'",
+             mailbox, uid, target_mailbox);
+
+    std::string url_path = escapeMailbox(mailbox) + ";UID=" + uid;
+    std::string command = "COPY " + uid + " " + target_mailbox;
+
+    auto response = performCommand(url_path, command);
+
+    ModifyResult result;
+    if (!response.error.empty()) {
+        result.success = false;
+        result.error = response.error;
+        return result;
+    }
+
+    // After copying, mark the original as deleted
+    auto delete_result = modify(mailbox, uid, "delete");
+    if (delete_result.failed()) {
+        result.success = false;
+        result.error = "Copy succeeded but delete failed: " + delete_result.error().message();
+    } else {
+        result.success = true;
+    }
+
+    return result;
+}
+
+common::Result<std::vector<std::string>> ImapClient::listMailboxes() {
+    LOG_INFO("IMAP list mailboxes");
+
+    auto response = performCommand("", "LIST \"\" \"*\"");
+
+    if (!response.error.empty()) {
+        return common::Error(common::ErrorCode::Internal,
+            "IMAP list failed: " + response.error);
+    }
+
+    std::vector<std::string> mailboxes;
+    std::istringstream stream(response.data);
+    std::string line;
+
+    // Parse LIST response: * LIST (\HasNoChildren) "/" "INBOX"
+    // Simple parsing instead of regex to avoid portability issues
+    while (std::getline(stream, line)) {
+        if (line.find("* LIST") == 0) {
+            // Find the last quoted string or unquoted mailbox name
+            size_t last_quote = line.rfind('"');
+            if (last_quote != std::string::npos && last_quote > 0) {
+                size_t second_last = line.rfind('"', last_quote - 1);
+                if (second_last != std::string::npos) {
+                    std::string mb = line.substr(second_last + 1, last_quote - second_last - 1);
+                    if (!mb.empty()) {
+                        mailboxes.push_back(mb);
+                    }
+                }
+            } else {
+                // No quotes, try to get the last space-separated token
+                size_t last_space = line.rfind(' ');
+                if (last_space != std::string::npos) {
+                    std::string mb = line.substr(last_space + 1);
+                    // Remove trailing CR/LF
+                    while (!mb.empty() && (mb.back() == '\r' || mb.back() == '\n')) {
+                        mb.pop_back();
+                    }
+                    if (!mb.empty()) {
+                        mailboxes.push_back(mb);
+                    }
+                }
+            }
+        }
+    }
+
+    LOG_INFO("IMAP found {} mailboxes", mailboxes.size());
+    return mailboxes;
+}
+
+} // namespace smartbotic::runner::imap

+ 134 - 0
src/runner/imap/imap_client.hpp

@@ -0,0 +1,134 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <optional>
+#include <common/error.hpp>
+
+namespace smartbotic::runner::imap {
+
+struct ImapCredentials {
+    std::string host;
+    int port = 993;
+    std::string username;
+    std::string password;
+    bool use_ssl = true;
+};
+
+struct EmailHeader {
+    std::string uid;
+    std::string subject;
+    std::string from;
+    std::string to;
+    std::string date;
+    bool seen = false;
+    bool flagged = false;
+};
+
+struct EmailContent {
+    std::string uid;
+    std::string subject;
+    std::string from;
+    std::string to;
+    std::string date;
+    std::string body;
+    std::string raw;
+};
+
+struct SearchResult {
+    std::vector<std::string> uids;
+    int count = 0;
+};
+
+struct ModifyResult {
+    bool success = false;
+    std::string error;
+};
+
+/**
+ * IMAP Client for email operations using libcurl
+ */
+class ImapClient {
+public:
+    explicit ImapClient(const ImapCredentials& credentials);
+    ~ImapClient();
+
+    // Disable copy
+    ImapClient(const ImapClient&) = delete;
+    ImapClient& operator=(const ImapClient&) = delete;
+
+    /**
+     * Search for emails matching criteria
+     * @param mailbox The mailbox to search (e.g., "INBOX")
+     * @param criteria IMAP search criteria (e.g., "UNSEEN", "ALL", "FROM \"user@example.com\"")
+     * @param limit Maximum number of results
+     * @return Search result with UIDs
+     */
+    common::Result<SearchResult> search(
+        const std::string& mailbox,
+        const std::string& criteria,
+        int limit = 50
+    );
+
+    /**
+     * Fetch full email content
+     * @param mailbox The mailbox containing the email
+     * @param uid The email UID
+     * @return Email content
+     */
+    common::Result<EmailContent> fetch(
+        const std::string& mailbox,
+        const std::string& uid
+    );
+
+    /**
+     * Modify email flags or move/delete
+     * @param mailbox The mailbox containing the email
+     * @param uid The email UID
+     * @param action Action to perform: "markRead", "markUnread", "flag", "unflag", "delete"
+     * @return Modify result
+     */
+    common::Result<ModifyResult> modify(
+        const std::string& mailbox,
+        const std::string& uid,
+        const std::string& action
+    );
+
+    /**
+     * Move email to another mailbox
+     * @param mailbox Source mailbox
+     * @param uid The email UID
+     * @param target_mailbox Destination mailbox
+     * @return Modify result
+     */
+    common::Result<ModifyResult> move(
+        const std::string& mailbox,
+        const std::string& uid,
+        const std::string& target_mailbox
+    );
+
+    /**
+     * List available mailboxes
+     * @return List of mailbox names
+     */
+    common::Result<std::vector<std::string>> listMailboxes();
+
+private:
+    struct ImapResponse {
+        std::string data;
+        std::string error;
+        long status_code = 0;
+    };
+
+    ImapResponse performCommand(
+        const std::string& url_path,
+        const std::string& custom_request = ""
+    );
+
+    std::string buildUrl(const std::string& path) const;
+    std::string escapeMailbox(const std::string& mailbox) const;
+
+    ImapCredentials credentials_;
+};
+
+} // namespace smartbotic::runner::imap

+ 64 - 0
src/runner/main.cpp

@@ -0,0 +1,64 @@
+#include <csignal>
+#include <iostream>
+#include "runner_service.hpp"
+#include "logging/logger.hpp"
+#include "config/config_loader.hpp"
+
+using namespace smartbotic;
+
+std::atomic<bool> g_shutdown{false};
+
+void signalHandler(int signal) {
+    LOG_INFO("Received signal {}, shutting down...", signal);
+    g_shutdown = true;
+}
+
+int main(int argc, char* argv[]) {
+    // Initialize logging
+    logging::LogConfig log_config;
+    log_config.name = "runner";
+    log_config.level = logging::LogLevel::Info;
+    logging::Logger::init(log_config);
+
+    LOG_INFO("SmartBotic Runner starting...");
+
+    // Load configuration
+    std::filesystem::path config_path = "./config/runner.json";
+    if (argc > 1) {
+        config_path = argv[1];
+    }
+
+    runner::RunnerServiceConfig config;
+    if (std::filesystem::exists(config_path)) {
+        config = runner::RunnerService::loadConfig(config_path);
+        LOG_INFO("Loaded configuration from {}", config_path.string());
+    } else {
+        LOG_WARN("Config file not found at {}, using defaults", config_path.string());
+    }
+
+    // Setup signal handlers
+    std::signal(SIGINT, signalHandler);
+    std::signal(SIGTERM, signalHandler);
+
+    // Create and start service
+    runner::RunnerService service(config);
+
+    try {
+        service.start();
+
+        // Wait for shutdown signal
+        while (!g_shutdown) {
+            std::this_thread::sleep_for(std::chrono::milliseconds(100));
+        }
+
+        service.stop();
+    } catch (const std::exception& e) {
+        LOG_ERROR("Fatal error: {}", e.what());
+        return 1;
+    }
+
+    LOG_INFO("SmartBotic Runner stopped");
+    logging::Logger::shutdown();
+
+    return 0;
+}

+ 336 - 0
src/runner/node_registry.cpp

@@ -0,0 +1,336 @@
+#include "node_registry.hpp"
+#include "logging/logger.hpp"
+#include "common/time_utils.hpp"
+
+namespace smartbotic::runner {
+
+using namespace common;
+
+nlohmann::json NodeDefinition::toJson() const {
+    nlohmann::json j;
+    j["id"] = id;
+    j["name"] = name;
+    j["category"] = category;
+    j["version"] = version;
+    j["description"] = description;
+    j["icon"] = icon;
+    j["isTrigger"] = is_trigger;
+    j["configSchema"] = config_schema;
+    j["inputSchema"] = input_schema;
+    j["outputSchema"] = output_schema;
+
+    j["inputs"] = nlohmann::json::array();
+    for (const auto& input : inputs) {
+        j["inputs"].push_back({
+            {"name", input.name},
+            {"displayName", input.display_name},
+            {"type", input.type},
+            {"required", input.required}
+        });
+    }
+
+    j["outputs"] = nlohmann::json::array();
+    for (const auto& output : outputs) {
+        nlohmann::json out_obj = {
+            {"name", output.name},
+            {"displayName", output.display_name},
+            {"type", output.type}
+        };
+        if (!output.color.empty()) {
+            out_obj["color"] = output.color;
+        }
+        j["outputs"].push_back(out_obj);
+    }
+
+    return j;
+}
+
+NodeDefinition NodeDefinition::fromProto(const proto::NodeDefinitionWithCode& proto) {
+    NodeDefinition node;
+    node.id = proto.id();
+    node.name = proto.name();
+    node.category = proto.category();
+    node.version = proto.version();
+    node.description = proto.description();
+    node.icon = proto.icon();
+    node.code = proto.code();
+    node.is_trigger = proto.is_trigger();
+    node.last_modified = proto.updated_at();
+
+    // Parse schemas
+    try {
+        if (!proto.config_schema().empty() && proto.config_schema() != "null") {
+            node.config_schema = nlohmann::json::parse(proto.config_schema());
+        }
+    } catch (...) {}
+
+    try {
+        if (!proto.input_schema().empty() && proto.input_schema() != "null") {
+            node.input_schema = nlohmann::json::parse(proto.input_schema());
+        }
+    } catch (...) {}
+
+    try {
+        if (!proto.output_schema().empty() && proto.output_schema() != "null") {
+            node.output_schema = nlohmann::json::parse(proto.output_schema());
+        }
+    } catch (...) {}
+
+    // Parse inputs
+    for (const auto& input : proto.inputs()) {
+        NodeIO io;
+        io.name = input.name();
+        io.display_name = input.display_name();
+        io.type = input.type();
+        io.required = input.required();
+        node.inputs.push_back(io);
+    }
+
+    // Parse outputs
+    for (const auto& output : proto.outputs()) {
+        NodeIO io;
+        io.name = output.name();
+        io.display_name = output.display_name();
+        io.type = output.type();
+        io.color = output.color();
+        node.outputs.push_back(io);
+    }
+
+    return node;
+}
+
+NodeRegistry::NodeRegistry(const NodeRegistryConfig& config)
+    : config_(config) {
+    // Create gRPC channel
+    channel_ = grpc::CreateChannel(config_.webserver_address, grpc::InsecureChannelCredentials());
+    stub_ = proto::NodeSyncService::NewStub(channel_);
+}
+
+NodeRegistry::~NodeRegistry() {
+    stop();
+}
+
+void NodeRegistry::start() {
+    if (running_ || !config_.sync_enabled) {
+        return;
+    }
+
+    running_ = true;
+
+    // Load initial nodes
+    auto result = loadNodesFromWebServer();
+    if (result.failed()) {
+        LOG_WARN("Failed to load initial nodes: {}", result.error().message());
+    }
+
+    // Start subscription thread for live updates
+    subscribe_thread_ = std::thread(&NodeRegistry::subscribeLoop, this);
+    LOG_INFO("Node registry sync started with {}", config_.webserver_address);
+}
+
+void NodeRegistry::stop() {
+    if (!running_) {
+        return;
+    }
+
+    LOG_INFO("Stopping node registry...");
+    running_ = false;
+
+    // Cancel any active gRPC stream to unblock subscribeLoop
+    {
+        std::lock_guard<std::mutex> lock(context_mutex_);
+        if (active_context_) {
+            active_context_->TryCancel();
+        }
+    }
+
+    if (subscribe_thread_.joinable()) {
+        subscribe_thread_.join();
+    }
+
+    LOG_INFO("Node registry sync stopped");
+}
+
+Result<void> NodeRegistry::loadNodesFromWebServer() {
+    proto::GetAllNodesRequest request;
+    proto::GetAllNodesResponse response;
+
+    grpc::ClientContext context;
+    context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));
+
+    auto status = stub_->GetAllNodes(&context, request, &response);
+
+    if (!status.ok()) {
+        connected_ = false;
+        return Error(ErrorCode::Internal, "Failed to get nodes: " + status.error_message());
+    }
+
+    connected_ = true;
+
+    std::unique_lock lock(mutex_);
+    nodes_.clear();
+
+    for (const auto& proto_node : response.nodes()) {
+        NodeDefinition node = NodeDefinition::fromProto(proto_node);
+        nodes_[node.id] = node;
+        LOG_DEBUG("Loaded node: {} ({})", node.id, node.name);
+    }
+
+    LOG_INFO("Loaded {} node definitions from webserver", nodes_.size());
+    return {};
+}
+
+void NodeRegistry::subscribeLoop() {
+    while (running_) {
+        proto::SubscribeToNodeChangesRequest request;
+        request.set_runner_id("runner_" + std::to_string(reinterpret_cast<uintptr_t>(this)));
+
+        grpc::ClientContext context;
+
+        // Store context pointer so stop() can cancel it
+        {
+            std::lock_guard<std::mutex> lock(context_mutex_);
+            active_context_ = &context;
+        }
+
+        auto reader = stub_->SubscribeToNodeChanges(&context, request);
+
+        if (!reader) {
+            {
+                std::lock_guard<std::mutex> lock(context_mutex_);
+                active_context_ = nullptr;
+            }
+            if (!running_) break;
+            LOG_WARN("Failed to subscribe to node changes, retrying...");
+            std::this_thread::sleep_for(std::chrono::milliseconds(config_.reconnect_interval_ms));
+            continue;
+        }
+
+        connected_ = true;
+        LOG_INFO("Subscribed to node changes from webserver");
+
+        proto::NodeChangeEvent event;
+        while (running_ && reader->Read(&event)) {
+            handleNodeChange(event);
+        }
+
+        // Clear context pointer
+        {
+            std::lock_guard<std::mutex> lock(context_mutex_);
+            active_context_ = nullptr;
+        }
+
+        connected_ = false;
+        auto status = reader->Finish();
+
+        if (!status.ok() && running_) {
+            LOG_WARN("Node change subscription ended: {}, reconnecting...", status.error_message());
+            std::this_thread::sleep_for(std::chrono::milliseconds(config_.reconnect_interval_ms));
+        }
+    }
+}
+
+void NodeRegistry::handleNodeChange(const proto::NodeChangeEvent& event) {
+    switch (event.type()) {
+        case proto::NodeChangeEvent::CHANGE_TYPE_CREATED:
+        case proto::NodeChangeEvent::CHANGE_TYPE_UPDATED: {
+            NodeDefinition node = NodeDefinition::fromProto(event.node());
+            {
+                std::unique_lock lock(mutex_);
+                nodes_[node.id] = node;
+            }
+            LOG_INFO("Node {}: {}",
+                     event.type() == proto::NodeChangeEvent::CHANGE_TYPE_CREATED ? "created" : "updated",
+                     node.id);
+
+            if (reload_callback_) {
+                reload_callback_(node.id);
+            }
+            break;
+        }
+
+        case proto::NodeChangeEvent::CHANGE_TYPE_DELETED: {
+            {
+                std::unique_lock lock(mutex_);
+                nodes_.erase(event.node_id());
+            }
+            LOG_INFO("Node deleted: {}", event.node_id());
+
+            if (reload_callback_) {
+                reload_callback_(event.node_id());
+            }
+            break;
+        }
+
+        default:
+            break;
+    }
+}
+
+std::optional<NodeDefinition> NodeRegistry::getNode(const std::string& id) const {
+    std::shared_lock lock(mutex_);
+
+    auto it = nodes_.find(id);
+    if (it != nodes_.end()) {
+        return it->second;
+    }
+    return std::nullopt;
+}
+
+std::vector<NodeDefinition> NodeRegistry::getAllNodes() const {
+    std::shared_lock lock(mutex_);
+
+    std::vector<NodeDefinition> result;
+    result.reserve(nodes_.size());
+
+    for (const auto& [id, node] : nodes_) {
+        result.push_back(node);
+    }
+
+    return result;
+}
+
+std::vector<NodeDefinition> NodeRegistry::getNodesByCategory(const std::string& category) const {
+    std::shared_lock lock(mutex_);
+
+    std::vector<NodeDefinition> result;
+
+    for (const auto& [id, node] : nodes_) {
+        if (node.category == category) {
+            result.push_back(node);
+        }
+    }
+
+    return result;
+}
+
+std::vector<NodeDefinition> NodeRegistry::getTriggerNodes() const {
+    std::shared_lock lock(mutex_);
+
+    std::vector<NodeDefinition> result;
+
+    for (const auto& [id, node] : nodes_) {
+        if (node.is_trigger) {
+            result.push_back(node);
+        }
+    }
+
+    return result;
+}
+
+Result<std::string> NodeRegistry::getNodeCode(const std::string& id) const {
+    std::shared_lock lock(mutex_);
+
+    auto it = nodes_.find(id);
+    if (it == nodes_.end()) {
+        return Error(ErrorCode::NotFound, "Node not found: " + id);
+    }
+
+    return it->second.code;
+}
+
+void NodeRegistry::setReloadCallback(NodeReloadCallback callback) {
+    reload_callback_ = std::move(callback);
+}
+
+} // namespace smartbotic::runner

+ 115 - 0
src/runner/node_registry.hpp

@@ -0,0 +1,115 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <shared_mutex>
+#include <filesystem>
+#include <thread>
+#include <atomic>
+#include <mutex>
+#include <functional>
+#include <memory>
+#include <nlohmann/json.hpp>
+#include <grpcpp/grpcpp.h>
+#include "proto/runner.grpc.pb.h"
+#include "common/error.hpp"
+
+namespace smartbotic::runner {
+
+// Node input/output definition
+struct NodeIO {
+    std::string name;
+    std::string display_name;
+    std::string type;
+    std::string color;  // Optional color for visual representation
+    bool required = false;
+};
+
+// Node definition
+struct NodeDefinition {
+    std::string id;
+    std::string name;
+    std::string category;
+    std::string version;
+    std::string description;
+    std::string icon;
+    std::string code;
+    nlohmann::json config_schema;
+    nlohmann::json input_schema;
+    nlohmann::json output_schema;
+    std::vector<NodeIO> inputs;
+    std::vector<NodeIO> outputs;
+    bool is_trigger = false;
+    int64_t last_modified = 0;
+
+    nlohmann::json toJson() const;
+    static NodeDefinition fromProto(const proto::NodeDefinitionWithCode& proto);
+};
+
+// Node reload callback
+using NodeReloadCallback = std::function<void(const std::string& node_id)>;
+
+// Node registry configuration
+struct NodeRegistryConfig {
+    std::string webserver_address = "localhost:9002";  // gRPC address for node sync
+    bool sync_enabled = true;
+    int reconnect_interval_ms = 5000;
+};
+
+// Node registry - manages JavaScript node definitions from webserver
+class NodeRegistry {
+public:
+    explicit NodeRegistry(const NodeRegistryConfig& config);
+    ~NodeRegistry();
+
+    // Start/stop node sync with webserver
+    void start();
+    void stop();
+
+    // Initial load of all nodes from webserver
+    common::Result<void> loadNodesFromWebServer();
+
+    // Get node by ID
+    std::optional<NodeDefinition> getNode(const std::string& id) const;
+
+    // Get all nodes
+    std::vector<NodeDefinition> getAllNodes() const;
+
+    // Get nodes by category
+    std::vector<NodeDefinition> getNodesByCategory(const std::string& category) const;
+
+    // Get trigger nodes
+    std::vector<NodeDefinition> getTriggerNodes() const;
+
+    // Get node code
+    common::Result<std::string> getNodeCode(const std::string& id) const;
+
+    // Set reload callback
+    void setReloadCallback(NodeReloadCallback callback);
+
+    // Connection status
+    bool isConnected() const { return connected_; }
+
+private:
+    void subscribeLoop();
+    void handleNodeChange(const proto::NodeChangeEvent& event);
+    void parseProtoNode(const proto::NodeDefinitionWithCode& proto, NodeDefinition& node);
+
+    NodeRegistryConfig config_;
+    std::unordered_map<std::string, NodeDefinition> nodes_;
+    mutable std::shared_mutex mutex_;
+
+    std::shared_ptr<grpc::Channel> channel_;
+    std::unique_ptr<proto::NodeSyncService::Stub> stub_;
+
+    std::thread subscribe_thread_;
+    std::atomic<bool> running_{false};
+    std::atomic<bool> connected_{false};
+    NodeReloadCallback reload_callback_;
+
+    // For cancelling active gRPC stream during shutdown
+    std::mutex context_mutex_;
+    grpc::ClientContext* active_context_{nullptr};
+};
+
+} // namespace smartbotic::runner

+ 679 - 0
src/runner/runner_service.cpp

@@ -0,0 +1,679 @@
+#include "runner_service.hpp"
+#include "common/uuid.hpp"
+#include "common/time_utils.hpp"
+#include "logging/logger.hpp"
+#include "proto/runner.grpc.pb.h"
+#include <grpcpp/health_check_service_interface.h>
+#include <sys/resource.h>
+#include <fstream>
+#include <curl/curl.h>
+
+namespace smartbotic::runner {
+
+using namespace common;
+
+// RunnerServiceImpl implementation
+RunnerServiceImpl::RunnerServiceImpl(WorkflowEngine& engine, NodeRegistry& registry,
+                                     storage::StorageClient& storage,
+                                     ExecutionEventCallback event_callback)
+    : engine_(engine), registry_(registry), storage_(storage), event_callback_(event_callback) {}
+
+grpc::Status RunnerServiceImpl::ExecuteWorkflow(grpc::ServerContext* context,
+                                                const proto::ExecuteWorkflowRequest* request,
+                                                proto::ExecuteWorkflowResponse* response) {
+    LOG_INFO("ExecuteWorkflow called for workflow: {}", request->workflow_id());
+
+    // Get workflow from database
+    auto workflow_result = storage_.get("workflows", request->workflow_id());
+    if (workflow_result.failed()) {
+        LOG_ERROR("Failed to get workflow from database: {}", workflow_result.error().message());
+        response->set_status(proto::EXECUTION_STATUS_FAILED);
+        auto* error = response->mutable_error();
+        error->set_code(static_cast<int32_t>(workflow_result.error().code()));
+        error->set_message(workflow_result.error().message());
+        return grpc::Status::OK;
+    }
+
+    // Parse workflow
+    auto workflow = Workflow::fromJson(workflow_result.value());
+
+    // Parse trigger data
+    nlohmann::json trigger_data;
+    if (!request->trigger_data().empty()) {
+        try {
+            trigger_data = nlohmann::json::parse(request->trigger_data());
+        } catch (...) {
+            trigger_data = request->trigger_data();
+        }
+    }
+
+    // Create callback to forward events
+    ExecutionCallback callback;
+    if (event_callback_) {
+        callback = [this, &workflow](const std::string& event_type, const nlohmann::json& data) {
+            nlohmann::json event_data = data;
+            event_data["workflowId"] = workflow.id;
+            event_callback_(event_type, event_data);
+        };
+    }
+
+    // Execute workflow with callback
+    LOG_INFO("Starting workflow execution...");
+    auto result = engine_.execute(workflow, request->trigger_type(), trigger_data, callback);
+
+    if (result.failed()) {
+        LOG_ERROR("Workflow execution failed: {}", result.error().message());
+
+        // Emit execution.failed event
+        if (event_callback_) {
+            event_callback_("execution.failed", {
+                {"executionId", ""},
+                {"workflowId", workflow.id},
+                {"error", result.error().message()}
+            });
+        }
+
+        response->set_status(proto::EXECUTION_STATUS_FAILED);
+        auto* error = response->mutable_error();
+        error->set_code(static_cast<int32_t>(result.error().code()));
+        error->set_message(result.error().message());
+        return grpc::Status::OK;
+    }
+    LOG_INFO("Workflow execution completed, execution_id: {}, status: {}",
+             result.value().execution_id, executionStatusToString(result.value().status));
+
+    // Emit final execution event based on status
+    if (event_callback_) {
+        auto& exec_result = result.value();
+        if (exec_result.status == ExecutionStatus::Completed) {
+            event_callback_("execution.completed", {
+                {"executionId", exec_result.execution_id},
+                {"workflowId", workflow.id},
+                {"output", exec_result.final_output}
+            });
+        } else if (exec_result.status == ExecutionStatus::Failed) {
+            std::string error_msg;
+            for (const auto& [node_id, node_result] : exec_result.node_results) {
+                if (!node_result.error.empty()) {
+                    error_msg = node_result.error;
+                    break;
+                }
+            }
+            event_callback_("execution.failed", {
+                {"executionId", exec_result.execution_id},
+                {"workflowId", workflow.id},
+                {"error", error_msg}
+            });
+        }
+    }
+
+    response->set_execution_id(result.value().execution_id);
+    response->set_status(static_cast<proto::ExecutionStatus>(result.value().status));
+
+    if (request->wait_for_completion()) {
+        response->set_result(result.value().final_output.dump());
+    }
+
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::CancelExecution(grpc::ServerContext* context,
+                                                const proto::CancelExecutionRequest* request,
+                                                proto::Empty* response) {
+    engine_.cancelExecution(request->execution_id());
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::ListNodes(grpc::ServerContext* context,
+                                          const proto::ListNodesRequest* request,
+                                          proto::ListNodesResponse* response) {
+    std::vector<NodeDefinition> nodes;
+
+    if (request->category().empty()) {
+        nodes = registry_.getAllNodes();
+    } else {
+        nodes = registry_.getNodesByCategory(request->category());
+    }
+
+    for (const auto& node : nodes) {
+        auto* proto_node = response->add_nodes();
+        proto_node->set_id(node.id);
+        proto_node->set_name(node.name);
+        proto_node->set_category(node.category);
+        proto_node->set_version(node.version);
+        proto_node->set_description(node.description);
+        proto_node->set_icon(node.icon);
+        proto_node->set_is_trigger(node.is_trigger);
+        proto_node->set_config_schema(node.config_schema.dump());
+        proto_node->set_input_schema(node.input_schema.dump());
+        proto_node->set_output_schema(node.output_schema.dump());
+
+        for (const auto& input : node.inputs) {
+            auto* proto_input = proto_node->add_inputs();
+            proto_input->set_name(input.name);
+            proto_input->set_display_name(input.display_name);
+            proto_input->set_type(input.type);
+            proto_input->set_required(input.required);
+        }
+
+        for (const auto& output : node.outputs) {
+            auto* proto_output = proto_node->add_outputs();
+            proto_output->set_name(output.name);
+            proto_output->set_display_name(output.display_name);
+            proto_output->set_type(output.type);
+            if (!output.color.empty()) {
+                proto_output->set_color(output.color);
+            }
+        }
+    }
+
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::ReloadNode(grpc::ServerContext* context,
+                                           const proto::ReloadNodeRequest* request,
+                                           proto::ReloadNodeResponse* response) {
+    // Nodes are now synced automatically from webserver
+    // Manual reload is no longer needed
+    auto node = registry_.getNode(request->node_id());
+
+    if (!node) {
+        response->set_success(false);
+        auto* error = response->mutable_error();
+        error->set_code(404);
+        error->set_message("Node not found: " + request->node_id());
+        return grpc::Status::OK;
+    }
+
+    response->set_success(true);
+    auto* proto_node = response->mutable_node();
+    proto_node->set_id(node->id);
+    proto_node->set_name(node->name);
+    proto_node->set_version(node->version);
+
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::ExecuteNode(grpc::ServerContext* context,
+                                            const proto::ExecuteNodeRequest* request,
+                                            proto::ExecuteNodeResponse* response) {
+    auto node_def = registry_.getNode(request->node_type());
+    if (!node_def) {
+        response->set_success(false);
+        response->set_error("Node type not found: " + request->node_type());
+        return grpc::Status::OK;
+    }
+
+    // Parse input and config
+    nlohmann::json input, config;
+    try {
+        if (!request->input().empty()) {
+            input = nlohmann::json::parse(request->input());
+        }
+        if (!request->config().empty()) {
+            config = nlohmann::json::parse(request->config());
+        }
+    } catch (const std::exception& e) {
+        response->set_success(false);
+        response->set_error("Invalid JSON: " + std::string(e.what()));
+        return grpc::Status::OK;
+    }
+
+    // Create execution context
+    engine::ScriptContext ctx;
+    ctx.execution_id = common::UUID::generate();
+    ctx.node_id = request->node_type();
+    ctx.input = input;
+    ctx.config = config;
+
+    // Execute
+    engine::ScriptEnginePool pool(1);
+    auto* engine = pool.acquire();
+    auto result = engine->execute(node_def->code, ctx);
+    pool.release(engine);
+
+    response->set_success(result.success);
+    response->set_output(result.output.dump());
+    response->set_error(result.error);
+    response->set_execution_time_ms(result.execution_time_ms);
+
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::GetNodeCode(grpc::ServerContext* context,
+                                            const proto::GetNodeCodeRequest* request,
+                                            proto::GetNodeCodeResponse* response) {
+    auto result = registry_.getNodeCode(request->node_id());
+
+    if (result.failed()) {
+        response->set_success(false);
+        auto* error = response->mutable_error();
+        error->set_code(static_cast<int32_t>(result.error().code()));
+        error->set_message(result.error().message());
+        return grpc::Status::OK;
+    }
+
+    response->set_success(true);
+    response->set_code(result.value());
+    // file_path no longer applicable - nodes stored in database
+
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::SaveNodeCode(grpc::ServerContext* context,
+                                             const proto::SaveNodeCodeRequest* request,
+                                             proto::SaveNodeCodeResponse* response) {
+    // Node modifications are now handled centrally by the webserver
+    response->set_success(false);
+    auto* error = response->mutable_error();
+    error->set_code(501);
+    error->set_message("Node modifications should be done through the webserver API");
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::CreateNode(grpc::ServerContext* context,
+                                           const proto::CreateNodeRequest* request,
+                                           proto::CreateNodeResponse* response) {
+    // Node creation is now handled centrally by the webserver
+    response->set_success(false);
+    auto* error = response->mutable_error();
+    error->set_code(501);
+    error->set_message("Node creation should be done through the webserver API");
+    return grpc::Status::OK;
+}
+
+grpc::Status RunnerServiceImpl::DeleteNode(grpc::ServerContext* context,
+                                           const proto::DeleteNodeRequest* request,
+                                           proto::DeleteNodeResponse* response) {
+    // Node deletion is now handled centrally by the webserver
+    response->set_success(false);
+    auto* error = response->mutable_error();
+    error->set_code(501);
+    error->set_message("Node deletion should be done through the webserver API");
+    return grpc::Status::OK;
+}
+
+// RunnerService implementation
+RunnerService::RunnerService(const RunnerServiceConfig& config)
+    : config_(config) {
+
+    // Initialize storage client
+    storage::StorageClientConfig storage_config;
+    storage_config.address = config_.database_address;
+    storage_ = std::make_unique<storage::StorageClient>(storage_config);
+
+    // Initialize credential client
+    credentials::CredentialClientConfig cred_config;
+    cred_config.address = config_.credential_service_address;
+    credential_client_ = std::make_unique<credentials::CredentialClient>(cred_config);
+
+    // Initialize node registry - now loads from webserver
+    config_.node_registry_config.webserver_address = config_.node_sync_address;
+    registry_ = std::make_unique<NodeRegistry>(config_.node_registry_config);
+
+    // Initialize workflow engine
+    config_.workflow_engine_config.max_concurrent_executions = config_.max_concurrent_executions;
+    engine_ = std::make_unique<WorkflowEngine>(*registry_, *storage_, config_.workflow_engine_config);
+
+    // Set up credential auth callback for workflow engine
+    engine_->setCredentialAuthCallback(
+        [this](const std::string& credential_id, const std::string& workflow_id)
+            -> common::Result<engine::CredentialAuth> {
+            auto result = credential_client_->getHttpAuth(credential_id, workflow_id);
+            if (result.failed()) {
+                return result.error();
+            }
+            engine::CredentialAuth auth;
+            auth.header_name = result.value().header_name;
+            auth.header_value = result.value().header_value;
+            return auth;
+        });
+
+    // Set up IMAP credential callback for workflow engine
+    engine_->setImapCredentialCallback(
+        [this](const std::string& credential_id, const std::string& workflow_id)
+            -> common::Result<engine::ImapCredential> {
+            auto result = credential_client_->getImapCredentials(credential_id, workflow_id);
+            if (result.failed()) {
+                return result.error();
+            }
+            engine::ImapCredential cred;
+            cred.host = result.value().host;
+            cred.port = result.value().port;
+            cred.username = result.value().username;
+            cred.password = result.value().password;
+            cred.use_ssl = result.value().use_ssl;
+            return cred;
+        });
+}
+
+RunnerService::~RunnerService() {
+    stop();
+}
+
+RunnerServiceConfig RunnerService::loadConfig(const std::filesystem::path& path) {
+    RunnerServiceConfig config;
+
+    auto result = config::Config::fromFile(path);
+    if (result.ok()) {
+        auto& cfg = result.value();
+
+        config.grpc_port = cfg.getOr<int>("grpc_port", 9003);
+        config.runner_id = cfg.getOr<std::string>("runner_id", "runner-1");
+        config.webserver_address = cfg.getOr<std::string>("webserver_address", "localhost:8080");
+        config.node_sync_address = cfg.getOr<std::string>("node_sync_address", "localhost:9002");
+        config.credential_service_address = cfg.getOr<std::string>("credential_service_address", "localhost:9003");
+        config.database_address = cfg.getOr<std::string>("database_address", "localhost:9001");
+
+        // Node sync configuration
+        config.node_registry_config.webserver_address = config.node_sync_address;
+        config.node_registry_config.sync_enabled =
+            cfg.getOr<bool>("node_sync.enabled", true);
+        config.node_registry_config.reconnect_interval_ms =
+            cfg.getOr<int>("node_sync.reconnect_interval_ms", 5000);
+
+        config.heartbeat_interval_sec =
+            cfg.getOr<int>("registration.heartbeat_interval_sec", 10);
+        config.max_concurrent_executions =
+            cfg.getOr<int>("registration.max_concurrent_executions", 10);
+
+        config.workflow_engine_config.default_timeout_ms =
+            cfg.getOr<int>("execution.default_timeout_ms", 60000);
+        config.workflow_engine_config.script_config.max_memory_mb =
+            cfg.getOr<int>("execution.max_memory_per_script_mb", 64);
+    }
+
+    return config;
+}
+
+void RunnerService::start() {
+    if (running_) {
+        return;
+    }
+
+    LOG_INFO("Starting runner service {}...", config_.runner_id);
+
+    // Start node registry (loads nodes from webserver and subscribes to changes)
+    registry_->start();
+
+    // Create event callback to send events to webserver
+    std::string webserver_url = "http://" + config_.webserver_address + "/api/v1/internal/execution-event";
+    ExecutionEventCallback event_callback = [webserver_url](const std::string& event_type, const nlohmann::json& data) {
+        LOG_DEBUG("Sending event {} to webserver", event_type);
+
+        // Send event to webserver via HTTP POST (non-blocking in separate thread)
+        std::thread([webserver_url, event_type, data]() {
+            CURL* curl = curl_easy_init();
+            if (!curl) {
+                LOG_ERROR("Failed to init curl for event {}", event_type);
+                return;
+            }
+
+            nlohmann::json payload;
+            payload["event"] = event_type;
+            payload["data"] = data;
+            std::string body = payload.dump();
+
+            curl_easy_setopt(curl, CURLOPT_URL, webserver_url.c_str());
+            curl_easy_setopt(curl, CURLOPT_POST, 1L);
+            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
+            curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
+
+            struct curl_slist* headers = nullptr;
+            headers = curl_slist_append(headers, "Content-Type: application/json");
+            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+            curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
+
+            // Discard response body (don't write to stdout)
+            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char*, size_t size, size_t nmemb, void*) -> size_t {
+                return size * nmemb;
+            });
+
+            CURLcode res = curl_easy_perform(curl);
+            if (res != CURLE_OK) {
+                LOG_ERROR("Failed to send event {} to webserver: {}", event_type, curl_easy_strerror(res));
+            }
+
+            curl_slist_free_all(headers);
+            curl_easy_cleanup(curl);
+        }).detach();
+    };
+
+    // Start gRPC server
+    service_impl_ = std::make_unique<RunnerServiceImpl>(*engine_, *registry_, *storage_, event_callback);
+
+    grpc::EnableDefaultHealthCheckService(true);
+    grpc::ServerBuilder builder;
+    builder.AddListeningPort("0.0.0.0:" + std::to_string(config_.grpc_port),
+                            grpc::InsecureServerCredentials());
+    builder.RegisterService(service_impl_.get());
+
+    server_ = builder.BuildAndStart();
+    LOG_INFO("Runner gRPC server listening on port {}", config_.grpc_port);
+
+    running_ = true;
+
+    // Register with webserver
+    registerWithWebServer();
+
+    // Start heartbeat
+    heartbeat_thread_ = std::thread(&RunnerService::heartbeatLoop, this);
+
+    LOG_INFO("Runner service {} started", config_.runner_id);
+}
+
+void RunnerService::stop() {
+    if (!running_) {
+        return;
+    }
+
+    LOG_INFO("Stopping runner service...");
+
+    // Signal shutdown to background threads
+    {
+        std::lock_guard<std::mutex> lock(shutdown_mutex_);
+        running_ = false;
+    }
+    shutdown_cv_.notify_all();
+
+    // Unregister
+    unregisterFromWebServer();
+
+    // Stop heartbeat (will wake up immediately now)
+    if (heartbeat_thread_.joinable()) {
+        heartbeat_thread_.join();
+    }
+
+    // Stop node registry
+    registry_->stop();
+
+    // Stop gRPC server
+    if (server_) {
+        server_->Shutdown();
+    }
+
+    LOG_INFO("Runner service stopped");
+}
+
+void RunnerService::registerWithWebServer() {
+    // Use HTTP to register with webserver
+    nlohmann::json body;
+    body["id"] = config_.runner_id;
+    body["address"] = "localhost:" + std::to_string(config_.grpc_port);
+
+    nlohmann::json capabilities;
+    std::vector<std::string> node_types;
+    for (const auto& node : registry_->getAllNodes()) {
+        node_types.push_back(node.id);
+    }
+    capabilities["nodeTypes"] = node_types;
+    capabilities["maxMemoryPerScriptMb"] = config_.workflow_engine_config.script_config.max_memory_mb;
+    capabilities["maxExecutionTimeoutSec"] = config_.workflow_engine_config.default_timeout_ms / 1000;
+    body["capabilities"] = capabilities;
+
+    std::string url = "http://" + config_.webserver_address + "/api/internal/runners/register";
+
+    CURL* curl = curl_easy_init();
+    if (!curl) {
+        LOG_WARN("Failed to initialize curl for registration");
+        return;
+    }
+
+    std::string response_data;
+    std::string body_str = body.dump();
+
+    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str());
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
+
+    struct curl_slist* headers = nullptr;
+    headers = curl_slist_append(headers, "Content-Type: application/json");
+    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t {
+        auto* data = static_cast<std::string*>(userdata);
+        data->append(ptr, size * nmemb);
+        return size * nmemb;
+    });
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
+
+    CURLcode res = curl_easy_perform(curl);
+
+    if (res == CURLE_OK) {
+        LOG_INFO("Runner registered with webserver");
+    } else {
+        LOG_WARN("Failed to register with webserver: {}", curl_easy_strerror(res));
+    }
+
+    curl_slist_free_all(headers);
+    curl_easy_cleanup(curl);
+}
+
+void RunnerService::heartbeatLoop() {
+    std::string url = "http://" + config_.webserver_address + "/api/internal/runners/heartbeat";
+
+    while (running_) {
+        // Wait for shutdown signal or timeout
+        {
+            std::unique_lock<std::mutex> lock(shutdown_mutex_);
+            if (shutdown_cv_.wait_for(lock,
+                    std::chrono::seconds(config_.heartbeat_interval_sec),
+                    [this] { return !running_.load(); })) {
+                // Shutdown signaled, exit loop
+                break;
+            }
+        }
+
+        if (!running_) break;
+
+        auto metrics = collectMetrics();
+
+        nlohmann::json body;
+        body["id"] = config_.runner_id;
+        body["status"] = "online";
+        body["metrics"] = {
+            {"activeExecutions", metrics.active_executions},
+            {"maxExecutions", metrics.max_executions},
+            {"memoryUsedBytes", metrics.memory_used_bytes},
+            {"memoryTotalBytes", metrics.memory_total_bytes},
+            {"cpuPercent", metrics.cpu_percent}
+        };
+
+        CURL* curl = curl_easy_init();
+        if (!curl) continue;
+
+        std::string body_str = body.dump();
+        std::string response_data;
+
+        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+        curl_easy_setopt(curl, CURLOPT_POST, 1L);
+        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str());
+        curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
+
+        struct curl_slist* headers = nullptr;
+        headers = curl_slist_append(headers, "Content-Type: application/json");
+        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t {
+            auto* data = static_cast<std::string*>(userdata);
+            data->append(ptr, size * nmemb);
+            return size * nmemb;
+        });
+        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
+
+        CURLcode res = curl_easy_perform(curl);
+
+        if (res != CURLE_OK) {
+            LOG_WARN("Heartbeat failed: {}", curl_easy_strerror(res));
+        }
+
+        curl_slist_free_all(headers);
+        curl_easy_cleanup(curl);
+    }
+}
+
+void RunnerService::unregisterFromWebServer() {
+    std::string url = "http://" + config_.webserver_address + "/api/internal/runners/unregister";
+
+    nlohmann::json body;
+    body["id"] = config_.runner_id;
+
+    CURL* curl = curl_easy_init();
+    if (!curl) return;
+
+    std::string body_str = body.dump();
+    std::string response_data;
+
+    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str());
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
+
+    struct curl_slist* headers = nullptr;
+    headers = curl_slist_append(headers, "Content-Type: application/json");
+    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t {
+        auto* data = static_cast<std::string*>(userdata);
+        data->append(ptr, size * nmemb);
+        return size * nmemb;
+    });
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
+
+    curl_easy_perform(curl);
+
+    curl_slist_free_all(headers);
+    curl_easy_cleanup(curl);
+}
+
+RunnerMetrics RunnerService::collectMetrics() {
+    RunnerMetrics metrics;
+
+    metrics.active_executions = engine_->getActiveExecutionCount();
+    metrics.max_executions = config_.max_concurrent_executions;
+
+    // Get memory usage
+    struct rusage usage;
+    if (getrusage(RUSAGE_SELF, &usage) == 0) {
+        metrics.memory_used_bytes = usage.ru_maxrss * 1024;  // KB to bytes
+    }
+
+    // Get total memory from /proc/meminfo
+    std::ifstream meminfo("/proc/meminfo");
+    std::string line;
+    while (std::getline(meminfo, line)) {
+        if (line.starts_with("MemTotal:")) {
+            std::istringstream iss(line);
+            std::string label;
+            int64_t kb;
+            iss >> label >> kb;
+            metrics.memory_total_bytes = kb * 1024;
+            break;
+        }
+    }
+
+    return metrics;
+}
+
+} // namespace smartbotic::runner

+ 139 - 0
src/runner/runner_service.hpp

@@ -0,0 +1,139 @@
+#pragma once
+
+#include <memory>
+#include <thread>
+#include <atomic>
+#include <mutex>
+#include <condition_variable>
+#include <grpcpp/grpcpp.h>
+#include "proto/runner.grpc.pb.h"
+#include "node_registry.hpp"
+#include "workflow_engine.hpp"
+#include "storage/storage_client.hpp"
+#include "credentials/credential_client.hpp"
+#include "config/config_loader.hpp"
+
+namespace smartbotic::runner {
+
+// Forward declaration
+struct RunnerMetrics;
+
+// Execution event callback type
+using ExecutionEventCallback = std::function<void(const std::string& event, const nlohmann::json& data)>;
+
+// gRPC Runner Service implementation
+class RunnerServiceImpl final : public proto::RunnerService::Service {
+public:
+    RunnerServiceImpl(WorkflowEngine& engine, NodeRegistry& registry,
+                      storage::StorageClient& storage,
+                      ExecutionEventCallback event_callback = nullptr);
+
+    void setEventCallback(ExecutionEventCallback callback) { event_callback_ = callback; }
+
+    grpc::Status ExecuteWorkflow(grpc::ServerContext* context,
+                                const proto::ExecuteWorkflowRequest* request,
+                                proto::ExecuteWorkflowResponse* response) override;
+
+    grpc::Status CancelExecution(grpc::ServerContext* context,
+                                 const proto::CancelExecutionRequest* request,
+                                 proto::Empty* response) override;
+
+    grpc::Status ListNodes(grpc::ServerContext* context,
+                          const proto::ListNodesRequest* request,
+                          proto::ListNodesResponse* response) override;
+
+    grpc::Status ReloadNode(grpc::ServerContext* context,
+                           const proto::ReloadNodeRequest* request,
+                           proto::ReloadNodeResponse* response) override;
+
+    grpc::Status ExecuteNode(grpc::ServerContext* context,
+                            const proto::ExecuteNodeRequest* request,
+                            proto::ExecuteNodeResponse* response) override;
+
+    grpc::Status GetNodeCode(grpc::ServerContext* context,
+                            const proto::GetNodeCodeRequest* request,
+                            proto::GetNodeCodeResponse* response) override;
+
+    grpc::Status SaveNodeCode(grpc::ServerContext* context,
+                             const proto::SaveNodeCodeRequest* request,
+                             proto::SaveNodeCodeResponse* response) override;
+
+    grpc::Status CreateNode(grpc::ServerContext* context,
+                           const proto::CreateNodeRequest* request,
+                           proto::CreateNodeResponse* response) override;
+
+    grpc::Status DeleteNode(grpc::ServerContext* context,
+                           const proto::DeleteNodeRequest* request,
+                           proto::DeleteNodeResponse* response) override;
+
+private:
+    WorkflowEngine& engine_;
+    NodeRegistry& registry_;
+    storage::StorageClient& storage_;
+    ExecutionEventCallback event_callback_;
+};
+
+// Runner service configuration
+struct RunnerServiceConfig {
+    int grpc_port = 9003;              // Runner's own gRPC server port
+    std::string runner_id = "runner-1";
+    std::string webserver_address = "localhost:8080";    // HTTP for registration
+    std::string node_sync_address = "localhost:9002";    // gRPC for node sync
+    std::string credential_service_address = "localhost:9003";  // gRPC for credentials
+    std::string database_address = "localhost:9001";
+    NodeRegistryConfig node_registry_config;
+    WorkflowEngineConfig workflow_engine_config;
+    int heartbeat_interval_sec = 10;
+    int max_concurrent_executions = 10;
+};
+
+// Main runner service
+class RunnerService {
+public:
+    explicit RunnerService(const RunnerServiceConfig& config);
+    ~RunnerService();
+
+    static RunnerServiceConfig loadConfig(const std::filesystem::path& path);
+
+    void start();
+    void stop();
+
+    // Get components
+    WorkflowEngine& engine() { return *engine_; }
+    NodeRegistry& registry() { return *registry_; }
+
+private:
+    void registerWithWebServer();
+    void heartbeatLoop();
+    void unregisterFromWebServer();
+    RunnerMetrics collectMetrics();
+
+    RunnerServiceConfig config_;
+
+    std::unique_ptr<storage::StorageClient> storage_;
+    std::unique_ptr<credentials::CredentialClient> credential_client_;
+    std::unique_ptr<NodeRegistry> registry_;
+    std::unique_ptr<WorkflowEngine> engine_;
+    std::unique_ptr<RunnerServiceImpl> service_impl_;
+    std::unique_ptr<grpc::Server> server_;
+
+    std::thread heartbeat_thread_;
+    std::atomic<bool> running_{false};
+
+    // Shutdown signaling
+    std::mutex shutdown_mutex_;
+    std::condition_variable shutdown_cv_;
+};
+
+// Runner metrics for heartbeat
+struct RunnerMetrics {
+    int active_executions = 0;
+    int max_executions = 10;
+    int64_t memory_used_bytes = 0;
+    int64_t memory_total_bytes = 0;
+    double cpu_percent = 0.0;
+    int64_t total_executions = 0;
+    int64_t failed_executions = 0;
+};
+
+} // namespace smartbotic::runner

+ 1445 - 0
src/runner/workflow_engine.cpp

@@ -0,0 +1,1445 @@
+#include "workflow_engine.hpp"
+#include "common/uuid.hpp"
+#include "common/time_utils.hpp"
+#include "logging/logger.hpp"
+#include <algorithm>
+#include <functional>
+#include <stack>
+#include <unordered_set>
+
+namespace smartbotic::runner {
+
+using namespace common;
+
+std::string nodeStatusToString(NodeStatus status) {
+    switch (status) {
+        case NodeStatus::Pending: return "pending";
+        case NodeStatus::Running: return "running";
+        case NodeStatus::Completed: return "completed";
+        case NodeStatus::Failed: return "failed";
+        case NodeStatus::Skipped: return "skipped";
+        default: return "unknown";
+    }
+}
+
+std::string executionStatusToString(ExecutionStatus status) {
+    switch (status) {
+        case ExecutionStatus::Pending: return "pending";
+        case ExecutionStatus::Running: return "running";
+        case ExecutionStatus::Completed: return "completed";
+        case ExecutionStatus::Failed: return "failed";
+        case ExecutionStatus::Cancelled: return "cancelled";
+        default: return "unknown";
+    }
+}
+
+Workflow Workflow::fromJson(const nlohmann::json& j) {
+    Workflow wf;
+    wf.id = j.value("_id", j.value("id", ""));
+    wf.name = j.value("name", "");
+    wf.active = j.value("active", false);
+    wf.settings = j.value("settings", nlohmann::json::object());
+
+    if (j.contains("nodes")) {
+        for (const auto& n : j["nodes"]) {
+            WorkflowNode node;
+            node.id = n.value("id", "");
+            node.type = n.value("type", "");
+            node.name = n.value("name", "");
+            node.config = n.value("config", nlohmann::json::object());
+            node.position = n.value("position", nlohmann::json::object());
+            node.disabled = n.value("disabled", false);
+            wf.nodes.push_back(node);
+        }
+    }
+
+    if (j.contains("connections")) {
+        for (const auto& c : j["connections"]) {
+            Connection conn;
+            conn.source_node_id = c.value("sourceNodeId", "");
+            conn.source_output = c.value("sourceOutput", "main");
+            conn.target_node_id = c.value("targetNodeId", "");
+            conn.target_input = c.value("targetInput", "data");
+            wf.connections.push_back(conn);
+        }
+    }
+
+    return wf;
+}
+
+nlohmann::json ExecutionResult::toJson() const {
+    nlohmann::json j;
+    // Note: id is managed by database as _id (execution_id is passed to storage_.insert)
+    j["workflowId"] = workflow_id;
+    j["workflowName"] = workflow_name;
+    j["status"] = executionStatusToString(status);
+    j["triggerType"] = trigger_type;
+    j["triggerData"] = trigger_data;
+    j["startedAt"] = started_at;
+    j["finishedAt"] = finished_at;
+    j["error"] = error;
+    j["output"] = final_output;
+
+    j["nodeExecutions"] = nlohmann::json::array();
+    for (const auto& [id, result] : node_results) {
+        nlohmann::json nr;
+        nr["nodeId"] = result.node_id;
+        nr["status"] = nodeStatusToString(result.status);
+        nr["startedAt"] = result.started_at;
+        nr["finishedAt"] = result.finished_at;
+        nr["input"] = result.input;
+        nr["output"] = result.output;
+        nr["error"] = result.error;
+        nr["retryCount"] = result.retry_count;
+        j["nodeExecutions"].push_back(nr);
+    }
+
+    // Include workflow snapshot for pinning feature
+    if (!workflow_snapshot.is_null()) {
+        j["workflowSnapshot"] = workflow_snapshot;
+    }
+
+    return j;
+}
+
+WorkflowEngine::WorkflowEngine(NodeRegistry& registry,
+                               storage::StorageClient& storage,
+                               const WorkflowEngineConfig& config)
+    : config_(config)
+    , registry_(registry)
+    , storage_(storage) {
+
+    script_pool_ = std::make_unique<engine::ScriptEnginePool>(
+        config.script_engine_pool_size, config.script_config);
+
+    ErrorHandlerConfig error_config;
+    error_handler_ = std::make_unique<ErrorHandler>(error_config);
+
+    // Initialize collection permissions
+    collection_permissions_ = std::make_unique<CollectionPermissions>(storage_);
+    collection_permissions_->loadPermissions();
+}
+
+WorkflowEngine::~WorkflowEngine() = default;
+
+Result<ExecutionResult> WorkflowEngine::execute(const Workflow& workflow,
+                                                 const std::string& trigger_type,
+                                                 const nlohmann::json& trigger_data,
+                                                 ExecutionCallback callback) {
+    if (active_count_ >= config_.max_concurrent_executions) {
+        return Error(ErrorCode::ResourceExhausted, "Max concurrent executions reached");
+    }
+
+    // Check for target node (single-node execution mode)
+    std::string target_node_id;
+    nlohmann::json actual_trigger_data = trigger_data;
+    if (trigger_data.contains("_targetNodeId")) {
+        target_node_id = trigger_data["_targetNodeId"].get<std::string>();
+        actual_trigger_data.erase("_targetNodeId");
+        LOG_INFO("Single-node execution mode: targeting node {}", target_node_id);
+    }
+
+    // Check for specific trigger node (multi-trigger selection)
+    std::string specified_trigger_id;
+    if (actual_trigger_data.contains("_triggerNodeId")) {
+        specified_trigger_id = actual_trigger_data["_triggerNodeId"].get<std::string>();
+        actual_trigger_data.erase("_triggerNodeId");
+        LOG_INFO("Using specified trigger node: {}", specified_trigger_id);
+    }
+
+    // Create execution record
+    ExecutionResult result;
+    result.execution_id = UUID::generatePrefixed("exec");
+    result.workflow_id = workflow.id;
+    result.workflow_name = workflow.name;
+    result.status = ExecutionStatus::Running;
+    result.trigger_type = trigger_type;
+    result.trigger_data = actual_trigger_data;
+    result.started_at = TimeUtils::nowMs();
+
+    // Create workflow snapshot for pinning feature
+    nlohmann::json snapshot;
+    snapshot["nodes"] = nlohmann::json::array();
+    for (const auto& node : workflow.nodes) {
+        nlohmann::json n;
+        n["id"] = node.id;
+        n["type"] = node.type;
+        n["name"] = node.name;
+        n["config"] = node.config;
+        n["position"] = node.position;
+        n["disabled"] = node.disabled;
+        snapshot["nodes"].push_back(n);
+    }
+    snapshot["connections"] = nlohmann::json::array();
+    for (const auto& conn : workflow.connections) {
+        nlohmann::json c;
+        c["sourceNodeId"] = conn.source_node_id;
+        c["sourceOutput"] = conn.source_output;
+        c["targetNodeId"] = conn.target_node_id;
+        c["targetInput"] = conn.target_input;
+        snapshot["connections"].push_back(c);
+    }
+    result.workflow_snapshot = snapshot;
+
+    ++active_count_;
+
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        active_executions_[result.execution_id] = result;
+    }
+
+    if (callback) {
+        callback("execution.started", {
+            {"executionId", result.execution_id},
+            {"workflowId", workflow.id}
+        });
+    }
+
+    LOG_INFO("Starting workflow execution: {} for workflow {}",
+             result.execution_id, workflow.id);
+
+    try {
+        // Build execution graph
+        std::unordered_map<std::string, std::vector<std::string>> dependencies;
+        std::unordered_map<std::string, std::vector<std::string>> dependents;
+        buildExecutionGraph(workflow, dependencies, dependents);
+
+        // Topological sort for execution order
+        auto execution_order = topologicalSort(workflow, dependencies);
+
+        // If targeting a specific node, filter execution order to only include
+        // the target node and its upstream dependencies
+        if (!target_node_id.empty()) {
+            std::unordered_set<std::string> nodes_to_execute;
+            std::function<void(const std::string&)> collectUpstream;
+            collectUpstream = [&](const std::string& node_id) {
+                if (nodes_to_execute.contains(node_id)) return;
+                nodes_to_execute.insert(node_id);
+                // Add all dependencies (upstream nodes)
+                if (dependencies.contains(node_id)) {
+                    for (const auto& dep : dependencies.at(node_id)) {
+                        collectUpstream(dep);
+                    }
+                }
+            };
+            collectUpstream(target_node_id);
+
+            // Filter execution_order to only include nodes in nodes_to_execute
+            std::vector<std::string> filtered_order;
+            for (const auto& node_id : execution_order) {
+                if (nodes_to_execute.contains(node_id)) {
+                    filtered_order.push_back(node_id);
+                }
+            }
+            execution_order = filtered_order;
+            LOG_INFO("Filtered execution order to {} nodes for target {}",
+                     execution_order.size(), target_node_id);
+        }
+
+        // Find trigger node - use specified trigger if provided, otherwise first trigger
+        std::string trigger_node_id;
+        if (!specified_trigger_id.empty()) {
+            // Verify the specified trigger exists and is a trigger node
+            for (const auto& node : workflow.nodes) {
+                if (node.id == specified_trigger_id) {
+                    auto node_def = registry_.getNode(node.type);
+                    if (node_def && node_def->is_trigger) {
+                        trigger_node_id = specified_trigger_id;
+                        LOG_INFO("Using specified trigger: {}", trigger_node_id);
+                    } else {
+                        LOG_WARN("Specified node {} is not a trigger, falling back to first trigger", specified_trigger_id);
+                    }
+                    break;
+                }
+            }
+        }
+
+        // Fallback: find first trigger node if not specified or invalid
+        if (trigger_node_id.empty()) {
+            for (const auto& node : workflow.nodes) {
+                auto node_def = registry_.getNode(node.type);
+                if (node_def && node_def->is_trigger) {
+                    trigger_node_id = node.id;
+                    break;
+                }
+            }
+        }
+
+        // Execute nodes in order
+        for (const auto& node_id : execution_order) {
+            // Check for cancellation
+            {
+                std::lock_guard<std::mutex> lock(mutex_);
+                if (cancelled_executions_.contains(result.execution_id)) {
+                    result.status = ExecutionStatus::Cancelled;
+                    result.error = "Execution cancelled";
+                    break;
+                }
+            }
+
+            // Find node
+            const WorkflowNode* node = nullptr;
+            for (const auto& n : workflow.nodes) {
+                if (n.id == node_id) {
+                    node = &n;
+                    break;
+                }
+            }
+
+            if (!node || node->disabled) {
+                continue;
+            }
+
+            // Skip other trigger nodes when a specific trigger is selected
+            auto node_def = registry_.getNode(node->type);
+            if (node_def && node_def->is_trigger && node_id != trigger_node_id) {
+                NodeExecutionResult skip_result;
+                skip_result.node_id = node_id;
+                skip_result.status = NodeStatus::Skipped;
+                skip_result.input = nlohmann::json::object();
+                skip_result.output = nlohmann::json::object();
+                skip_result.started_at = TimeUtils::nowMs();
+                skip_result.finished_at = skip_result.started_at;
+
+                result.node_results[node_id] = skip_result;
+
+                if (callback) {
+                    callback("node.skipped", {
+                        {"executionId", result.execution_id},
+                        {"nodeId", node_id},
+                        {"status", "skipped"},
+                        {"reason", "Not the selected trigger"}
+                    });
+                }
+
+                LOG_DEBUG("Trigger node {} skipped - not the selected trigger", node_id);
+                continue;
+            }
+
+            // Collect input from connected nodes
+            nlohmann::json input;
+            if (node_id == trigger_node_id) {
+                input = actual_trigger_data;
+            } else {
+                input = collectNodeInput(node_id, workflow, result.node_results);
+            }
+
+            // Check if node should be skipped (inactive branch)
+            if (input.contains("_skip") && input["_skip"].get<bool>()) {
+                NodeExecutionResult skip_result;
+                skip_result.node_id = node_id;
+                skip_result.status = NodeStatus::Skipped;
+                skip_result.input = input;
+                skip_result.output = nlohmann::json::object();
+                skip_result.started_at = TimeUtils::nowMs();
+                skip_result.finished_at = skip_result.started_at;
+
+                result.node_results[node_id] = skip_result;
+
+                if (callback) {
+                    callback("node.skipped", {
+                        {"executionId", result.execution_id},
+                        {"nodeId", node_id},
+                        {"status", "skipped"},
+                        {"reason", "Inactive branch"}
+                    });
+                }
+
+                LOG_DEBUG("Node {} skipped due to inactive branch", node_id);
+                continue;
+            }
+
+            // Evaluate expressions in node config
+            WorkflowNode evaluated_node = *node;
+            evaluated_node.config = evaluateExpressions(node->config, input, result.node_results, workflow);
+
+            // Execute node
+            auto node_result = executeNode(evaluated_node, input, result.execution_id, workflow);
+            result.node_results[node_id] = node_result;
+
+            if (callback) {
+                nlohmann::json event_data = {
+                    {"executionId", result.execution_id},
+                    {"nodeId", node_id},
+                    {"status", nodeStatusToString(node_result.status)},
+                    {"output", node_result.output}
+                };
+                if (!node_result.error.empty()) {
+                    event_data["error"] = node_result.error;
+                }
+                callback("node." + nodeStatusToString(node_result.status), event_data);
+            }
+
+            // Check for loop node
+            if (node_result.status == NodeStatus::Completed &&
+                node_result.output.contains("_isLoop") &&
+                node_result.output["_isLoop"].get<bool>()) {
+
+                LOG_INFO("Loop node {} detected, starting iteration", node_id);
+
+                // Build loop context
+                LoopContext loop_ctx;
+                for (const auto& item : node_result.output["_items"]) {
+                    loop_ctx.items.push_back(item);
+                }
+                loop_ctx.output_field = node_result.output.value("_outputField", "results");
+                loop_ctx.item_variable = node_result.output.value("_itemVariable", "item");
+                loop_ctx.index_variable = node_result.output.value("_indexVariable", "index");
+                loop_ctx.continue_on_error = node_result.output.value("_continueOnError", true);
+
+                // Execute loop iterations
+                bool loop_success = executeLoopBody(node_id, workflow, loop_ctx, result, callback);
+
+                // Update loop node result with collected results
+                auto& loop_result = result.node_results[node_id];
+                loop_result.output["_loopCompleted"] = true;
+                loop_result.output[loop_ctx.output_field] = loop_ctx.results;
+                loop_result.output["_activeBranch"] = "done";
+
+                // Create done output
+                nlohmann::json done_data;
+                done_data[loop_ctx.output_field] = loop_ctx.results;
+                done_data["totalProcessed"] = loop_ctx.results.size();
+                done_data["data"] = node_result.output.value("data", nlohmann::json::object());
+                loop_result.output["done"] = done_data;
+
+                if (callback) {
+                    callback("loop.completed", {
+                        {"executionId", result.execution_id},
+                        {"nodeId", node_id},
+                        {"totalItems", loop_ctx.items.size()},
+                        {"processedItems", loop_ctx.results.size()}
+                    });
+                }
+
+                if (!loop_success && !loop_ctx.continue_on_error) {
+                    result.status = ExecutionStatus::Failed;
+                    result.error = "Loop iteration failed";
+                    break;
+                }
+
+                continue;  // Loop handles its own downstream execution
+            }
+
+            // Check for failure
+            if (node_result.status == NodeStatus::Failed) {
+                bool should_continue = workflow.settings.value("continueOnError", false);
+                if (!should_continue) {
+                    result.status = ExecutionStatus::Failed;
+                    result.error = "Node " + node_id + " failed: " + node_result.error;
+                    break;
+                }
+            }
+        }
+
+        // Set final status if not already set
+        if (result.status == ExecutionStatus::Running) {
+            result.status = ExecutionStatus::Completed;
+
+            // Get output from last executed node
+            if (!execution_order.empty()) {
+                auto it = result.node_results.find(execution_order.back());
+                if (it != result.node_results.end()) {
+                    result.final_output = it->second.output;
+                }
+            }
+        }
+    } catch (const std::exception& e) {
+        result.status = ExecutionStatus::Failed;
+        result.error = e.what();
+    }
+
+    result.finished_at = TimeUtils::nowMs();
+
+    // Store execution result
+    storeExecution(result);
+
+    --active_count_;
+
+    {
+        std::lock_guard<std::mutex> lock(mutex_);
+        active_executions_.erase(result.execution_id);
+        cancelled_executions_.erase(result.execution_id);
+    }
+
+    if (callback) {
+        callback("execution." + executionStatusToString(result.status), {
+            {"executionId", result.execution_id},
+            {"status", executionStatusToString(result.status)},
+            {"output", result.final_output}
+        });
+    }
+
+    LOG_INFO("Workflow execution {} completed with status: {}",
+             result.execution_id, executionStatusToString(result.status));
+
+    return result;
+}
+
+void WorkflowEngine::cancelExecution(const std::string& execution_id) {
+    std::lock_guard<std::mutex> lock(mutex_);
+    cancelled_executions_.insert(execution_id);
+    LOG_INFO("Cancellation requested for execution: {}", execution_id);
+}
+
+int WorkflowEngine::getActiveExecutionCount() const {
+    return active_count_.load();
+}
+
+std::optional<ExecutionResult> WorkflowEngine::getExecutionResult(const std::string& execution_id) {
+    std::lock_guard<std::mutex> lock(mutex_);
+    auto it = active_executions_.find(execution_id);
+    if (it != active_executions_.end()) {
+        return it->second;
+    }
+    return std::nullopt;
+}
+
+void WorkflowEngine::buildExecutionGraph(const Workflow& workflow,
+                                         std::unordered_map<std::string, std::vector<std::string>>& dependencies,
+                                         std::unordered_map<std::string, std::vector<std::string>>& dependents) {
+    // Initialize with all nodes
+    for (const auto& node : workflow.nodes) {
+        dependencies[node.id] = {};
+        dependents[node.id] = {};
+    }
+
+    // Identify loop nodes
+    std::unordered_set<std::string> loop_nodes;
+    for (const auto& node : workflow.nodes) {
+        if (node.type == "loop") {
+            loop_nodes.insert(node.id);
+        }
+    }
+
+    // For each loop node, find nodes in the loop body (connected to "loop" output)
+    // These are nodes that should not be treated as dependencies of the loop
+    std::unordered_map<std::string, std::unordered_set<std::string>> loop_body_nodes;
+    for (const auto& loop_id : loop_nodes) {
+        std::unordered_set<std::string> body_nodes;
+        std::queue<std::string> to_process;
+
+        // Find nodes directly connected to loop's "loop" output
+        for (const auto& conn : workflow.connections) {
+            if (conn.source_node_id == loop_id && conn.source_output == "loop") {
+                to_process.push(conn.target_node_id);
+            }
+        }
+
+        // BFS to find all nodes reachable from loop body start
+        while (!to_process.empty()) {
+            std::string node_id = to_process.front();
+            to_process.pop();
+
+            if (body_nodes.contains(node_id) || node_id == loop_id) {
+                continue;
+            }
+            body_nodes.insert(node_id);
+
+            // Find downstream nodes
+            for (const auto& conn : workflow.connections) {
+                if (conn.source_node_id == node_id && conn.target_node_id != loop_id) {
+                    to_process.push(conn.target_node_id);
+                }
+            }
+        }
+        loop_body_nodes[loop_id] = body_nodes;
+    }
+
+    // Build from connections, excluding feedback connections to loop nodes
+    for (const auto& conn : workflow.connections) {
+        // Check if this is a feedback connection to a loop node
+        bool is_feedback = false;
+        if (loop_nodes.contains(conn.target_node_id)) {
+            // If the source is in the loop body, it's a feedback connection
+            const auto& body = loop_body_nodes[conn.target_node_id];
+            if (body.contains(conn.source_node_id)) {
+                is_feedback = true;
+                LOG_DEBUG("Excluding feedback connection {} -> {} (loop body)",
+                         conn.source_node_id, conn.target_node_id);
+            }
+        }
+
+        if (!is_feedback) {
+            dependencies[conn.target_node_id].push_back(conn.source_node_id);
+            dependents[conn.source_node_id].push_back(conn.target_node_id);
+        }
+    }
+}
+
+std::vector<std::string> WorkflowEngine::topologicalSort(
+    const Workflow& workflow,
+    const std::unordered_map<std::string, std::vector<std::string>>& dependencies) {
+
+    std::vector<std::string> result;
+    std::unordered_map<std::string, int> in_degree;
+    std::queue<std::string> queue;
+
+    // Calculate in-degrees
+    for (const auto& node : workflow.nodes) {
+        in_degree[node.id] = dependencies.at(node.id).size();
+        if (in_degree[node.id] == 0) {
+            queue.push(node.id);
+        }
+    }
+
+    // Process queue
+    while (!queue.empty()) {
+        std::string node_id = queue.front();
+        queue.pop();
+        result.push_back(node_id);
+
+        // Reduce in-degree of dependents
+        for (const auto& conn : workflow.connections) {
+            if (conn.source_node_id == node_id) {
+                --in_degree[conn.target_node_id];
+                if (in_degree[conn.target_node_id] == 0) {
+                    queue.push(conn.target_node_id);
+                }
+            }
+        }
+    }
+
+    return result;
+}
+
+NodeExecutionResult WorkflowEngine::executeNode(const WorkflowNode& node,
+                                                const nlohmann::json& input,
+                                                const std::string& execution_id,
+                                                const Workflow& workflow) {
+    NodeExecutionResult result;
+    result.node_id = node.id;
+    result.input = input;
+    result.started_at = TimeUtils::nowMs();
+    result.status = NodeStatus::Running;
+
+    // Get node definition
+    auto node_def = registry_.getNode(node.type);
+    if (!node_def) {
+        result.status = NodeStatus::Failed;
+        result.error = "Node type not found: " + node.type;
+        result.finished_at = TimeUtils::nowMs();
+        return result;
+    }
+
+    // Helper to get per-workflow storage permission for a collection
+    // Returns: "none", "read-only", or "read-write"
+    auto getWorkflowAccess = [&workflow, this](const std::string& collection) -> std::string {
+        // System collections are always protected
+        if (collection_permissions_->isSystemCollection(collection)) {
+            return "none";
+        }
+
+        // Check workflow settings for storage permissions
+        if (workflow.settings.contains("storagePermissions")) {
+            const auto& storage_perms = workflow.settings["storagePermissions"];
+
+            // Check collection-specific permissions
+            if (storage_perms.contains("collections") &&
+                storage_perms["collections"].is_object() &&
+                storage_perms["collections"].contains(collection)) {
+                return storage_perms["collections"][collection].get<std::string>();
+            }
+
+            // Fall back to default access for this workflow
+            if (storage_perms.contains("defaultAccess")) {
+                return storage_perms["defaultAccess"].get<std::string>();
+            }
+        }
+
+        // If no workflow-specific permissions, deny by default
+        return "none";
+    };
+
+    auto canReadCollection = [&getWorkflowAccess](const std::string& collection) -> bool {
+        std::string access = getWorkflowAccess(collection);
+        return access == "read-only" || access == "read-write";
+    };
+
+    auto canWriteCollection = [&getWorkflowAccess](const std::string& collection) -> bool {
+        std::string access = getWorkflowAccess(collection);
+        return access == "read-write";
+    };
+
+    // Prepare script context
+    engine::ScriptContext ctx;
+    ctx.execution_id = execution_id;
+    ctx.node_id = node.id;
+    ctx.input = input;
+    ctx.config = node.config;
+    ctx.log_handler = [&node](const std::string& level, const std::string& msg) {
+        if (level == "error") {
+            LOG_ERROR("[Node {}] {}", node.id, msg);
+        } else if (level == "warn") {
+            LOG_WARN("[Node {}] {}", node.id, msg);
+        } else if (level == "debug") {
+            LOG_DEBUG("[Node {}] {}", node.id, msg);
+        } else {
+            LOG_INFO("[Node {}] {}", node.id, msg);
+        }
+    };
+
+    // Storage API callbacks with per-workflow permission checks
+    ctx.storage_get_doc = [this, canReadCollection](const std::string& collection, const std::string& id)
+        -> common::Result<nlohmann::json> {
+        if (!canReadCollection(collection)) {
+            return common::Error(common::ErrorCode::PermissionDenied,
+                "No read access to collection: " + collection);
+        }
+        return storage_.get(collection, id);
+    };
+
+    ctx.storage_insert = [this, canWriteCollection](const std::string& collection, const nlohmann::json& data,
+                                const std::string& id, int64_t ttl_ms)
+        -> common::Result<std::string> {
+        if (!canWriteCollection(collection)) {
+            return common::Error(common::ErrorCode::PermissionDenied,
+                "No write access to collection: " + collection);
+        }
+        return storage_.insert(collection, data, id, ttl_ms);
+    };
+
+    ctx.storage_update = [this, canWriteCollection](const std::string& collection, const std::string& id,
+                                const nlohmann::json& data, int64_t expected_version, bool partial)
+        -> common::Result<int64_t> {
+        if (!canWriteCollection(collection)) {
+            return common::Error(common::ErrorCode::PermissionDenied,
+                "No write access to collection: " + collection);
+        }
+        return storage_.update(collection, id, data, expected_version, partial);
+    };
+
+    ctx.storage_delete = [this, canWriteCollection](const std::string& collection, const std::string& id,
+                                int64_t expected_version)
+        -> common::Result<void> {
+        if (!canWriteCollection(collection)) {
+            return common::Error(common::ErrorCode::PermissionDenied,
+                "No write access to collection: " + collection);
+        }
+        return storage_.remove(collection, id, expected_version);
+    };
+
+    ctx.storage_query = [this, canReadCollection](const std::string& collection, const engine::StorageQueryOptions& options)
+        -> common::Result<engine::StorageQueryResult> {
+        if (!canReadCollection(collection)) {
+            return common::Error(common::ErrorCode::PermissionDenied,
+                "No read access to collection: " + collection);
+        }
+
+        // Convert engine query options to storage query options
+        storage::QueryOptions storage_opts;
+        storage_opts.filters = options.filters;
+        storage_opts.sorts = options.sorts;
+        storage_opts.fields = options.fields;
+        storage_opts.page = options.page;
+        storage_opts.page_size = options.page_size;
+
+        auto result = storage_.query(collection, storage_opts);
+        if (result.failed()) {
+            return common::Error(result.error().code(), result.error().message());
+        }
+
+        engine::StorageQueryResult engine_result;
+        engine_result.documents = result.value().documents;
+        engine_result.total_count = result.value().total_count;
+        engine_result.has_more = result.value().has_more;
+        return engine_result;
+    };
+
+    ctx.storage_list_collections = [this, canReadCollection](bool include_system)
+        -> common::Result<std::vector<std::string>> {
+        auto all_collections = storage_.listCollections();
+        std::vector<std::string> accessible;
+
+        for (const auto& coll : all_collections) {
+            // Skip system collections unless explicitly requested
+            if (!include_system && collection_permissions_->isSystemCollection(coll)) {
+                continue;
+            }
+            // Only include collections the workflow has at least read access to
+            if (canReadCollection(coll)) {
+                accessible.push_back(coll);
+            }
+        }
+
+        return accessible;
+    };
+
+    // Credential API callback
+    ctx.credentials_get = [this, &workflow](const std::string& credential_id)
+        -> common::Result<engine::CredentialAuth> {
+        if (!credential_auth_callback_) {
+            return common::Error(common::ErrorCode::Unavailable,
+                "Credentials API not configured");
+        }
+        return credential_auth_callback_(credential_id, workflow.id);
+    };
+
+    // IMAP Credential API callback
+    ctx.credentials_get_imap = [this, &workflow](const std::string& credential_id)
+        -> common::Result<engine::ImapCredential> {
+        if (!imap_credential_callback_) {
+            return common::Error(common::ErrorCode::Unavailable,
+                "IMAP Credentials API not configured");
+        }
+        return imap_credential_callback_(credential_id, workflow.id);
+    };
+
+    // Execute with retry logic
+    int max_retries = 0;
+    if (node.config.contains("retries")) {
+        max_retries = node.config["retries"].get<int>();
+    }
+
+    for (int attempt = 0; attempt <= max_retries; ++attempt) {
+        result.retry_count = attempt;
+
+        auto* script_engine = script_pool_->acquire();
+        auto script_result = script_engine->execute(node_def->code, ctx);
+        script_pool_->release(script_engine);
+
+        if (script_result.success) {
+            result.status = NodeStatus::Completed;
+            result.output = script_result.output;
+            LOG_DEBUG("Node {} completed successfully", node.id);
+            break;
+        } else {
+            result.error = script_result.error;
+            LOG_ERROR("Node {} execution failed: {}", node.id, script_result.error);
+            if (attempt == max_retries) {
+                result.status = NodeStatus::Failed;
+            } else {
+                LOG_WARN("Node {} attempt {} failed, retrying: {}",
+                        node.id, attempt + 1, result.error);
+                std::this_thread::sleep_for(std::chrono::milliseconds(1000 * (attempt + 1)));
+            }
+        }
+    }
+
+    result.finished_at = TimeUtils::nowMs();
+    return result;
+}
+
+nlohmann::json WorkflowEngine::collectNodeInput(
+    const std::string& node_id,
+    const Workflow& workflow,
+    const std::unordered_map<std::string, NodeExecutionResult>& results) {
+
+    nlohmann::json input;
+    bool has_any_active_input = false;
+    bool all_inputs_from_branches = true;
+
+    for (const auto& conn : workflow.connections) {
+        if (conn.target_node_id == node_id) {
+            auto it = results.find(conn.source_node_id);
+            if (it != results.end()) {
+                // Check if source node was skipped - propagate skip
+                if (it->second.status == NodeStatus::Skipped) {
+                    continue;  // Skip input from skipped nodes
+                }
+
+                if (it->second.status == NodeStatus::Completed) {
+                    const auto& output = it->second.output;
+
+                    // Check for conditional branching (_activeBranch marker)
+                    if (output.contains("_activeBranch")) {
+                        std::string active_branch = output["_activeBranch"].get<std::string>();
+
+                        // If connection is to a specific output (e.g., "true" or "false")
+                        // check if it matches the active branch
+                        if (!conn.source_output.empty() && conn.source_output != "main") {
+                            if (conn.source_output != active_branch) {
+                                // This branch is NOT active - mark for skip
+                                LOG_DEBUG("Node {} skipped: connected to inactive branch '{}' (active: '{}')",
+                                         node_id, conn.source_output, active_branch);
+                                continue;  // Skip this connection - wrong branch
+                            }
+                            // Active branch - use the data
+                            if (output.contains(conn.source_output)) {
+                                input[conn.target_input] = output[conn.source_output];
+                            } else {
+                                input[conn.target_input] = output.value("data", output);
+                            }
+                            has_any_active_input = true;
+                        } else {
+                            // "main" output - pass through regardless of branch
+                            input[conn.target_input] = output;
+                            has_any_active_input = true;
+                            all_inputs_from_branches = false;
+                        }
+                    } else {
+                        // No branching - standard input collection
+                        all_inputs_from_branches = false;
+                        if (!conn.source_output.empty() && conn.source_output != "main") {
+                            if (output.contains(conn.source_output)) {
+                                input[conn.target_input] = output[conn.source_output];
+                            }
+                        } else {
+                            input[conn.target_input] = output;
+                        }
+                        has_any_active_input = true;
+                    }
+                }
+            }
+        }
+    }
+
+    // If this node only receives input from branching nodes and none are active,
+    // mark it for skipping
+    if (all_inputs_from_branches && !has_any_active_input) {
+        // Check if there were any connections at all
+        bool has_connections = false;
+        for (const auto& conn : workflow.connections) {
+            if (conn.target_node_id == node_id) {
+                has_connections = true;
+                break;
+            }
+        }
+        if (has_connections) {
+            input["_skip"] = true;
+        }
+    }
+
+    return input;
+}
+
+void WorkflowEngine::storeExecution(const ExecutionResult& result) {
+    storage_.insert("executions", result.toJson(), result.execution_id,
+                   7 * 24 * 60 * 60 * 1000);  // 7-day TTL
+}
+
+std::vector<std::string> WorkflowEngine::findLoopBodyNodes(
+    const std::string& loop_node_id,
+    const Workflow& workflow) {
+
+    std::vector<std::string> body_nodes;
+
+    // Find nodes connected to the "loop" output
+    for (const auto& conn : workflow.connections) {
+        if (conn.source_node_id == loop_node_id && conn.source_output == "loop") {
+            body_nodes.push_back(conn.target_node_id);
+        }
+    }
+
+    return body_nodes;
+}
+
+std::vector<std::string> WorkflowEngine::findDoneNodes(
+    const std::string& loop_node_id,
+    const Workflow& workflow) {
+
+    std::vector<std::string> done_nodes;
+
+    // Find nodes connected to the "done" output
+    for (const auto& conn : workflow.connections) {
+        if (conn.source_node_id == loop_node_id && conn.source_output == "done") {
+            done_nodes.push_back(conn.target_node_id);
+        }
+    }
+
+    return done_nodes;
+}
+
+bool WorkflowEngine::executeLoopBody(
+    const std::string& loop_node_id,
+    const Workflow& workflow,
+    const LoopContext& loop_ctx,
+    ExecutionResult& result,
+    ExecutionCallback callback) {
+
+    // Find nodes connected to "loop" output
+    auto body_start_nodes = findLoopBodyNodes(loop_node_id, workflow);
+
+    if (body_start_nodes.empty()) {
+        LOG_WARN("Loop node {} has no body nodes connected", loop_node_id);
+        return true;
+    }
+
+    // Build subgraph of nodes reachable from loop body
+    std::unordered_set<std::string> body_nodes_set;
+    std::queue<std::string> to_process;
+
+    for (const auto& start : body_start_nodes) {
+        to_process.push(start);
+    }
+
+    // Find all nodes in the loop body (nodes reachable before done output)
+    auto done_nodes = findDoneNodes(loop_node_id, workflow);
+    std::unordered_set<std::string> done_nodes_set(done_nodes.begin(), done_nodes.end());
+
+    while (!to_process.empty()) {
+        std::string node_id = to_process.front();
+        to_process.pop();
+
+        // Skip if already processed, is a done node, or is the loop node itself (loop-back connection)
+        if (body_nodes_set.contains(node_id) || done_nodes_set.contains(node_id) ||
+            node_id == loop_node_id) {
+            continue;
+        }
+
+        body_nodes_set.insert(node_id);
+
+        // Find downstream nodes
+        for (const auto& conn : workflow.connections) {
+            if (conn.source_node_id == node_id) {
+                to_process.push(conn.target_node_id);
+            }
+        }
+    }
+
+    // Convert to vector and sort topologically
+    std::vector<std::string> body_node_ids(body_nodes_set.begin(), body_nodes_set.end());
+
+    // Simple topological sort for body nodes
+    std::unordered_map<std::string, int> in_degree;
+    for (const auto& nid : body_node_ids) {
+        in_degree[nid] = 0;
+    }
+    for (const auto& conn : workflow.connections) {
+        if (body_nodes_set.contains(conn.source_node_id) &&
+            body_nodes_set.contains(conn.target_node_id)) {
+            in_degree[conn.target_node_id]++;
+        }
+    }
+
+    std::vector<std::string> sorted_body;
+    std::queue<std::string> ready;
+    for (const auto& nid : body_node_ids) {
+        if (in_degree[nid] == 0) {
+            ready.push(nid);
+        }
+    }
+    while (!ready.empty()) {
+        std::string nid = ready.front();
+        ready.pop();
+        sorted_body.push_back(nid);
+        for (const auto& conn : workflow.connections) {
+            if (conn.source_node_id == nid && body_nodes_set.contains(conn.target_node_id)) {
+                if (--in_degree[conn.target_node_id] == 0) {
+                    ready.push(conn.target_node_id);
+                }
+            }
+        }
+    }
+
+    // Mutable copy of loop context for iteration tracking
+    LoopContext ctx = loop_ctx;
+    bool all_succeeded = true;
+
+    // Log the body nodes for debugging
+    LOG_INFO("Loop body contains {} nodes: ", sorted_body.size());
+    for (const auto& nid : sorted_body) {
+        LOG_INFO("  - Body node: {}", nid);
+    }
+
+    // Execute body for each item
+    for (size_t i = 0; i < ctx.items.size(); ++i) {
+        ctx.current_index = i;
+
+        if (callback) {
+            callback("loop.iteration.start", {
+                {"executionId", result.execution_id},
+                {"nodeId", loop_node_id},
+                {"index", i},
+                {"totalItems", ctx.items.size()},
+                {"item", ctx.items[i]}
+            });
+        }
+
+        LOG_INFO("Loop iteration {}/{}", i + 1, ctx.items.size());
+
+        // Create iteration-specific results storage
+        std::unordered_map<std::string, NodeExecutionResult> iteration_results;
+
+        // Create input for first body node with current item
+        nlohmann::json loop_input;
+        loop_input[ctx.item_variable] = ctx.items[i];
+        loop_input[ctx.index_variable] = i;
+        loop_input["totalItems"] = ctx.items.size();
+        loop_input["isFirst"] = (i == 0);
+        loop_input["isLast"] = (i == ctx.items.size() - 1);
+        loop_input["data"] = ctx.items[i];  // Also provide as data for compatibility
+        // Add currentItem/currentIndex to match loop.js node output structure
+        loop_input["currentItem"] = ctx.items[i];
+        loop_input["currentIndex"] = i;
+
+        bool iteration_failed = false;
+
+        // Execute body nodes in order
+        for (const auto& body_node_id : sorted_body) {
+            // Find the node
+            const WorkflowNode* body_node = nullptr;
+            for (const auto& n : workflow.nodes) {
+                if (n.id == body_node_id) {
+                    body_node = &n;
+                    break;
+                }
+            }
+
+            if (!body_node || body_node->disabled) continue;
+
+            // Collect input - from iteration results or loop input
+            nlohmann::json node_input;
+            bool has_loop_input = false;
+            bool has_active_upstream = false;
+
+            for (const auto& conn : workflow.connections) {
+                if (conn.target_node_id == body_node_id) {
+                    LOG_INFO("  Connection to {}: source={}, source_output={}, target_input={}",
+                             body_node_id, conn.source_node_id, conn.source_output, conn.target_input);
+                    if (conn.source_node_id == loop_node_id) {
+                        // Input from loop node - pass directly (loop_input already has data field)
+                        LOG_INFO("  -> Using loop_input (has currentItem: {})",
+                                 loop_input.contains("currentItem") ? "yes" : "no");
+                        node_input = loop_input;
+                        has_loop_input = true;
+                        has_active_upstream = true;
+                    } else {
+                        // Input from previous body node
+                        auto it = iteration_results.find(conn.source_node_id);
+                        if (it != iteration_results.end() &&
+                            it->second.status == NodeStatus::Completed) {
+
+                            // Check for branch filtering (IF condition handling)
+                            const auto& source_output = it->second.output;
+                            if (source_output.contains("_activeBranch")) {
+                                std::string active_branch = source_output["_activeBranch"].get<std::string>();
+                                // Only use this input if connection matches active branch
+                                // or if source_output is "main" (default)
+                                if (conn.source_output != active_branch && conn.source_output != "main") {
+                                    // This connection is from an inactive branch - skip this input
+                                    LOG_DEBUG("Skipping input from {} via inactive branch {} (active: {})",
+                                             conn.source_node_id, conn.source_output, active_branch);
+                                    continue;
+                                }
+                            }
+
+                            node_input[conn.target_input] = source_output;
+                            has_active_upstream = true;
+                        }
+                    }
+                }
+            }
+
+            // Skip this node if it only has upstream from inactive branches
+            if (!has_loop_input && !has_active_upstream && !node_input.empty()) {
+                LOG_DEBUG("Skipping node {} - no active upstream connections", body_node_id);
+                continue;
+            }
+
+            // Also skip if node has no input at all and requires upstream
+            if (!has_loop_input && node_input.empty()) {
+                // Check if this node has any connections - if yes, it needs input
+                bool needs_upstream = false;
+                for (const auto& conn : workflow.connections) {
+                    if (conn.target_node_id == body_node_id && conn.source_node_id != loop_node_id) {
+                        auto it = iteration_results.find(conn.source_node_id);
+                        if (it != iteration_results.end()) {
+                            needs_upstream = true;
+                            break;
+                        }
+                    }
+                }
+                if (needs_upstream) {
+                    LOG_DEBUG("Skipping node {} - needs upstream input but none available", body_node_id);
+                    continue;
+                }
+                node_input = loop_input;
+            }
+
+            // Execute the body node
+            LOG_INFO("Executing body node {} with input: {}", body_node_id, node_input.dump());
+
+            // Send running event before execution
+            if (callback) {
+                callback("loop.node.running", {
+                    {"executionId", result.execution_id},
+                    {"nodeId", body_node_id},
+                    {"iteration", i}
+                });
+            }
+
+            auto body_result = executeNode(*body_node, node_input, result.execution_id, workflow);
+            iteration_results[body_node_id] = body_result;
+
+            // Store in main results with iteration suffix
+            std::string result_key = body_node_id + "_iter_" + std::to_string(i);
+            result.node_results[result_key] = body_result;
+
+            if (callback) {
+                nlohmann::json event_data = {
+                    {"executionId", result.execution_id},
+                    {"nodeId", body_node_id},
+                    {"iteration", i},
+                    {"status", nodeStatusToString(body_result.status)},
+                    {"output", body_result.output}
+                };
+                if (!body_result.error.empty()) {
+                    event_data["error"] = body_result.error;
+                }
+                callback("loop.node." + nodeStatusToString(body_result.status), event_data);
+            }
+
+            if (body_result.status == NodeStatus::Failed) {
+                iteration_failed = true;
+                all_succeeded = false;
+                if (!ctx.continue_on_error) {
+                    break;
+                }
+            }
+        }
+
+        // Collect result from last body node
+        if (!sorted_body.empty()) {
+            auto last_it = iteration_results.find(sorted_body.back());
+            if (last_it != iteration_results.end()) {
+                ctx.results.push_back(last_it->second.output);
+            } else {
+                ctx.results.push_back(nullptr);
+            }
+        } else {
+            ctx.results.push_back(ctx.items[i]);
+        }
+
+        if (callback) {
+            callback("loop.iteration.end", {
+                {"executionId", result.execution_id},
+                {"nodeId", loop_node_id},
+                {"index", i},
+                {"success", !iteration_failed}
+            });
+        }
+
+        if (iteration_failed && !ctx.continue_on_error) {
+            break;
+        }
+    }
+
+    // Copy results back (ctx was passed by const ref, but we used a mutable copy)
+    const_cast<LoopContext&>(loop_ctx).results = ctx.results;
+
+    return all_succeeded;
+}
+
+nlohmann::json WorkflowEngine::getValueByPath(const nlohmann::json& obj, const std::string& path) {
+    if (path.empty() || !obj.is_object()) {
+        return obj;
+    }
+
+    std::vector<std::string> parts;
+    std::string current;
+    bool in_bracket = false;
+
+    for (char c : path) {
+        if (c == '[') {
+            if (!current.empty()) {
+                parts.push_back(current);
+                current.clear();
+            }
+            in_bracket = true;
+        } else if (c == ']') {
+            if (!current.empty()) {
+                parts.push_back(current);
+                current.clear();
+            }
+            in_bracket = false;
+        } else if (c == '.' && !in_bracket) {
+            if (!current.empty()) {
+                parts.push_back(current);
+                current.clear();
+            }
+        } else if (c != '"' && c != '\'') {
+            current += c;
+        }
+    }
+    if (!current.empty()) {
+        parts.push_back(current);
+    }
+
+    nlohmann::json value = obj;
+    for (const auto& part : parts) {
+        if (value.is_null()) {
+            return nullptr;
+        }
+
+        // Handle special "length" property for arrays
+        if (part == "length" && value.is_array()) {
+            value = static_cast<int64_t>(value.size());
+            continue;
+        }
+
+        // Try as array index
+        try {
+            size_t idx = std::stoull(part);
+            if (value.is_array() && idx < value.size()) {
+                value = value[idx];
+                continue;
+            }
+        } catch (...) {}
+
+        // Try as object key
+        if (value.is_object() && value.contains(part)) {
+            value = value[part];
+        } else {
+            return nullptr;
+        }
+    }
+
+    return value;
+}
+
+std::string WorkflowEngine::resolveExpression(
+    const std::string& expr,
+    const nlohmann::json& input,
+    const std::unordered_map<std::string, NodeExecutionResult>& results,
+    const Workflow& workflow) {
+
+    std::string trimmed = expr;
+    // Trim whitespace
+    size_t start = trimmed.find_first_not_of(" \t\n\r");
+    size_t end = trimmed.find_last_not_of(" \t\n\r");
+    if (start != std::string::npos && end != std::string::npos) {
+        trimmed = trimmed.substr(start, end - start + 1);
+    }
+
+    // Use JavaScript evaluation for all expressions
+    nlohmann::json resolved_value = evaluateJavaScriptExpression(trimmed, input, results, workflow);
+
+    // Convert to string
+    if (resolved_value.is_string()) {
+        return resolved_value.get<std::string>();
+    } else if (resolved_value.is_number_integer()) {
+        return std::to_string(resolved_value.get<int64_t>());
+    } else if (resolved_value.is_number_float()) {
+        return std::to_string(resolved_value.get<double>());
+    } else if (resolved_value.is_boolean()) {
+        return resolved_value.get<bool>() ? "true" : "false";
+    } else if (resolved_value.is_null()) {
+        return "";
+    } else {
+        return resolved_value.dump();
+    }
+}
+
+nlohmann::json WorkflowEngine::evaluateExpressions(
+    const nlohmann::json& config,
+    const nlohmann::json& input,
+    const std::unordered_map<std::string, NodeExecutionResult>& results,
+    const Workflow& workflow) {
+
+    if (config.is_string()) {
+        std::string str = config.get<std::string>();
+        std::string result;
+        size_t pos = 0;
+
+        while (pos < str.length()) {
+            size_t expr_start = str.find("{{", pos);
+            if (expr_start == std::string::npos) {
+                result += str.substr(pos);
+                break;
+            }
+
+            result += str.substr(pos, expr_start - pos);
+
+            size_t expr_end = str.find("}}", expr_start);
+            if (expr_end == std::string::npos) {
+                result += str.substr(expr_start);
+                break;
+            }
+
+            std::string expression = str.substr(expr_start + 2, expr_end - expr_start - 2);
+            result += resolveExpression(expression, input, results, workflow);
+            pos = expr_end + 2;
+        }
+
+        // If the entire string was a single expression, try to preserve type
+        if (str.find("{{") == 0 && str.rfind("}}") == str.length() - 2 &&
+            str.find("{{", 2) == std::string::npos) {
+            std::string expression = str.substr(2, str.length() - 4);
+            // Re-resolve to get original type
+            std::string trimmed = expression;
+            size_t start = trimmed.find_first_not_of(" \t\n\r");
+            size_t end = trimmed.find_last_not_of(" \t\n\r");
+            if (start != std::string::npos && end != std::string::npos) {
+                trimmed = trimmed.substr(start, end - start + 1);
+            }
+
+            // Try to get the actual JSON value
+            if (trimmed.rfind("data.", 0) == 0) {
+                std::string path = trimmed.substr(5);
+                nlohmann::json data = input.contains("data") ? input["data"] : input;
+                nlohmann::json value = getValueByPath(data, path);
+                if (!value.is_null()) {
+                    return value;
+                }
+            }
+        }
+
+        return result;
+    } else if (config.is_object()) {
+        nlohmann::json result = nlohmann::json::object();
+        for (auto& [key, value] : config.items()) {
+            result[key] = evaluateExpressions(value, input, results, workflow);
+        }
+        return result;
+    } else if (config.is_array()) {
+        nlohmann::json result = nlohmann::json::array();
+        for (const auto& item : config) {
+            result.push_back(evaluateExpressions(item, input, results, workflow));
+        }
+        return result;
+    }
+
+    return config;
+}
+
+nlohmann::json WorkflowEngine::evaluateJavaScriptExpression(
+    const std::string& expression,
+    const nlohmann::json& input,
+    const std::unordered_map<std::string, NodeExecutionResult>& results,
+    const Workflow& workflow) {
+
+    // Build $node object with all node outputs indexed by name
+    nlohmann::json node_outputs = nlohmann::json::object();
+    for (const auto& node : workflow.nodes) {
+        auto it = results.find(node.id);
+        if (it != results.end() && it->second.status == NodeStatus::Completed) {
+            node_outputs[node.name] = it->second.output;
+        }
+    }
+
+    // Build JavaScript code to evaluate the expression
+    // Wrap in module.exports.execute format expected by ScriptEngine
+    std::string js_code = R"(
+        const $node = )" + node_outputs.dump() + R"(;
+
+        async function execute(config, input, context) {
+            const $trigger = input;
+            const data = input.data || input;
+            return )" + expression + R"(;
+        }
+
+        module.exports = { execute };
+    )";
+
+    // Use script engine to evaluate
+    engine::ScriptContext ctx;
+    ctx.execution_id = "expr_eval";
+    ctx.node_id = "expression";
+    ctx.workflow_id = workflow.id;
+    ctx.input = input;
+    ctx.config = nlohmann::json::object();
+
+    auto* script_engine = script_pool_->acquire();
+    auto result = script_engine->execute(js_code, ctx);
+    script_pool_->release(script_engine);
+
+    if (result.success) {
+        return result.output;
+    } else {
+        LOG_WARN("JavaScript expression evaluation failed: {} - Error: {}", expression, result.error);
+        return nullptr;
+    }
+}
+
+} // namespace smartbotic::runner

+ 229 - 0
src/runner/workflow_engine.hpp

@@ -0,0 +1,229 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+#include <queue>
+#include <mutex>
+#include <condition_variable>
+#include <thread>
+#include <atomic>
+#include <nlohmann/json.hpp>
+#include "node_registry.hpp"
+#include "error_handler.hpp"
+#include "collection_permissions.hpp"
+#include "engine/script_engine.hpp"
+#include "storage/storage_client.hpp"
+#include "common/error.hpp"
+
+namespace smartbotic::runner {
+
+// Node execution status
+enum class NodeStatus {
+    Pending,
+    Running,
+    Completed,
+    Failed,
+    Skipped
+};
+
+std::string nodeStatusToString(NodeStatus status);
+
+// Workflow node instance
+struct WorkflowNode {
+    std::string id;
+    std::string type;
+    std::string name;
+    nlohmann::json config;
+    nlohmann::json position;
+    bool disabled = false;
+};
+
+// Connection between nodes
+struct Connection {
+    std::string source_node_id;
+    std::string source_output;
+    std::string target_node_id;
+    std::string target_input;
+};
+
+// Workflow definition
+struct Workflow {
+    std::string id;
+    std::string name;
+    bool active = false;
+    std::vector<WorkflowNode> nodes;
+    std::vector<Connection> connections;
+    nlohmann::json settings;
+
+    static Workflow fromJson(const nlohmann::json& j);
+};
+
+// Node execution result
+struct NodeExecutionResult {
+    std::string node_id;
+    NodeStatus status = NodeStatus::Pending;
+    nlohmann::json input;
+    nlohmann::json output;
+    std::string error;
+    int64_t started_at = 0;
+    int64_t finished_at = 0;
+    int retry_count = 0;
+};
+
+// Execution status
+enum class ExecutionStatus {
+    Pending,
+    Running,
+    Completed,
+    Failed,
+    Cancelled
+};
+
+std::string executionStatusToString(ExecutionStatus status);
+
+// Execution result
+struct ExecutionResult {
+    std::string execution_id;
+    std::string workflow_id;
+    std::string workflow_name;
+    ExecutionStatus status = ExecutionStatus::Pending;
+    std::string trigger_type;
+    nlohmann::json trigger_data;
+    int64_t started_at = 0;
+    int64_t finished_at = 0;
+    std::unordered_map<std::string, NodeExecutionResult> node_results;
+    std::string error;
+    nlohmann::json final_output;
+    nlohmann::json workflow_snapshot;  // Snapshot of workflow at execution time
+
+    nlohmann::json toJson() const;
+};
+
+// Execution callback for progress updates
+using ExecutionCallback = std::function<void(const std::string& event,
+                                             const nlohmann::json& data)>;
+
+// Workflow engine configuration
+struct WorkflowEngineConfig {
+    int64_t default_timeout_ms = 60000;
+    int max_concurrent_executions = 10;
+    size_t script_engine_pool_size = 4;
+    engine::ScriptEngineConfig script_config;
+};
+
+// Credential auth callback type
+using CredentialAuthCallback = std::function<common::Result<engine::CredentialAuth>(
+    const std::string& credential_id, const std::string& workflow_id)>;
+
+// IMAP credential callback type
+using ImapCredentialCallback = std::function<common::Result<engine::ImapCredential>(
+    const std::string& credential_id, const std::string& workflow_id)>;
+
+// Workflow execution engine
+class WorkflowEngine {
+public:
+    WorkflowEngine(NodeRegistry& registry,
+                   storage::StorageClient& storage,
+                   const WorkflowEngineConfig& config = {});
+    ~WorkflowEngine();
+
+    // Execute workflow
+    common::Result<ExecutionResult> execute(const Workflow& workflow,
+                                            const std::string& trigger_type,
+                                            const nlohmann::json& trigger_data,
+                                            ExecutionCallback callback = nullptr);
+
+    // Cancel execution
+    void cancelExecution(const std::string& execution_id);
+
+    // Get active execution count
+    int getActiveExecutionCount() const;
+
+    // Get execution result
+    std::optional<ExecutionResult> getExecutionResult(const std::string& execution_id);
+
+    // Set credential auth callback (called by runner service to provide credential access)
+    void setCredentialAuthCallback(CredentialAuthCallback callback) { credential_auth_callback_ = callback; }
+
+    // Set IMAP credential callback (called by runner service to provide IMAP credential access)
+    void setImapCredentialCallback(ImapCredentialCallback callback) { imap_credential_callback_ = callback; }
+
+private:
+    void buildExecutionGraph(const Workflow& workflow,
+                            std::unordered_map<std::string, std::vector<std::string>>& dependencies,
+                            std::unordered_map<std::string, std::vector<std::string>>& dependents);
+
+    std::vector<std::string> topologicalSort(const Workflow& workflow,
+                                             const std::unordered_map<std::string, std::vector<std::string>>& dependencies);
+
+    NodeExecutionResult executeNode(const WorkflowNode& node,
+                                   const nlohmann::json& input,
+                                   const std::string& execution_id,
+                                   const Workflow& workflow);
+
+    nlohmann::json collectNodeInput(const std::string& node_id,
+                                    const Workflow& workflow,
+                                    const std::unordered_map<std::string, NodeExecutionResult>& results);
+
+    void storeExecution(const ExecutionResult& result);
+
+    // Loop execution support
+    struct LoopContext {
+        std::vector<nlohmann::json> items;
+        std::string output_field;
+        std::string item_variable;
+        std::string index_variable;
+        bool continue_on_error;
+        std::vector<nlohmann::json> results;
+        size_t current_index = 0;
+    };
+
+    bool executeLoopBody(const std::string& loop_node_id,
+                        const Workflow& workflow,
+                        const LoopContext& loop_ctx,
+                        ExecutionResult& result,
+                        ExecutionCallback callback);
+
+    std::vector<std::string> findLoopBodyNodes(const std::string& loop_node_id,
+                                               const Workflow& workflow);
+
+    std::vector<std::string> findDoneNodes(const std::string& loop_node_id,
+                                           const Workflow& workflow);
+
+    // Expression evaluation support
+    nlohmann::json evaluateExpressions(const nlohmann::json& config,
+                                       const nlohmann::json& input,
+                                       const std::unordered_map<std::string, NodeExecutionResult>& results,
+                                       const Workflow& workflow);
+
+    std::string resolveExpression(const std::string& expr,
+                                  const nlohmann::json& input,
+                                  const std::unordered_map<std::string, NodeExecutionResult>& results,
+                                  const Workflow& workflow);
+
+    nlohmann::json getValueByPath(const nlohmann::json& obj, const std::string& path);
+
+    // JavaScript expression evaluation using QuickJS
+    nlohmann::json evaluateJavaScriptExpression(
+        const std::string& expression,
+        const nlohmann::json& input,
+        const std::unordered_map<std::string, NodeExecutionResult>& results,
+        const Workflow& workflow);
+
+    WorkflowEngineConfig config_;
+    NodeRegistry& registry_;
+    storage::StorageClient& storage_;
+    std::unique_ptr<engine::ScriptEnginePool> script_pool_;
+    std::unique_ptr<ErrorHandler> error_handler_;
+    std::unique_ptr<CollectionPermissions> collection_permissions_;
+    CredentialAuthCallback credential_auth_callback_;
+    ImapCredentialCallback imap_credential_callback_;
+
+    std::unordered_map<std::string, ExecutionResult> active_executions_;
+    std::unordered_set<std::string> cancelled_executions_;
+    mutable std::mutex mutex_;
+    std::atomic<int> active_count_{0};
+};
+
+} // namespace smartbotic::runner

+ 237 - 0
src/tools/migrate_nodes.cpp

@@ -0,0 +1,237 @@
+/**
+ * Node Migration Tool
+ *
+ * Migrates existing JavaScript node files from the filesystem to the
+ * central database via the webserver API.
+ *
+ * Usage: migrate_nodes [options]
+ *   -d, --dir <path>       Directory containing .js node files (default: ./nodes)
+ *   -w, --webserver <url>  Webserver address (default: http://localhost:8080)
+ *   -t, --token <token>    API token for authentication (required)
+ *   -n, --dry-run          Show what would be migrated without making changes
+ *   -h, --help             Show this help message
+ */
+
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <filesystem>
+#include <vector>
+#include <string>
+#include <cstring>
+#include <curl/curl.h>
+#include <nlohmann/json.hpp>
+
+namespace fs = std::filesystem;
+
+struct Options {
+    std::string nodes_dir = "./nodes";
+    std::string webserver_url = "http://localhost:8080";
+    std::string api_token;
+    bool dry_run = false;
+    bool help = false;
+};
+
+void printUsage(const char* program) {
+    std::cout << "Node Migration Tool\n\n"
+              << "Migrates JavaScript node files from filesystem to database.\n\n"
+              << "Usage: " << program << " [options]\n\n"
+              << "Options:\n"
+              << "  -d, --dir <path>       Directory containing .js node files (default: ./nodes)\n"
+              << "  -w, --webserver <url>  Webserver address (default: http://localhost:8080)\n"
+              << "  -t, --token <token>    API token for authentication (required)\n"
+              << "  -n, --dry-run          Show what would be migrated without making changes\n"
+              << "  -h, --help             Show this help message\n";
+}
+
+Options parseArgs(int argc, char* argv[]) {
+    Options opts;
+
+    for (int i = 1; i < argc; ++i) {
+        std::string arg = argv[i];
+
+        if (arg == "-h" || arg == "--help") {
+            opts.help = true;
+        } else if (arg == "-n" || arg == "--dry-run") {
+            opts.dry_run = true;
+        } else if ((arg == "-d" || arg == "--dir") && i + 1 < argc) {
+            opts.nodes_dir = argv[++i];
+        } else if ((arg == "-w" || arg == "--webserver") && i + 1 < argc) {
+            opts.webserver_url = argv[++i];
+        } else if ((arg == "-t" || arg == "--token") && i + 1 < argc) {
+            opts.api_token = argv[++i];
+        }
+    }
+
+    return opts;
+}
+
+// Callback for curl response
+size_t writeCallback(char* ptr, size_t size, size_t nmemb, std::string* data) {
+    data->append(ptr, size * nmemb);
+    return size * nmemb;
+}
+
+// Read file contents
+std::string readFile(const fs::path& path) {
+    std::ifstream file(path);
+    if (!file.is_open()) {
+        throw std::runtime_error("Failed to open file: " + path.string());
+    }
+    std::stringstream buffer;
+    buffer << file.rdbuf();
+    return buffer.str();
+}
+
+// Create a node via API
+bool createNode(CURL* curl, const std::string& webserver_url, const std::string& token,
+                const std::string& code, std::string& error) {
+    std::string url = webserver_url + "/api/v1/nodes";
+    std::string response;
+
+    nlohmann::json body;
+    body["code"] = code;
+
+    std::string body_str = body.dump();
+
+    curl_easy_reset(curl);
+    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
+    curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str());
+    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body_str.size()));
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
+
+    struct curl_slist* headers = nullptr;
+    headers = curl_slist_append(headers, "Content-Type: application/json");
+
+    std::string auth_header = "Authorization: Bearer " + token;
+    headers = curl_slist_append(headers, auth_header.c_str());
+
+    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
+
+    CURLcode res = curl_easy_perform(curl);
+
+    long http_code = 0;
+    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
+
+    curl_slist_free_all(headers);
+
+    if (res != CURLE_OK) {
+        error = curl_easy_strerror(res);
+        return false;
+    }
+
+    if (http_code == 201) {
+        return true;
+    }
+
+    // Parse error from response
+    try {
+        auto json_response = nlohmann::json::parse(response);
+        if (json_response.contains("error")) {
+            error = json_response["error"].get<std::string>();
+        } else {
+            error = "HTTP " + std::to_string(http_code);
+        }
+    } catch (...) {
+        error = "HTTP " + std::to_string(http_code);
+    }
+
+    return false;
+}
+
+int main(int argc, char* argv[]) {
+    Options opts = parseArgs(argc, argv);
+
+    if (opts.help) {
+        printUsage(argv[0]);
+        return 0;
+    }
+
+    if (opts.api_token.empty() && !opts.dry_run) {
+        std::cerr << "Error: API token is required (use -t or --token)\n";
+        printUsage(argv[0]);
+        return 1;
+    }
+
+    fs::path nodes_dir(opts.nodes_dir);
+    if (!fs::exists(nodes_dir)) {
+        std::cerr << "Error: Nodes directory not found: " << nodes_dir << "\n";
+        return 1;
+    }
+
+    // Find all .js files
+    std::vector<fs::path> node_files;
+    for (const auto& entry : fs::recursive_directory_iterator(nodes_dir)) {
+        if (entry.is_regular_file() && entry.path().extension() == ".js") {
+            node_files.push_back(entry.path());
+        }
+    }
+
+    if (node_files.empty()) {
+        std::cout << "No .js files found in " << nodes_dir << "\n";
+        return 0;
+    }
+
+    std::cout << "Found " << node_files.size() << " node files to migrate\n";
+
+    if (opts.dry_run) {
+        std::cout << "\n[DRY RUN] Would migrate:\n";
+        for (const auto& file : node_files) {
+            auto relative = fs::relative(file, nodes_dir);
+            std::cout << "  - " << relative.string() << "\n";
+        }
+        return 0;
+    }
+
+    // Initialize curl
+    curl_global_init(CURL_GLOBAL_ALL);
+    CURL* curl = curl_easy_init();
+    if (!curl) {
+        std::cerr << "Error: Failed to initialize curl\n";
+        return 1;
+    }
+
+    int migrated = 0;
+    int skipped = 0;
+    int failed = 0;
+
+    for (const auto& file : node_files) {
+        auto relative = fs::relative(file, nodes_dir);
+        std::cout << "Migrating: " << relative.string() << " ... ";
+        std::cout.flush();
+
+        try {
+            std::string code = readFile(file);
+            std::string error;
+
+            if (createNode(curl, opts.webserver_url, opts.api_token, code, error)) {
+                std::cout << "OK\n";
+                ++migrated;
+            } else {
+                if (error.find("already exists") != std::string::npos) {
+                    std::cout << "SKIPPED (already exists)\n";
+                    ++skipped;
+                } else {
+                    std::cout << "FAILED: " << error << "\n";
+                    ++failed;
+                }
+            }
+        } catch (const std::exception& e) {
+            std::cout << "FAILED: " << e.what() << "\n";
+            ++failed;
+        }
+    }
+
+    curl_easy_cleanup(curl);
+    curl_global_cleanup();
+
+    std::cout << "\nMigration complete:\n"
+              << "  Migrated: " << migrated << "\n"
+              << "  Skipped:  " << skipped << "\n"
+              << "  Failed:   " << failed << "\n";
+
+    return (failed > 0) ? 1 : 0;
+}

+ 143 - 0
src/webserver/api/auth_controller.cpp

@@ -0,0 +1,143 @@
+#include "auth_controller.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::webserver::api {
+
+AuthController::AuthController(auth::AuthStore& auth_store, auth::AuthMiddleware& middleware)
+    : auth_store_(auth_store), middleware_(middleware) {}
+
+void AuthController::registerRoutes(httplib::Server& server) {
+    // Login - no auth required
+    server.Post("/api/v1/auth/login", [this](const httplib::Request& req, httplib::Response& res) {
+        auth::AuthContext ctx;
+        login(req, res, ctx);
+    });
+
+    // Logout - auth required
+    server.Post("/api/v1/auth/logout", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](const httplib::Request& req, httplib::Response& res,
+                                                  const auth::AuthContext& ctx) {
+            logout(req, res, ctx);
+        });
+    });
+
+    // Refresh - no auth required (uses refresh token)
+    server.Post("/api/v1/auth/refresh", [this](const httplib::Request& req, httplib::Response& res) {
+        auth::AuthContext ctx;
+        refresh(req, res, ctx);
+    });
+
+    // Me - auth required
+    server.Get("/api/v1/auth/me", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](const httplib::Request& req, httplib::Response& res,
+                                                  const auth::AuthContext& ctx) {
+            me(req, res, ctx);
+        });
+    });
+}
+
+void AuthController::login(const httplib::Request& req, httplib::Response& res,
+                           const auth::AuthContext& ctx) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        std::string username = body.value("username", "");
+        std::string password = body.value("password", "");
+
+        if (username.empty() || password.empty()) {
+            sendError(res, "Username and password required", 400);
+            return;
+        }
+
+        std::string ip = req.get_header_value("X-Forwarded-For");
+        if (ip.empty()) {
+            ip = req.remote_addr;
+        }
+        std::string user_agent = req.get_header_value("User-Agent");
+
+        auto result = auth_store_.login(username, password, ip, user_agent);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 401);
+            return;
+        }
+
+        auto& login_resp = result.value();
+        nlohmann::json response;
+        response["accessToken"] = login_resp.access_token;
+        response["refreshToken"] = login_resp.refresh_token;
+        response["expiresAt"] = login_resp.expires_at;
+        response["user"] = login_resp.user.toJson();
+
+        sendJson(res, response);
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void AuthController::logout(const httplib::Request& req, httplib::Response& res,
+                            const auth::AuthContext& ctx) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+        std::string session_id = body.value("sessionId", "");
+
+        if (!session_id.empty()) {
+            auth_store_.logout(session_id);
+        }
+
+        sendJson(res, {{"success", true}});
+    } catch (...) {
+        sendJson(res, {{"success", true}});
+    }
+}
+
+void AuthController::refresh(const httplib::Request& req, httplib::Response& res,
+                             const auth::AuthContext& ctx) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+        std::string refresh_token = body.value("refreshToken", "");
+
+        if (refresh_token.empty()) {
+            sendError(res, "Refresh token required", 400);
+            return;
+        }
+
+        auto result = auth_store_.refreshTokens(refresh_token);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 401);
+            return;
+        }
+
+        auto& login_resp = result.value();
+        nlohmann::json response;
+        response["accessToken"] = login_resp.access_token;
+        response["refreshToken"] = login_resp.refresh_token;
+        response["expiresAt"] = login_resp.expires_at;
+
+        sendJson(res, response);
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void AuthController::me(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx) {
+    auto result = auth_store_.getUser(ctx.user_id);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 404);
+        return;
+    }
+
+    sendJson(res, result.value().toJson());
+}
+
+void AuthController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void AuthController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 42 - 0
src/webserver/api/auth_controller.hpp

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_store.hpp"
+#include "../auth/auth_middleware.hpp"
+
+namespace smartbotic::webserver::api {
+
+class AuthController {
+public:
+    AuthController(auth::AuthStore& auth_store, auth::AuthMiddleware& middleware);
+
+    // Register routes
+    void registerRoutes(httplib::Server& server);
+
+private:
+    // POST /api/v1/auth/login
+    void login(const httplib::Request& req, httplib::Response& res,
+               const auth::AuthContext& ctx);
+
+    // POST /api/v1/auth/logout
+    void logout(const httplib::Request& req, httplib::Response& res,
+                const auth::AuthContext& ctx);
+
+    // POST /api/v1/auth/refresh
+    void refresh(const httplib::Request& req, httplib::Response& res,
+                 const auth::AuthContext& ctx);
+
+    // GET /api/v1/auth/me
+    void me(const httplib::Request& req, httplib::Response& res,
+            const auth::AuthContext& ctx);
+
+    // Helper
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    auth::AuthStore& auth_store_;
+    auth::AuthMiddleware& middleware_;
+};
+
+} // namespace smartbotic::webserver::api

+ 197 - 0
src/webserver/api/credential_controller.cpp

@@ -0,0 +1,197 @@
+#include "credential_controller.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::webserver::api {
+
+using namespace credentials;
+
+CredentialController::CredentialController(CredentialStore& credential_store,
+                                           auth::AuthMiddleware& middleware)
+    : credential_store_(credential_store)
+    , middleware_(middleware) {}
+
+void CredentialController::registerRoutes(httplib::Server& server) {
+    // List credentials - requires admin role
+    server.Get("/api/v1/credentials", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](const httplib::Request& req,
+                                                          httplib::Response& res,
+                                                          const auth::AuthContext& ctx) {
+            listCredentials(req, res, ctx);
+        });
+    });
+
+    // Get credential - requires admin role
+    server.Get(R"(/api/v1/credentials/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](const httplib::Request& req,
+                                                          httplib::Response& res,
+                                                          const auth::AuthContext& ctx) {
+            getCredential(req, res, ctx);
+        });
+    });
+
+    // Create credential - requires admin role
+    server.Post("/api/v1/credentials", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](const httplib::Request& req,
+                                                          httplib::Response& res,
+                                                          const auth::AuthContext& ctx) {
+            createCredential(req, res, ctx);
+        });
+    });
+
+    // Update credential - requires admin role
+    server.Put(R"(/api/v1/credentials/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](const httplib::Request& req,
+                                                          httplib::Response& res,
+                                                          const auth::AuthContext& ctx) {
+            updateCredential(req, res, ctx);
+        });
+    });
+
+    // Delete credential - requires admin role
+    server.Delete(R"(/api/v1/credentials/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](const httplib::Request& req,
+                                                          httplib::Response& res,
+                                                          const auth::AuthContext& ctx) {
+            deleteCredential(req, res, ctx);
+        });
+    });
+
+    // Refresh OAuth2 token - requires admin role
+    server.Post(R"(/api/v1/credentials/([^/]+)/refresh)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](const httplib::Request& req,
+                                                          httplib::Response& res,
+                                                          const auth::AuthContext& ctx) {
+            refreshToken(req, res, ctx);
+        });
+    });
+
+    LOG_INFO("Credential API routes registered");
+}
+
+void CredentialController::listCredentials(const httplib::Request& req, httplib::Response& res,
+                                           const auth::AuthContext& ctx) {
+    auto result = credential_store_.list();
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    nlohmann::json credentials_json = nlohmann::json::array();
+    for (const auto& cred : result.value()) {
+        credentials_json.push_back(cred.toJson());
+    }
+
+    sendJson(res, {{"credentials", credentials_json}});
+}
+
+void CredentialController::getCredential(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    std::string id = req.matches[1].str();
+
+    auto result = credential_store_.get(id);
+    if (result.failed()) {
+        int status = result.error().code() == common::ErrorCode::NotFound ? 404 : 500;
+        sendError(res, result.error().message(), status);
+        return;
+    }
+
+    sendJson(res, result.value().toJson());
+}
+
+void CredentialController::createCredential(const httplib::Request& req, httplib::Response& res,
+                                            const auth::AuthContext& ctx) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        auto request_result = CreateCredentialRequest::fromJson(body);
+        if (request_result.failed()) {
+            sendError(res, request_result.error().message(), 400);
+            return;
+        }
+
+        auto result = credential_store_.create(request_result.value(), ctx.user_id);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 400);
+            return;
+        }
+
+        sendJson(res, result.value().toJson(), 201);
+    } catch (const nlohmann::json::exception& e) {
+        sendError(res, "Invalid JSON: " + std::string(e.what()), 400);
+    }
+}
+
+void CredentialController::updateCredential(const httplib::Request& req, httplib::Response& res,
+                                            const auth::AuthContext& ctx) {
+    std::string id = req.matches[1].str();
+
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        auto request_result = UpdateCredentialRequest::fromJson(body);
+        if (request_result.failed()) {
+            sendError(res, request_result.error().message(), 400);
+            return;
+        }
+
+        auto result = credential_store_.update(id, request_result.value());
+        if (result.failed()) {
+            int status = result.error().code() == common::ErrorCode::NotFound ? 404 : 400;
+            sendError(res, result.error().message(), status);
+            return;
+        }
+
+        // Return updated credential
+        auto get_result = credential_store_.get(id);
+        if (get_result.ok()) {
+            sendJson(res, get_result.value().toJson());
+        } else {
+            sendJson(res, {{"success", true}});
+        }
+    } catch (const nlohmann::json::exception& e) {
+        sendError(res, "Invalid JSON: " + std::string(e.what()), 400);
+    }
+}
+
+void CredentialController::deleteCredential(const httplib::Request& req, httplib::Response& res,
+                                            const auth::AuthContext& ctx) {
+    std::string id = req.matches[1].str();
+
+    auto result = credential_store_.remove(id);
+    if (result.failed()) {
+        int status = result.error().code() == common::ErrorCode::NotFound ? 404 : 500;
+        sendError(res, result.error().message(), status);
+        return;
+    }
+
+    sendJson(res, {{"success", true}});
+}
+
+void CredentialController::refreshToken(const httplib::Request& req, httplib::Response& res,
+                                        const auth::AuthContext& ctx) {
+    std::string id = req.matches[1].str();
+
+    auto result = credential_store_.refreshOAuth2Token(id);
+    if (result.failed()) {
+        int status = 400;
+        if (result.error().code() == common::ErrorCode::NotFound) {
+            status = 404;
+        }
+        sendError(res, result.error().message(), status);
+        return;
+    }
+
+    sendJson(res, {{"success", true}});
+}
+
+void CredentialController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void CredentialController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 51 - 0
src/webserver/api/credential_controller.hpp

@@ -0,0 +1,51 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_middleware.hpp"
+#include "credentials/credential_store.hpp"
+
+namespace smartbotic::webserver::api {
+
+class CredentialController {
+public:
+    CredentialController(credentials::CredentialStore& credential_store,
+                        auth::AuthMiddleware& middleware);
+
+    // Register routes
+    void registerRoutes(httplib::Server& server);
+
+private:
+    // GET /api/v1/credentials - List all credentials (metadata only)
+    void listCredentials(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+
+    // GET /api/v1/credentials/:id - Get credential metadata
+    void getCredential(const httplib::Request& req, httplib::Response& res,
+                      const auth::AuthContext& ctx);
+
+    // POST /api/v1/credentials - Create credential
+    void createCredential(const httplib::Request& req, httplib::Response& res,
+                         const auth::AuthContext& ctx);
+
+    // PUT /api/v1/credentials/:id - Update credential
+    void updateCredential(const httplib::Request& req, httplib::Response& res,
+                         const auth::AuthContext& ctx);
+
+    // DELETE /api/v1/credentials/:id - Delete credential
+    void deleteCredential(const httplib::Request& req, httplib::Response& res,
+                         const auth::AuthContext& ctx);
+
+    // POST /api/v1/credentials/:id/refresh - Refresh OAuth2 token
+    void refreshToken(const httplib::Request& req, httplib::Response& res,
+                     const auth::AuthContext& ctx);
+
+    // Helper methods
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    credentials::CredentialStore& credential_store_;
+    auth::AuthMiddleware& middleware_;
+};
+
+} // namespace smartbotic::webserver::api

+ 246 - 0
src/webserver/api/database_controller.cpp

@@ -0,0 +1,246 @@
+#include "database_controller.hpp"
+#include "logging/logger.hpp"
+#include <unordered_set>
+
+namespace smartbotic::webserver::api {
+
+DatabaseController::DatabaseController(storage::StorageClient& storage,
+                                        auth::AuthMiddleware& middleware)
+    : storage_(storage), middleware_(middleware) {}
+
+void DatabaseController::registerRoutes(httplib::Server& server) {
+    // List collections
+    server.Get("/api/v1/database/collections", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            listCollections(req, res, ctx);
+        });
+    });
+
+    // Create collection
+    server.Post("/api/v1/database/collections", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            createCollection(req, res, ctx);
+        });
+    });
+
+    // Get documents in a collection
+    server.Get(R"(/api/v1/database/collections/([^/]+)/documents)",
+               [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            getDocuments(req, res, ctx);
+        });
+    });
+
+    // Get single document
+    server.Get(R"(/api/v1/database/collections/([^/]+)/documents/([^/]+))",
+               [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            getDocument(req, res, ctx);
+        });
+    });
+
+    // Create document
+    server.Post(R"(/api/v1/database/collections/([^/]+)/documents)",
+                [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            createDocument(req, res, ctx);
+        });
+    });
+
+    // Update document
+    server.Put(R"(/api/v1/database/collections/([^/]+)/documents/([^/]+))",
+               [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            updateDocument(req, res, ctx);
+        });
+    });
+
+    // Delete document
+    server.Delete(R"(/api/v1/database/collections/([^/]+)/documents/([^/]+))",
+                  [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            deleteDocument(req, res, ctx);
+        });
+    });
+}
+
+void DatabaseController::listCollections(const httplib::Request& req, httplib::Response& res,
+                                          const auth::AuthContext& ctx) {
+    auto collections = storage_.listCollections();
+
+    sendJson(res, {{"collections", collections}});
+}
+
+void DatabaseController::createCollection(const httplib::Request& req, httplib::Response& res,
+                                           const auth::AuthContext& ctx) {
+    nlohmann::json body;
+    try {
+        body = nlohmann::json::parse(req.body);
+    } catch (...) {
+        sendError(res, "Invalid JSON body", 400);
+        return;
+    }
+
+    std::string name = body.value("name", "");
+    if (name.empty()) {
+        sendError(res, "Collection name is required", 400);
+        return;
+    }
+
+    // Prevent creating system collection names
+    static const std::unordered_set<std::string> SYSTEM_COLLECTIONS = {
+        "users", "sessions", "api_keys", "credentials", "collection_permissions",
+        "workflows", "workflow_groups", "executions", "runners", "nodes"
+    };
+
+    if (SYSTEM_COLLECTIONS.contains(name)) {
+        sendError(res, "Cannot create a collection with a system name: " + name, 400);
+        return;
+    }
+
+    int64_t default_ttl_ms = body.value("defaultTtlMs", 0);
+
+    auto result = storage_.createCollection(name, {}, default_ttl_ms);
+
+    if (result.failed()) {
+        if (result.error().code() == common::ErrorCode::AlreadyExists) {
+            sendError(res, "Collection already exists: " + name, 409);
+        } else {
+            sendError(res, result.error().message(), 500);
+        }
+        return;
+    }
+
+    sendJson(res, {{"name", name}, {"success", true}}, 201);
+}
+
+void DatabaseController::getDocuments(const httplib::Request& req, httplib::Response& res,
+                                       const auth::AuthContext& ctx) {
+    std::string collection = req.matches[1];
+
+    int page = 1;
+    int page_size = 50;
+
+    if (req.has_param("page")) {
+        page = std::stoi(req.get_param_value("page"));
+    }
+    if (req.has_param("pageSize")) {
+        page_size = std::stoi(req.get_param_value("pageSize"));
+    }
+
+    storage::QueryOptions options;
+    options.page = page;
+    options.page_size = page_size;
+
+    auto result = storage_.query(collection, options);
+
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    nlohmann::json documents = nlohmann::json::array();
+    for (const auto& doc : result.value().documents) {
+        documents.push_back(doc);
+    }
+
+    sendJson(res, {
+        {"documents", documents},
+        {"total", result.value().total_count},
+        {"page", page},
+        {"pageSize", page_size}
+    });
+}
+
+void DatabaseController::getDocument(const httplib::Request& req, httplib::Response& res,
+                                      const auth::AuthContext& ctx) {
+    std::string collection = req.matches[1];
+    std::string id = req.matches[2];
+
+    auto result = storage_.get(collection, id);
+
+    if (result.failed()) {
+        sendError(res, result.error().message(), 404);
+        return;
+    }
+
+    sendJson(res, result.value());
+}
+
+void DatabaseController::createDocument(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    std::string collection = req.matches[1];
+
+    nlohmann::json body;
+    try {
+        body = nlohmann::json::parse(req.body);
+    } catch (...) {
+        sendError(res, "Invalid JSON body", 400);
+        return;
+    }
+
+    // Generate ID if not provided
+    std::string id = body.value("id", "");
+
+    auto result = storage_.insert(collection, body, id);
+
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    sendJson(res, {{"id", result.value()}, {"success", true}}, 201);
+}
+
+void DatabaseController::updateDocument(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    std::string collection = req.matches[1];
+    std::string id = req.matches[2];
+
+    nlohmann::json body;
+    try {
+        body = nlohmann::json::parse(req.body);
+    } catch (...) {
+        sendError(res, "Invalid JSON body", 400);
+        return;
+    }
+
+    // Ensure ID in body matches URL
+    body["id"] = id;
+
+    auto result = storage_.update(collection, id, body);
+
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    sendJson(res, {{"id", id}, {"success", true}});
+}
+
+void DatabaseController::deleteDocument(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    std::string collection = req.matches[1];
+    std::string id = req.matches[2];
+
+    auto result = storage_.remove(collection, id);
+
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    sendJson(res, {{"id", id}, {"success", true}});
+}
+
+void DatabaseController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void DatabaseController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 39 - 0
src/webserver/api/database_controller.hpp

@@ -0,0 +1,39 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_middleware.hpp"
+#include "storage/storage_client.hpp"
+
+namespace smartbotic::webserver::api {
+
+class DatabaseController {
+public:
+    DatabaseController(storage::StorageClient& storage, auth::AuthMiddleware& middleware);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    void listCollections(const httplib::Request& req, httplib::Response& res,
+                         const auth::AuthContext& ctx);
+    void createCollection(const httplib::Request& req, httplib::Response& res,
+                          const auth::AuthContext& ctx);
+    void getDocuments(const httplib::Request& req, httplib::Response& res,
+                      const auth::AuthContext& ctx);
+    void getDocument(const httplib::Request& req, httplib::Response& res,
+                     const auth::AuthContext& ctx);
+    void createDocument(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+    void updateDocument(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+    void deleteDocument(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    storage::StorageClient& storage_;
+    auth::AuthMiddleware& middleware_;
+};
+
+} // namespace smartbotic::webserver::api

+ 191 - 0
src/webserver/api/execution_controller.cpp

@@ -0,0 +1,191 @@
+#include "execution_controller.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::webserver::api {
+
+ExecutionController::ExecutionController(storage::StorageClient& storage,
+                                         auth::AuthMiddleware& middleware,
+                                         WebSocketServer& ws_server)
+    : storage_(storage), middleware_(middleware), ws_server_(ws_server) {}
+
+void ExecutionController::registerRoutes(httplib::Server& server) {
+    server.Get("/api/v1/executions", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            listExecutions(req, res, ctx);
+        });
+    });
+
+    server.Get(R"(/api/v1/executions/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            getExecution(req, res, ctx);
+        });
+    });
+
+    server.Post(R"(/api/v1/executions/([^/]+)/cancel)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            cancelExecution(req, res, ctx);
+        });
+    });
+
+    server.Post(R"(/api/v1/executions/([^/]+)/retry)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            retryExecution(req, res, ctx);
+        });
+    });
+
+    // Internal endpoint for runner to send execution events (no auth required from internal network)
+    server.Post("/api/v1/internal/execution-event", [this](const httplib::Request& req, httplib::Response& res) {
+        receiveExecutionEvent(req, res);
+    });
+}
+
+void ExecutionController::listExecutions(const httplib::Request& req, httplib::Response& res,
+                                          const auth::AuthContext& ctx) {
+    storage::QueryOptions options;
+
+    if (req.has_param("workflowId")) {
+        options.filters.push_back({"workflowId", req.get_param_value("workflowId")});
+    }
+
+    if (req.has_param("status")) {
+        options.filters.push_back({"status", req.get_param_value("status")});
+    }
+
+    int page = 1;
+    int page_size = 20;
+    if (req.has_param("page")) {
+        page = std::stoi(req.get_param_value("page"));
+    }
+    if (req.has_param("pageSize")) {
+        page_size = std::stoi(req.get_param_value("pageSize"));
+    }
+    options.page = page;
+    options.page_size = page_size;
+    options.sorts.push_back({"startedAt", false});
+
+    auto result = storage_.query("executions", options);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    nlohmann::json response;
+    response["executions"] = result.value().documents;
+    response["total"] = result.value().total_count;
+    response["page"] = page;
+    response["pageSize"] = page_size;
+    response["hasMore"] = result.value().has_more;
+
+    sendJson(res, response);
+}
+
+void ExecutionController::getExecution(const httplib::Request& req, httplib::Response& res,
+                                        const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    auto result = storage_.get("executions", id);
+    if (result.failed()) {
+        sendError(res, "Execution not found", 404);
+        return;
+    }
+
+    sendJson(res, result.value());
+}
+
+void ExecutionController::cancelExecution(const httplib::Request& req, httplib::Response& res,
+                                           const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    // TODO: Send cancel request to runner via gRPC
+    // For now, just update status in database
+
+    auto result = storage_.update("executions", id,
+                                  {{"status", "cancelled"}}, 0, true);
+    if (result.failed()) {
+        sendError(res, "Execution not found", 404);
+        return;
+    }
+
+    sendJson(res, {{"success", true}, {"status", "cancelled"}});
+}
+
+void ExecutionController::retryExecution(const httplib::Request& req, httplib::Response& res,
+                                          const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    // Get original execution
+    auto exec_result = storage_.get("executions", id);
+    if (exec_result.failed()) {
+        sendError(res, "Execution not found", 404);
+        return;
+    }
+
+    // TODO: Re-execute workflow via runner
+    sendJson(res, {{"success", true}, {"message", "Retry queued"}});
+}
+
+void ExecutionController::receiveExecutionEvent(const httplib::Request& req, httplib::Response& res) {
+    LOG_DEBUG("Received execution event request: {}", req.body);
+
+    try {
+        auto body = nlohmann::json::parse(req.body);
+        std::string event_type = body.value("event", "");
+        nlohmann::json data = body.value("data", nlohmann::json::object());
+
+        if (event_type.empty()) {
+            LOG_WARN("Execution event missing type");
+            sendError(res, "Missing event type", 400);
+            return;
+        }
+
+        // Map event types to WebSocket channels
+        std::string execution_id = data.value("executionId", "");
+        std::string workflow_id = data.value("workflowId", "");
+        std::string channel;
+
+        if (event_type == "execution.started") {
+            channel = "executions." + execution_id + ".started";
+        } else if (event_type == "execution.completed") {
+            channel = "executions." + execution_id + ".completed";
+        } else if (event_type == "execution.failed") {
+            channel = "executions." + execution_id + ".failed";
+        } else if (event_type == "execution.cancelled") {
+            channel = "executions." + execution_id + ".cancelled";
+        } else if (event_type.starts_with("loop.node.")) {
+            // Loop node events: loop.node.running, loop.node.completed, loop.node.failed
+            std::string node_status = event_type.substr(10);  // Remove "loop.node." prefix
+            channel = "executions." + execution_id + ".loop_node_" + node_status;
+        } else if (event_type.starts_with("loop.iteration.")) {
+            // Loop iteration events: loop.iteration.start, loop.iteration.end
+            std::string iteration_event = event_type.substr(15);  // Remove "loop.iteration." prefix
+            channel = "executions." + execution_id + ".loop_iteration_" + iteration_event;
+        } else if (event_type.starts_with("node.")) {
+            // Node events: node.running, node.completed, node.failed
+            std::string node_status = event_type.substr(5);  // Remove "node." prefix
+            channel = "executions." + execution_id + ".node_" + node_status;
+        } else {
+            channel = "executions." + execution_id + "." + event_type;
+        }
+
+        // Broadcast to WebSocket clients
+        ws_server_.broadcast(channel, data);
+
+        LOG_INFO("Broadcast execution event: {} -> {}", event_type, channel);
+        sendJson(res, {{"success", true}});
+
+    } catch (const std::exception& e) {
+        sendError(res, std::string("Invalid request: ") + e.what(), 400);
+    }
+}
+
+void ExecutionController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void ExecutionController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 37 - 0
src/webserver/api/execution_controller.hpp

@@ -0,0 +1,37 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_middleware.hpp"
+#include "../websocket_server.hpp"
+#include "storage/storage_client.hpp"
+
+namespace smartbotic::webserver::api {
+
+class ExecutionController {
+public:
+    ExecutionController(storage::StorageClient& storage, auth::AuthMiddleware& middleware,
+                        WebSocketServer& ws_server);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    void listExecutions(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+    void getExecution(const httplib::Request& req, httplib::Response& res,
+                      const auth::AuthContext& ctx);
+    void cancelExecution(const httplib::Request& req, httplib::Response& res,
+                         const auth::AuthContext& ctx);
+    void retryExecution(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+    void receiveExecutionEvent(const httplib::Request& req, httplib::Response& res);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    storage::StorageClient& storage_;
+    auth::AuthMiddleware& middleware_;
+    WebSocketServer& ws_server_;
+};
+
+} // namespace smartbotic::webserver::api

+ 333 - 0
src/webserver/api/node_controller.cpp

@@ -0,0 +1,333 @@
+#include "node_controller.hpp"
+#include "../grpc/node_sync_service.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::webserver::api {
+
+NodeController::NodeController(nodes::NodeStore& node_store,
+                               auth::AuthMiddleware& middleware,
+                               grpc::NodeSyncServiceImpl* sync_service)
+    : node_store_(node_store), middleware_(middleware), sync_service_(sync_service) {}
+
+void NodeController::registerRoutes(httplib::Server& server) {
+    server.Get("/api/v1/nodes", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            listNodes(req, res, ctx);
+        });
+    });
+
+    server.Post("/api/v1/nodes", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            createNode(req, res, ctx);
+        });
+    });
+
+    server.Get(R"(/api/v1/nodes/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            getNode(req, res, ctx);
+        });
+    });
+
+    server.Get(R"(/api/v1/nodes/([^/]+)/code)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            getNodeCode(req, res, ctx);
+        });
+    });
+
+    server.Put(R"(/api/v1/nodes/([^/]+)/code)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            saveNodeCode(req, res, ctx);
+        });
+    });
+
+    server.Delete(R"(/api/v1/nodes/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            deleteNode(req, res, ctx);
+        });
+    });
+
+    // Migration endpoint
+    server.Post("/api/v1/nodes/migrate", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            migrateNodes(req, res, ctx);
+        });
+    });
+}
+
+void NodeController::listNodes(const httplib::Request& req, httplib::Response& res,
+                                const auth::AuthContext& ctx) {
+    std::string category;
+    if (req.has_param("category")) {
+        category = req.get_param_value("category");
+    }
+
+    auto result = node_store_.list(category);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    nlohmann::json nodes_json = nlohmann::json::array();
+    for (const auto& node : result.value()) {
+        nlohmann::json n;
+        n["id"] = node.id;
+        n["name"] = node.name;
+        n["category"] = node.category;
+        n["version"] = node.version;
+        n["description"] = node.description;
+        n["icon"] = node.icon;
+        n["isTrigger"] = node.is_trigger;
+        n["ownerId"] = node.owner_id;
+        n["createdAt"] = node.created_at;
+        n["updatedAt"] = node.updated_at;
+
+        if (!node.config_schema.is_null()) {
+            n["configSchema"] = node.config_schema;
+        }
+        if (!node.input_schema.is_null()) {
+            n["inputSchema"] = node.input_schema;
+        }
+        if (!node.output_schema.is_null()) {
+            n["outputSchema"] = node.output_schema;
+        }
+
+        nlohmann::json inputs = nlohmann::json::array();
+        for (const auto& input : node.inputs) {
+            inputs.push_back({
+                {"name", input.name},
+                {"displayName", input.display_name},
+                {"type", input.type},
+                {"required", input.required}
+            });
+        }
+        n["inputs"] = inputs;
+
+        nlohmann::json outputs = nlohmann::json::array();
+        for (const auto& output : node.outputs) {
+            nlohmann::json out_obj = {
+                {"name", output.name},
+                {"displayName", output.display_name},
+                {"type", output.type}
+            };
+            if (!output.color.empty()) {
+                out_obj["color"] = output.color;
+            }
+            outputs.push_back(out_obj);
+        }
+        n["outputs"] = outputs;
+
+        nodes_json.push_back(n);
+    }
+
+    sendJson(res, {{"nodes", nodes_json}, {"total", nodes_json.size()}});
+}
+
+void NodeController::getNode(const httplib::Request& req, httplib::Response& res,
+                              const auth::AuthContext& ctx) {
+    std::string node_id = req.matches[1];
+
+    auto result = node_store_.get(node_id);
+    if (result.failed()) {
+        sendError(res, "Node not found", 404);
+        return;
+    }
+
+    const auto& node = result.value();
+    nlohmann::json n;
+    n["id"] = node.id;
+    n["name"] = node.name;
+    n["category"] = node.category;
+    n["version"] = node.version;
+    n["description"] = node.description;
+    n["icon"] = node.icon;
+    n["isTrigger"] = node.is_trigger;
+    n["ownerId"] = node.owner_id;
+    n["createdAt"] = node.created_at;
+    n["updatedAt"] = node.updated_at;
+
+    if (!node.config_schema.is_null()) {
+        n["configSchema"] = node.config_schema;
+    }
+    if (!node.input_schema.is_null()) {
+        n["inputSchema"] = node.input_schema;
+    }
+    if (!node.output_schema.is_null()) {
+        n["outputSchema"] = node.output_schema;
+    }
+
+    nlohmann::json inputs = nlohmann::json::array();
+    for (const auto& input : node.inputs) {
+        inputs.push_back({
+            {"name", input.name},
+            {"displayName", input.display_name},
+            {"type", input.type},
+            {"required", input.required}
+        });
+    }
+    n["inputs"] = inputs;
+
+    nlohmann::json outputs = nlohmann::json::array();
+    for (const auto& output : node.outputs) {
+        nlohmann::json out_obj = {
+            {"name", output.name},
+            {"displayName", output.display_name},
+            {"type", output.type}
+        };
+        if (!output.color.empty()) {
+            out_obj["color"] = output.color;
+        }
+        outputs.push_back(out_obj);
+    }
+    n["outputs"] = outputs;
+
+    sendJson(res, n);
+}
+
+void NodeController::getNodeCode(const httplib::Request& req, httplib::Response& res,
+                                  const auth::AuthContext& ctx) {
+    std::string node_id = req.matches[1];
+
+    auto result = node_store_.get(node_id);
+    if (result.failed()) {
+        sendError(res, "Node not found", 404);
+        return;
+    }
+
+    sendJson(res, {{"code", result.value().code}});
+}
+
+void NodeController::saveNodeCode(const httplib::Request& req, httplib::Response& res,
+                                   const auth::AuthContext& ctx) {
+    std::string node_id = req.matches[1];
+
+    nlohmann::json body;
+    try {
+        body = nlohmann::json::parse(req.body);
+    } catch (...) {
+        sendError(res, "Invalid JSON body", 400);
+        return;
+    }
+
+    std::string code = body.value("code", "");
+    if (code.empty()) {
+        sendError(res, "Code is required", 400);
+        return;
+    }
+
+    auto result = node_store_.update(node_id, code);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 400);
+        return;
+    }
+
+    // Notify runners of the update
+    if (sync_service_) {
+        sync_service_->notifyNodeUpdated(result.value());
+    }
+
+    sendJson(res, {
+        {"success", true},
+        {"nodeId", node_id}
+    });
+}
+
+void NodeController::createNode(const httplib::Request& req, httplib::Response& res,
+                                 const auth::AuthContext& ctx) {
+    nlohmann::json body;
+    try {
+        body = nlohmann::json::parse(req.body);
+    } catch (...) {
+        sendError(res, "Invalid JSON body", 400);
+        return;
+    }
+
+    std::string code = body.value("code", "");
+    if (code.empty()) {
+        sendError(res, "code is required", 400);
+        return;
+    }
+
+    auto result = node_store_.create(code, ctx.user_id);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 400);
+        return;
+    }
+
+    // Notify runners of the new node
+    if (sync_service_) {
+        sync_service_->notifyNodeCreated(result.value());
+    }
+
+    const auto& node = result.value();
+    nlohmann::json response;
+    response["id"] = node.id;
+    response["name"] = node.name;
+    response["category"] = node.category;
+    response["version"] = node.version;
+
+    sendJson(res, response, 201);
+}
+
+void NodeController::deleteNode(const httplib::Request& req, httplib::Response& res,
+                                 const auth::AuthContext& ctx) {
+    std::string node_id = req.matches[1];
+
+    auto result = node_store_.remove(node_id);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 400);
+        return;
+    }
+
+    // Notify runners of the deletion
+    if (sync_service_) {
+        sync_service_->notifyNodeDeleted(node_id);
+    }
+
+    sendJson(res, {
+        {"success", true},
+        {"nodeId", node_id}
+    });
+}
+
+void NodeController::migrateNodes(const httplib::Request& req, httplib::Response& res,
+                                   const auth::AuthContext& ctx) {
+    nlohmann::json body;
+    try {
+        body = nlohmann::json::parse(req.body);
+    } catch (...) {
+        sendError(res, "Invalid JSON body", 400);
+        return;
+    }
+
+    std::string nodes_path = body.value("nodesPath", "./nodes");
+
+    auto result = node_store_.migrateFromFiles(nodes_path);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 400);
+        return;
+    }
+
+    // Notify runners of all newly migrated nodes
+    if (sync_service_) {
+        for (const auto& node : result.value()) {
+            sync_service_->notifyNodeCreated(node);
+        }
+    }
+
+    sendJson(res, {
+        {"success", true},
+        {"migratedCount", result.value().size()}
+    });
+}
+
+void NodeController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void NodeController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 46 - 0
src/webserver/api/node_controller.hpp

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_middleware.hpp"
+#include "../nodes/node_store.hpp"
+
+namespace smartbotic::webserver::grpc {
+    class NodeSyncServiceImpl;
+}
+
+namespace smartbotic::webserver::api {
+
+class NodeController {
+public:
+    NodeController(nodes::NodeStore& node_store,
+                   auth::AuthMiddleware& middleware,
+                   grpc::NodeSyncServiceImpl* sync_service = nullptr);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    void listNodes(const httplib::Request& req, httplib::Response& res,
+                   const auth::AuthContext& ctx);
+    void getNode(const httplib::Request& req, httplib::Response& res,
+                 const auth::AuthContext& ctx);
+    void getNodeCode(const httplib::Request& req, httplib::Response& res,
+                     const auth::AuthContext& ctx);
+    void saveNodeCode(const httplib::Request& req, httplib::Response& res,
+                      const auth::AuthContext& ctx);
+    void createNode(const httplib::Request& req, httplib::Response& res,
+                    const auth::AuthContext& ctx);
+    void deleteNode(const httplib::Request& req, httplib::Response& res,
+                    const auth::AuthContext& ctx);
+    void migrateNodes(const httplib::Request& req, httplib::Response& res,
+                      const auth::AuthContext& ctx);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    nodes::NodeStore& node_store_;
+    auth::AuthMiddleware& middleware_;
+    grpc::NodeSyncServiceImpl* sync_service_;  // Optional: for notifying runners
+};
+
+} // namespace smartbotic::webserver::api

+ 173 - 0
src/webserver/api/runner_controller.cpp

@@ -0,0 +1,173 @@
+#include "runner_controller.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::webserver::api {
+
+RunnerController::RunnerController(runners::RunnerRegistry& registry,
+                                   auth::AuthMiddleware& middleware)
+    : registry_(registry), middleware_(middleware) {}
+
+void RunnerController::registerRoutes(httplib::Server& server) {
+    server.Get("/api/v1/runners", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            listRunners(req, res, ctx);
+        });
+    });
+
+    server.Get(R"(/api/v1/runners/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            getRunner(req, res, ctx);
+        });
+    });
+
+    // Runner registration endpoint (no auth - runners use internal API key)
+    server.Post("/api/internal/runners/register", [this](const httplib::Request& req, httplib::Response& res) {
+        registerRunner(req, res);
+    });
+
+    // Runner heartbeat endpoint
+    server.Post("/api/internal/runners/heartbeat", [this](const httplib::Request& req, httplib::Response& res) {
+        heartbeat(req, res);
+    });
+
+    // Runner unregister endpoint
+    server.Post("/api/internal/runners/unregister", [this](const httplib::Request& req, httplib::Response& res) {
+        unregisterRunner(req, res);
+    });
+}
+
+void RunnerController::listRunners(const httplib::Request& req, httplib::Response& res,
+                                    const auth::AuthContext& ctx) {
+    std::vector<runners::Runner> runners;
+
+    if (req.has_param("status") && req.get_param_value("status") == "online") {
+        runners = registry_.getOnlineRunners();
+    } else {
+        runners = registry_.getAllRunners();
+    }
+
+    nlohmann::json response;
+    response["runners"] = nlohmann::json::array();
+
+    for (const auto& runner : runners) {
+        response["runners"].push_back(runner.toJson());
+    }
+
+    response["total"] = runners.size();
+    response["online"] = registry_.getOnlineRunnerCount();
+
+    sendJson(res, response);
+}
+
+void RunnerController::getRunner(const httplib::Request& req, httplib::Response& res,
+                                  const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    auto runner = registry_.getRunner(id);
+    if (!runner) {
+        sendError(res, "Runner not found", 404);
+        return;
+    }
+
+    sendJson(res, runner->toJson());
+}
+
+void RunnerController::registerRunner(const httplib::Request& req, httplib::Response& res) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        runners::Runner runner;
+        runner.id = body.value("id", "");
+        runner.address = body.value("address", "");
+
+        if (runner.id.empty() || runner.address.empty()) {
+            sendError(res, "id and address are required", 400);
+            return;
+        }
+
+        if (body.contains("capabilities")) {
+            runner.capabilities = runners::RunnerCapabilities::fromJson(body["capabilities"]);
+        }
+        if (body.contains("labels")) {
+            runner.labels = body["labels"].get<std::map<std::string, std::string>>();
+        }
+
+        auto result = registry_.registerRunner(runner);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 500);
+            return;
+        }
+
+        LOG_INFO("Runner registered via HTTP: {} at {}", runner.id, runner.address);
+        sendJson(res, {{"success", true}, {"id", runner.id}});
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void RunnerController::heartbeat(const httplib::Request& req, httplib::Response& res) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        std::string runner_id = body.value("id", "");
+        if (runner_id.empty()) {
+            sendError(res, "id is required", 400);
+            return;
+        }
+
+        runners::RunnerStatus status = runners::RunnerStatus::Online;
+        if (body.contains("status")) {
+            status = runners::runnerStatusFromString(body["status"]);
+        }
+
+        runners::RunnerMetrics metrics;
+        if (body.contains("metrics")) {
+            metrics = runners::RunnerMetrics::fromJson(body["metrics"]);
+        }
+
+        auto result = registry_.heartbeat(runner_id, status, metrics);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 404);
+            return;
+        }
+
+        sendJson(res, {{"success", true}});
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void RunnerController::unregisterRunner(const httplib::Request& req, httplib::Response& res) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        std::string runner_id = body.value("id", "");
+        if (runner_id.empty()) {
+            sendError(res, "id is required", 400);
+            return;
+        }
+
+        auto result = registry_.unregisterRunner(runner_id);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 404);
+            return;
+        }
+
+        LOG_INFO("Runner unregistered via HTTP: {}", runner_id);
+        sendJson(res, {{"success", true}});
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void RunnerController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void RunnerController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 34 - 0
src/webserver/api/runner_controller.hpp

@@ -0,0 +1,34 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_middleware.hpp"
+#include "../runners/runner_registry.hpp"
+
+namespace smartbotic::webserver::api {
+
+class RunnerController {
+public:
+    RunnerController(runners::RunnerRegistry& registry, auth::AuthMiddleware& middleware);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    void listRunners(const httplib::Request& req, httplib::Response& res,
+                     const auth::AuthContext& ctx);
+    void getRunner(const httplib::Request& req, httplib::Response& res,
+                   const auth::AuthContext& ctx);
+
+    // Internal endpoints for runner registration
+    void registerRunner(const httplib::Request& req, httplib::Response& res);
+    void heartbeat(const httplib::Request& req, httplib::Response& res);
+    void unregisterRunner(const httplib::Request& req, httplib::Response& res);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    runners::RunnerRegistry& registry_;
+    auth::AuthMiddleware& middleware_;
+};
+
+} // namespace smartbotic::webserver::api

+ 226 - 0
src/webserver/api/user_controller.cpp

@@ -0,0 +1,226 @@
+#include "user_controller.hpp"
+#include "logging/logger.hpp"
+
+namespace smartbotic::webserver::api {
+
+UserController::UserController(auth::AuthStore& auth_store, auth::AuthMiddleware& middleware)
+    : auth_store_(auth_store), middleware_(middleware) {}
+
+void UserController::registerRoutes(httplib::Server& server) {
+    server.Get("/api/v1/users", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            listUsers(req, res, ctx);
+        });
+    });
+
+    server.Get(R"(/api/v1/users/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            getUser(req, res, ctx);
+        });
+    });
+
+    server.Post("/api/v1/users", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            createUser(req, res, ctx);
+        });
+    });
+
+    server.Put(R"(/api/v1/users/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            updateUser(req, res, ctx);
+        });
+    });
+
+    server.Delete(R"(/api/v1/users/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireRole(req, res, "admin", [this](auto& req, auto& res, auto& ctx) {
+            deleteUser(req, res, ctx);
+        });
+    });
+
+    server.Post(R"(/api/v1/users/([^/]+)/change-password)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            changePassword(req, res, ctx);
+        });
+    });
+}
+
+void UserController::listUsers(const httplib::Request& req, httplib::Response& res,
+                                const auth::AuthContext& ctx) {
+    int page = 1;
+    int page_size = 20;
+
+    if (req.has_param("page")) {
+        page = std::stoi(req.get_param_value("page"));
+    }
+    if (req.has_param("pageSize")) {
+        page_size = std::stoi(req.get_param_value("pageSize"));
+    }
+
+    auto result = auth_store_.listUsers(page, page_size);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    nlohmann::json response;
+    response["users"] = nlohmann::json::array();
+    for (const auto& user : result.value()) {
+        response["users"].push_back(user.toJson());
+    }
+    response["page"] = page;
+    response["pageSize"] = page_size;
+
+    sendJson(res, response);
+}
+
+void UserController::getUser(const httplib::Request& req, httplib::Response& res,
+                              const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    // Users can only view their own profile unless admin
+    if (id != ctx.user_id && ctx.role != "admin") {
+        sendError(res, "Forbidden", 403);
+        return;
+    }
+
+    auto result = auth_store_.getUser(id);
+    if (result.failed()) {
+        sendError(res, "User not found", 404);
+        return;
+    }
+
+    sendJson(res, result.value().toJson());
+}
+
+void UserController::createUser(const httplib::Request& req, httplib::Response& res,
+                                 const auth::AuthContext& ctx) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        std::string username = body.value("username", "");
+        std::string email = body.value("email", "");
+        std::string password = body.value("password", "");
+        std::string role = body.value("role", "user");
+
+        if (username.empty() || email.empty() || password.empty()) {
+            sendError(res, "Username, email, and password are required", 400);
+            return;
+        }
+
+        auto result = auth_store_.createUser(username, email, password, role);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 400);
+            return;
+        }
+
+        LOG_INFO("User created: {} by admin {}", username, ctx.user_id);
+        sendJson(res, result.value().toJson(), 201);
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void UserController::updateUser(const httplib::Request& req, httplib::Response& res,
+                                 const auth::AuthContext& ctx) {
+    try {
+        std::string id = req.matches[1];
+
+        // Users can only update their own profile unless admin
+        if (id != ctx.user_id && ctx.role != "admin") {
+            sendError(res, "Forbidden", 403);
+            return;
+        }
+
+        auto body = nlohmann::json::parse(req.body);
+
+        // Non-admins can't change role
+        if (ctx.role != "admin") {
+            body.erase("role");
+        }
+
+        // Don't allow password change through this endpoint
+        body.erase("password");
+        body.erase("passwordHash");
+
+        auto result = auth_store_.updateUser(id, body);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 400);
+            return;
+        }
+
+        sendJson(res, result.value().toJson());
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void UserController::deleteUser(const httplib::Request& req, httplib::Response& res,
+                                 const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    // Can't delete yourself
+    if (id == ctx.user_id) {
+        sendError(res, "Cannot delete your own account", 400);
+        return;
+    }
+
+    auto result = auth_store_.deleteUser(id);
+    if (result.failed()) {
+        sendError(res, "User not found", 404);
+        return;
+    }
+
+    LOG_INFO("User deleted: {} by admin {}", id, ctx.user_id);
+    sendJson(res, {{"success", true}});
+}
+
+void UserController::changePassword(const httplib::Request& req, httplib::Response& res,
+                                     const auth::AuthContext& ctx) {
+    try {
+        std::string id = req.matches[1];
+
+        // Users can only change their own password
+        if (id != ctx.user_id && ctx.role != "admin") {
+            sendError(res, "Forbidden", 403);
+            return;
+        }
+
+        auto body = nlohmann::json::parse(req.body);
+
+        std::string old_password = body.value("oldPassword", "");
+        std::string new_password = body.value("newPassword", "");
+
+        // Admin can change without old password
+        if (ctx.role != "admin" && old_password.empty()) {
+            sendError(res, "Current password required", 400);
+            return;
+        }
+
+        if (new_password.empty()) {
+            sendError(res, "New password required", 400);
+            return;
+        }
+
+        auto result = auth_store_.changePassword(id, old_password, new_password);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 400);
+            return;
+        }
+
+        sendJson(res, {{"success", true}});
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void UserController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void UserController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 37 - 0
src/webserver/api/user_controller.hpp

@@ -0,0 +1,37 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_store.hpp"
+#include "../auth/auth_middleware.hpp"
+
+namespace smartbotic::webserver::api {
+
+class UserController {
+public:
+    UserController(auth::AuthStore& auth_store, auth::AuthMiddleware& middleware);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    void listUsers(const httplib::Request& req, httplib::Response& res,
+                   const auth::AuthContext& ctx);
+    void getUser(const httplib::Request& req, httplib::Response& res,
+                 const auth::AuthContext& ctx);
+    void createUser(const httplib::Request& req, httplib::Response& res,
+                    const auth::AuthContext& ctx);
+    void updateUser(const httplib::Request& req, httplib::Response& res,
+                    const auth::AuthContext& ctx);
+    void deleteUser(const httplib::Request& req, httplib::Response& res,
+                    const auth::AuthContext& ctx);
+    void changePassword(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    auth::AuthStore& auth_store_;
+    auth::AuthMiddleware& middleware_;
+};
+
+} // namespace smartbotic::webserver::api

+ 134 - 0
src/webserver/api/webhook_controller.cpp

@@ -0,0 +1,134 @@
+#include "webhook_controller.hpp"
+#include "logging/logger.hpp"
+#include "common/time_utils.hpp"
+#include "proto/runner.grpc.pb.h"
+#include <grpcpp/grpcpp.h>
+
+namespace smartbotic::webserver::api {
+
+using namespace common;
+
+WebhookController::WebhookController(storage::StorageClient& storage,
+                                     runners::RunnerRegistry& registry,
+                                     runners::LoadBalancer& load_balancer,
+                                     WebSocketServer& ws_server)
+    : storage_(storage)
+    , registry_(registry)
+    , load_balancer_(load_balancer)
+    , ws_server_(ws_server) {}
+
+void WebhookController::registerRoutes(httplib::Server& server) {
+    // Match any HTTP method for webhooks
+    auto webhook_handler = [this](const httplib::Request& req, httplib::Response& res) {
+        handleWebhook(req, res);
+    };
+
+    server.Get(R"(/webhook/([^/]+)(.*))", webhook_handler);
+    server.Post(R"(/webhook/([^/]+)(.*))", webhook_handler);
+    server.Put(R"(/webhook/([^/]+)(.*))", webhook_handler);
+    server.Delete(R"(/webhook/([^/]+)(.*))", webhook_handler);
+    server.Patch(R"(/webhook/([^/]+)(.*))", webhook_handler);
+}
+
+void WebhookController::handleWebhook(const httplib::Request& req, httplib::Response& res) {
+    std::string workflow_id = req.matches[1];
+    std::string path = req.matches[2];
+
+    LOG_DEBUG("Webhook received for workflow {}: {} {}", workflow_id, req.method, path);
+
+    // Get workflow
+    auto workflow_result = storage_.get("workflows", workflow_id);
+    if (workflow_result.failed()) {
+        sendError(res, "Workflow not found", 404);
+        return;
+    }
+
+    auto& workflow = workflow_result.value();
+
+    // Check if workflow is active
+    if (!workflow.value("active", false)) {
+        sendError(res, "Workflow is not active", 400);
+        return;
+    }
+
+    // Select a runner
+    auto runner = load_balancer_.selectRunner();
+    if (!runner) {
+        sendError(res, "No runners available", 503);
+        return;
+    }
+
+    // Build trigger data from request
+    nlohmann::json trigger_data;
+    trigger_data["method"] = req.method;
+    trigger_data["path"] = path;
+    trigger_data["query"] = nlohmann::json::object();
+    for (const auto& [key, value] : req.params) {
+        trigger_data["query"][key] = value;
+    }
+    trigger_data["headers"] = nlohmann::json::object();
+    for (const auto& [key, value] : req.headers) {
+        trigger_data["headers"][key] = value;
+    }
+    if (!req.body.empty()) {
+        try {
+            trigger_data["body"] = nlohmann::json::parse(req.body);
+        } catch (...) {
+            trigger_data["body"] = req.body;
+        }
+    }
+
+    // Execute workflow via gRPC
+    auto channel = grpc::CreateChannel(runner->address, grpc::InsecureChannelCredentials());
+    auto stub = proto::RunnerService::NewStub(channel);
+
+    proto::ExecuteWorkflowRequest grpc_req;
+    grpc_req.set_workflow_id(workflow_id);
+    grpc_req.set_trigger_type("webhook");
+    grpc_req.set_trigger_data(trigger_data.dump());
+    grpc_req.set_wait_for_completion(true);  // Wait for webhook response
+    grpc_req.set_timeout_ms(30000);  // 30 second timeout
+
+    proto::ExecuteWorkflowResponse grpc_res;
+    grpc::ClientContext grpc_ctx;
+    grpc_ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(35));
+
+    auto status = stub->ExecuteWorkflow(&grpc_ctx, grpc_req, &grpc_res);
+
+    if (!status.ok()) {
+        LOG_ERROR("Webhook execution failed: {}", status.error_message());
+        sendError(res, "Execution failed: " + status.error_message(), 500);
+        return;
+    }
+
+    // Broadcast execution event
+    ws_server_.broadcast("executions." + grpc_res.execution_id() + ".completed", {
+        {"executionId", grpc_res.execution_id()},
+        {"workflowId", workflow_id},
+        {"status", grpc_res.status()}
+    });
+
+    // Return result
+    if (!grpc_res.result().empty()) {
+        try {
+            auto result = nlohmann::json::parse(grpc_res.result());
+            sendJson(res, result);
+        } catch (...) {
+            res.set_content(grpc_res.result(), "text/plain");
+        }
+    } else {
+        sendJson(res, {{"executionId", grpc_res.execution_id()}, {"status", "completed"}});
+    }
+}
+
+void WebhookController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void WebhookController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 32 - 0
src/webserver/api/webhook_controller.hpp

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "storage/storage_client.hpp"
+#include "../runners/load_balancer.hpp"
+#include "../websocket_server.hpp"
+
+namespace smartbotic::webserver::api {
+
+class WebhookController {
+public:
+    WebhookController(storage::StorageClient& storage,
+                     runners::RunnerRegistry& registry,
+                     runners::LoadBalancer& load_balancer,
+                     WebSocketServer& ws_server);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    void handleWebhook(const httplib::Request& req, httplib::Response& res);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    storage::StorageClient& storage_;
+    runners::RunnerRegistry& registry_;
+    runners::LoadBalancer& load_balancer_;
+    WebSocketServer& ws_server_;
+};
+
+} // namespace smartbotic::webserver::api

+ 322 - 0
src/webserver/api/workflow_controller.cpp

@@ -0,0 +1,322 @@
+#include "workflow_controller.hpp"
+#include "common/uuid.hpp"
+#include "common/time_utils.hpp"
+#include "logging/logger.hpp"
+#include <grpcpp/grpcpp.h>
+
+namespace smartbotic::webserver::api {
+
+using namespace common;
+
+WorkflowController::WorkflowController(storage::StorageClient& storage,
+                                       auth::AuthMiddleware& middleware,
+                                       runners::RunnerRegistry& registry,
+                                       runners::LoadBalancer& load_balancer,
+                                       WebSocketServer& ws_server)
+    : storage_(storage)
+    , middleware_(middleware)
+    , registry_(registry)
+    , load_balancer_(load_balancer)
+    , ws_server_(ws_server) {}
+
+void WorkflowController::registerRoutes(httplib::Server& server) {
+    server.Get("/api/v1/workflows", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            listWorkflows(req, res, ctx);
+        });
+    });
+
+    server.Get(R"(/api/v1/workflows/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            getWorkflow(req, res, ctx);
+        });
+    });
+
+    server.Post("/api/v1/workflows", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            createWorkflow(req, res, ctx);
+        });
+    });
+
+    server.Put(R"(/api/v1/workflows/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            updateWorkflow(req, res, ctx);
+        });
+    });
+
+    server.Delete(R"(/api/v1/workflows/([^/]+))", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            deleteWorkflow(req, res, ctx);
+        });
+    });
+
+    server.Post(R"(/api/v1/workflows/([^/]+)/execute)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            executeWorkflow(req, res, ctx);
+        });
+    });
+
+    server.Post(R"(/api/v1/workflows/([^/]+)/activate)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            activateWorkflow(req, res, ctx);
+        });
+    });
+
+    server.Post(R"(/api/v1/workflows/([^/]+)/deactivate)", [this](const httplib::Request& req, httplib::Response& res) {
+        middleware_.requireAuth(req, res, [this](auto& req, auto& res, auto& ctx) {
+            deactivateWorkflow(req, res, ctx);
+        });
+    });
+}
+
+void WorkflowController::listWorkflows(const httplib::Request& req, httplib::Response& res,
+                                        const auth::AuthContext& ctx) {
+    storage::QueryOptions options;
+
+    // Parse query params
+    if (req.has_param("groupId")) {
+        options.filters.push_back({"groupId", req.get_param_value("groupId")});
+    }
+
+    int page = 1;
+    int page_size = 20;
+    if (req.has_param("page")) {
+        page = std::stoi(req.get_param_value("page"));
+    }
+    if (req.has_param("pageSize")) {
+        page_size = std::stoi(req.get_param_value("pageSize"));
+    }
+    options.page = page;
+    options.page_size = page_size;
+    options.sorts.push_back({"updatedAt", false});
+
+    auto result = storage_.query("workflows", options);
+    if (result.failed()) {
+        sendError(res, result.error().message(), 500);
+        return;
+    }
+
+    nlohmann::json response;
+    response["workflows"] = result.value().documents;
+    response["total"] = result.value().total_count;
+    response["page"] = page;
+    response["pageSize"] = page_size;
+    response["hasMore"] = result.value().has_more;
+
+    sendJson(res, response);
+}
+
+void WorkflowController::getWorkflow(const httplib::Request& req, httplib::Response& res,
+                                      const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    auto result = storage_.get("workflows", id);
+    if (result.failed()) {
+        sendError(res, "Workflow not found", 404);
+        return;
+    }
+
+    sendJson(res, result.value());
+}
+
+void WorkflowController::createWorkflow(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    try {
+        auto body = nlohmann::json::parse(req.body);
+
+        // Generate ID
+        std::string id = UUID::generatePrefixed("wf");
+
+        // Set owner - metadata fields (_id, _createdAt, _updatedAt, _version) are handled by database
+        body["ownerId"] = ctx.user_id;
+        body["active"] = body.value("active", false);
+
+        // Validate required fields
+        if (!body.contains("name") || body["name"].get<std::string>().empty()) {
+            sendError(res, "Name is required", 400);
+            return;
+        }
+
+        auto result = storage_.insert("workflows", body, id);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 500);
+            return;
+        }
+
+        // Get the created workflow with metadata
+        auto workflow = storage_.get("workflows", id);
+        if (workflow.failed()) {
+            sendError(res, "Failed to retrieve created workflow", 500);
+            return;
+        }
+
+        // Broadcast to WebSocket clients
+        ws_server_.broadcast("workflows.created", workflow.value());
+
+        LOG_INFO("Workflow created: {} by user {}", id, ctx.user_id);
+        sendJson(res, workflow.value(), 201);
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void WorkflowController::updateWorkflow(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    try {
+        std::string id = req.matches[1];
+        auto body = nlohmann::json::parse(req.body);
+
+        // Prevent updating metadata fields (managed by database)
+        body.erase("id");
+        body.erase("_id");
+        body.erase("ownerId");
+        body.erase("createdAt");
+        body.erase("_createdAt");
+        body.erase("updatedAt");
+        body.erase("_updatedAt");
+        body.erase("version");
+        body.erase("_version");
+
+        // Note: updatedAt is managed by database automatically
+
+        auto result = storage_.update("workflows", id, body, 0, true);
+        if (result.failed()) {
+            sendError(res, result.error().message(), 404);
+            return;
+        }
+
+        // Get updated workflow
+        auto workflow = storage_.get("workflows", id);
+        if (workflow.ok()) {
+            ws_server_.broadcast("workflows.updated", workflow.value());
+            sendJson(res, workflow.value());
+        } else {
+            sendJson(res, {{"id", id}, {"version", result.value()}});
+        }
+    } catch (const std::exception& e) {
+        sendError(res, "Invalid request body", 400);
+    }
+}
+
+void WorkflowController::deleteWorkflow(const httplib::Request& req, httplib::Response& res,
+                                         const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    auto result = storage_.remove("workflows", id);
+    if (result.failed()) {
+        sendError(res, "Workflow not found", 404);
+        return;
+    }
+
+    ws_server_.broadcast("workflows.deleted", {{"id", id}});
+
+    LOG_INFO("Workflow deleted: {} by user {}", id, ctx.user_id);
+    sendJson(res, {{"success", true}});
+}
+
+void WorkflowController::executeWorkflow(const httplib::Request& req, httplib::Response& res,
+                                          const auth::AuthContext& ctx) {
+    std::string workflow_id = req.matches[1];
+
+    // Get workflow
+    auto workflow_result = storage_.get("workflows", workflow_id);
+    if (workflow_result.failed()) {
+        sendError(res, "Workflow not found", 404);
+        return;
+    }
+
+    auto& workflow = workflow_result.value();
+
+    // Select runner
+    auto runner = load_balancer_.selectRunner();
+    if (!runner) {
+        sendError(res, "No runners available", 503);
+        return;
+    }
+
+    // Parse trigger data from body
+    nlohmann::json trigger_data;
+    try {
+        if (!req.body.empty()) {
+            trigger_data = nlohmann::json::parse(req.body);
+        }
+    } catch (...) {}
+
+    // Call runner to execute workflow
+    auto channel = grpc::CreateChannel(runner->address, grpc::InsecureChannelCredentials());
+    auto stub = proto::RunnerService::NewStub(channel);
+
+    proto::ExecuteWorkflowRequest grpc_req;
+    grpc_req.set_workflow_id(workflow_id);
+    grpc_req.set_trigger_type("manual");
+    grpc_req.set_trigger_data(trigger_data.dump());
+    grpc_req.set_wait_for_completion(false);
+
+    proto::ExecuteWorkflowResponse grpc_res;
+    grpc::ClientContext grpc_ctx;
+    grpc_ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));
+
+    auto status = stub->ExecuteWorkflow(&grpc_ctx, grpc_req, &grpc_res);
+
+    if (!status.ok()) {
+        LOG_ERROR("Failed to execute workflow: {}", status.error_message());
+        sendError(res, "Failed to execute workflow: " + status.error_message(), 500);
+        return;
+    }
+
+    nlohmann::json response;
+    response["executionId"] = grpc_res.execution_id();
+    response["status"] = grpc_res.status();
+    response["runnerId"] = runner->id;
+
+    // Broadcast execution started
+    ws_server_.broadcast("executions." + grpc_res.execution_id() + ".started", {
+        {"executionId", grpc_res.execution_id()},
+        {"workflowId", workflow_id},
+        {"runnerId", runner->id}
+    });
+
+    LOG_INFO("Workflow {} execution started: {} on runner {}",
+             workflow_id, grpc_res.execution_id(), runner->id);
+    sendJson(res, response, 202);
+}
+
+void WorkflowController::activateWorkflow(const httplib::Request& req, httplib::Response& res,
+                                           const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    auto result = storage_.update("workflows", id, {{"active", true}, {"updatedAt", TimeUtils::nowMs()}}, 0, true);
+    if (result.failed()) {
+        sendError(res, "Workflow not found", 404);
+        return;
+    }
+
+    ws_server_.broadcast("workflows.activated", {{"id", id}});
+    sendJson(res, {{"success", true}, {"active", true}});
+}
+
+void WorkflowController::deactivateWorkflow(const httplib::Request& req, httplib::Response& res,
+                                             const auth::AuthContext& ctx) {
+    std::string id = req.matches[1];
+
+    auto result = storage_.update("workflows", id, {{"active", false}, {"updatedAt", TimeUtils::nowMs()}}, 0, true);
+    if (result.failed()) {
+        sendError(res, "Workflow not found", 404);
+        return;
+    }
+
+    ws_server_.broadcast("workflows.deactivated", {{"id", id}});
+    sendJson(res, {{"success", true}, {"active", false}});
+}
+
+void WorkflowController::sendJson(httplib::Response& res, const nlohmann::json& data, int status) {
+    res.status = status;
+    res.set_content(data.dump(), "application/json");
+}
+
+void WorkflowController::sendError(httplib::Response& res, const std::string& message, int status) {
+    res.status = status;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::api

+ 55 - 0
src/webserver/api/workflow_controller.hpp

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <httplib.h>
+#include <nlohmann/json.hpp>
+#include "../auth/auth_middleware.hpp"
+#include "storage/storage_client.hpp"
+#include "../runners/runner_registry.hpp"
+#include "../runners/load_balancer.hpp"
+#include "../websocket_server.hpp"
+#include "proto/runner.grpc.pb.h"
+
+namespace smartbotic::webserver::api {
+
+class WorkflowController {
+public:
+    WorkflowController(storage::StorageClient& storage,
+                      auth::AuthMiddleware& middleware,
+                      runners::RunnerRegistry& registry,
+                      runners::LoadBalancer& load_balancer,
+                      WebSocketServer& ws_server);
+
+    void registerRoutes(httplib::Server& server);
+
+private:
+    // CRUD operations
+    void listWorkflows(const httplib::Request& req, httplib::Response& res,
+                       const auth::AuthContext& ctx);
+    void getWorkflow(const httplib::Request& req, httplib::Response& res,
+                     const auth::AuthContext& ctx);
+    void createWorkflow(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+    void updateWorkflow(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+    void deleteWorkflow(const httplib::Request& req, httplib::Response& res,
+                        const auth::AuthContext& ctx);
+
+    // Workflow operations
+    void executeWorkflow(const httplib::Request& req, httplib::Response& res,
+                         const auth::AuthContext& ctx);
+    void activateWorkflow(const httplib::Request& req, httplib::Response& res,
+                          const auth::AuthContext& ctx);
+    void deactivateWorkflow(const httplib::Request& req, httplib::Response& res,
+                            const auth::AuthContext& ctx);
+
+    void sendJson(httplib::Response& res, const nlohmann::json& data, int status = 200);
+    void sendError(httplib::Response& res, const std::string& message, int status);
+
+    storage::StorageClient& storage_;
+    auth::AuthMiddleware& middleware_;
+    runners::RunnerRegistry& registry_;
+    runners::LoadBalancer& load_balancer_;
+    WebSocketServer& ws_server_;
+};
+
+} // namespace smartbotic::webserver::api

+ 101 - 0
src/webserver/auth/auth_middleware.cpp

@@ -0,0 +1,101 @@
+#include "auth_middleware.hpp"
+#include <nlohmann/json.hpp>
+
+namespace smartbotic::webserver::auth {
+
+AuthMiddleware::AuthMiddleware(JwtUtils& jwt, AuthStore& auth_store)
+    : jwt_(jwt), auth_store_(auth_store) {}
+
+AuthContext AuthMiddleware::authenticate(const httplib::Request& req) {
+    AuthContext ctx;
+
+    // Get Authorization header
+    auto auth_header = req.get_header_value("Authorization");
+    if (auth_header.empty()) {
+        return ctx;
+    }
+
+    // Extract bearer token
+    auto token = JwtUtils::extractBearerToken(auth_header);
+    if (!token) {
+        return ctx;
+    }
+
+    // Verify token
+    auto payload_result = jwt_.verifyToken(*token);
+    if (payload_result.failed()) {
+        return ctx;
+    }
+
+    auto& payload = payload_result.value();
+
+    // Check token type
+    if (payload.token_type != "access") {
+        return ctx;
+    }
+
+    ctx.user_id = payload.user_id;
+    ctx.username = payload.username;
+    ctx.role = payload.role;
+    ctx.authenticated = true;
+
+    return ctx;
+}
+
+bool AuthMiddleware::hasRole(const AuthContext& ctx, const std::string& required_role) {
+    if (!ctx.authenticated) {
+        return false;
+    }
+
+    // Admin has all roles
+    if (ctx.role == "admin") {
+        return true;
+    }
+
+    return ctx.role == required_role;
+}
+
+void AuthMiddleware::requireAuth(const httplib::Request& req, httplib::Response& res,
+                                 Handler handler) {
+    auto ctx = authenticate(req);
+    if (!ctx.authenticated) {
+        sendUnauthorized(res, "Authentication required");
+        return;
+    }
+
+    handler(req, res, ctx);
+}
+
+void AuthMiddleware::requireRole(const httplib::Request& req, httplib::Response& res,
+                                  const std::string& role, Handler handler) {
+    auto ctx = authenticate(req);
+    if (!ctx.authenticated) {
+        sendUnauthorized(res, "Authentication required");
+        return;
+    }
+
+    if (!hasRole(ctx, role)) {
+        sendForbidden(res, "Insufficient permissions");
+        return;
+    }
+
+    handler(req, res, ctx);
+}
+
+void AuthMiddleware::optionalAuth(const httplib::Request& req, httplib::Response& res,
+                                   Handler handler) {
+    auto ctx = authenticate(req);
+    handler(req, res, ctx);
+}
+
+void AuthMiddleware::sendUnauthorized(httplib::Response& res, const std::string& message) {
+    res.status = 401;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+void AuthMiddleware::sendForbidden(httplib::Response& res, const std::string& message) {
+    res.status = 403;
+    res.set_content(nlohmann::json{{"error", message}}.dump(), "application/json");
+}
+
+} // namespace smartbotic::webserver::auth

+ 54 - 0
src/webserver/auth/auth_middleware.hpp

@@ -0,0 +1,54 @@
+#pragma once
+
+#include <string>
+#include <functional>
+#include <optional>
+#include <httplib.h>
+#include "jwt_utils.hpp"
+#include "auth_store.hpp"
+
+namespace smartbotic::webserver::auth {
+
+// Request context with authenticated user info
+struct AuthContext {
+    std::string user_id;
+    std::string username;
+    std::string role;
+    bool authenticated = false;
+};
+
+// Auth middleware
+class AuthMiddleware {
+public:
+    AuthMiddleware(JwtUtils& jwt, AuthStore& auth_store);
+
+    // Authenticate request from Authorization header
+    AuthContext authenticate(const httplib::Request& req);
+
+    // Check if user has required role
+    bool hasRole(const AuthContext& ctx, const std::string& required_role);
+
+    // Middleware handlers
+    using Handler = std::function<void(const httplib::Request&, httplib::Response&, const AuthContext&)>;
+
+    // Require authentication
+    void requireAuth(const httplib::Request& req, httplib::Response& res,
+                    Handler handler);
+
+    // Require specific role
+    void requireRole(const httplib::Request& req, httplib::Response& res,
+                    const std::string& role, Handler handler);
+
+    // Optional auth (sets context but doesn't require it)
+    void optionalAuth(const httplib::Request& req, httplib::Response& res,
+                     Handler handler);
+
+private:
+    void sendUnauthorized(httplib::Response& res, const std::string& message);
+    void sendForbidden(httplib::Response& res, const std::string& message);
+
+    JwtUtils& jwt_;
+    AuthStore& auth_store_;
+};
+
+} // namespace smartbotic::webserver::auth

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff