windmill-labs/windmill · error · Error

${what} must be a non-empty step name without `/` or dot seg

Error message

${what} must be a non-empty step name without `/` or dot segments

What it means

assertUsableStepKey validates approval step keys because the key travels as a single URL path segment when getApprovalUrls mints resume/cancel URLs. An empty key, a key containing `/` or `\\`, or a dot segment (`.`/`..`) would produce URLs getApprovalUrls can never address, so the client rejects it up-front with this error naming the offending parameter via `what` (typescript-client/client.ts:1701).

Source

Thrown at typescript-client/client.ts:1701

) => Promise<Jsonified<Awaited<ReturnType<T>>>>;

export interface TaskOptions {
  timeout?: number;
  tag?: string;
  cache_ttl?: number;
  priority?: number;
  concurrency_limit?: number;
  concurrency_key?: string;
  concurrency_time_window_s?: number;
}

/** A step key travels as one path segment when its URLs are minted, so it must be
 *  non-empty and free of `/` and dot segments — otherwise `waitForApproval` would
 *  accept a key `getApprovalUrls` can never address. */
function assertUsableStepKey(key: string, what: string): void {
  const k = key.trim();
  if (k === "" || k === "." || k === ".." || key.includes("/") || key.includes("\\")) {
    throw new Error(`${what} must be a non-empty step name without \`/\` or dot segments`);
  }
}

export let _workflowCtx: WorkflowCtx | null = null;
export function setWorkflowCtx(ctx: WorkflowCtx | null) {
  _workflowCtx = ctx;
  Reflect.set(globalThis, "__wmill_wf_ctx", ctx);
}


export class WorkflowCtx {
  private completed: Record<string, any>;
  /** Null-prototype: step keys are caller-supplied, and a plain object would
   *  resolve `toString`/`constructor`/`__proto__` off `Object.prototype`. */
  private counters: Record<string, number> = Object.create(null);
  /** Every key handed out by `_allocKey`, so distinct names can't alias one key. */
  private _usedKeys = new Set<string>();
  private pending: Array<{

View on GitHub (pinned to e474e8803c)

Solutions

  1. Provide a non-empty key of path-segment-safe characters: letters, digits, `-`, `_`.
  2. Sanitize dynamic keys before passing: strip or replace `/` and `\\`, reject dot-only values.
  3. Encode hierarchy with a safe separator like `--` instead of `/`.
  4. Add your own pre-call validation so bad keys fail before reaching the client.

Example fix

// before
await waitForApproval({ key: `approvals/${ticketId}` }); // throws: contains '/'
// after
const key = `approvals--${ticketId}`; // path-segment safe
await waitForApproval({ key });
Defensive patterns

Strategy: validation

Validate before calling

function assertSafeKey(key: string, what = 'step key'): string {
  const k = key.trim();
  if (k === '' || k === '.' || k === '..' || key.includes('/') || key.includes('\\')) {
    throw new Error(`${what} must be a non-empty path-segment-safe name`);
  }
  return k;
}

Type guard

const isUsableStepKey = (k: unknown): k is string =>
  typeof k === 'string' && k.trim() !== '' && !k.includes('/') && !k.includes('\\') && k !== '.' && k !== '..';

Try / catch

try {
  await waitForApproval({ key });
} catch (e) {
  if (e.message.includes('must be a non-empty step name')) {
    await waitForApproval({ key: sanitizeKey(key) });
  } else throw e;
}

Prevention

When it happens

Trigger: Passing an empty string, `"."`, `".."`, or a key containing slashes/backslashes as the `key` option to waitForApproval, or as a step name wherever assertUsableStepKey runs.

Common situations: Building keys dynamically from user input or file paths (`approvals/2024-05`); interpolating empty template variables into the key; Windows-style backslash separators; copying a step path including slashes into the key field.

Related errors


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