How to Integrate Custom Logic into Twenty CRM Workflows: A Complete Guide to Code Steps
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. - 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. - 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.
The Workflow Runner (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:
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) compiles and validates your script, storing the bundle within the version record.
GraphQL mutation to create a code step
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:
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) 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):
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:
mutation PublishWorkflowVersion($workflowVersionId: ID!) {
publishWorkflowVersion(input: { workflowVersionId: $workflowVersionId }) {
id
status
}
}
Debugging execution
Run the workflow and inspect results via the CLI:
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 (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:
recordService– Perform CRUD operations (findOne,create,update,delete) on any standard or custom object.emailService– Send templated emails via thesendmethod.integrationService– Execute calls to external APIs registered in the workspace.workflowService– Trigger child workflows programmatically usingrun.
Example: Sending notifications based on record state
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
sourcefield, compiled byCodeStepBuildServiceand executed in a sandboxed VM. - Context injection provides
payload,record, and typedservices(includingrecordServiceandemailService) to your logic. - Triggers can be manual, scheduled, or automated via
AutomatedTriggerWorkspaceServicelistening to database events. - Publishing via
publishWorkflowVersionactivates 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. Check the metadata field for custom outputs returned by your Code Steps.
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 →