Basic Tool Use

Foundation Models supports tool calling to extend model capabilities beyond their training data. Tools are functions that the AI can call to access external data and perform actions.

Prerequisites and Context

This chapter builds on the session management from earlier chapters and structured generation concepts from the previous chapter. You should understand how to create sessions, handle responses, and work with @Generable types. Tools extend these capabilities by accessing real-world data and performing actions beyond what the model learned during training.

What You Will Learn

By the end of this chapter, you will be able to:

  • Understand how tool calling works and when to use it
  • Implement the Tool protocol to create custom functions for the AI
  • Use @Generable types for type-safe tool arguments and outputs
  • Build practical tools like web search and API integrations
  • Handle tool errors gracefully and provide meaningful fallbacks
  • Apply tool building best practices for focused, reliable functionality

How Tools Work

Tool calling was probably the toughest concept for me to understand two years ago, but breaking it down into steps makes it much easier to grasp. Here is an example when you ask about the weather:

  1. User asks: “Is it hotter in New Delhi or San Francisco?”
  2. AI determines: It needs weather data for both cities
  3. AI calls tools: Makes weather API calls for each city
  4. Tools provide the results: Structured response returned to the model
  5. AI processes results: Compares temperatures from both locations
  6. AI responds: Provides comparison with actual weather data

Here is a basic workflow when working with tools:

  • Define Tools: You create and define the functions the AI can call, similar to how you write data-fetching code in your apps
  • Register Tools: You add the tools to the session. The model determines which tool to call based on the user’s request
  • Handle Calls: When requested, the model will call the external tool, which can be endpoints to third-party services or system functions like adding a reminder
  • Return Results: Provide structured data for AI responses

This makes tools powerful for extending the model’s capabilities by connecting to real-world services and APIs. Instead of the model hallucinating answers, you can get correct and up-to-date information from external sources.

Understanding the Tool Protocol

Foundation Models defines tools through the Tool protocol. This protocol provides a structured way to create functions that the AI can call when it needs external data or wants to perform actions.

Here is the basic structure of any tool:

struct MyTool: Tool {
    let name = "toolName"
    let description = "What this tool does"
    
    @Generable
    struct Arguments {
        // Define what parameters the tool accepts
    }
    
    @concurrent func call(arguments: Arguments) async throws -> Output {
        // Your tool logic goes here
        // Return data the AI can understand
    }
}

Starting in iOS 26.4, the Tool protocol requires the @concurrent attribute on call(arguments:). This attribute, introduced in Swift 6.2 as part of SE-0461, means the method runs on the concurrent thread pool rather than inheriting the caller’s actor isolation. Without it, nonisolated async functions now default to running on the caller’s actor. In practice, your tool code should not assume it runs on any particular actor. If you need to touch the UI or @MainActor-isolated state inside a tool, wrap that work in await MainActor.run { } explicitly.

Tool Components Explained

Name and Description The AI uses these to understand when to call your tool. Clear naming and descriptions help the model decide which tool to invoke and when.

Use clear, action-oriented names like calculateTip, sendEmail, or getCurrentWeather. Avoid abbreviations—use getUserLocation instead of getUsrLoc. As with your Swift function names, be specific about the action: saveUserPreferences rather than just save.

For the description, explain what the tool does and when to use it. You can include the context about the data it returns. The most important part is to be specific about the tool’s purpose.

// Good examples
let name = "calculateTip"
let description = "Calculates tip amount and total bill based on bill amount and tip percentage"

let name = "getCurrentWeather"  
let description = "Gets current weather conditions for a specific city including temperature, humidity, and conditions"

// Poor examples - too vague
let name = "calc"
let description = "Does math"

let name = "weather"
let description = "Weather stuff"

Arguments with @Generable Use @Generable to define type-safe parameters. The @Guide attribute helps the AI understand how to use each parameter:

@Generable
struct Arguments {
    @Guide(description: "The search query to look up")
    var query: String
    
    @Guide(description: "Maximum number of results to return", .range(1...10))
    var maxResults: Int?
}

Tool Output Return any type that conforms to PromptRepresentable. This could be a String, an array, or a custom @Generable struct:

@Generable
struct SearchResult {
    let title: String
    let content: String
    let url: String
}

@concurrent func call(arguments: Arguments) async throws -> [SearchResult] {
    // Perform search and return structured results
    let results = try await performSearch(query: arguments.query)
    return results
}

API Integration: Search Tool

