Skip to Content
DocumentationFeaturesAI Tool Calling

AI Tool Calling

Enable the LLM to call workflow tools (function calling). The LLM decides which tools to invoke, and LlmDelegateToolCallsTool executes them.

Create a Tool for the LLM

Tools exposed to the LLM need a description so the LLM knows when to use them:

import { z } from 'zod'; import { BaseTool, Tool, ToolResult } from '@loopstack/common'; @Tool({ uiConfig: { description: 'Retrieve weather information.', }, schema: z.object({ location: z.string(), }), }) export class GetWeather extends BaseTool { async call(_args: unknown): Promise<ToolResult> { return { type: 'text', data: 'Mostly sunny, 14C, rain in the afternoon.' }; } }

Tool Calling Workflow

import { LlmGenerateTextTool, LlmGenerateTextResult, LlmMessageDocument, LlmDelegateToolCallsTool, LlmDelegateResult, } from '@loopstack/llm-provider-module'; import { BaseWorkflow, Final, Guard, Initial, InjectTool, ToolResult, Transition, Workflow } from '@loopstack/common'; @Workflow({ uiConfig: __dirname + '/tool-call.ui.yaml' }) export class ToolCallWorkflow extends BaseWorkflow { @InjectTool({ provider: 'claude', model: 'claude-sonnet-4-6', tools: ['getWeather'] }) llmGenerateText: LlmGenerateTextTool; @InjectTool({ provider: 'claude' }) llmDelegateToolCalls: LlmDelegateToolCallsTool; @InjectTool() getWeather: GetWeather; llmResult?: LlmGenerateTextResult; delegateResult?: LlmDelegateResult; @Initial({ to: 'ready' }) async setup() { await this.repository.save(LlmMessageDocument, { role: 'user', content: 'How is the weather in Berlin?', }); } @Transition({ from: 'ready', to: 'prompt_executed' }) async llmTurn() { const result: ToolResult<LlmGenerateTextResult> = await this.llmGenerateText.call({ messagesSearchTag: 'message', }); this.llmResult = result.data; } @Transition({ from: 'prompt_executed', to: 'awaiting_tools', priority: 10 }) @Guard('hasToolCalls') async executeToolCalls() { const result: ToolResult<LlmDelegateResult> = await this.llmDelegateToolCalls.call({ message: this.llmResult!.message, }); this.delegateResult = result.data; } hasToolCalls() { return this.llmResult?.message.stopReason === 'tool_use'; } @Transition({ from: 'awaiting_tools', to: 'ready' }) @Guard('allToolsComplete') async toolsComplete() {} allToolsComplete() { return this.delegateResult?.allCompleted; } @Final({ from: 'prompt_executed' }) async respond() { await this.repository.save(LlmMessageDocument, this.llmResult!.message, { meta: { response: this.llmResult!.response, provider: 'claude' }, }); } }

How the Loop Works

setup → llmTurn → [hasToolCalls?] ├─ yes → executeToolCalls → toolsComplete → llmTurn (loop) └─ no → respond (done)
  1. llmGenerateText is called — the tools array is configured on @InjectTool
  2. If the LLM returns stopReason: 'tool_use', the guard routes to executeToolCalls
  3. llmDelegateToolCalls executes the requested tools and stores results
  4. The loop continues back to the LLM
  5. When no more tool calls are needed, the fallback @Final fires

Key Concepts

  • tools array — Configured on @InjectTool(), lists tool property names the LLM can call (must match @InjectTool() property names)
  • llmDelegateToolCalls — Executes tool calls from the LLM response message
  • message.stopReason === 'tool_use' — The LLM wants to call a tool
  • allCompleted — All delegated tool calls have finished
  • @Guard + priority — Routes between tool calling and final response

Registry References

Last updated on