Skip to content

fix(webhooks): schedule a no-show task for every subscriber - #29835

Open
AlgoArtist06 wants to merge 1 commit into
calcom:mainfrom
AlgoArtist06:fix/no-show-webhooks-multiple-subscribers
Open

fix(webhooks): schedule a no-show task for every subscriber#29835
AlgoArtist06 wants to merge 1 commit into
calcom:mainfrom
AlgoArtist06:fix/no-show-webhooks-multiple-subscribers

Conversation

@AlgoArtist06

@AlgoArtist06 AlgoArtist06 commented Jul 25, 2026

Copy link
Copy Markdown

What does this PR do?

When an event type had more than one active webhook subscribed to the same Cal Video no-show trigger (AFTER_HOSTS_CAL_VIDEO_NO_SHOW / AFTER_GUESTS_CAL_VIDEO_NO_SHOW), only the first subscriber's no-show webhook was scheduled. The rest were dropped, and on the booking-confirmation path the failure also aborted the remaining webhook work for that booking.

Root cause

Task is unique on (referenceUid, type):

referenceUid String?
@@unique([referenceUid, type])

Every no-show task for a booking was created with referenceUid: booking.uid and a type that only distinguishes host from guest. So N subscribers to the same trigger all resolved to the same (referenceUid, type) pair. The first insert won; every later one threw P2002.

Because scheduleNoShowTriggers awaits Promise.all(...), that P2002 propagated out of handleConfirmation, so the BOOKING_CREATED payloads dispatched after the call were never sent — this is the second symptom described in the issue.

The fix

Scope the task reference to the subscriber instead of only the booking, via a small shared helper (packages/features/webhooks/lib/noShowTaskReference.ts):

getNoShowTaskReferenceUid({ bookingUid, webhookId }) // `${bookingUid}_${webhookId}`
  • Both creation sites now use it: scheduleNoShowTriggers (booking flow) and scheduleNoShowTaskForBooking (webhook create/update flow). Each subscriber gets its own independently scheduled task, and no insert conflicts with another subscriber's.
  • cancelNoShowTasksForBooking matches every subscriber's reference for a booking, so cancelling a booking still clears all of its no-show tasks. It also accepts an optional webhookId, which the "trigger removed from one webhook" path now passes so that deactivating one subscriber no longer cancels its siblings' tasks.
  • Cancellation still matches the bare booking.uid form, so tasks already queued before this change stay cancellable after deploy. No schema change or migration is needed.

Booking uids are base58 short-uuids, so the _ separator cannot appear in the booking portion of the reference and the prefix match stays unambiguous.

Visual Demo (For contributors especially)

N/A — backend-only change with no UI surface. The behaviour is demonstrated by the tests below, including one that reproduces the original P2002 against a real PostgreSQL database.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox. — N/A, no documented behaviour or env var changes.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

No environment variables are required. Minimal test data is an event type with two active webhooks pointing at different URLs, both subscribed to the same no-show trigger, both with time + timeUnit set, plus a Cal Video booking.

Expected happy path: each subscriber gets its own Task row (triggerHostNoShowWebhook / triggerGuestNoShowWebhook) with a distinct referenceUid, and the BOOKING_CREATED webhooks for that booking are still delivered.

Automated tests:

# Unit — subscriber fan-out and cancellation behaviour
TZ=UTC yarn vitest run \
  packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.test.ts \
  packages/features/webhooks/lib/cancelNoShowTasksForBooking.test.ts

# Integration — needs a database; asserts against the real unique constraint
TZ=UTC VITEST_MODE=integration yarn vitest run \
  packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.integration-test.ts

Results on this branch: 11 unit tests pass, 4 integration tests pass. The new integration test schedules a task for every subscriber sharing the same no-show trigger fails with Unique constraint failed on the fields: ("referenceUid", type) when the source fix is reverted, which is the exact failure from the issue.

yarn type-check:ci --force passes (9/9 packages, no TS errors). yarn lint (biome lint, the CI gate) reports no errors on the changed files, and biome format reports nothing to reformat.

For full transparency: biome check also raises assist/source/organizeImports on scheduleTrigger.ts and scheduleNoShowTriggers.integration-test.ts, but that is pre-existing on main for those same two files (verified against the main copies), and applying it would reorder unrelated pre-existing imports. I left it out to keep the diff scoped to the fix.

Note on the existing integration fixtures

