AI agents become useful when they can take action.
They can query an order, inspect an invoice, search internal documents, or start a refund. Each action crosses a boundary into your application. The arguments behind that action must be treated as untrusted input.
Laravel AI SDK v0.11.1 added support for validating tool request arguments with Laravel’s validator. Version v0.11.2 hardened tool-result handling across conversations. Together, these releases make it easier to stop invalid tool calls before they reach your database or business logic.
This tutorial builds an order lookup tool. You will define a schema, validate every argument, handle failures, and test the agent without making a real provider request.
Why tool arguments need server-side validation
An LLM can produce valid JSON and still provide invalid arguments.
It might:
- Invent an order number.
- Send an email address with extra text.
- Use the wrong data type.
- Omit a required value.
- Request another user’s record.
- Pass an unsupported filter or operation.
A tool schema helps guide the model. It does not replace validation inside your application. JSON Schema describes what the model should send. Laravel validation decides what your server will accept.
That distinction matters for every php web framework application that gives an agent access to real data. It matters even more for write operations such as refunds, cancellations, and account changes.

Prerequisites
You will need:
- Laravel 12 or 13.
- PHP 8.3 or later.
- The Laravel AI SDK.
- A configured AI provider if you want to run the agent against a real model.
- An
Ordermodel with fields such asnumber,user_id,customer_email,status,total, andcreated_at.
Install the SDK with Composer:
composer require laravel/ai
The Laravel AI SDK documentation covers provider configuration, agents, tools, testing, and conversation storage.
If the SDK is already installed, update it to the hardened patch release:
composer update laravel/ai
The validation feature landed in v0.11.1. Version v0.11.2 preserves tool-result failure status across conversations.
1. Generate an agent and tool
Laravel provides Artisan commands for both classes:
php artisan make:agent OrderSupportAgent
php artisan make:tool LookupOrder
The generated tool belongs in app/Ai/Tools. An AI SDK tool implements the Laravel\Ai\Contracts\Tool interface.
The interface has three responsibilities:
- Describe when the tool should be used.
- Define its input schema.
- Execute the tool with a
Laravel\Ai\Tools\Request.
That request object contains the arguments supplied by the model.
2. Define a constrained tool schema
Open app/Ai/Tools/LookupOrder.php and define the tool:
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class LookupOrder implements Tool
{
public function __construct(
protected int $userId,
) {
//
}
public function description(): Stringable|string
{
return 'Look up one order belonging to the authenticated user by order number or email address.';
}
public function schema(JsonSchema $schema): array
{
return [
'order_id' => $schema->string()
->description('The order number, such as ORD-AB12CD34.'),
'email' => $schema->string()
->description('The customer email address associated with the order.'),
];
}
public function handle(Request $request): Stringable|string
{
// Validation will be added in the next step.
return 'Order lookup is not implemented yet.';
}
}
The schema gives the model useful structure. It also improves tool selection and reduces malformed calls.
However, neither field is marked as required. The user may provide either an order number or an email address. That business rule belongs in Laravel validation.
3. Validate every request with Laravel’s validator
The tool request exposes a validate() method. It uses Laravel’s normal validation system and returns only validated data.
public function handle(Request $request): Stringable|string
{
$validated = $request->validate([
'order_id' => [
'nullable',
'string',
'required_without:email',
'regex:/^ORD-[A-Z0-9]{8}$/i',
],
'email' => [
'nullable',
'email',
'max:255',
'required_without:order_id',
],
], [
'order_id.required_without' => 'Provide an order number or an email address.',
'email.required_without' => 'Provide an order number or an email address.',
'order_id.regex' => 'The order number must look like ORD-AB12CD34.',
]);
$orderId = isset($validated['order_id'])
? strtoupper(trim($validated['order_id']))
: null;
$email = isset($validated['email'])
? strtolower(trim($validated['email']))
: null;
$order = Order::query()
->select([
'number',
'status',
'total',
'created_at',
])
->where('user_id', $this->userId)
->when($orderId, fn ($query) => $query->where('number', $orderId))
->when($email, fn ($query) => $query->where('customer_email', $email))
->first();
if (! $order) {
return json_encode([
'found' => false,
'message' => 'No matching order was found.',
], JSON_THROW_ON_ERROR);
}
return json_encode([
'found' => true,
'order' => [
'number' => $order->number,
'status' => $order->status,
'total' => $order->total,
'created_at' => $order->created_at?->toISOString(),
],
], JSON_THROW_ON_ERROR);
}
The constructor-scoped user ID is important. The model never supplies it. The tool receives it from your application code, so a prompt cannot switch the lookup to another customer.
The query also selects only fields the agent needs. Avoid returning private notes, payment tokens, internal identifiers, or unrestricted model data.
The Laravel validation documentation lists the available rules. You can use custom rule objects, conditional rules, allowlists, and database-aware constraints just as you would for an HTTP request.
4. Let the agent recover from invalid arguments
When validation fails during an agent run, the SDK returns the validation messages to the model as the tool result. The tool’s database query does not run.
The model can then correct its arguments and call the tool again.
For example, the model might first send:
{
"order_id": "last order",
"email": ""
}
The validator rejects that request because order_id does not match the expected format. The model receives the message:
The order number must look like ORD-AB12CD34.
It can ask the user for a valid order number or retry with a corrected value.
Add the tool to your agent:
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\LookupOrder;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
#[MaxSteps(5)]
class OrderSupportAgent implements Agent, HasTools
{
use Promptable;
public function __construct(
protected int $userId,
) {
//
}
public function instructions(): string
{
return <<<'INSTRUCTIONS'
You help users with their orders.
Use the order lookup tool when the user asks about an order.
Never invent an order number.
If the tool returns validation feedback, correct the arguments or ask the user for the missing information.
Only discuss orders returned for the current user.
INSTRUCTIONS;
}
public function tools(): iterable
{
return [
new LookupOrder($this->userId),
];
}
}
Prompt the agent from a controller or service:
$response = (new OrderSupportAgent($request->user()->id))
->prompt($request->string('message')->toString());
return response()->json([
'message' => (string) $response,
]);
The agent loop now has a clear boundary:
- The model chooses a tool.
- Laravel validates the arguments.
- The tool queries only the current user’s records.
- Validation feedback returns to the model when needed.
- The model responds or retries within the step limit.
5. Treat refund tools as a separate risk level
The same validation pattern works for a refund tool:
$validated = $request->validate([
'order_id' => [
'required',
'string',
'regex:/^ORD-[A-Z0-9]{8}$/i',
],
'reason' => [
'required',
'string',
'max:500',
],
]);
Validation is not authorization. A valid order number does not prove that a refund should happen.
For sensitive actions, combine validation with:
- Authorization policies.
- A user or staff approval step.
- Idempotency using the tool call ID.
- A transaction around the mutation.
- Audit logging.
- A refund amount calculated by your application, not the model.
The AI SDK supports human approval for approvable tools. Review the human tool approval documentation before allowing an agent to perform irreversible actions.
Whether you are evaluating php developer tools or learning how to build rest api with php, the principle is the same: validate at the boundary, then authorize the operation.
What could go wrong?
The schema is treated as a security boundary
It is not. Providers and models can produce unexpected values. Always validate inside handle().
The model supplies the user ID
Do not expose user_id as a tool argument. Pass it into the tool constructor from authenticated application state.
Validation errors are hidden from the model
Do not catch validation failures and replace them with a generic exception. The SDK needs the validation result to help the model self-correct.
A tool returns too much data
Select explicit columns and cap result sizes. Tool output becomes part of the agent’s context window.
A refund runs twice
Use the provider’s tool call identifier or your own idempotency key. Then enforce uniqueness at the database or payment-provider boundary.
A query accepts model-controlled SQL fragments
Never accept raw table names, column names, operators, or SQL expressions without strict allowlists. Prefer focused Eloquent tools for known queries.
Testing with fakes
The SDK’s documented testing API is agent-specific. Use OrderSupportAgent::fake() to avoid real provider calls:
<?php
use App\Ai\Agents\OrderSupportAgent;
it('prompts the order support agent', function () {
OrderSupportAgent::fake([
'Your order is currently being prepared.',
]);
$response = (new OrderSupportAgent(userId: 42))
->prompt('Where is my order?');
expect((string) $response)
->toBe('Your order is currently being prepared.');
OrderSupportAgent::assertPrompted('Where is my order?');
});
This is the Laravel AI SDK equivalent of the AI::fake() pattern often used in application-level AI wrappers. If your project exposes its own AI facade, call AI::fake() in that wrapper’s tests. For the package itself, use the agent’s fake() method shown in the official testing documentation.
Test validation separately. Direct tool tests verify that invalid arguments fail before any query runs:
<?php
use App\Ai\Tools\LookupOrder;
use Illuminate\Validation\ValidationException;
use Laravel\Ai\Tools\Request as ToolRequest;
it('rejects malformed order arguments', function () {
$tool = new LookupOrder(userId: 42);
expect(fn () => $tool->handle(new ToolRequest([
'order_id' => 'last order',
'email' => '',
])))->toThrow(ValidationException::class);
});
You can also test a valid request with a database factory:
it('returns an order for the current user', function () {
$order = Order::factory()->create([
'user_id' => 42,
'number' => 'ORD-AB12CD34',
]);
$result = (new LookupOrder(userId: 42))->handle(
new ToolRequest([
'order_id' => $order->number,
])
);
expect(json_decode((string) $result, true))
->toMatchArray([
'found' => true,
'order' => [
'number' => 'ORD-AB12CD34',
],
]);
});
Keep both tests. The agent fake checks orchestration without network calls. The direct tool tests check the security boundary itself.
Conclusion
Tool calling gives Laravel AI agents access to your application. Validation keeps that access narrow and predictable.
Define a schema to guide the model. Validate the actual request with $request->validate(). Scope sensitive data in application code. Return actionable validation feedback so the agent can self-correct. Add authorization and approval for irreversible operations.
Laravel AI SDK v0.11.1 makes the validation step part of the tool workflow. Version v0.11.2 strengthens failure handling when conversations continue. The result is a small, familiar Laravel pattern for a new class of application input: every argument an AI agent asks your code to use.