Now that you understand the basic structure, here is a practical tool that demonstrates real value. Through experimentation, I determined that the model’s training data has a cutoff around October 2023. For example, if I ask for the current president of the US, here is the response:

As of October 2023, the President of the United States is Joe Biden.

While this is one of the reasons you should not depend on the model for factual data, you can correct it by using a simple search tool.

Here is a search tool implementation using Tavily API, which provides high-quality search results for AI apps.

Note: I am not affiliated with or sponsored by Tavily. I chose this API because Apple’s sample projects already include weather tool examples, and I wanted to show a different use case. This code is adapted from my implementation in the Zenther app, where I use it to fetch accurate nutritional facts.

struct SearchTool: Tool {
    let name = "searchWeb"
    let description = "Search the web for information on any topic using Tavily API"

    @Generable
    struct Arguments {
        @Guide(description: "The search query to look up")
        var query: String
    }

    // Structured search result data
    struct SearchResult: Encodable {
        let title: String
        let content: String
        let url: String
        let score: Double
    }

    // Call this API in your server instead. Do NOT do this in production app!
    @AppStorage("tavilyAPIKey") private var tavilyAPIKey: String = ""

    @concurrent func call(arguments: Arguments) async throws -> some PromptRepresentable {
        let searchQuery = arguments.query.trimmingCharacters(in: .whitespacesAndNewlines)

        guard !searchQuery.isEmpty else {
            return createErrorOutput(for: searchQuery, error: SearchError.emptyQuery)
        }

        guard !tavilyAPIKey.isEmpty else {
            return createErrorOutput(for: searchQuery, error: SearchError.missingAPIKey)
        }

        do {
            let results = try await performSearch(query: searchQuery)
            return createSuccessOutput(from: results, query: searchQuery)
        } catch {
            return createErrorOutput(for: searchQuery, error: error)
        }
    }

    private func performSearch(query: String) async throws -> [SearchResult] {
        let url = URL(string: "https://api.tavily.com/search")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer (tavilyAPIKey)", forHTTPHeaderField: "Authorization")

        let requestBody = [
            "query": query,
            "max_results": 3,
            "include_answer": false,
            "include_raw_content": false
        ] as [String : Any]

        request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw SearchError.apiError
        }

        let searchResponse = try JSONDecoder().decode(TavilySearchResponse.self, from: data)

        return searchResponse.results.map { result in
            SearchResult(
                title: result.title,
                content: result.content,
                url: result.url,
                score: result.score
            )
        }
    }

    private func createSuccessOutput(from results: [SearchResult], query: String) -> GeneratedContent {
        let summary = results.map {
            "($0.title)\n($0.content)\nSource: ($0.url)"
        }.joined(separator: "\n\n")

        return GeneratedContent(properties: [
            "query": query,
            "resultCount": results.count,
            "summary": summary,
            "status": "success"
        ])
    }

    private func createErrorOutput(for query: String, error: Error) -> GeneratedContent {
        GeneratedContent(properties: [
            "query": query,
            "error": "Unable to perform search: (error.localizedDescription)",
            "resultCount": 0,
            "summary": "Search failed for query: '(query)'",
            "status": "error"
        ])
    }
}

// Tavily API response structures
struct TavilySearchResponse: Decodable, Sendable {
    let results: [TavilySearchResult]
}

struct TavilySearchResult: Decodable, Sendable {
    let title: String
    let content: String
    let url: String
    let score: Double
}

// Custom error types for better error handling
enum SearchError: Error, LocalizedError {
    case emptyQuery
    case invalidURL
    case apiError
    case missingAPIKey

    var errorDescription: String? {
        switch self {
        case .emptyQuery:
            return "Search query cannot be empty"
        case .invalidURL:
            return "Invalid search URL"
        case .apiError:
            return "Search API request failed"
        case .missingAPIKey:
            return "Tavily API key is required. Please configure it in Settings."
        }
    }
}

This implementation shows how to integrate a third-party search API and handle errors gracefully. The tool provides real value by accessing external data that the model cannot know from its training.

Using Tools in Your App

Here is how to set up a session with the search tool:

#Playground {
    let instructions = "The user will provide a search term. Use the SearchTool to perform the search and output the response to them, summarising the top results."

    let prompt = "Who is the current president of US?"

    let session = LanguageModelSession(tools: [SearchTool()], instructions: instructions)

    // Ask question and let AI use tools as needed
    let response = try await session.respond(to: prompt)

    print(response.content)
}

The model automatically determines which tools to call based on the user’s question. Here is the output:

The current President of the United States is Donald John Trump. He took office on January 20, 2025.

