How to Add AI Features to Your Laravel 12 SaaS with the New AI SDK

By LaraSpeed Team · · 14 min read

1. The Laravel AI SDK: What Just Dropped

On February 5, 2026, Taylor Otwell released the Laravel AI SDK — a first-party package that makes adding AI to your Laravel app as natural as using Eloquent or queues. No more cobbling together raw HTTP calls to OpenAI, wrestling with different provider APIs, or building your own agent framework. With composer require laravel/ai, you get a unified, expressive interface for AI text generation, agents with tools, structured output, image generation, embeddings, audio, and more.

In this Laravel AI SDK tutorial, we'll go beyond the "hello world" examples and build three real AI-powered features for a SaaS application:

  1. An AI customer support agent that answers questions using your docs and conversation history.
  2. Automatic content generation with structured output for product descriptions.
  3. AI-powered analytics summaries that explain your data in plain English.

Each example uses the real SDK API with working code you can drop into your Laravel SaaS application. Let's build.

2. Installation and Configuration

The Laravel AI SDK requires PHP 8.4+ and Laravel 12. Install it with Composer:

composer require laravel/ai

# Publish config and migrations
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"

# Run migrations (for conversation persistence)
php artisan migrate

Add your AI provider keys to .env. The SDK supports OpenAI, Anthropic, Gemini, Mistral, Cohere, xAI, and Ollama out of the box:

# .env — add the providers you plan to use
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

The SDK uses Prism under the hood, so you get provider failover for free. If OpenAI is down, your app can automatically fall back to Anthropic. Configure your preferred default provider in config/ai.php:

// config/ai.php
'providers' => [
    'openai' => [
        'driver' => 'openai',
        'key' => env('OPENAI_API_KEY'),
    ],
    'anthropic' => [
        'driver' => 'anthropic',
        'key' => env('ANTHROPIC_API_KEY'),
    ],
],

That's it. No service providers to register, no facades to configure. The SDK auto-discovers everything.

3. Understanding Agents and Tools

The core concept in the Laravel AI SDK is the Agent. An agent is a PHP class that wraps an AI model with instructions, tools, and optional conversation memory. Think of it as a specialized AI worker that knows your business domain.

Generate an agent with Artisan:

php artisan make:agent SupportAgent

This creates an agent class at app/Ai/Agents/SupportAgent.php. An agent implements the Agent contract and must define an instructions() method — the system prompt that shapes the agent's behavior:

<?php

namespace App\Ai\Agents;

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;

class SupportAgent implements Agent
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a helpful customer support agent for a SaaS product.';
    }
}

Use the agent with one line:

$response = (new SupportAgent)->prompt('How do I reset my password?');
return (string) $response;

Tools are the real power. They let the AI call your PHP code — look up a customer, check subscription status, query your database, or trigger an action. The agent decides when to use a tool based on the conversation context. Generate a tool with:

php artisan make:tool LookupCustomer

4. Build an AI Customer Support Agent

Let's build a real AI support agent for a Laravel AI SaaS app. This agent can answer customer questions, check their subscription status, look up their recent activity, and search your documentation — all through natural conversation.

First, create the tools the agent needs:

<?php

namespace App\Ai\Tools;

use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;

class LookupCustomer implements Tool
{
    public function description(): string
    {
        return 'Look up a customer by email address to retrieve their account details and subscription status.';
    }

