AI & LLMsTool UseClaudeAgentsAnthropic SDK

Tool Use and Function Calling with Claude: Agentic Workflows

How to give Claude tools, handle tool calls in a loop, and build reliable agentic workflows.

March 31, 2026

tool use function calling claude

A language model that can only generate text has a ceiling. It can explain how to check the weather but it can't check the weather. Tool use breaks through that ceiling — Claude can now call functions you define, use the results, and continue reasoning based on real data.

This guide covers how tool use works in the Anthropic SDK, how to build a reliable tool-call loop, and the patterns that make agentic workflows production-ready.

How Tool Use Works

You define tools as JSON schema descriptions of functions. Claude decides when to call them, constructs the arguments, and you execute the actual function. Claude then uses the result to continue its response.

The basic flow:

  1. Send a message with a tools array describing available functions
  2. Claude returns a tool_use content block with the function name and arguments
  3. You execute the function and return the result
  4. Claude uses the result to form its final response
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic()

// Step 1: Define tools
const tools: Anthropic.Tool[] = [
  {
    name: 'get_post_count',
    description: 'Returns the number of published posts for a given tenant',
    input_schema: {
      type: 'object' as const,
      properties: {
        tenantSlug: {
          type: 'string',
          description: 'The tenant slug (e.g., "arunabh-blog")',
        },
      },
      required: ['tenantSlug'],
    },
  },
]

// Step 2: Send initial message
const response = await client.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  tools,
  messages: [
    { role: 'user', content: 'How many posts does arunabh-blog have?' },
  ],
})

The Tool Call Loop

A single round-trip isn't always enough. Claude might need to call multiple tools in sequence, or call the same tool with different arguments. Build a loop:

async function runAgentLoop(
  initialMessage: string,
  tools: Anthropic.Tool[],
  toolHandlers: Record<string, (input: unknown) => Promise<unknown>>
): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: initialMessage },
  ]

  while (true) {
    const response = await client.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 1024,
      tools,
      messages,
    })

    // If Claude is done, return its final text response
    if (response.stop_reason === 'end_turn') {
      const textBlock = response.content.find(b => b.type === 'text')
      return textBlock?.type === 'text' ? textBlock.text : ''
    }

    // If Claude wants to use tools, execute them
    if (response.stop_reason === 'tool_use') {
      // Add Claude's response (including tool_use blocks) to messages
      messages.push({ role: 'assistant', content: response.content })

      // Execute each tool call and collect results
      const toolResults: Anthropic.ToolResultBlockParam[] = []

      for (const block of response.content) {
        if (block.type !== 'tool_use') continue

        const handler = toolHandlers[block.name]
        if (!handler) {
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: `Error: unknown tool "${block.name}"`,
            is_error: true,
          })
          continue
        }

        try {
          const result = await handler(block.input)
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: JSON.stringify(result),
          })
        } catch (err) {
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: `Error: ${(err as Error).message}`,
            is_error: true,
          })
        }
      }

      // Add tool results and continue the loop
      messages.push({ role: 'user', content: toolResults })
    }
  }
}

Real Tool Handlers

Here's a practical set of tools for a blog management agent:

const blogTools: Anthropic.Tool[] = [
  {
    name: 'list_posts',
    description: 'List published posts for a tenant, optionally filtered by category',
    input_schema: {
      type: 'object' as const,
      properties: {
        tenantSlug: { type: 'string' },
        categorySlug: { type: 'string', description: 'Optional category filter' },
        limit: { type: 'number', default: 10 },
      },
      required: ['tenantSlug'],
    },
  },
  {
    name: 'get_post',
    description: 'Get the full content of a single post by slug',
    input_schema: {
      type: 'object' as const,
      properties: {
        slug: { type: 'string' },
      },
      required: ['slug'],
    },
  },
  {
    name: 'create_draft',
    description: 'Create a draft post (status: draft, not published)',
    input_schema: {
      type: 'object' as const,
      properties: {
        title: { type: 'string' },
        slug: { type: 'string' },
        description: { type: 'string' },
        mdxContent: { type: 'string' },
        tenantSlug: { type: 'string' },
        categorySlug: { type: 'string' },
      },
      required: ['title', 'slug', 'description', 'mdxContent', 'tenantSlug'],
    },
  },
]

const blogHandlers = {
  list_posts: async (input: unknown) => {
    const { tenantSlug, categorySlug, limit } = input as {
      tenantSlug: string
      categorySlug?: string
      limit?: number
    }
    // ... fetch from Payload API
  },
  get_post: async (input: unknown) => {
    const { slug } = input as { slug: string }
    // ... fetch from Payload API
  },
  create_draft: async (input: unknown) => {
    const params = input as { title: string; slug: string; /* ... */ }
    // ... create via Payload REST API
  },
}

Tool Descriptions Matter

The description field is the most important part of a tool definition. Claude reads it to decide when to call the tool and how to use it. Be specific:

// ❌ Vague
{ name: 'search', description: 'Search for things' }

// ✅ Specific
{
  name: 'search_posts',
  description: 'Full-text search across all published posts. Use when the user asks about specific topics, keywords, or wants to find related content. Returns post titles, slugs, and descriptions.',
}

The description should answer: when should Claude use this, what does it return, and what are its limitations.

Preventing Infinite Loops

Add a maximum iteration count to the agent loop:

async function runAgentLoop(
  initialMessage: string,
  tools: Anthropic.Tool[],
  handlers: Record<string, (input: unknown) => Promise<unknown>>,
  maxIterations = 10
): Promise<string> {
  let iterations = 0
  // ... same loop as before, but:
  while (iterations < maxIterations) {
    iterations++
    // ... rest of loop
  }
  throw new Error(`Agent loop exceeded ${maxIterations} iterations`)
}

Ten iterations is usually more than enough for practical tasks. If your agent needs more, it's likely stuck in a reasoning loop — add better tool descriptions or break the task into smaller steps.

Key Takeaways

  • Tool use requires a loop: Claude might need multiple rounds of tool calls before it can answer
  • Always add tool results to the message history as tool_result blocks — Claude needs the full context
  • Tool descriptions drive Claude's decision-making; be specific about when to use each tool and what it returns
  • Handle tool errors gracefully by returning is_error: true rather than throwing — let Claude decide how to recover
  • Cap your loop iterations to prevent runaway agents; 10 is a reasonable default