windmill-labs/windmill · error · Error
WAC step key "${options.key}" is already used in this workfl
Error message
WAC step key "${options.key}" is already used in this workflow. Give each waitForApproval() its own key so getApprovalUrls() can address it. What it means
Within one workflow, each waitForApproval must have a distinct explicit key. When a duplicate is detected, the client refuses to silently rename it to `<key>_2` because the caller would then hold resume URLs pointing at the *first* step, which fail with 'resume request already sent' and park the workflow until timeout. The error names the duplicated key and asks for unique keys per step (typescript-client/client.ts:1856).
Source
Thrown at typescript-client/client.ts:1856
return steps;
}
_waitForApproval(options?: {
timeout?: number;
form?: object;
selfApproval?: boolean;
key?: string;
}): PromiseLike<{ value: any; approver: string; approved: boolean }> {
this._rethrowSwallowed();
if (options?.key !== undefined) assertUsableStepKey(options.key, "waitForApproval key");
const key = this._allocKey(options?.key || "approval");
// An explicit key is an identifier callers mint URLs against, so silently
// renaming a duplicate to `<key>_2` would hand them a URL for the *first*
// step — which then fails with "resume request already sent" and parks the
// workflow until timeout. Unnamed approvals keep auto-numbering.
if (options?.key && key !== options.key) {
throw new Error(
`WAC step key "${options.key}" is already used in this workflow. ` +
`Give each waitForApproval() its own key so getApprovalUrls() can address it.`,
);
}
if (key in this.completed) {
const value = this.completed[key];
return { then: (resolve: any) => resolve(value) };
}
// In child job mode, return never-resolving thenable (same as _nextStep)
if (this._executingKey !== null) {
return { then: () => new Promise(() => {}) };
}
// Throw immediately — approval is always a blocking step
console.log(`\n--- WAC: approval(${key}) ---`);
this._raiseSuspend({View on GitHub (pinned to e474e8803c)
Solutions
- Give each waitForApproval a unique key (suffix with loop index, item id, or step purpose).
- Omit the `key` option entirely to keep auto-numbering when uniqueness is not needed.
- In loops, build the key from the iteration variable: `waitForApproval({ key: `approve-${i}` })`.
- Audit the workflow for repeated `key:` literals.
Example fix
// before
for (const item of items) {
await waitForApproval({ key: 'approve-item' }); // duplicate on 2nd iteration
}
// after
for (const [i, item] of items.entries()) {
await waitForApproval({ key: `approve-item-${i}` });
} Defensive patterns
Strategy: validation
Validate before calling
function assertUniqueApprovalKey(key: string, seen: Set<string>) {
if (seen.has(key)) throw new Error(`duplicate approval key: ${key}`);
seen.add(key);
} Try / catch
try {
await waitForApproval({ key: 'approve-item' });
} catch (e) {
if (e.message.includes('is already used in this workflow')) {
await waitForApproval({ key: 'approve-item-' + crypto.randomUUID() });
} else throw e;
} Prevention
- Never hard-code the same key literal in loops or copied steps.
- Omit `key` to let the client auto-number when uniqueness is not needed.
- Keep a set of used keys while building the workflow and assert uniqueness.
When it happens
Trigger: Calling waitForApproval({ key: "ship-it" }) twice in the same workflow, or in a loop whose iterations reuse the same literal key.
Common situations: Copy-pasting an approval step and forgetting to change the key; loops creating one approval per item but hard-coding the key; dynamic branches generating approvals with colliding keys.
Related errors
- waitForApproval can only be called inside a workflow()
- WAC step key "{requested_key}" is already used in this workf
- No workflow() entrypoint found. Wrap your main function with
- PlanSlotTakenError
- Invalid note at index ${index}: duplicate note id "${n.id}"
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/b6380cc7fd32ac38.
Report an issue: GitHub.