    public function handle(Request $request): string
    {
        $user = User::where('email', $request['email'])->first();

        if (! $user) {
            return 'No customer found with that email address.';
        }

        $team = $user->currentTeam;
        $subscription = $team?->subscription('default');

        return json_encode([
            'name' => $user->name,
            'email' => $user->email,
            'team' => $team?->name,
            'plan' => $subscription?->stripe_price ?? 'free',
            'status' => $subscription?->stripe_status ?? 'no subscription',
            'member_since' => $user->created_at->toDateString(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'email' => $schema->string()->required(),
        ];
    }
}
<?php

namespace App\Ai\Tools;

use App\Models\Ticket;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;

class GetRecentTickets implements Tool
{
    public function description(): string
    {
        return 'Retrieve the customer\'s recent support tickets to understand their history.';
    }

    public function handle(Request $request): string
    {
        $tickets = Ticket::where('email', $request['email'])
            ->latest()
            ->limit(5)
            ->get(['subject', 'status', 'created_at']);

        return $tickets->isEmpty()
            ? 'No previous support tickets found.'
            : $tickets->toJson();
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'email' => $schema->string()->required(),
        ];
    }
}

Now build the agent that uses these tools along with the SDK's built-in SimilaritySearch for searching your docs:

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\GetRecentTickets;
use App\Ai\Tools\LookupCustomer;
use App\Models\DocSection;
use App\Models\User;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;
use Laravel\Ai\Tools\SimilaritySearch;

#[\Laravel\Ai\Attributes\MaxSteps(8)]
#[\Laravel\Ai\Attributes\Temperature(0.3)]
class SupportAgent implements Agent, HasTools, Conversational
{
    use Promptable, RemembersConversations;

    public function instructions(): string
    {
        return <<<'PROMPT'
        You are a friendly, knowledgeable customer support agent. You help
        customers with their questions about the product. When a customer
        provides their email, look up their account to give personalized help.
        Search the documentation when answering product questions. Be concise
        and helpful. If you cannot resolve an issue, offer to escalate to a
        human agent.
        PROMPT;
    }

    public function tools(): iterable
    {
        return [
            new LookupCustomer,
            new GetRecentTickets,
            SimilaritySearch::usingModel(
                model: DocSection::class,
                column: 'embedding',
                minSimilarity: 0.6,
                limit: 5,
            ),
        ];
    }
}

Wire it up in a controller with conversation persistence so the agent remembers context across messages:

<?php

namespace App\Http\Controllers;

use App\Ai\Agents\SupportAgent;
use Illuminate\Http\Request;

class AiSupportController extends Controller
{
    public function start(Request $request)
    {
        $response = (new SupportAgent)
            ->forUser($request->user())
            ->prompt($request->input('message'));

        return response()->json([
            'reply' => (string) $response,
            'conversation_id' => $response->conversationId,
        ]);
    }

    public function reply(Request $request, string $conversationId)
    {
        $response = (new SupportAgent)
            ->continue($conversationId, as: $request->user())
            ->prompt($request->input('message'));

        return response()->json([
            'reply' => (string) $response,
        ]);
    }
}

The agent automatically decides when to look up a customer, search docs, or respond from its own knowledge. The SDK handles the entire tool-calling loop — the agent can make up to 8 tool calls per turn (configured via #[MaxSteps(8)]) before generating its final answer.

5. Auto-Generate Product Descriptions

Many SaaS products let users create listings, products, or projects. AI can generate descriptions, titles, and metadata automatically. The SDK's structured output ensures you get clean, typed data back — not raw text you need to parse.

php artisan make:agent ProductDescriptionGenerator --structured
<?php

namespace App\Ai\Agents;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;

#[\Laravel\Ai\Attributes\Temperature(0.7)]
class ProductDescriptionGenerator implements Agent, HasStructuredOutput
{
    use Promptable;

    public function instructions(): string
    {
        return <<<'PROMPT'
        You are a professional copywriter for an e-commerce SaaS platform.
        Generate compelling product descriptions that are SEO-friendly,
        highlight key benefits, and encourage purchases. Keep descriptions
        between 100-200 words. Generate exactly 3 SEO tags.
        PROMPT;
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()->required(),
            'short_description' => $schema->string()->required(),
            'long_description' => $schema->string()->required(),
            'seo_tags' => $schema->array(
                items: $schema->string()
            )->required(),
            'suggested_category' => $schema->string()->required(),
        ];
    }
}

Use it in a controller to auto-fill product forms:

<?php

namespace App\Http\Controllers;

use App\Ai\Agents\ProductDescriptionGenerator;
use Illuminate\Http\Request;

class AiContentController extends Controller
{
    public function generateDescription(Request $request)
    {
        $request->validate([
            'product_name' => 'required|string|max:255',
            'features' => 'required|string',
        ]);

        $response = (new ProductDescriptionGenerator)->prompt(
            "Generate a product description for: {$request->product_name}. "
            . "Key features: {$request->features}"
        );

        // $response is array-accessible thanks to structured output
        return response()->json([
            'title' => $response['title'],
            'short_description' => $response['short_description'],
            'long_description' => $response['long_description'],
            'seo_tags' => $response['seo_tags'],
            'suggested_category' => $response['suggested_category'],
        ]);
    }
}

The structured output schema guarantees the AI returns exactly the fields you expect. No regex parsing, no JSON.decode errors. The SDK validates the response against your schema and retries if the output doesn't match.

You can also queue generation for bulk operations:

use Laravel\Ai\Responses\AgentResponse;

(new ProductDescriptionGenerator)
    ->queue("Generate a description for: $productName. Features: $features")
    ->then(function (AgentResponse $response) use ($product) {
        $product->update([
            'description' => $response['long_description'],
            'seo_tags' => $response['seo_tags'],
        ]);
    });

6. AI-Powered Analytics Summaries

Instead of dashboards full of charts that users struggle to interpret, give them AI-generated summaries. Build an analytics agent that reads your data and produces plain-English insights:

php artisan make:agent AnalyticsAgent --structured
php artisan make:tool QueryMetrics
<?php

namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\DB;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;

class QueryMetrics implements Tool
{
    public function __construct(private int $teamId) {}

