Title: Actions System Architecture Description: Technical overview of Actions system architecture and implementation patterns Tags: actions, architecture, technical --- Actions System Architecture This document provides a detailed technical overview of the Actions system architecture, including component relationships, data flow, and implementation patterns. High-Level Architecture ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Client │ │ Shared │ │ Server │ │ (Frontend) │ │ (Business │ │ (Backend) │ │ │ │ Logic) │ │ │ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ • Visual Editor │────│ • Action Types │────│ • Block Registry│ │ • Action UI │ │ • Executor │ │ • Server Blocks │ │ • Workflow View │ │ • Validation │ │ • Trigger Logic │ │ • CRUD Interface│ │ • Schema │ │ • REST API │ │ • DataView │ │ • TypeSchema │ │ • Persistence │ └─────────────────┘ └─────────────────┘ └─────────────────┘ Core Components Action Definition (packages/shared/src/actions/action.ts) The central interface that defines a complete workflow: interface Action extends TenantEntityWithAccess { title: string; // Required descriptive name description?: string; // Optional detailed explanation triggers?: ActionTrigger[]; // What starts the workflow blocks?: ActionBlock[]; // Processing units connections?: ActionSocketConnection[]; // Data flow } Key Properties: Inherits from TenantEntityWithAccess for multi-tenant support Now includes required title field for user identification Optional description field for detailed workflow documentation Contains arrays of triggers, blocks, and connections Represents a complete executable workflow Integrated with TypeSchema for automatic form generation Action Triggers (packages/shared/src/actions/action-trigger.ts) Define when workflows should execute: enum ActionTriggers { DOCUMENT_CREATED = "documentCreated", DOCUMENT_UPDATED = "documentUpdated", DOCUMENT_DELETED = "documentDeleted" } interface ActionTrigger { id: string; triggerName: string; // Must match ActionTriggers enum configuration?: { [key: string]: any }; } Trigger Data Flow: Document operation occurs (create/update/delete) Server detects change and identifies applicable Actions Executor invokes Actions with matching trigger names Trigger data flows to connected blocks Action Blocks (packages/shared/src/actions/action-block.ts) Reusable processing units that perform specific tasks: interface ActionBlock { id: string; blockName: string; // Must match registered block name configuration?: { [key: string]: any }; } interface ActionBlockImpl { execute: (socketName: string, data: any) => Promise; } Block Registry Pattern: Blocks are registered in ActionBlockRegistry by name Server blocks are registered in createServerBlockRegistry() Each block implements the ActionBlockImpl interface Blocks are stateless and tenant-aware Socket System (packages/shared/src/actions/action-socket.ts) Provides type-safe data flow between components: interface ActionSocket { name: string; type: ActionSocketType; // Data type validation direction: ActionSocketDirection; // IN or OUT } interface ActionSocketConnection { sourceBlockId: string; // Where data comes from sourceSocketName: string; // Output socket name targetBlockId: string; // Where data goes to targetSocketName: string; // Input socket name } Socket Types: Primitive: string, number, boolean Arrays: string[], number[], boolean[] Complex: object, object[] Special: null Action Executor (packages/shared/src/actions/action-executor.ts) Orchestrates workflow execution: interface ActionExecutor { executeActionsByTrigger({ triggerName, data }: { triggerName: string; data: any; }): Promise; } Execution Flow: Trigger Matching: Find Actions with matching trigger names Sequential Execution: Process each matching Action Connection Following: Execute connected blocks based on socket connections Error Handling: Capture errors and continue execution Logging: Record execution details for debugging User Interface Components Visual Flow Editor (apps/backoffice/src/routes/.../ActionFlowEditor.svelte) Provides an interactive canvas for workflow creation using @xyflow/svelte: interface FlowEditorProps { action: Action; onTriggerAdd: () => void; onBlockAdd: () => void; } Visual Elements: Nodes: Represent triggers (blue) and blocks (green) Edges: Show connections and data flow Canvas: Interactive area with zoom, pan, and drag capabilities Controls: Zoom controls and fit-to-view functionality Node Rendering: // Trigger nodes { id: trigger-${trigger.id}, type: 'default', position: { x: 100, y: 100 + index * 120 }, data: { label: 🔗 ${trigger.triggerName}, trigger } } // Block nodes { id: block-${block.id}, type: 'default', position: { x: 400, y: 100 + index * 120 }, data: { label: ⚙️ ${block.blockName}, block } } Backoffice CRUD Interface Complete management interface integrated with the project settings: Actions List Page (+page.svelte): Uses DataViewComponent with ActionSchema Displays title, description, and metadata Provides create, edit, and delete operations Integrated search and filtering Create/Edit Dialog (NewActionDialog.svelte): Uses FormComponent with ActionSchema Validates required title field Optional description field REST API integration for persistence REST API Endpoints: // CRUD operations GET /api/projects/[projectId]/actions POST /api/projects/[projectId]/actions GET /api/projects/[projectId]/actions/[actionId] PUT /api/projects/[projectId]/actions/[actionId] DELETE /api/projects/[projectId]/actions/[actionId] Component Selection System Modal-based selection for adding triggers and blocks: TriggerBlockSelectionModal.svelte: Trigger Options: Document Created/Updated/Deleted Block Options: Send Email, Update Database, Call Webhook, Conditional Logic Interactive Selection: Click-to-add interface Type Safety: Ensures valid trigger/block names Available Components: // Trigger Types const AVAILABLE_TRIGGERS = [ { name: "documentCreated", label: "Document Created", icon: "🔗" }, { name: "documentUpdated", label: "Document Updated", icon: "🔗" }, { name: "documentDeleted", label: "Document Deleted", icon: "🔗" } ]; // Block Types const AVAILABLE_BLOCKS = [ { name: "send-mail", label: "Send Email", icon: "⚙️" }, { name: "update-database", label: "Update Database", icon: "⚙️" }, { name: "call-webhook", label: "Call Webhook", icon: "⚙️" }, { name: "conditional-logic", label: "Conditional Logic", icon: "⚙️" } ]; Data Flow Architecture Trigger Execution Flow Document Change → Trigger Detection → Action Matching → Block Execution ↓ ↓ ↓ ↓ User Action Server Service Executor Logic Block Registry Block Connection Flow Trigger ↓ (connection: trigger → block1.in) Block 1 (execute) ↓ (result: block1.out → block2.in) Block 2 (execute) ↓ (result: block2.success → block3.in) Block 3 (execute) Error Handling Flow Block Execution Error ↓ Capture Error Message ↓ Log to ActionExecutionResult ↓ Continue with Next Block/Action ↓ Return All Results (success + failures) Implementation Patterns Registry Pattern Block Registration: // Server registry creation export function createServerBlockRegistry(): ActionBlockRegistry { const registry = new ActionBlockRegistry(); registry.registerBlock(BLOCKSENDMAIL, { execute: sendMailBlockFn }); return registry; } Usage Benefits: Decoupled block implementations Easy to add new block types Dynamic block discovery Testable in isolation Socket-Based Communication Type Safety: enum ActionSocketType { STRING = "string", OBJECT = "object", // ... other types } Connection Validation: Source and target socket types must be compatible Socket names must exist on respective blocks Connections form a directed acyclic graph (DAG) Async Execution Model Sequential Processing: Actions execute sequentially within a trigger Blocks within an Action execute based on connections Each block execution is awaited before proceeding Errors don't stop the entire execution chain Multi-Tenant Architecture Tenant Isolation: Actions inherit from TenantEntityWithAccess Block execution receives tenant context Data is scoped to the tenant Access control enforced at Action level Performance Considerations Execution Efficiency Blocks execute sequentially (not parallel) for predictable ordering Registry lookup is O(1) for block resolution Connection traversal is O(n) where n = number of connections Memory Usage Stateless block implementations Data flows through connections without persistence Execution results include only logs and final output Error Recovery Individual block failures don't halt entire workflows Execution continues with remaining Actions/blocks Comprehensive logging for debugging Extension Points Custom Blocks Implement ActionBlockImpl interface Register in appropriate registry (client/server) Define input/output sockets Handle configuration parameters Custom Triggers Add new trigger types to ActionTriggers enum Implement trigger detection logic Ensure proper data structure for trigger events Custom Socket Types Extend ActionSocketType enum Implement type validation Ensure serialization compatibility Testing Strategy Unit Tests Individual block implementations Registry functionality Socket type validation Connection logic Integration Tests Complete workflow execution Error handling scenarios Multi-tenant data isolation Performance under load End-to-End Tests UI workflow creation Trigger-to-completion flows Real-world usage scenarios Cross-component integration