The two pre-existing fixtures in scheduleNoShowTriggers.integration-test.ts omitted uid, so referenceUid was written as NULL — and PostgreSQL treats NULLs as distinct, which is why the collision never showed up there. I added a uid to those fixtures (the flow's type already requires uid: string, and real callers always pass one) so the test exercises the same key that production does.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective

Task rows are unique on (referenceUid, type), and every Cal Video no-show
task for a booking was created with referenceUid: booking.uid. Multiple
active webhooks subscribed to the same no-show trigger therefore resolved
to the same key, so only the first subscriber was scheduled and the
resulting P2002 propagated out of handleConfirmation, preventing the
BOOKING_CREATED payloads dispatched after that call from being sent.

Scope the task reference to the subscriber so each active webhook gets its
own independently scheduled task, and match every subscriber's reference
when cancelling a booking's no-show tasks. Cancellation also accepts a
webhookId so removing a trigger from one webhook no longer cancels its
siblings' tasks, and it still matches the bare booking uid so tasks queued
before this change stay cancellable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @AlgoArtist06! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions Bot added the 🐛 bug Something isn't working label Jul 25, 2026
@AlgoArtist06
AlgoArtist06 marked this pull request as ready for review July 25, 2026 08:57
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

No-show task references now combine the booking UID with the webhook ID for host and guest scheduling. Cancellation accepts optional webhook scoping and matches both legacy booking-only references and new scoped references, while restricting deletion to relevant no-show task types. Unit and integration tests cover multiple subscribers, distinct references, payloads, cancellation scope, legacy tasks, non-no-show triggers, and non-Cal Video bookings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: scheduling a no-show task for every subscriber.
Description check ✅ Passed The description matches the code changes and explains the subscriber-scoped no-show task fix.
Linked Issues check ✅ Passed The PR schedules independent tasks per subscriber, preserves booking-wide cancellation, and covers host, guest, and booking-confirmation paths for #29655.
Out of Scope Changes check ✅ Passed The added helper, task updates, and tests all support the no-show scheduling fix without unrelated scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/features/webhooks/lib/scheduleTrigger.ts`:
- Line 628: Update the deleteMany calls in the no-specific-trigger paths of
scheduleTrigger to retain referenceFilter matching while restricting type to
triggerHostNoShowWebhook or triggerGuestNoShowWebhook. Apply this constraint to
both affected deletion paths and leave targeted-trigger deletion behavior
unchanged.
- Around line 615-617: Update the referenceFilter construction in the no-show
cancellation flow to match both the webhook-specific reference UID and the
legacy booking-only reference when webhookId is supplied. Preserve
getNoShowTaskReferenceUidFilter(bookingUid) behavior for calls without
webhookId, ensuring updateTriggerForExistingBookings cancels both task formats.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: acbad9c3-1373-4b8d-bd2a-a8000b3d11a8

📥 Commits

Reviewing files that changed from the base of the PR and between 3894f37 and 6303824.

📒 Files selected for processing (6)
  • packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.integration-test.ts
  • packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.test.ts
  • packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts
  • packages/features/webhooks/lib/cancelNoShowTasksForBooking.test.ts
  • packages/features/webhooks/lib/noShowTaskReference.ts
  • packages/features/webhooks/lib/scheduleTrigger.ts

Comment on lines +615 to +617
const referenceFilter = webhookId
? { referenceUid: getNoShowTaskReferenceUid({ bookingUid, webhookId }) }
: getNoShowTaskReferenceUidFilter(bookingUid);

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include the legacy UID when cancelling one subscriber.

When webhookId is supplied, this filter excludes pre-deployment tasks keyed only by bookingUid. updateTriggerForExistingBookings now always supplies webhookId for a removed no-show subscription, leaving its legacy task scheduled.

Proposed fix
 const referenceFilter = webhookId
-  ? { referenceUid: getNoShowTaskReferenceUid({ bookingUid, webhookId }) }
+  ? {
+      OR: [
+        { referenceUid: bookingUid },
+        { referenceUid: getNoShowTaskReferenceUid({ bookingUid, webhookId }) },
+      ],
+    }
   : getNoShowTaskReferenceUidFilter(bookingUid);

As per PR objectives, cancellation must preserve legacy booking-only references.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const referenceFilter = webhookId
? { referenceUid: getNoShowTaskReferenceUid({ bookingUid, webhookId }) }
: getNoShowTaskReferenceUidFilter(bookingUid);
const referenceFilter = webhookId
? {
OR: [
{ referenceUid: bookingUid },
{ referenceUid: getNoShowTaskReferenceUid({ bookingUid, webhookId }) },
],
}
: getNoShowTaskReferenceUidFilter(bookingUid);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/features/webhooks/lib/scheduleTrigger.ts` around lines 615 - 617,
Update the referenceFilter construction in the no-show cancellation flow to
match both the webhook-specific reference UID and the legacy booking-only
reference when webhookId is supplied. Preserve
getNoShowTaskReferenceUidFilter(bookingUid) behavior for calls without
webhookId, ensuring updateTriggerForExistingBookings cancels both task formats.

where: { ...referenceFilter, type: "triggerGuestNoShowWebhook" },
});
} else {
await prisma.task.deleteMany({ where: referenceFilter });

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restrict these deletes to no-show task types.

Both paths delete every task type sharing the reference UID. Keep the reference matching, but add type: { in: ["triggerHostNoShowWebhook", "triggerGuestNoShowWebhook"] } when no specific trigger is supplied.

As per PR objectives, cancellation must delete only relevant no-show tasks.

Also applies to: 638-641

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/features/webhooks/lib/scheduleTrigger.ts` at line 628, Update the
deleteMany calls in the no-specific-trigger paths of scheduleTrigger to retain
referenceFilter matching while restricting type to triggerHostNoShowWebhook or
triggerGuestNoShowWebhook. Apply this constraint to both affected deletion paths
and leave targeted-trigger deletion behavior unchanged.

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

Labels

🐛 bug Something isn't working size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No-show webhooks aren't all scheduled when an event type has multiple subscribers

1 participant