# How to Integrate Custom Logic into Twenty CRM Workflows: A Complete Guide to Code Steps

> Integrate custom logic into Twenty CRM workflows with Code Steps. Execute sandboxed JavaScript/TypeScript snippets to enhance CRM data interactions and automate complex tasks efficiently.

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

---

**You can integrate custom logic into Twenty CRM workflows by creating Code Steps—sandboxed JavaScript/TypeScript snippets that execute within the Workflow Runner and interact with CRM data via injected services like `recordService` and `emailService`.**

Twenty CRM (twentyhq/twenty) provides a flexible workflow engine that lets you extend automation beyond standard actions. By leveraging **Code Steps** in workflow versions, you can inject custom business logic directly into the execution pipeline without redeploying the server. This guide walks through the exact GraphQL mutations, source file locations, and service APIs needed to implement custom logic safely within Twenty's sandboxed environment.

## Understanding the Twenty CRM Workflow Engine

The workflow system in `twentyhq/twenty` rests on three core entities defined in the server's standard objects layer:

- **Workflow** – A metadata container that groups versions and configuration, defined in [`packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts).
- **Workflow Version** – An immutable definition containing an ordered list of steps (including *code steps*) and a trigger, stored in [`packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts).
- **Workflow Run** – A single execution instance of a version that tracks status, timestamps, and output, located in [`packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts).

The **Workflow Runner** ([`packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts)) orchestrates execution by iterating over steps and invoking the compiled code inside a sandboxed VM.

## Step 1: Create a Workflow Version

Before adding logic, you need a workflow version to hold your steps. Use the GraphQL mutation defined in [`packages/twenty-server/src/modules/workflow/workflow-version/workflow-version.resolver.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/workflow-version/workflow-version.resolver.ts):

```graphql
mutation CreateWorkflowVersion($workflowId: ID!, $name: String!) {
  createWorkflowVersion(input: {
    workflowId: $workflowId
    name: $name
    trigger: { type: MANUAL }
  }) {
    id
    name
  }
}

```

Retain the returned `id` as your `workflowVersionId` for the next steps.

## Step 2: Add a Code Step with Custom Logic

A **Code Step** stores executable JavaScript or TypeScript in its `source` field. When processed, the `CodeStepBuildService` ([`packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/code-step/services/code-step-build.service.ts)) compiles and validates your script, storing the bundle within the version record.

### GraphQL mutation to create a code step

```graphql
mutation AddCodeStep($workflowVersionId: ID!, $name: String!, $source: String!) {
  createWorkflowVersionStep(input: {
    workflowVersionId: $workflowVersionId
    type: CODE
    name: $name
    source: $source
  }) {
    id
    type
  }
}

```

### Example custom logic script

The `source` field must export an async `run` function that receives a **context** object containing `payload`, `record`, `workflowRun`, and `services`:

```js
export async function run({ payload, services }) {
  // Access the record service to query CRM data
  const contact = await services.recordService.findOne('contact', payload.contactId);
  
  // Implement custom business logic
  if (contact.title === 'CEO') {
    await services.recordService.update('deal', payload.dealId, {
      priority: 'high'
    });
  }
  
  // Return data accessible to subsequent steps
  return { wasUpdated: true };
}

```

When the workflow triggers, the `WorkflowTriggerWorkspaceService` ([`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)) prepares the context and invokes the compiled code inside a `vm2` sandbox.

## Step 3: Configure a Trigger to Execute the Workflow

Workflows launch via manual invocation, scheduled cron jobs, or automated database events. To react to record changes automatically, register an automated trigger using the `AutomatedTriggerWorkspaceService` ([`packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts)):

```graphql
mutation CreateAutomatedTrigger($workflowVersionId: ID!, $eventName: String!) {
  createAutomatedTrigger(input: {
    type: DATABASE_EVENT
    workflowVersionId: $workflowVersionId
    settings: {
      eventName: $eventName
    }
  }) {
    id
  }
}

```

Replace `$eventName` with events like `contact.created` or `deal.updated`. The service registers a listener that instantiates a **Workflow Run** whenever the event fires.

