Skip to content

Introduce webhook v2#17456

Merged
charlesBochet merged 14 commits intomainfrom
introduce-webhook-v2
Jan 27, 2026
Merged

Introduce webhook v2#17456
charlesBochet merged 14 commits intomainfrom
introduce-webhook-v2

Conversation

@charlesBochet
Copy link
Copy Markdown
Member

@charlesBochet charlesBochet commented Jan 26, 2026

Migrate webhook to v2 entity

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 26, 2026

Greptile Overview

Greptile Summary

This PR refactors the webhook system to use the metadata-modules architecture pattern (webhook v2), introducing universalIdentifier and applicationId fields to align webhooks with the flat entity framework used by other metadata entities.

Key changes:

  • Moved webhook CRUD operations from core-modules to metadata-modules using the flat entity pattern
  • Added database migration to backfill universalIdentifier (set to existing id) and applicationId (linked to workspace custom application)
  • Split webhook service: new WebhookService in metadata-modules handles CRUD, legacy WebhookQueryService in core-modules handles job queries
  • Implemented full validation pipeline with FlatWebhookValidatorService for create/update/delete operations
  • Added workspace cache support via WorkspaceFlatWebhookMapCacheService
  • Updated REST and GraphQL APIs to use new metadata service while maintaining backward compatibility

Issue found:

  • Migration query at line 19-24 uses a subquery that could return NULL if no custom application exists for a workspace, causing NOT NULL constraint violation when setting applicationId

Confidence Score: 2/5

  • migration has critical risk of failing on production data due to potential NULL constraint violation
  • the migration could fail if workspaces have webhooks but the custom application lookup returns NULL, violating the NOT NULL constraint on applicationId. While the workspace entity requires workspaceCustomApplicationId, the migration's subquery approach doesn't guarantee a fallback value.
  • packages/twenty-server/src/database/typeorm/core/migrations/common/1769200000000-addUniversalIdentifierAndApplicationIdToWebhook.ts - migration query needs to handle edge cases where custom application lookup fails

Important Files Changed

Filename Overview
packages/twenty-server/src/database/typeorm/core/migrations/common/1769200000000-addUniversalIdentifierAndApplicationIdToWebhook.ts adds universalIdentifier and applicationId to webhook table with potential migration failure risk
packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts new metadata-layer webhook service using flat entity pattern for CRUD operations
packages/twenty-server/src/engine/metadata-modules/webhook/webhook.resolver.ts GraphQL resolver for webhook operations with proper permissions and interceptors
packages/twenty-server/src/engine/core-modules/webhook/webhook.service.ts renamed to WebhookQueryService, now only handles findByOperations for backward compatibility with jobs
packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service.ts validates webhook creation, update, and deletion with proper URL and secret validation
packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts REST controller updated to use new metadata webhook service

Sequence Diagram

