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

  1. Give each waitForApproval a unique key (suffix with loop index, item id, or step purpose).
  2. Omit the `key` option entirely to keep auto-numbering when uniqueness is not needed.
  3. In loops, build the key from the iteration variable: `waitForApproval({ key: `approve-${i}` })`.
  4. 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

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


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/b6380cc7fd32ac38. Report an issue: GitHub.