    public function description(): string
    {
        return 'Query business metrics for the current team. Returns revenue, signups, churn, and usage data for the specified period.';
    }

    public function handle(Request $request): string
    {
        $period = $request['period']; // 'week', 'month', 'quarter'
        $startDate = match ($period) {
            'week' => now()->subWeek(),
            'month' => now()->subMonth(),
            'quarter' => now()->subQuarter(),
        };

        $metrics = [
            'revenue' => DB::table('payments')
                ->where('team_id', $this->teamId)
                ->where('created_at', '>=', $startDate)
                ->sum('amount') / 100,
            'new_users' => DB::table('team_user')
                ->where('team_id', $this->teamId)
                ->where('created_at', '>=', $startDate)
                ->count(),
            'active_users' => DB::table('activity_log')
                ->where('team_id', $this->teamId)
                ->where('created_at', '>=', $startDate)
                ->distinct('user_id')
                ->count(),
            'churn_count' => DB::table('subscriptions')
                ->where('team_id', $this->teamId)
                ->where('ends_at', '>=', $startDate)
                ->whereNotNull('ends_at')
                ->count(),
        ];

        return json_encode($metrics);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'period' => $schema->string()->enum(['week', 'month', 'quarter'])->required(),
        ];
    }
}
<?php

namespace App\Ai\Agents;

use App\Ai\Tools\QueryMetrics;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class AnalyticsAgent implements Agent, HasTools, HasStructuredOutput
{
    use Promptable;

    public function __construct(private int $teamId) {}

    public function instructions(): string
    {
        return <<<'PROMPT'
        You are a business analytics advisor. Analyze the team's metrics and
        provide actionable insights. Focus on trends, anomalies, and specific
        recommendations. Be data-driven and concise.
        PROMPT;
    }

    public function tools(): iterable
    {
        return [
            new QueryMetrics($this->teamId),
        ];
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'summary' => $schema->string()->required(),
            'highlights' => $schema->array(
                items: $schema->string()
            )->required(),
            'concerns' => $schema->array(
                items: $schema->string()
            )->required(),
            'recommendations' => $schema->array(
                items: $schema->string()
            )->required(),
        ];
    }
}

Display the AI summary on your SaaS dashboard:

// In a controller or Livewire component
$insights = (new AnalyticsAgent(currentTeam()->id))
    ->prompt('Analyze our business performance for the past month.');

// $insights['summary'] = "Revenue grew 23% month-over-month to $12,450..."
// $insights['highlights'] = ["Revenue up 23%", "12 new signups", ...]
// $insights['concerns'] = ["Churn increased by 2 users", ...]
// $insights['recommendations'] = ["Consider a re-engagement campaign...", ...]

7. Streaming Responses to the Frontend

For chat-style interfaces, you want tokens to appear in real-time, not wait for the entire response. The SDK makes streaming trivial — just swap prompt() for stream():

// routes/web.php
use App\Ai\Agents\SupportAgent;

