Skip to content

Custom Tools

Extend Iris's capabilities by creating custom tools that integrate with your services and data. Custom tools have full access to Laravel's ecosystem -databases, APIs, queues, and more.

Tool Anatomy

Every tool is a class that extends Prism's Tool base class. Here's the structure:

php
<?php

declare(strict_types=1);

namespace App\Tools;

use App\Models\User;
use Prism\Prism\Tool;

class ExampleTool extends Tool
{
    public function __construct(
        protected User $user,              // Injected by Iris
        protected SomeService $service,    // Any other dependencies
    ) {
        $this
            ->as('tool_name')              // Name Iris uses to call it
            ->for('Description of what the tool does')  // Helps Iris decide when to use it
            ->withStringParameter('param', 'Description')  // Define parameters
            ->using($this);                // Tell Prism to use __invoke
    }

    public function __invoke(string $param): string
    {
        // Your tool logic here
        return 'Result message';
    }
}

For complete documentation on parameter types, return values, and advanced patterns, see the Prism Tools Documentation.

Registering Tools

Register your tools in config/iris-custom.php. Your tools are appended to the core tools:

php
return [
    'tools' => [
        App\Extensions\Tools\FetchNotesTool::class,
        App\Extensions\Tools\WeatherTool::class,
    ],
];

To disable core tools you don't need:

php
return [
    'disabled_tools' => [
        App\Tools\GenerateImageTool::class,
        App\Tools\Calendar\CreateCalendarEventTool::class,
    ],
];

Injecting the User

Iris resolves tools from Laravel's container with the authenticated user bound. This lets you scope tool behavior to the current user automatically:

php
<?php

declare(strict_types=1);

namespace App\Extensions\Tools;

use App\Models\User;
use Prism\Prism\Tool;

class FetchNotesTool extends Tool
{
    public function __construct(
        protected User $user,
    ) {
        $this
            ->as('fetch_notes')
            ->for('Fetch the user\'s saved notes')
            ->using($this);
    }

    public function __invoke(): string
    {
        $notes = $this->user
            ->notes()
            ->latest()
            ->take(10)
            ->get();

        if ($notes->isEmpty()) {
            return 'No notes found.';
        }

        return $notes
            ->map(fn ($note) => "- {$note->title}: {$note->content}")
            ->join("\n");
    }
}

You can also inject any service registered in Laravel's container:

php
public function __construct(
    protected User $user,
    protected WeatherService $weather,
    protected CacheManager $cache,
) {
    // ...
}

Return Values

Tools return strings that Iris incorporates into its response. Follow these guidelines:

Success Responses

Be concise and informative. Include relevant details Iris can relay to the user:

php
// Good
return "Note created: '{$note->title}' (ID: {$note->id})";

// Too verbose
return "The note creation operation completed successfully. The note with the title '{$note->title}' has been saved to the database with ID {$note->id}. You can now reference this note in future conversations.";

Empty Results

Clearly indicate when no results were found:

php
if ($notes->isEmpty()) {
    return 'No notes found.';
}

// Or with more context
if ($notes->isEmpty()) {
    return 'No notes found matching that search. Try different keywords.';
}

Error Responses

Return error messages as strings -don't throw exceptions unless something is truly broken. Iris can explain the error to the user:

php
public function __invoke(string $location): string
{
    $apiKey = $this->user->settings->weather_api_key;

    if (! $apiKey) {
        return 'Weather API key not configured. Add one in Settings > Integrations.';
    }

    try {
        return $this->weather->current($location, $apiKey);
    } catch (RateLimitException $e) {
        return 'Weather service rate limit reached. Try again in a few minutes.';
    } catch (Throwable $e) {
        report($e);  // Log the actual error
        return 'Unable to fetch weather data. The service may be temporarily unavailable.';
    }
}

TIP

Return user-friendly messages. Iris will present these to the user, so "Weather API key not configured" is better than "Missing WEATHER_API_KEY environment variable."

Complete Example: Task Management Tool

Here's a complete example showing a tool that manages tasks:

php
<?php

declare(strict_types=1);

