Corteksa
GuidesAutomation

Backend Integration

Automation — Backend Integration

For backend developers extending or maintaining the workflow engine. The engine is registry-driven: a new trigger, action, or variable is a self-registering class plus a module provider entry — annotate, don't wire.

Add a trigger evaluator

  1. Add the value to enums/trigger-type.enum.ts.
  2. Create trigger-evaluators/<name>.evaluator.ts implementing ITriggerEvaluator (readonly triggerType, evaluate(config, context): Promise<boolean>).
  3. Register it: add it to WorkflowModule providers and to the TriggerEvaluatorRegistry constructor (it self-registers by triggerType).
  4. Add its config interface to interfaces/trigger-config.interface.ts and, if the event source differs, teach WorkflowEngineService.preFilterMatch (data events) or WorkflowMessageListener (message events) how to route it.

Add an action executor

  1. Add the value to enums/action-type.enum.ts.
  2. Create action-executors/<name>.executor.ts implementing IActionExecutor (readonly actionType, execute(config, context): Promise<ActionResult>).
  3. Add its config to interfaces/action-config.interface.ts, register in WorkflowModule + ActionExecutorRegistry.
  4. If the action mutates CRM data, write through the records/messaging modules with sourceType: 'automation' so the resulting data event is skipped by the engine (loop prevention). Return { success, detail, error? }.

ActionExecutorRegistry.execute enriches context.record with any {{relation.field}} tokens (via RelationVariableService) before your executor interpolates, so parent-record fields are uniformly available.

Add a variable resolver

Implement IVariableResolver (category, canResolve, resolve) and add it to VariableResolverRegistry in resolution order (record → trigger → message → system). The registry interpolates &#123;&#123;token&#125;&#125; templates for every action.

Services

ServiceJob
WorkflowServiceCRUD, activation gate (toggle), slug + limit checks, step replace
WorkflowEngineServiceData-event listener; loads active workflows (Redis cache), pre-filters, enqueues
WorkflowProcessorBull worker: the run pipeline (all handlers @TenantScoped())
WorkflowStepExecutorServiceRuns steps[] (Action / Delay / Condition / Approval) with branching
WorkflowSchedulerServiceRegisters/removes repeatable cron jobs for scheduled workflows
WorkflowSessionLinkValidatorThe activation gate for message-triggered workflows
WorkflowExecutionService / WorkflowStatsServiceHistory queries, retry, per-workflow + overview stats
WorkflowMetaServiceBuilder metadata (triggers, actions, operators, object fields, placeholders)

Database tables

TableHolds
workflowsDefinition: trigger_type/config, filter_config, action_type/config, is_active, execution_count
workflow_stepsMulti-step sequence (step_order, step_type, step_config)
workflow_executionsRun history (status, trigger_snapshot, filter_result, action_result, error_*, duration_ms)
workflow_templatesPre-built gallery templates (system + shared)
workflow_approvalsPending approval gates (approver_admin_ids, status, expires_at)

Every table carries a nullable workspace_id for hyper-tenant scoping.

Queue & jobs

One Bull queue, workflow (WORKFLOW_QUEUE), attempts: 3, exponential backoff 2 s. Handlers: execute-workflow, execute-scheduled-workflow, execute-scheduled-action, resume-workflow-step.

Events

The processor emits internal events consumed by WorkflowExecutionListenerWorkflowEventsGateway (WebSocket):

  • workflow.execution.completed / .failed / .skipped
  • workflow.approval.resolved (approve/reject)

See Events.

Multi-tenant safety

The engine runs on the shared hyper-tenant DB. Every processor handler is @TenantScoped() and all DB access goes through repoProvider (never a raw DataSource — it would bypass the app.workspace_id GUC and read/write zero rows on hyper). WorkflowService.replaceSteps uses repoProvider.transaction for the same reason. See Data isolation before touching any query or worker.

On this page