Route::post('/ai/chat/stream', function (Request $request) {
    return (new SupportAgent)
        ->continue($request->input('conversation_id'), as: $request->user())
        ->stream($request->input('message'));
})->middleware('auth');

The SDK returns a streamed response that works with standard Server-Sent Events. On the frontend, consume it with the fetch API:

// Frontend JavaScript
async function sendMessage(message, conversationId) {
    const response = await fetch('/ai/chat/stream', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        },
        body: JSON.stringify({ message, conversation_id: conversationId }),
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let fullText = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        fullText += chunk;
        updateChatUI(fullText);
    }
}

If you're using a React or Vue frontend, the SDK also supports the Vercel AI SDK protocol with one method call:

return (new SupportAgent)
    ->stream($request->input('message'))
    ->usingVercelDataProtocol();

For real-time updates to multiple clients (e.g., team members watching a shared AI conversation), broadcast events over WebSockets:

use Illuminate\Broadcasting\Channel;

(new SupportAgent)->broadcastOnQueue(
    $request->input('message'),
    new Channel('team.' . currentTeam()->id . '.ai-chat'),
);

8. Embeddings and RAG for Knowledge Bases

Retrieval-Augmented Generation (RAG) lets your AI agent answer questions using your own data — documentation, help articles, or product data. The Laravel AI SDK has first-class support for embeddings and vector search.

Set up your database with vector columns (requires PostgreSQL with pgvector):

// Migration
use Illuminate\Support\Facades\Schema;

Schema::ensureVectorExtensionExists();

Schema::create('doc_sections', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('content');
    $table->string('source_url')->nullable();
    $table->vector('embedding', dimensions: 1536)->index();
    $table->timestamps();
});

Create embeddings when you add or update documentation:

use Illuminate\Support\Str;
use App\Models\DocSection;

// When saving a doc section, generate its embedding
$section = DocSection::create([
    'title' => 'How to configure billing',
    'content' => $content,
    'source_url' => '/docs/#/billing',
    'embedding' => Str::of($content)->toEmbeddings(cache: true),
]);

The SDK's built-in SimilaritySearch tool (which we used in our support agent earlier) automatically queries these embeddings. When a user asks "How do I set up Stripe?", the agent:

  1. Generates an embedding for the query
  2. Finds the most similar doc sections via vector search
  3. Uses those sections as context to generate an accurate answer
// The SimilaritySearch tool handles all of this:
SimilaritySearch::usingModel(
    model: DocSection::class,
    column: 'embedding',
    minSimilarity: 0.6,
    limit: 5,
    query: fn ($q) => $q->where('published', true),
)

For a complete RAG pipeline, seed your doc sections from your existing documentation using a simple Artisan command that chunks content, generates embeddings, and stores them in the database.

9. Testing AI Features Without API Costs

The SDK's testing utilities let you fake AI responses in your test suite — no API calls, no costs, deterministic results. This is critical for CI/CD pipelines.

<?php

use App\Ai\Agents\SupportAgent;
use App\Ai\Agents\ProductDescriptionGenerator;
use App\Models\User;

test('support agent responds to customer questions', function () {
    SupportAgent::fake(['I can help you reset your password. Go to Settings > Security > Reset Password.']);

    $user = User::factory()->create();

    $response = $this->actingAs($user)
        ->postJson('/ai/chat', [
            'message' => 'How do I reset my password?',
        ]);

    $response->assertOk()
        ->assertJsonFragment(['reply' => 'I can help you reset your password. Go to Settings > Security > Reset Password.']);

    SupportAgent::assertPrompted(fn ($prompt) =>
        str_contains($prompt->prompt, 'reset my password')
    );
});

test('product description generator returns structured output', function () {
    ProductDescriptionGenerator::fake([json_encode([
        'title' => 'Premium Widget Pro',
        'short_description' => 'The best widget for professionals.',
        'long_description' => 'A detailed description of the widget...',
        'seo_tags' => ['widget', 'premium', 'professional'],
        'suggested_category' => 'Tools',
    ])]);

    $response = $this->actingAs(User::factory()->create())
        ->postJson('/ai/generate-description', [
            'product_name' => 'Premium Widget',
            'features' => 'durable, lightweight, professional-grade',
        ]);

    $response->assertOk()
        ->assertJsonPath('title', 'Premium Widget Pro')
        ->assertJsonCount(3, 'seo_tags');

    ProductDescriptionGenerator::assertPrompted();
});