namespace App\Extensions\Tools;

use App\Models\Task;
use App\Models\User;
use Prism\Prism\Tool;

class CreateTaskTool extends Tool
{
    public function __construct(
        protected User $user,
    ) {
        $this
            ->as('create_task')
            ->for('Create a new task or todo item for the user')
            ->withStringParameter('title', 'Task title')
            ->withStringParameter('description', 'Task description (optional)')
            ->withStringParameter('dueDate', 'Due date in YYYY-MM-DD format (optional)')
            ->withStringParameter('priority', 'Priority: low, medium, or high (default: medium)')
            ->using($this);
    }

    public function __invoke(
        string $title,
        string $description = '',
        ?string $dueDate = null,
        string $priority = 'medium',
    ): string {
        // Validate priority
        if (! in_array($priority, ['low', 'medium', 'high'])) {
            return "Invalid priority '{$priority}'. Use low, medium, or high.";
        }

        // Parse due date if provided
        $parsedDueDate = null;
        if ($dueDate) {
            try {
                $parsedDueDate = Carbon::parse($dueDate);
            } catch (Throwable) {
                return "Couldn't parse due date '{$dueDate}'. Use YYYY-MM-DD format.";
            }
        }

        // Create the task
        $task = $this->user->tasks()->create([
            'title' => $title,
            'description' => $description,
            'due_date' => $parsedDueDate,
            'priority' => $priority,
        ]);

        $response = "Task created: '{$task->title}'";

        if ($parsedDueDate) {
            $response .= " (due {$parsedDueDate->format('M j, Y')})";
        }

        return $response;
    }
}

Testing Tools

Test tools by binding a user to the container and resolving the tool:

php
<?php

use App\Extensions\Tools\FetchNotesTool;
use App\Models\Note;
use App\Models\User;

it('fetches notes for the authenticated user', function () {
    $user = User::factory()
        ->has(Note::factory()->count(3))
        ->create();

    app()->instance(User::class, $user);

    $tool = resolve(FetchNotesTool::class);
    $result = $tool();

    expect($result)->toContain($user->notes->first()->title);
});

it('handles users with no notes', function () {
    $user = User::factory()->create();

    app()->instance(User::class, $user);

    $tool = resolve(FetchNotesTool::class);
    $result = $tool();

    expect($result)->toBe('No notes found.');
});

it('respects the limit parameter', function () {
    $user = User::factory()
        ->has(Note::factory()->count(20))
        ->create();

    app()->instance(User::class, $user);

    $tool = resolve(FetchNotesTool::class);
    $result = $tool(limit: 5);

    // Only 5 notes should appear
    expect(substr_count($result, '- '))->toBe(5);
});

Tool Description Best Practices

The for() description helps Iris decide when to use your tool. Make it clear and specific:

php
// Good - specific about what it does
->for('Fetch the user\'s saved notes, optionally filtered by tag')

// Good - mentions when to use it
->for('Get current weather conditions for a location. Use when user asks about weather.')

// Bad - too vague
->for('Handle notes')

// Bad - too technical
->for('Executes SELECT query against notes table with pagination')

Organizing Tools

For complex applications, organize tools into subdirectories:

app/
├── Extensions/
│   └── Tools/
│       ├── Notes/
│       │   ├── FetchNotesTool.php
│       │   ├── CreateNoteTool.php
│       │   └── DeleteNoteTool.php
│       ├── Tasks/
│       │   ├── ListTasksTool.php
│       │   └── CreateTaskTool.php
│       └── Weather/
│           └── GetWeatherTool.php

Register them all in config:

php
return [
    'tools' => [
        App\Extensions\Tools\Notes\FetchNotesTool::class,
        App\Extensions\Tools\Notes\CreateNoteTool::class,
        App\Extensions\Tools\Notes\DeleteNoteTool::class,
        App\Extensions\Tools\Tasks\ListTasksTool::class,
        App\Extensions\Tools\Tasks\CreateTaskTool::class,
        App\Extensions\Tools\Weather\GetWeatherTool::class,
    ],
];