# How Workflows Work in Twenty CRM: A Complete Technical Guide to Automation

> Explore Twenty CRM workflows: a technical guide to automation. Understand how declarative, versioned graphs trigger and execute actions step-by-step with a robust backend engine.

- Repository: [Twenty/twenty](https://github.com/twentyhq/twenty)
- Tags: deep-dive
- Published: 2026-03-27

---

**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:

1. **Trigger definition** – A user configures a start condition (record creation, manual button, cron schedule, or webhook).
2. **Version activation** – The selected workflow version is marked **ACTIVE** and automated infrastructure (cron jobs, database listeners, or command-menu items) is provisioned.
3. **Run creation** – When the trigger fires, the system creates a `WorkflowRun` entity with status `ENQUEUED` (for manual triggers) or `NOT_STARTED` (for automated triggers).
4. **Runner execution** – The `WorkflowRunnerWorkspaceService` picks the run from the queue, checks throttling limits, and hands control to the executor.
5. **Step-by-step execution** – The `WorkflowExecutorWorkspaceService` walks the flow graph, invoking concrete actions while persisting step status (`RUNNING`, `SUCCESS`, `FAILED_SAFELY`).
6. **Finalization** – The runner computes the final status (`COMPLETED`, `FAILED`, or `STOPPED`) 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`](https://github.com/twentyhq/twenty/blob/main/workflow-trigger.workspace-service.ts) |
| **WorkflowRunnerWorkspaceService** | Entry point for new runs, handles throttling, creates `WorkflowRun` records, and queues jobs. | [`workflow-runner.workspace-service.ts`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/workflow-action.factory.ts) |
| **WorkflowAction Interface** | Guarantees each action implements `execute(input): Promise<WorkflowActionOutput>`. | [`workflow-action.interface.ts`](https://github.com/twentyhq/twenty/blob/main/workflow-action.interface.ts) |
| **Trigger Types** | Enum describing every possible start condition (`MANUAL`, `CRON`, `WEBHOOK`, `DATABASE_EVENT`). | [`workflow-trigger.type.ts`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/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.buildCodeStepsFromSourceForSteps` to compile any code steps.
- **Status update**: It updates the `WorkflowVersion` status to `ACTIVE`.
- **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 through `enableAutomatedTrigger`.

```typescript
// 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`](https://github.com/twentyhq/twenty/blob/main/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 to `ENQUEUED` and adding a `RunWorkflowJob` to `MessageQueue.workflowQueue`.
- **Automated triggers**: Calls `createNotStartedWorkflowRunAndTriggerEnqueueJob`, setting status to `NOT_STARTED`.

```typescript
// 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`](https://github.com/twentyhq/twenty/blob/main/workflow-executor.workspace-service.ts) contains the core execution logic. Its `executeFromStep` method processes each step recursively:

1. **Load state**: Retrieves the `WorkflowRun` and current step definition.
2. **Skip/fail-safe check**: Calls `shouldExecuteStep` to respect conditional logic.
3. **Action resolution**: Uses `WorkflowActionFactory.get(step.type)` to obtain the concrete implementation.
4. **Execution**: Invokes `workflowAction.execute()` with the current context and run metadata.
5. **Persistence**: Updates step status via `updateWorkflowRunStepInfo`.
6. **Continuation**: Computes next step IDs via `getNextStepIdsToExecute` and 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`.

```typescript
// 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`](https://github.com/twentyhq/twenty/blob/main/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`:

```typescript
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:**
1. `runWorkflowVersion` validates the version and forwards to `WorkflowRunnerWorkspaceService.run`.
2. `run` creates a `WorkflowRun` (status `ENQUEUED`) and adds a `RunWorkflowJob` to the queue.
3. `RunWorkflowJob` invokes `WorkflowExecutorWorkspaceService.executeFromSteps`.
4. The executor walks the graph until reaching a terminal state.
5. `WorkflowRunWorkspaceService` persists the final status (`COMPLETED` or `FAILED`).

## Summary

- **Declarative architecture**: Workflows are versioned graphs stored as JSON definitions, activated through `WorkflowTriggerWorkspaceService.activateWorkflowVersion`.
- **Queued execution**: Runs are created with status `ENQUEUED` (manual) or `NOT_STARTED` (auto) and processed asynchronously via `MessageQueue.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 `WorkflowActionFactory` maps step types (`CODE`, `IF_ELSE`, `ITERATOR`, etc.) to implementations conforming to the `WorkflowAction` interface.
- **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`](https://github.com/twentyhq/twenty/blob/main/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.