docs: clarify idempotency for external side effects - #4650
docs: clarify idempotency for external side effects#4650bharathkumar39293 wants to merge 1 commit into
Conversation
|
WalkthroughUpdated the idempotency documentation to describe coordination between Trigger.dev task deduplication and external provider idempotency. Added guidance that task deduplication does not guarantee exactly-once external side effects. Added refund examples that derive Trigger.dev and Stripe idempotency keys from a deterministic business operation ID. Documented retries or crashes after provider request acceptance. Merge Risk: 🟡 Moderate · up to The documentation could cause duplicate refunds if a task replays after Stripe has pruned its idempotency key; merge should wait for the retention limit and reconciliation guidance to be added or explicitly accepted. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Thanks for your contribution! We require all external PRs to be opened in draft status first so you can address CodeRabbit review comments and ensure CI passes before requesting a review. Please re-open this PR as a draft. See CONTRIBUTING.md for details. |
| ```ts trigger/refund.ts | ||
| import { task } from "@trigger.dev/sdk"; | ||
| import Stripe from "stripe"; | ||
|
|
||
| const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); | ||
|
|
||
| export const refundPayment = task({ | ||
| id: "refund-payment", | ||
| run: async (payload: { | ||
| paymentIntentId: string; | ||
| amount: number; | ||
| operationId: string; | ||
| }) => { | ||
| await stripe.refunds.create( | ||
| { | ||
| payment_intent: payload.paymentIntentId, | ||
| amount: payload.amount, | ||
| }, | ||
| { | ||
| // Stripe uses this key to make retries of the same request idempotent. | ||
| idempotencyKey: payload.operationId, | ||
| } | ||
| ); | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ```ts trigger/initiate-refund.ts | ||
| import { task } from "@trigger.dev/sdk"; | ||
| import { refundPayment } from "./refund"; | ||
|
|
||
| export const initiateRefund = task({ | ||
| id: "initiate-refund", | ||
| run: async (payload: { | ||
| refundId: string; | ||
| paymentIntentId: string; | ||
| amount: number; | ||
| }) => { | ||
| // Use an ID that identifies this business operation, not a random value. | ||
| const operationId = `refund:${payload.refundId}`; | ||
|
|
||
| await refundPayment.trigger( | ||
| { | ||
| paymentIntentId: payload.paymentIntentId, | ||
| amount: payload.amount, | ||
| operationId, | ||
| }, | ||
| { | ||
| // Trigger.dev deduplicates this child task trigger when this task retries. | ||
| idempotencyKey: operationId, | ||
| } | ||
| ); | ||
| }, | ||
| }); | ||
| ``` |
There was a problem hiding this comment.
🟡 Two related file examples are shown as separate code blocks instead of a grouped multi-file example
The new two-file refund example is written as two stand-alone code blocks (starting at docs/idempotency.mdx:75) instead of the documented multi-file grouping component, so readers see the files as unrelated snippets rather than one example.
Impact: The docs render inconsistently with the rest of the site for multi-file examples, making the two-file walkthrough harder to follow.
Documentation convention for multi-file examples
docs/CLAUDE.md explicitly lists <CodeGroup> as the component to use for "Multi-language/multi-file code examples". The PR adds trigger/refund.ts (docs/idempotency.mdx:75-100) and trigger/initiate-refund.ts (docs/idempotency.mdx:102-129) back-to-back with filename tags, which is exactly the multi-file case; they should be wrapped in a <CodeGroup> block.
Prompt for agents
The two new code blocks in docs/idempotency.mdx (trigger/refund.ts and trigger/initiate-refund.ts) form a single multi-file example. Per docs/CLAUDE.md, multi-file code examples should be wrapped in a <CodeGroup> component so Mintlify renders them as tabs. Wrap the two consecutive fenced blocks in <CodeGroup> ... </CodeGroup>.
Was this helpful? React with 👍 or 👎 to provide feedback.
| await refundPayment.trigger( | ||
| { | ||
| paymentIntentId: payload.paymentIntentId, | ||
| amount: payload.amount, | ||
| operationId, | ||
| }, | ||
| { | ||
| // Trigger.dev deduplicates this child task trigger when this task retries. | ||
| idempotencyKey: operationId, | ||
| } | ||
| ); |
There was a problem hiding this comment.
🔍 Raw-string idempotency key in the child trigger is run-scoped, which limits the claimed deduplication
The example passes operationId as a raw string to refundPayment.trigger(...). As documented later in the same page (docs/idempotency.mdx:172-174), a raw string defaults to "run" scope, i.e. hashed with the parent run ID. So the deduplication only holds across retries of the same initiateRefund run — if initiateRefund is triggered twice for the same refundId, two child runs are created and only Stripe's own idempotency key prevents a double refund. The inline comment ("deduplicates this child task trigger when this task retries") is accurate, but the section intro at docs/idempotency.mdx:73 ("use a deterministic business-operation ID ... Trigger.dev deduplicates child task triggers") reads as if it dedupes globally. Consider using idempotencyKeys.create(operationId, { scope: "global" }) or stating the scope caveat explicitly.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dc1f2cc9-facf-470b-b973-ca433a78cb44
📒 Files selected for processing (1)
docs/idempotency.mdx
Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review.
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
- GitHub Check: code-quality / code-quality
- GitHub Check: check-broken-links
🧰 Additional context used
📓 Path-based instructions (1)
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/CLAUDE.md)
docs/**/*.mdx: MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format
Use Mintlify components for structured content: , , , , , , /, /
Always import from@trigger.dev/sdkin code examples (never from@trigger.dev/sdk/v3)
Code examples must be complete and runnable where possible
Use language tags in code fences:typescript,bash,json
Files:
docs/idempotency.mdx
🧠 Learnings (2)
📚 Learning: 2026-03-10T12:44:14.176Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3200
File: docs/config/config-file.mdx:353-368
Timestamp: 2026-03-10T12:44:14.176Z
Learning: In the trigger.dev repo, docs PRs are often companions to implementation PRs. When reviewing docs PRs (MDX files under docs/), check the PR description for any companion/related PR references and verify that the documented features exist in those companion PRs before flagging missing implementations. This ensures docs stay in sync with code changes across related PRs.
Applied to files:
docs/idempotency.mdx
📚 Learning: 2026-04-30T20:30:29.458Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3226
File: docs/ai-chat/quick-start.mdx:13-13
Timestamp: 2026-04-30T20:30:29.458Z
Learning: In this repo’s documentation MDX files (`docs/**/*.mdx`), use `ts` and `tsx` (not `typescript`) as the code-fence language tags for TypeScript/TSX snippets. Do not flag `ts`/`tsx` code-fence language tags as incorrect in any docs MDX file, since this is the site-wide Mintlify-compatible convention.
Applied to files:
docs/idempotency.mdx
| // Stripe uses this key to make retries of the same request idempotent. | ||
| idempotencyKey: payload.operationId, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target excerpt ---'
sed -n '1,145p' docs/idempotency.mdx
printf '%s\n' '--- relevant references ---'
rg -n -C 3 'idempotency|operationId|refund|exactly.?once|Stripe' docs/idempotency.mdxRepository: triggerdotdev/trigger.dev
Length of output: 27150
🌐 Web query:
Stripe API idempotent requests retention at least 24 hours reused key after pruning official documentation
💡 Result:
In the Stripe API, idempotency keys are retained for at least 24 hours [1][2]. After this retention period, Stripe automatically prunes or removes the keys from its system [1][2]. If you reuse an idempotency key after the original request's data has been pruned (i.e., after the 24-hour window), Stripe will treat the subsequent request as a new, unique request rather than a retry of the original [1][3]. When reusing an idempotency key, the Stripe idempotency layer performs a validation check: it compares the parameters of the incoming request against those of the original request [1][4]. If the parameters do not match the original request, Stripe will return an idempotency error to prevent accidental misuse [1][5]. If the parameters do match, the system returns the cached result of the original request, provided the key has not yet been pruned [1][2].
Citations:
- 1: https://docs.stripe.com/api/idempotent_requests
- 2: https://docs.stripe.com/error-low-level?locale=en-GB
- 3: https://docs.stripe.com/api/idempotent_requests?api-version=2026-03-25.dahlia
- 4: https://docs.stripe.com/api/idempotent_requests?api-version=2024-12-18.acacia
- 5: https://docs.stripe.com/changelog/2015-09-03/reuse-idempotency-tokens-alter-error
Document Stripe’s idempotency retention limit.
Stripe retains API idempotency keys for at least 24 hours and can prune them afterward. If this task replays after pruning, reusing payload.operationId can create a second refund. State this limit. If a refund can replay later, persist the business operation and Stripe refund ID, then reconcile with Stripe before issuing another request.
Summary
Validation
git diff --checkpasses.Changelog
Clarify idempotency guidance for external side effects.