Tool Building Best Practices

The search tool example demonstrates several patterns worth following:

Keep tools focused

Each tool should do one thing well.

// Good: Specific tool
struct WeatherTool: Tool { /* Gets weather */ }

// Less good: Generic tool that tries to do everything  
struct DataTool: Tool { /* Weather, news, stocks, etc. */ }

Write clear descriptions

Help the AI understand when to use your tool:

let description = "Retrieve the latest weather information for a city using OpenMeteo API"
// Not: "Get weather" or "Weather stuff"

Use structured arguments

Use @Generable for type-safe input:

@Generable
struct Arguments {
    @Guide(description: "The city to get weather for")
    var city: String
}

Handle errors

Always provide useful error responses.

// Good: Meaningful error handling
@concurrent func call(arguments: Arguments) async throws -> some PromptRepresentable {
    do {
        let data = try await fetchData()
        return GeneratedContent(data)
    } catch {
        return GeneratedContent(
            properties: [
                "error": "Unable to fetch data: (error.localizedDescription)",
                "success": false
            ]
        )
    }
}

Health Data Integration with Zenther

Building on these best practices, here is a real-world application from my app Zenther. The app uses tool calling to enable natural conversations about health data. Instead of navigating through different screens to find workout stats or nutrition information, users can ask the AI fitness partner questions like “How did my workouts look this week?” or “How many calories should I burn more to manage my weekly average?”

This approach made the chat integration genuinely helpful rather than just a gimmick. The model accesses HealthKit data and provides personalized feedback based on actual numbers instead of generic advice.

Here is the HealthDataTool implementation from the Zenther app:

struct HealthDataTool: Tool {
    let name = "fetchHealthData"
    let description = "Fetch current health data including steps, heart rate, sleep, and other metrics"

    @Generable
    struct Arguments {
        @Guide(description: "The type of health data to fetch: 'today', 'weekly', or specific metric like 'steps', 'heartRate', 'sleep', 'activeEnergy', 'distance'")
        var dataType: String
    }

    @concurrent func call(arguments: Arguments) async throws -> some PromptRepresentable {        
        switch arguments.dataType.lowercased() {
        case "today":
            return await fetchTodayData()
        case "weekly":
            return await fetchWeeklyData()
        case "steps":
            return await fetchSpecificMetric(type: MetricType.steps)
        case "heartrate":
            return await fetchSpecificMetric(type: MetricType.heartRate)
        case "sleep":
            return await fetchSpecificMetric(type: MetricType.sleep)
        default:
            return createErrorOutput(error: "Invalid data type. Use 'today', 'weekly', 'steps', 'heartRate', 'sleep', 'activeEnergy', or 'distance'.")
        }
    }
    
    private func fetchTodayData() async -> GeneratedContent {
        let healthManager = HealthDataManager.shared
        let metricsJSON = 
            """
            {
                "steps": (Int(healthManager.todaySteps)),
                "activeEnergy": (Int(healthManager.todayActiveEnergy)),
                "distance": (String(format: "%.2f", healthManager.todayDistance)),
                "heartRate": (Int(healthManager.currentHeartRate)),
                "sleep": (String(format: "%.1f", healthManager.lastNightSleep))
            }
            """
        
        return GeneratedContent(properties: [
            "status": "success",
            "dataType": "today",
            "metrics": metricsJSON,
            "message": "Today's health data retrieved successfully"
        ])
    }
    
    private func createErrorOutput(error: String) -> GeneratedContent {
        GeneratedContent(properties: [
            "status": "error",
            "error": error,
            "message": "Failed to fetch health data"
        ])
    }
}

These tools allow the AI to ground its responses in real data rather than generic fitness advice. Instead of saying “you should exercise more,” it can say “I noticed you missed your usual Wednesday workout this week - would you like to schedule a make-up session?”

What’s Next

Tools help you make the most out of the on-device foundation models. Instead of the model being limited to its training data, it can now call your code to get fresh information or perform actions. The Tool protocol provides a clean interface for this: you define what your tool does and what arguments it needs, and the model figures out when to call it.

The key insight is keeping tools simple and focused. A calculator tool does math. A search tool searches. A weather tool gets weather. When you try to make one tool do everything, the model gets confused about when to use it. Better to have five focused tools than one that tries to do everything.

Now that you understand how to build individual tools, the next chapter explores advanced chat patterns for building production-ready conversation interfaces. You will learn how to manage context, handle conversation memory, and orchestrate multi-turn interactions that combine sessions, generation controls, and tool calling into robust chat experiences.