fix(ashby): repair broken idempotency dedup and clarify duplicate-webhook errors#5518
Conversation
…hook errors extractIdempotencyId looked for a webhookActionId field that does not exist anywhere in Ashby's webhook payload schema (confirmed against the live OpenAPI spec), so every delivery — including Ashby's own retries — got a fresh random dedup key and could re-execute workflows. Derive the key from the affected resource's id plus its updatedAt/decidedAt instead. Also surface a clear, actionable message when Ashby's webhook.create rejects a request as a duplicate (seen repeatedly in production logs, where the outbox retried the same failing subscription for ~23 minutes before dead-lettering) instead of the generic API error passthrough. Also add the interview stage `type` field to trigger outputs (present in Ashby's schema, useful for a stage-change trigger) and fix the employmentType description to match Ashby's actual enum values.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Trigger input shaping maps Subscription UX surfaces a specific message when Ashby returns a duplicate-webhook error during Refactor: Reviewed by Cursor Bugbot for commit b866793. Configure here. |
…ptions acceptanceStatus listed a non-existent "WaitingOnResponse" value and offerStatus was missing "WaitingOnApprovalDefinition" — both caught on a second pass re-checking every enum against Ashby's live OpenAPI spec.
Greptile SummaryThis PR fixes Ashby webhook handling and updates the related trigger outputs. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (8): Last reviewed commit: "fix(ashby): fold offer id into the idemp..." | Re-trigger Greptile |
… risks - Return null (skip dedup) when application.updatedAt is absent instead of collapsing to an empty string, which could collide two distinct events sharing an application id onto the same key. - Drop decidedAt from the offerCreate key. It's populated only after the fact, so including it gave a retry of the same delivery a different key than the original attempt once the candidate responded, defeating dedup. offer.id alone is already stable and unique per created offer.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9b2ac1f. Configure here.
Returning null when application.updatedAt was missing avoided false collisions between distinct events, but also disabled retry dedup entirely for that payload shape — an Ashby retry would get a random key from the idempotency service's own fallback and re-run the workflow. Extract the fallback-fingerprint helper (sha256 of a stably-serialized payload) out of salesforce.ts into the shared providers/utils.ts, and use it in ashby.ts: identical retried bytes hash identically (dedup still works), while two genuinely different events hash differently (no false collision).
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit eab3861. Configure here.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ab0dcfa. Configure here.
Hashing only data.application missed other fields (e.g. offer on candidateHire) when updatedAt is absent, so two deliveries sharing an application snapshot but differing elsewhere in data could collide onto the same idempotency key.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2a1862a. Configure here.
TriggerOutput reserves the 'type' key for the output's own JSON type (e.g. 'string'), so a nested field literally named 'type' inside currentInterviewStage collided with that meta-field and broke the Record<string, TriggerOutput> cast — passing local type-check (which turbo was silently serving a stale cached pass for) but failing Next.js's build-time type check in CI. Renamed to stageType, matching the existing eventType-style convention used elsewhere for API fields literally called 'type'.
|
@cursor review |
…ed data too Renaming the field in the output schema alone (to fix the TriggerOutput build collision) left a mismatch: Ashby's real payload still has currentInterviewStage.type, so a picker/expression using the schema's declared stageType would resolve to undefined at runtime. formatInput now renames the field in the actual delivered payload so it matches what the schema declares.
|
@cursor review |
When application.updatedAt is present, the key ignored sibling data fields — a candidateHire delivery carries both application and offer, so two deliveries sharing an application snapshot could collide even though they concern different offers. Append offer.id to the discriminator when present; it's the only sibling object Ashby's schema ever pairs with application.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b866793. Configure here.
Summary
/validate-triggeraudit on the Ashby webhook trigger against Ashby's live API docs, and checked production logs for real-world failuresextractIdempotencyIdwas reading awebhookActionIdfield that doesn't exist anywhere in Ashby's webhook payload schema (verified against the live OpenAPI spec) — every delivery, including Ashby's own retries, got a random dedup key that could re-execute workflows. Now derives a stable key from the affected resource's id +updatedAt/decidedAtcreateSubscriptionhammering Ashby for ~23 minutes on a "Duplicate webhook" error before dead-lettering, with only the raw Ashby error surfaced. Added a specific, actionable message (Ashby has nowebhook.listendpoint, so this can't self-heal — the user has to clean up the stale webhook in Ashby)typefield to trigger outputs (present in Ashby's schema, useful for the stage-change trigger) and fixed theemploymentTypedescription to match Ashby's real enum valuesType of Change
Testing
ashby.test.tscovering auth, matchEvent, formatInput, and the fixed idempotency logic (13 tests, all passing)bun run type-check,bun run lint,bun run check:api-validationall pass cleanChecklist