## Step 4: Publish and Debug Your Workflow

Workflow versions remain inactive until published. Activate your logic with the `publishWorkflowVersion` mutation, which triggers `WorkflowTriggerWorkspaceService.enableAutomatedTrigger`:

```graphql
mutation PublishWorkflowVersion($workflowVersionId: ID!) {
  publishWorkflowVersion(input: { workflowVersionId: $workflowVersionId }) {
    id
    status
  }
}

```

### Debugging execution

Run the workflow and inspect results via the CLI:

```bash
yarn nx run twenty-server:command workflow:run --runId <run-id>

```

The Workflow Runner executes each step sequentially, capturing returns and errors on the **Workflow Run** entity. Standard view fields generated by [`compute-standard-workflow-run-view-fields.util.ts`](https://github.com/twentyhq/twenty/blob/main/compute-standard-workflow-run-view-fields.util.ts) ([`packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-workflow-run-view-fields.util.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-workflow-run-view-fields.util.ts)) expose `status`, `startedAt`, `endedAt`, and related workflow metadata in the UI.

## Step 5: Leverage Workspace Services for Advanced Logic

Twenty exposes typed **workspace services** to Code Steps through the `services` argument, defined in [`packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts):

- **`recordService`** – Perform CRUD operations (`findOne`, `create`, `update`, `delete`) on any standard or custom object.
- **`emailService`** – Send templated emails via the `send` method.
- **`integrationService`** – Execute calls to external APIs registered in the workspace.
- **`workflowService`** – Trigger child workflows programmatically using `run`.

### Example: Sending notifications based on record state

```js
export async function run({ payload, services }) {
  const { recordService, emailService } = services;
  
  const deal = await recordService.findOne('deal', payload.dealId);
  
  if (deal.stage === 'won') {
    await emailService.send({
      to: deal.ownerEmail,
      template: 'deal-won',
      variables: { dealName: deal.name }
    });
  }
}

```

These services provide full IntelliSense and type safety, ensuring your custom logic interacts correctly with the CRM data layer.

## Summary

- **Workflow architecture** relies on three entities—Workflow, Workflow Version, and Workflow Run—managed by the Workflow Runner.
- **Code Steps** accept raw JavaScript/TypeScript in the `source` field, compiled by `CodeStepBuildService` and executed in a sandboxed VM.
- **Context injection** provides `payload`, `record`, and typed `services` (including `recordService` and `emailService`) to your logic.
- **Triggers** can be manual, scheduled, or automated via `AutomatedTriggerWorkspaceService` listening to database events.
- **Publishing** via `publishWorkflowVersion` activates the version and enables automated triggers.
- **Debugging** is supported through CLI commands and standard view fields that expose run metadata and execution output.

## Frequently Asked Questions

### What programming language do Code Steps use?

Code Steps accept **JavaScript** or **TypeScript** code stored in the `source` field of a workflow step. The `CodeStepBuildService` compiles TypeScript automatically, allowing you to use modern syntax and type annotations even though the code executes inside a sandboxed Node.js VM at runtime.

### Is the custom logic execution secure?

Yes. Twenty CRM executes Code Steps inside a **sandboxed VM** using `vm2`, which isolates the script from the host server environment. The only permitted external interactions occur through the explicitly injected `services` object (e.g., `recordService`, `emailService`), preventing unauthorized system access or code injection vulnerabilities.

### Can I call external APIs from a Code Step?

Yes. Use the **`integrationService`** available in the `services` argument to call external APIs registered within your workspace. This service manages authentication and request handling securely, ensuring outbound calls comply with your configured integration settings without exposing raw network access to the sandboxed script.

### How do I debug a failed workflow run?

You can debug runs using the CLI command `yarn nx run twenty-server:command workflow:run --runId <run-id>`. Additionally, the **Workflow Run** entity stores execution status, error messages, and returned payloads, all viewable in the Twenty CRM UI through standard view fields generated by [`compute-standard-workflow-run-view-fields.util.ts`](https://github.com/twentyhq/twenty/blob/main/compute-standard-workflow-run-view-fields.util.ts). Check the `metadata` field for custom outputs returned by your Code Steps.