How Workflows Work in Twenty CRM: A Complete Technical Guide to Automation
Twenty CRM workflows are declarative, versioned graphs of actions triggered by events (manual, cron, webhooks, or database changes) and executed step-by-step by a backend engine that queues runs, walks the flow graph, and persists results with built-in throttling and billing.
Workflows in Twenty CRM power the platform’s no-code automation capabilities, allowing users to chain actions like sending emails, executing code, or branching logic without writing backend infrastructure. According to the twentyhq/twenty source code, the entire automation engine lives in the twenty-server package under modules/workflow and follows a strict six-phase lifecycle from trigger definition to final status computation.
The Workflow Lifecycle: From Trigger to Completion
The automation engine processes every workflow through six distinct phases:
- Trigger definition – A user configures a start condition (record creation, manual button, cron schedule, or webhook).
- Version activation – The selected workflow version is marked ACTIVE and automated infrastructure (cron jobs, database listeners, or command-menu items) is provisioned.
- Run creation – When the trigger fires, the system creates a
WorkflowRunentity with statusENQUEUED(for manual triggers) orNOT_STARTED(for automated triggers). - Runner execution – The
WorkflowRunnerWorkspaceServicepicks the run from the queue, checks throttling limits, and hands control to the executor. - Step-by-step execution – The
WorkflowExecutorWorkspaceServicewalks the flow graph, invoking concrete actions while persisting step status (RUNNING,SUCCESS,FAILED_SAFELY). - Finalization – The runner computes the final status (
COMPLETED,FAILED, orSTOPPED) and emits usage events for billing.
Core Components of the Automation Engine
| Component | Responsibility | Key Source File |
|---|---|---|
| WorkflowTriggerWorkspaceService | Validates triggers, activates/deactivates versions, creates command-menu items for manual triggers, registers cron/DB listeners. | workflow-trigger.workspace-service.ts |
| WorkflowRunnerWorkspaceService | Entry point for new runs, handles throttling, creates WorkflowRun records, and queues jobs. |
workflow-runner.workspace-service.ts |
| WorkflowExecutorWorkspaceService | Core engine that walks the flow graph, executes steps via the WorkflowActionFactory, updates step infos, and decides next steps. |
workflow-executor.workspace-service.ts |
| WorkflowActionFactory | Maps a step’s type (e.g., CODE, IF_ELSE, ITERATOR, SEND_EMAIL) to a concrete WorkflowAction implementation. |
workflow-action.factory.ts |
| WorkflowAction Interface | Guarantees each action implements execute(input): Promise<WorkflowActionOutput>. |
workflow-action.interface.ts |
| Trigger Types | Enum describing every possible start condition (MANUAL, CRON, WEBHOOK, DATABASE_EVENT). |
workflow-trigger.type.ts |
Workflow Activation and Version Management
When a user publishes a workflow version, the system calls WorkflowTriggerWorkspaceService.activateWorkflowVersion in packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts.
This method performs three critical operations:
- Validation and building: It validates the version and uses
codeStepBuildService.buildCodeStepsFromSourceForStepsto compile any code steps. - Status update: It updates the
WorkflowVersionstatus toACTIVE. - Infrastructure provisioning: For manual triggers, it creates a command-menu entry via
createOrUpdateCommandMenuItem. For automated triggers (cron or database events), it registers the appropriate listener throughenableAutomatedTrigger.
// From activateWorkflowVersion in workflow-trigger.workspace-service.ts
await this.codeStepBuildService.buildCodeStepsFromSourceForSteps({
workspaceId,
steps: workflowVersion.steps ?? [],
});
await this.performActivationSteps(
workflow,
workflowVersion,
workflowRepository,
workflowVersionRepository,
workspaceId,
);
Run Creation, Throttling, and Queueing
The WorkflowRunnerWorkspaceService.run method in workflow-runner.workspace-service.ts serves as the entry point for executing workflows. It implements hard throttling logic and distinguishes between manual and automated trigger paths:
- Hard throttling: If limits are exceeded, it creates a failed run via
createFailedWorkflowRun. - Manual triggers: Calls
enqueueWorkflowRun, setting status toENQUEUEDand adding aRunWorkflowJobtoMessageQueue.workflowQueue. - Automated triggers: Calls
createNotStartedWorkflowRunAndTriggerEnqueueJob, setting status toNOT_STARTED.
// Path selection logic from workflow-runner.workspace-service.ts
if (isManualTrigger) {
return this.enqueueWorkflowRun({ /* … */ });
}
return this.createNotStartedWorkflowRunAndTriggerEnqueueJob({ /* … */ });
The Execution Engine: Walking the Flow Graph
The WorkflowExecutorWorkspaceService in workflow-executor.workspace-service.ts contains the core execution logic. Its executeFromStep method processes each step recursively:
- Load state: Retrieves the
WorkflowRunand current step definition. - Skip/fail-safe check: Calls
shouldExecuteStepto respect conditional logic. - Action resolution: Uses
WorkflowActionFactory.get(step.type)to obtain the concrete implementation. - Execution: Invokes
workflowAction.execute()with the current context and run metadata. - Persistence: Updates step status via
updateWorkflowRunStepInfo. - Continuation: Computes next step IDs via
getNextStepIdsToExecuteand recurses until completion.
To prevent long-running workflows from blocking the queue, the executor enforces a maximum of 20 steps per job (MAX_EXECUTED_STEPS_COUNT = 20). If exceeded, it schedules a continuation job via continueExecutionFromStepInAnotherJob.
// Execution flow from workflow-executor.workspace-service.ts
const workflowAction = this.workflowActionFactory.get(step.type);
await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({
stepId,
stepInfo: { ...stepInfos[stepId], status: StepStatus.RUNNING },
workflowRunId,
workspaceId,
});
actionOutput = await workflowAction.execute({
currentStepId: stepId,
steps,
context: getWorkflowRunContext(stepInfos),
runInfo: { workflowRunId, workspaceId },
});
After execution completes, computeWorkflowRunStatus determines the final state and emits a USAGE_RECORDED event via sendWorkflowNodeRunEvent for billing purposes.
Supported Trigger Types
Twenty CRM supports five primary trigger types defined in workflow-trigger.type.ts:
- MANUAL: User-initiated via command menu or button click.
- CRON: Time-based scheduled execution.
- WEBHOOK: External HTTP requests initiating flows.
- DATABASE_EVENT: Record creation, updates, or deletions.
- RECORD_CRUD: Specific CRUD operations on CRM records.
Each trigger type requires different activation logic in WorkflowTriggerWorkspaceService, with manual triggers creating command-menu items and automated triggers registering event listeners or cron schedules.
Practical Example: Programmatically Triggering a Workflow
You can manually trigger a workflow version programmatically using the WorkflowTriggerWorkspaceService:
import { WorkflowTriggerWorkspaceService } from '@/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
import { ActorMetadata } from 'twenty-shared/types';
async function startManualWorkflow(
workspaceId: string,
workflowVersionId: string,
payload: Record<string, unknown>,
user: ActorMetadata,
) {
const triggerService = new WorkflowTriggerWorkspaceService(/* NestJS injected deps */);
// runWorkflowVersion creates a run and immediately enqueues it
const workflowRunId = await triggerService.runWorkflowVersion({
workspaceId,
workflowVersionId,
payload,
createdBy: user,
});
console.log('Workflow run queued:', workflowRunId);
}
Execution flow:
runWorkflowVersionvalidates the version and forwards toWorkflowRunnerWorkspaceService.run.runcreates aWorkflowRun(statusENQUEUED) and adds aRunWorkflowJobto the queue.RunWorkflowJobinvokesWorkflowExecutorWorkspaceService.executeFromSteps.- The executor walks the graph until reaching a terminal state.
WorkflowRunWorkspaceServicepersists the final status (COMPLETEDorFAILED).
Summary
- Declarative architecture: Workflows are versioned graphs stored as JSON definitions, activated through
WorkflowTriggerWorkspaceService.activateWorkflowVersion. - Queued execution: Runs are created with status
ENQUEUED(manual) orNOT_STARTED(auto) and processed asynchronously viaMessageQueue.workflowQueue. - Step-limited execution: The executor processes maximum 20 steps per job to prevent queue blocking, automatically scheduling continuation jobs for longer workflows.
- Extensible actions: The
WorkflowActionFactorymaps step types (CODE,IF_ELSE,ITERATOR, etc.) to implementations conforming to theWorkflowActioninterface. - Built-in billing: The system emits usage events after execution for per-node billing tracking.
Frequently Asked Questions
What happens if a workflow step fails during execution?
If a step fails, the WorkflowExecutorWorkspaceService captures the error and updates the step status to FAILED_SAFELY via updateWorkflowRunStepInfo. Depending on the workflow configuration, the executor either stops the run (resulting in final status FAILED) or continues to the next step if error handling is configured. The final status is computed by computeWorkflowRunStatus after all possible steps complete.
How does Twenty CRM handle long-running workflows that might block the queue?
The execution engine enforces a hard limit of 20 steps per job (MAX_EXECUTED_STEPS_COUNT = 20). When a workflow exceeds this limit, the WorkflowExecutorWorkspaceService calls continueExecutionFromStepInAnotherJob to schedule the remaining steps in a new queue job. This ensures that long-running automation flows do not monopolize the MessageQueue.workflowQueue and degrade system performance.
Can workflows be triggered by external webhooks or only internal events?
Yes, Twenty CRM supports external webhook triggers. The WorkflowTriggerType enum in workflow-trigger.type.ts includes WEBHOOK as a valid trigger type. When activated via WorkflowTriggerWorkspaceService, webhook triggers register URL endpoints that external systems can call to initiate workflow runs, which are then processed through the same runner and executor services as database or cron triggers.
What is the difference between ENQUEUED and NOT_STARTED workflow run statuses?
ENQUEUED status is assigned to runs triggered manually via runWorkflowVersion, indicating they are waiting in MessageQueue.workflowQueue for immediate processing. NOT_STARTED is used for automated triggers (cron, database events), where the run record is created first and then a separate enqueue job (WorkflowRunEnqueueJob) is scheduled to add it to the processing queue. This distinction allows the system to handle throttling and manual approval flows differently from automatic event-driven execution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →