Corteksa
GuidesAutomation

Architecture

Automation — Architecture

The engine plugs into the CRM's existing event bus. Everything is async: an event enqueues a Bull job, and a worker evaluates and runs the workflow so a slow action never blocks the request that caused it.

Trigger → action flow

Event source ──▶ Engine (enqueue) ──▶ Bull queue 'workflow' ──▶ WorkflowProcessor
   data/msg/cron         pre-filter          (Redis, durable)     evaluate → run

WorkflowProcessor:
  lock (idempotency) → load workflow → build context → evaluate trigger
    → evaluate filter → resolve {{variables}} → execute action → run steps
    → write execution log → update stats → emit WS event → release lock

Event sources (how a workflow gets enqueued)

SourceWhereJob
Data eventsWorkflowEngineService @OnEvent(DATA_EVENTS.CREATED/UPDATED)execute-workflow
Message eventsWorkflowMessageListener (Redis pub/sub on messaging channels)execute-workflow
ScheduledWorkflowSchedulerService (Bull repeatable cron job)execute-scheduled-workflow

The engine loads active workflows (cached in Redis, 5-min TTL), runs a cheap in-process pre-filter (matching trigger_type + object_slug), and enqueues one job per candidate. Data events with sourceType === 'automation' are skipped (loop prevention — an action's own write can't re-trigger it).

Registry pattern

Triggers, actions, and variables are pluggable strategies, each self-registering into a registry keyed by its type — annotate, don't wire:

RegistryKeyed byMembers
TriggerEvaluatorRegistryevaluator.triggerTypeRecordCreated, FieldChanged, StatusChanged, MessageReceived, FirstMessage, Scheduled
ActionExecutorRegistryexecutor.actionTypeUpdateStatus, UpdateField, SendMessage, SendTemplate, SendWebhook, CreateRecord, GenerateRecords, Noop
VariableResolverRegistryresolution orderRecordTriggerMessageSystem

Each registry ingests its members in its constructor and dispatches by type at runtime. Adding a trigger/action/resolver is a new class + a module provider entry — no switch to edit.

The processor

WorkflowProcessor (Bull @Processor('workflow')) owns four job handlers, all @TenantScoped() so DB access is workspace-pinned on hyper-tenant:

JobPurpose
execute-workflowData- and message-triggered runs
execute-scheduled-workflowA cron tick: fire once, or scan an object's records for date reminders
execute-scheduled-actionA single paced send enqueued by the scheduled scan (anti-ban pacing)
resume-workflow-stepResume a multi-step flow after a Delay or an Approval

Each run writes a workflow_executions row (completed / failed / skipped), updates execution_count / last_executed_at, meters the run against the workspace's billing allowance, and emits an execution event.

Scheduler

WorkflowSchedulerService registers a Bull repeatable cron job per active scheduled workflow (cron_expression + timezone). Activation/deactivation (toggle) and edits re-sync the schedule. On a tick with an object_slug, the processor pages the object's records, folds any reminder spec into a relative-date condition (within_next / within_past / on_date), de-dupes per occurrence in Redis, and fires the action for each match.

Dependency direction

Controllers → Services (Workflow/Execution/Template/Meta/Approval) → repoProvider → DB
Engine / Listener / Scheduler → Bull queue → WorkflowProcessor
   WorkflowProcessor → TriggerEvaluatorRegistry → ITriggerEvaluator[]
                     → FilterEvaluatorService → OperatorEvaluatorService
                     → VariableResolverRegistry → IVariableResolver[]
                     → ActionExecutorRegistry → IActionExecutor[]
                          → MessagingModule (send) · RecordsModule (write) · HTTP (webhook)
   WorkflowProcessor → EventEmitter → WorkflowExecutionListener → WorkflowEventsGateway (WS)

The original design reference lives in src/api/v1/workflow/ARCHITECTURE.md (scheduled reminders are documented in SCHEDULED-REMINDERS.md).

Next

On this page