windmill-labs/windmill · error · Error

taskScript("${path}") can only be called inside a workflow()

Error message

taskScript("${path}") can only be called inside a workflow()

What it means

taskScript(path) is a step-composition helper that must run under an active workflow() context: it delegates to ctx._nextStep to append a script step to the workflow being built. When no workflow context is registered, the wrapper throws this error naming the path (typescript-client/client.ts:2267), because a script task outside workflow() has nothing to be scheduled onto.

Source

Thrown at typescript-client/client.ts:2267

/**
 * Create a task that dispatches to a separate Windmill script.
 *
 * @example
 * const extract = taskScript("f/data/extract");
 * // inside workflow: await extract({ url: "https://..." })
 */
export function taskScript(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any> {
  const name = path.split("/").pop() || path;
  const wrapper = function (...args: any[]) {
    const ctx: WorkflowCtx | null = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");
    if (ctx) {
      const kwargs = args.length === 1 && typeof args[0] === "object" && args[0] !== null
        ? args[0]
        : args.reduce((acc, v, i) => { acc[`arg${i}`] = v; return acc; }, {} as Record<string, any>);
      return ctx._nextStep(name, path, kwargs, "script", options);
    }
    throw new Error(`taskScript("${path}") can only be called inside a workflow()`);
  };
  Object.defineProperty(wrapper, "name", { value: name });
  (wrapper as any)._is_task = true;
  (wrapper as any)._task_path = path;
  return wrapper;
}

/**
 * Create a task that dispatches to a separate Windmill flow.
 *
 * @example
 * const pipeline = taskFlow("f/etl/pipeline");
 * // inside workflow: await pipeline({ input: data })
 */
export function taskFlow(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any> {
  const name = path.split("/").pop() || path;
  const wrapper = function (...args: any[]) {
    const ctx: WorkflowCtx | null = _workflowCtx ?? Reflect.get(globalThis, "__wmill_wf_ctx");

View on GitHub (pinned to e474e8803c)

Solutions

  1. Move the taskScript call inside the workflow() callback so the context is active.
  2. If calling from a helper, keep the helper synchronous within the workflow callback scope.
  3. For tests, use a harness/stub that installs a workflow context (setWorkflowCtx).
  4. Check that an earlier await did not detach the call from the active workflow context.

Example fix

// before
export async function main() {
  await greet('world'); // taskScript outside workflow() -> throws
}
// after
export async function main() {
  await workflow(async () => {
    await greet('world'); // inside workflow context
  });
}
Defensive patterns

Strategy: try-catch

Validate before calling

const inWorkflow = () =>
  Boolean(_workflowCtx ?? Reflect.get(globalThis, '__wmill_wf_ctx'));

Type guard

function requireWorkflowCtx(): WorkflowCtx {
  const ctx: WorkflowCtx | null = _workflowCtx ?? Reflect.get(globalThis, '__wmill_wf_ctx');
  if (!ctx) throw new Error('taskScript called outside workflow()');
  return ctx;
}

Try / catch

try {
  await greet('world');
} catch (e) {
  if (e.message.includes('can only be called inside a workflow()')) {
    // fall back to direct execution for standalone runs
    await windmill.runScript('u/greet', { args: ['world'] });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling a taskScript-created function at module top level, inside a plain main(), or after the workflow() callback returned; context not propagated into async callbacks or nested closures.

Common situations: Calling the task in unit tests without a workflow harness; awaiting the task outside the workflow callback; refactoring moved the call out of workflow() scope; the global ctx unset because multiple client bundles were loaded.

Related errors


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