test('ai features are not called in tests without faking', function () {
    SupportAgent::fake()->preventStrayPrompts();

    // This will throw if any prompt is made without faking
    // Ensures you never accidentally hit the API in tests
});

The preventStrayPrompts() method is especially useful — add it to your base test case to ensure no test ever hits a real AI API. This pairs perfectly with your existing Pest test suite.

10. Production Considerations

Before shipping AI features to your users, consider these production essentials:

Rate Limiting and Cost Control

AI API calls cost money. Protect your budget with Laravel's rate limiter:

// routes/web.php
Route::middleware(['auth', 'throttle:ai'])->group(function () {
    Route::post('/ai/chat', [AiSupportController::class, 'start']);
    Route::post('/ai/chat/{conversationId}', [AiSupportController::class, 'reply']);
});

// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('ai', function ($request) {
    // Free plan: 10 AI requests per hour
    // Pro plan: 100 AI requests per hour
    $limit = $request->user()->currentTeam?->subscribed('default') ? 100 : 10;

    return Limit::perHour($limit)->by($request->user()->id);
});

Provider Failover

Use the SDK's built-in failover to ensure uptime. If your primary provider goes down, the request automatically retries with the next provider:

use Laravel\Ai\Enums\Lab;

$response = (new SupportAgent)->prompt(
    'How do I upgrade my plan?',
    provider: [Lab::OpenAI, Lab::Anthropic], // Falls back automatically
);

Middleware for Logging and Monitoring

Track AI usage, costs, and performance with agent middleware:

php artisan make:agent-middleware TrackAiUsage
<?php

namespace App\Ai\Middleware;

use Closure;
use Illuminate\Support\Facades\DB;
use Laravel\Ai\AgentPrompt;
use Laravel\Ai\Contracts\AgentMiddleware;
use Laravel\Ai\Responses\AgentResponse;

class TrackAiUsage implements AgentMiddleware
{
    public function handle(AgentPrompt $prompt, Closure $next)
    {
        $startTime = microtime(true);

        return $next($prompt)->then(function (AgentResponse $response) use ($startTime) {
            DB::table('ai_usage_logs')->insert([
                'team_id' => currentTeam()?->id,
                'user_id' => auth()->id(),
                'agent' => get_class($response->agent),
                'input_tokens' => $response->usage->inputTokens ?? 0,
                'output_tokens' => $response->usage->outputTokens ?? 0,
                'duration_ms' => (microtime(true) - $startTime) * 1000,
                'created_at' => now(),
            ]);
        });
    }
}

Gate AI Features by Plan

In a multi-tenant SaaS, you'll want to restrict AI features to paid plans:

// Middleware or controller check
if (! currentTeam()->subscribedToPrice('price_pro_monthly')) {
    return response()->json([
        'error' => 'AI features are available on the Pro plan.',
        'upgrade_url' => route('pricing'),
    ], 403);
}

11. Conclusion

The Laravel AI SDK makes adding AI to your Laravel app a remarkably smooth experience. With first-party support for agents, tools, structured output, streaming, embeddings, and testing — you can build production-grade AI features in an afternoon instead of weeks.

Here's what we built in this Laravel AI SDK tutorial:

  1. An AI customer support agent with tool-calling, doc search via RAG, and conversation memory
  2. Automated content generation with validated structured output
  3. AI analytics summaries that turn raw metrics into actionable insights
  4. Real-time streaming responses for chat interfaces
  5. Full test coverage with fake responses — zero API costs in CI
  6. Production-ready rate limiting, failover, and usage tracking

The SDK handles the hard parts — tool-calling loops, provider switching, response validation, conversation persistence — so you can focus on building the AI features that differentiate your SaaS.

LaraSpeed has everything your SaaS needs — add AI features in an afternoon.

Auth, billing, multi-tenancy, admin panel, API, tests, and CI/CD are already built. Drop in the Laravel AI SDK and start shipping AI-powered features today.

One-time purchase · Full source code · 14-day money-back guarantee