vercel/ai · error

Tool " " does not have an execute function. Client-side…

Error message

Tool "${toolCall.toolName}" does not have an execute function. Client-side tools should be filtered before calling executeTool.

What it means

executeTool requires the resolved tool to have an `execute` function. Tools without execute (client-side / confirm-style tools) must be filtered out before executeTool is called; encountering one here means an unexecutable tool reached server-side execution.

Solutions

  1. Filter out tools without an execute function before calling executeTool.
  2. Add an execute implementation to the tool if it should run server-side.
  3. Handle client-side tools in the client layer so the model's call never reaches executeTool.

Example fix

// before
await executeTool({ toolCall, tools });
// after
const serverTools = Object.fromEntries(
  Object.entries(tools).filter(([, t]) => typeof t.execute === 'function'),
);
await executeTool({ toolCall, tools: serverTools });
Defensive patterns

Strategy: validation

Validate before calling

const executable = Object.values(tools).every(t => typeof t.execute === 'function');
if (!executable) {
  // remove or handle tools lacking execute before calling executeTool
}

Type guard

function hasExecute(t: unknown): t is { execute: Function } {
  return !!t && typeof (t as any).execute === 'function';
}

Try / catch

try {
  await executeTool({ toolCall, tools });
} catch (e) {
  if (e instanceof Error && e.message.includes('does not have an execute function')) {
    // route to client-side handling / confirmation flow
  } else throw e;
}

Prevention

When it happens

Trigger: A tool defined without `execute` (e.g. a tool needing human confirmation or client-side execution) is included in the tools map handed to executeTool, and the model calls it.

Common situations: Human-in-the-loop confirmation tools not filtered before the server step; mixing client tools into a server tools registry; constructing tools programmatically and forgetting execute.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/b989b6a901795182. Report an issue: GitHub.

Appendix: source

Thrown at packages/workflow/src/workflow-agent.ts:2951

  messages: LanguageModelV4Prompt,
): ModelMessage[] {
  const withoutAssistantToolCall =
    messages.at(-1)?.role === 'assistant' ? messages.slice(0, -1) : messages;
  return withoutAssistantToolCall as unknown as ModelMessage[];
}

async function executeTool(
  toolCall: { toolCallId: string; toolName: string; input: unknown },
  tools: ToolSet,
  messages: LanguageModelV4Prompt,
  context?: unknown,
  download?: DownloadFunction,
  sandbox?: SandboxSession,
): Promise<WorkflowToolExecutionResult> {
  const tool = tools[toolCall.toolName];
  if (!tool) throw new Error(`Tool "${toolCall.toolName}" not found`);
  if (typeof tool.execute !== 'function') {
    throw new Error(
      `Tool "${toolCall.toolName}" does not have an execute function. ` +
        `Client-side tools should be filtered before calling executeTool.`,
    );
  }
  // Input is already parsed and validated by streamModelCall's parseToolCall
  const parsedInput = toolCall.input;
  let toolResult: unknown;

  try {
    // Extract execute function to avoid binding `this` to the tool object.
    // If we called `tool.execute(...)` directly, JavaScript would bind `this`
    // to `tool`, which contains non-serializable properties like `inputSchema`.
    // When the execute function is a workflow step (marked with 'use step'),
    // the step system captures `this` for serialization, causing failures.
    const { execute } = tool;
    toolResult = await execute(parsedInput, {
      toolCallId: toolCall.toolCallId,
      // Pass the conversation messages to the tool so it has context about the conversation

View on GitHub (pinned to 69428b1f8b)