sequenceDiagram
    participant Client
    participant GraphQL as GraphQL Resolver
    participant WebhookService as Webhook Service
    participant AppService as Application Service
    participant Migration as Migration Service
    participant Validator as Webhook Validator
    participant Cache as Flat Entity Cache
    participant DB as Database

    Note over Client,DB: Create Webhook Flow
    Client->>GraphQL: createWebhook(input)
    GraphQL->>WebhookService: create(input, workspaceId)
    WebhookService->>WebhookService: normalizeTargetUrl(targetUrl)
    WebhookService->>WebhookService: validateTargetUrl(targetUrl)
    WebhookService->>AppService: findWorkspaceTwentyStandardAndCustomApplicationOrThrow()
    AppService->>Cache: getOrRecompute(flatApplicationMaps)
    Cache-->>AppService: return application maps
    AppService-->>WebhookService: return workspaceCustomFlatApplication
    WebhookService->>WebhookService: fromCreateWebhookInputToFlatWebhookToCreate()
    WebhookService->>Migration: validateBuildAndRunWorkspaceMigration()
    Migration->>Validator: validateFlatWebhookCreation()
    Validator->>Validator: validateTargetUrl()
    Validator->>Validator: check secret is non-empty
    Validator-->>Migration: validation result
    Migration->>DB: INSERT webhook entity
    DB-->>Migration: success
    Migration->>Cache: invalidate flatWebhookMaps
    Migration-->>WebhookService: migration complete
    WebhookService->>Cache: getOrRecomputeManyOrAllFlatEntityMaps(flatWebhookMaps)
    Cache->>DB: SELECT webhooks
    DB-->>Cache: webhook entities
    Cache-->>WebhookService: updated flatWebhookMaps
    WebhookService->>WebhookService: fromFlatWebhookToWebhookDto()
    WebhookService-->>GraphQL: return WebhookDTO
    GraphQL-->>Client: return webhook

    Note over Client,DB: Webhook Event Triggering
    participant EventEmitter as Event Emitter
    participant JobQueue as Webhook Job Queue
    participant WebhookJob as Call Webhook Job
    participant QueryService as WebhookQueryService
    
    EventEmitter->>JobQueue: emit workspace event batch
    JobQueue->>WebhookJob: process event
    WebhookJob->>QueryService: findByOperations(workspaceId, operations)
    QueryService->>DB: SELECT webhooks WHERE operations CONTAINS
    DB-->>QueryService: matching webhooks
    QueryService-->>WebhookJob: webhook entities
    WebhookJob->>WebhookJob: transformEventBatchToWebhookEvents()
    WebhookJob->>JobQueue: queue individual webhook calls
    JobQueue-->>WebhookJob: jobs queued
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 56 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/engine/core-modules/webhook/webhook.module.ts">

<violation number="1" location="packages/twenty-server/src/engine/core-modules/webhook/webhook.module.ts:19">
P1: CoreWebhookModule no longer imports PermissionsModule, but WebhookController still uses SettingsPermissionGuard which injects PermissionsService. Since WebhookModule doesn’t export PermissionsService, this module will fail to resolve the guard’s dependency at runtime. Re-add PermissionsModule to the imports.</violation>
</file>

<file name="packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts">

<violation number="1" location="packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts:67">
P2: The update body no longer uses a DTO class, so validation decorators on `UpdateWebhookInputUpdates` are skipped. This allows invalid payload types to reach the service and can cause runtime errors or inconsistent data. Use the DTO class for the `@Body()` type so validation runs.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}

