windmill-labs/windmill · error · RuntimeError

WAC step key "{requested_key}" is already used in this workf

Error message

WAC step key "{requested_key}" is already used in this workflow. Give each wait_for_approval() its own key so get_approval_urls() can address it.

What it means

In WAC (workflow-as-code) runs each wait_for_approval must have a unique explicit key because get_approval_urls mints URLs addressed by that key. On a duplicate explicit key the library refuses to silently rename it to <key>_2 — that would hand the caller a URL for the first step, which then fails with 'resume request already sent' and parks the workflow until timeout — and raises this RuntimeError instead.

Source

Thrown at python-client/wmill/wmill/client.py:2996

        )

    async def _wait_for_approval(
        self,
        timeout: int = 1800,
        form: dict | None = None,
        self_approval: bool = True,
        key: str | None = None,
    ):
        if key is not None:
            _assert_usable_step_key(key, "wait_for_approval key")
        requested_key, key = key, self._alloc_key(key or "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 requested_key and key != requested_key:
            raise RuntimeError(
                f'WAC step key "{requested_key}" is already used in this workflow. '
                "Give each wait_for_approval() its own key so get_approval_urls() can address it."
            )

        if key in self._completed:
            return self._completed[key]

        if self._executing_key is not None:
            await _asyncio.Future()

        print(f"\n--- WAC: wait_for_approval({key}) ---")
        raise _StepSuspend({
            "mode": "approval",
            "key": key,
            "timeout": timeout,
            "form": form,
            "self_approval_disabled": not self_approval,
            "steps": [],

View on GitHub (pinned to e474e8803c)

Solutions

  1. Give each call a unique explicit key, e.g. f"approval-{i}" in loops
  2. Omit the key on later approvals to get automatic numbering
  3. Search the workflow for duplicated key= arguments to wait_for_approval
  4. Thread a unique suffix parameter through shared approval helpers

Example fix

// before
for i, approver in enumerate(approvers):
    await client.wait_for_approval(approvers=[approver], key="approval")
// after
for i, approver in enumerate(approvers):
    await client.wait_for_approval(approvers=[approver], key=f"approval-{i}")
Defensive patterns

Strategy: try-catch

Validate before calling

seen = set()
def unique_key(k: str, seen: set) -> str:
    if k in seen:
        raise ValueError(f"duplicate WAC step key {k!r}")
    seen.add(k)
    return k

Try / catch

key_counts = {}
key_counts[k] = key_counts.get(k, 0) + 1
if key_counts[k] > 1:
    k = f"{k}-{key_counts[k]}"  # pre-deduplicate instead of hitting the error

Prevention

When it happens

Trigger: Calling wait_for_approval with the same explicit key twice in one WAC workflow, typically in a loop with a fixed key literal or copy-pasted approval steps.

Common situations: Loop-based approvals where the loop index was not interpolated into the key; duplicated approval steps after refactoring; reused template code without renaming the key.

Related errors


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