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
- Add the value to
enums/trigger-type.enum.ts. - Create
trigger-evaluators/<name>.evaluator.tsimplementingITriggerEvaluator(readonly triggerType,evaluate(config, context): Promise<boolean>). - Register it: add it to
WorkflowModuleproviders and to theTriggerEvaluatorRegistryconstructor (it self-registers bytriggerType). - Add its config interface to
interfaces/trigger-config.interface.tsand, if the event source differs, teachWorkflowEngineService.preFilterMatch(data events) orWorkflowMessageListener(message events) how to route it.
Add an action executor
- Add the value to
enums/action-type.enum.ts. - Create
action-executors/<name>.executor.tsimplementingIActionExecutor(readonly actionType,execute(config, context): Promise<ActionResult>). - Add its config to
interfaces/action-config.interface.ts, register inWorkflowModule+ActionExecutorRegistry. - 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 {{token}} templates for every action.
Services
| Service | Job |
|---|---|
WorkflowService | CRUD, activation gate (toggle), slug + limit checks, step replace |
WorkflowEngineService | Data-event listener; loads active workflows (Redis cache), pre-filters, enqueues |
WorkflowProcessor | Bull worker: the run pipeline (all handlers @TenantScoped()) |
WorkflowStepExecutorService | Runs steps[] (Action / Delay / Condition / Approval) with branching |
WorkflowSchedulerService | Registers/removes repeatable cron jobs for scheduled workflows |
WorkflowSessionLinkValidator | The activation gate for message-triggered workflows |
WorkflowExecutionService / WorkflowStatsService | History queries, retry, per-workflow + overview stats |
WorkflowMetaService | Builder metadata (triggers, actions, operators, object fields, placeholders) |
Database tables
| Table | Holds |
|---|---|
workflows | Definition: trigger_type/config, filter_config, action_type/config, is_active, execution_count |
workflow_steps | Multi-step sequence (step_order, step_type, step_config) |
workflow_executions | Run history (status, trigger_snapshot, filter_result, action_result, error_*, duration_ms) |
workflow_templates | Pre-built gallery templates (system + shared) |
workflow_approvals | Pending 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 WorkflowExecutionListener →
WorkflowEventsGateway (WebSocket):
workflow.execution.completed/.failed/.skippedworkflow.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.