@Patch(':id')
async update(
@Param('id') id: string,
@Body() updateWebhookDto: UpdateWebhookInput,
@Body()
updateWebhookDto: {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The update body no longer uses a DTO class, so validation decorators on UpdateWebhookInputUpdates are skipped. This allows invalid payload types to reach the service and can cause runtime errors or inconsistent data. Use the DTO class for the @Body() type so validation runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts, line 67:

<comment>The update body no longer uses a DTO class, so validation decorators on `UpdateWebhookInputUpdates` are skipped. This allows invalid payload types to reach the service and can cause runtime errors or inconsistent data. Use the DTO class for the `@Body()` type so validation runs.</comment>

<file context>
@@ -40,48 +40,53 @@ export class WebhookController {
     @Param('id') id: string,
-    @Body() updateWebhookDto: UpdateWebhookInput,
+    @Body()
+    updateWebhookDto: {
+      targetUrl?: string;
+      operations?: string[];
</file context>
Fix with Cubic

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 27, 2026

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:6251

This environment will automatically shut down when the PR is closed or after 5 hours.

Copy link
Copy Markdown
Contributor

@prastoin prastoin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there ! Made a quick review found two main points that need to be clarified before merging
Could you please also add integration test coverage that covers all validation exceptions ?
Please let me know !

  • perf concerns on find all web hook service handler
  • invalid migration -> we should already introduce the save point pattern here ( backfill and migration upgrade command might be added later though )

Copy link
Copy Markdown
Contributor

@prastoin prastoin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks !

new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
)
return webhooks
.map(fromWebhookEntityToFlatWebhook)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Could introduce a from entity to flat too


if (!isDefined(flatWebhook)) {
if (!isDefined(webhook)) {
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should we throw ?

`ALTER TABLE "core"."webhook" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."webhook" ALTER COLUMN "applicationId" SET NOT NULL`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 27, 2026

📊 API Changes Report

GraphQL Schema Changes

GraphQL Schema Changes

[log]
Detected the following changes (18) between schemas:

[log] ✖ Type DeleteWebhookInput was removed
[log] ✖ Type GetWebhookInput was removed
[log] ✖ Field Mutation.deleteWebhook changed type from Boolean! to Webhook!
[log] ✖ Argument id: UUID! added to field Mutation.deleteWebhook
[log] ✖ Argument input: DeleteWebhookInput! was removed from field Mutation.deleteWebhook
[log] ✖ Argument id: UUID! added to field Query.webhook
[log] ✖ Argument input: GetWebhookInput! was removed from field Query.webhook
[log] ✖ Input field description was removed from input object type UpdateWebhookInput
[log] ✖ Input field operations was removed from input object type UpdateWebhookInput
[log] ✖ Input field secret was removed from input object type UpdateWebhookInput
[log] ✖ Input field targetUrl was removed from input object type UpdateWebhookInput
[log] ✖ Input field update of type UpdateWebhookInputUpdates! was added to input object type UpdateWebhookInput
[log] ⚠ Enum value webhook was added to enum AllMetadataName
[log] ✔ Input field id of type UUID was added to input object type CreateWebhookInput
[log] ✔ Field Mutation.updateWebhook changed type from Webhook to Webhook!
[log] ✔ Input field UpdateWebhookInput.id has description The id of the webhook to update
[log] ✔ Type UpdateWebhookInputUpdates was added
[log] ✔ Field applicationId was added to object type Webhook
[error] Detected 12 breaking changes
⚠️ Breaking changes or errors detected in GraphQL schema

[log] 
Detected the following changes (18) between schemas:

[log] ✖  Type DeleteWebhookInput was removed
[log] ✖  Type GetWebhookInput was removed
[log] ✖  Field Mutation.deleteWebhook changed type from Boolean! to Webhook!
[log] ✖  Argument id: UUID! added to field Mutation.deleteWebhook
[log] ✖  Argument input: DeleteWebhookInput! was removed from field Mutation.deleteWebhook
[log] ✖  Argument id: UUID! added to field Query.webhook
[log] ✖  Argument input: GetWebhookInput! was removed from field Query.webhook
[log] ✖  Input field description was removed from input object type UpdateWebhookInput
[log] ✖  Input field operations was removed from input object type UpdateWebhookInput
[log] ✖  Input field secret was removed from input object type UpdateWebhookInput
[log] ✖  Input field targetUrl was removed from input object type UpdateWebhookInput
[log] ✖  Input field update of type UpdateWebhookInputUpdates! was added to input object type UpdateWebhookInput
[log] ⚠  Enum value webhook was added to enum AllMetadataName
[log] ✔  Input field id of type UUID was added to input object type CreateWebhookInput
[log] ✔  Field Mutation.updateWebhook changed type from Webhook to Webhook!
[log] ✔  Input field UpdateWebhookInput.id has description The id of the webhook to update
[log] ✔  Type UpdateWebhookInputUpdates was added
[log] ✔  Field applicationId was added to object type Webhook
[error] Detected 12 breaking changes
Error generating diff

GraphQL Metadata Schema Changes

GraphQL Metadata Schema Changes

[log]
Detected the following changes (18) between schemas:

[log] ✖ Type DeleteWebhookInput was removed
[log] ✖ Type GetWebhookInput was removed
[log] ✖ Field Mutation.deleteWebhook changed type from Boolean! to Webhook!
[log] ✖ Argument id: UUID! added to field Mutation.deleteWebhook
[log] ✖ Argument input: DeleteWebhookInput! was removed from field Mutation.deleteWebhook
[log] ✖ Argument id: UUID! added to field Query.webhook
[log] ✖ Argument input: GetWebhookInput! was removed from field Query.webhook
[log] ✖ Input field description was removed from input object type UpdateWebhookInput
[log] ✖ Input field operations was removed from input object type UpdateWebhookInput
[log] ✖ Input field secret was removed from input object type UpdateWebhookInput
[log] ✖ Input field targetUrl was removed from input object type UpdateWebhookInput
[log] ✖ Input field update of type UpdateWebhookInputUpdates! was added to input object type UpdateWebhookInput
[log] ⚠ Enum value webhook was added to enum AllMetadataName
[log] ✔ Input field id of type UUID was added to input object type CreateWebhookInput
[log] ✔ Field Mutation.updateWebhook changed type from Webhook to Webhook!
[log] ✔ Input field UpdateWebhookInput.id has description The id of the webhook to update
[log] ✔ Type UpdateWebhookInputUpdates was added
[log] ✔ Field applicationId was added to object type Webhook
[error] Detected 12 breaking changes
⚠️ Breaking changes or errors detected in GraphQL metadata schema

[log] 
Detected the following changes (18) between schemas:

[log] ✖  Type DeleteWebhookInput was removed
[log] ✖  Type GetWebhookInput was removed
[log] ✖  Field Mutation.deleteWebhook changed type from Boolean! to Webhook!
[log] ✖  Argument id: UUID! added to field Mutation.deleteWebhook
[log] ✖  Argument input: DeleteWebhookInput! was removed from field Mutation.deleteWebhook
[log] ✖  Argument id: UUID! added to field Query.webhook
[log] ✖  Argument input: GetWebhookInput! was removed from field Query.webhook
[log] ✖  Input field description was removed from input object type UpdateWebhookInput
[log] ✖  Input field operations was removed from input object type UpdateWebhookInput
[log] ✖  Input field secret was removed from input object type UpdateWebhookInput
[log] ✖  Input field targetUrl was removed from input object type UpdateWebhookInput
[log] ✖  Input field update of type UpdateWebhookInputUpdates! was added to input object type UpdateWebhookInput
[log] ⚠  Enum value webhook was added to enum AllMetadataName
[log] ✔  Input field id of type UUID was added to input object type CreateWebhookInput
[log] ✔  Field Mutation.updateWebhook changed type from Webhook to Webhook!
[log] ✔  Input field UpdateWebhookInput.id has description The id of the webhook to update
[log] ✔  Type UpdateWebhookInputUpdates was added
[log] ✔  Field applicationId was added to object type Webhook
[error] Detected 12 breaking changes
Error generating diff

⚠️ Please review these API changes carefully before merging.

⚠️ Breaking Change Protocol

Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.

🔄 Options:

  1. If this IS a breaking change: Add "breaking" to your PR title and add BREAKING CHANGE: to your commit message
  2. If this is NOT a breaking change: The API diff tool may have false positives - please review carefully

For breaking changes, add to commit message:

feat: add new API endpoint

BREAKING CHANGE: removed deprecated field from User schema

Comment on lines +76 to +83
const flatWebhookToCreate = fromCreateWebhookInputToFlatWebhookToCreate({
createWebhookInput: {
...input,
targetUrl: normalizedTargetUrl,
},
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The CreateWebhookInput DTO allows an empty operations array, which creates a webhook that will never be triggered because it cannot match any events.
Severity: MEDIUM

Suggested Fix

Add the @ArrayNotEmpty() decorator from class-validator to the operations field in the CreateWebhookInput DTO. This will ensure that any attempt to create or update a webhook with an empty operations array is rejected with a validation error.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts#L76-L83

Potential issue: The `operations` field in the `CreateWebhookInput` DTO is validated
with `@IsArray()`, which permits empty arrays. No other validation, such as
`@ArrayNotEmpty()`, is present. When a user creates a webhook with an empty `operations`
array, this value is persisted to the database. The job that triggers webhooks uses an
`ArrayContains` query to find relevant webhooks based on event operations. An empty
`operations` array will never match any event, causing the webhook to be silently
non-functional without any error feedback to the user.

Did we get this right? 👍 / 👎 to inform future reviews.

@charlesBochet charlesBochet added this pull request to the merge queue Jan 27, 2026
Merged via the queue into main with commit bc77918 Jan 27, 2026
76 checks passed
@charlesBochet charlesBochet deleted the introduce-webhook-v2 branch January 27, 2026 15:44
@twenty-eng-sync
Copy link
Copy Markdown

Hey @charlesBochet! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

1 similar comment
@twenty-eng-sync
Copy link
Copy Markdown

Hey @charlesBochet! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants