Home » What’s New in Laravel 13: AI SDK, JSON:API Resources and Vector Search
Title card reading What's New in Laravel 13, released 17 March 2026, requires PHP 8.3 to 8.5, with labels for the Laravel AI SDK, JSON:API resources, vector search and PreventRequestForgery
Laravel PHP

What’s New in Laravel 13: AI SDK, JSON:API Resources and Vector Search

Laravel 13 landed on 17 March 2026. That keeps the framework to its annual, roughly first-quarter cadence.

This major release is an unusual one. The framework team spent the cycle deliberately minimising breaking changes. As a result, the official upgrade estimate from Laravel 12 is ten minutes. The feature list, meanwhile, is one of the widest in years: a first-party AI SDK, JSON:API resources, vector similarity search in the query builder, queue routing, and a rewritten request forgery middleware.

This article walks through what changed, what breaks, and how to decide whether to upgrade. Every claim here comes from the official release notes and upgrade guide, checked on 9 August 2026. Some capabilities live in a separately versioned first-party package rather than in laravel/framework itself. This article flags those, because the distinction changes how you plan and budget the work.

Release status and supported versions

Laravel 13 is a stable release, not a beta or a development branch. The support table in the official release notes gives these dates.

  • Released: 17 March 2026.
  • Supported PHP versions: 8.3 to 8.5. PHP 8.3 is the new minimum.
  • Bug fixes for Laravel 13: until Q3 2027.
  • Security fixes for Laravel 13: until 17 March 2028.
  • Laravel 12 bug fixes: until 13 August 2026. Security fixes: until 24 February 2027.

That last line deserves attention. Laravel 12 leaves its bug-fix window in August 2026, and security patches continue only until February 2027. Applications on 12 therefore already sit inside the period where upstream will not fix a non-security bug. Laravel 11 passed the end of its security window in March 2026.

Who should care about this release

Teams on Laravel 12 face a small job. The upgrade is mostly a dependency bump plus one rename, and the support dates make it worth scheduling now.

On Laravel 11 or older, the PHP 8.3 floor matters more than anything specific to 13. So do the accumulated changes from the majors you skipped.

Choosing a stack for a new project changes the question entirely. There the interesting part is the new surface area, not the upgrade path. The AI SDK, JSON:API resources and vector search each remove a category of third-party dependency that teams previously had to select and glue together.

The Laravel AI SDK

The headline feature is the Laravel AI SDK. It offers one API for text generation, tool-calling agents, embeddings, images, audio and vector-store integrations.

Be precise about what ships where. The SDK is a separate Composer package, laravel/ai. It does not come bundled with laravel/framework, and a fresh application does not include it.

composer require laravel/ai

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate

Provider credentials come from environment variables. Each capability carries its own supported provider list. Text generation covers OpenAI, Anthropic, Gemini, Azure, Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter and OpenAI-compatible endpoints. Images, text-to-speech, speech-to-text, embeddings and reranking each support their own subset.

Agents are classes, not prompt strings

The core building block is an agent: a PHP class that holds its instructions, conversation context, tools and output schema. An Artisan command generates one, optionally with a structured output schema.

php artisan make:agent SalesCoach
php artisan make:agent SalesCoach --structured

A structured agent implements two contracts. The instructions method returns the system prompt. The schema method describes the shape the model must return, using the framework JSON schema builder.

<?php

namespace App\Ai\Agents;

use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
use Stringable;

class SalesCoach implements Agent, HasStructuredOutput
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): Stringable|string
    {
        return 'You are a sales coach, analyzing transcripts and providing feedback and an overall sales strength score.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'feedback' => $schema->string()->required(),
            'score' => $schema->integer()->min(1)->max(10)->required(),
        ];
    }
}

Prompting the agent returns a response object. Cast it to a string for plain text. Read it like an array when the agent declares a structured output schema.

$response = (new SalesCoach)->prompt('Analyze this sales transcript...');

// Plain text agents...
return (string) $response;

// Structured agents expose their schema keys...
return $response['score'];

Tools are plain classes too: a description, a JSON schema for the arguments, and a handle method that receives the validated request. The SDK can stream a response to the browser, broadcast it, queue it, or persist it as a conversation. It also ships a human approval workflow for tool calls.

use App\Ai\Agents\SalesCoach;

Route::get('/coach', function () {
    return (new SalesCoach)->stream('Analyze this sales transcript...');
});

Where the AI SDK helps, and where it does not

The value here is consistency rather than novelty. Provider SDKs already existed. What the framework adds is a single agent abstraction, first-class structured output, and queueing and streaming that behave like the rest of Laravel. It also gives you one place to swap providers. For a team hand-rolling an HTTP client wrapper around one provider, that removes a real amount of bespoke code.

The trade-offs match any AI integration, and the SDK solves none of them. You still pay per token. You still need API keys in every environment, including CI. The publish step adds database migrations for conversation persistence. Prompt behaviour still differs between providers even when the API surface does not. Treat provider-agnostic as a code-level property, not a behavioural guarantee.

Vector similarity search in the query builder

Laravel 13 adds vector similarity clauses directly to the query builder. These work only on PostgreSQL connections with the pgvector extension. MySQL and SQLite have no equivalent. Vector columns need the extension first, and the schema builder can ensure it inside a migration.

Schema::ensureVectorExtensionExists();

The main entry point is whereVectorSimilarTo. It filters by cosine similarity and, by default, also orders results by relevance. The minSimilarity threshold runs from 0.0 to 1.0, where 1.0 means identical.

$documents = DB::table('documents')
    ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
    ->limit(10)
    ->get();

Pass a plain string instead of a vector and Laravel generates the embedding for you through the AI SDK. That single line connects the two headline features. It also slips an external API call into a database query.

$documents = DB::table('documents')
    ->whereVectorSimilarTo('embedding', 'Best wineries in Napa Valley')
    ->limit(10)
    ->get();

For more control, turn the ordering off and use the distance methods directly. That suits a ranking where relevance is only one signal among several.

$documents = DB::table('documents')
    ->select('*')
    ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $queryEmbedding)
    ->limit(10)
    ->get();

The Str helper also produces embeddings from any string. That helps when you are populating the column rather than querying it.

use Illuminate\Support\Str;

$embeddings = Str::of('Napa Valley has great wine.')->toEmbeddings();

Two limitations decide whether this fits your application. The first is the database. On MySQL these clauses are not an option, and a dedicated vector store remains the answer.

The second is cost and latency. The string form of whereVectorSimilarTo embeds the query text on every call. A search endpoint under load therefore fires one embedding request per search, unless you cache the vector yourself. In practice we would generate and cache query embeddings explicitly for anything user-facing, and keep the string form for prototypes and background jobs.

JSON:API resources

Laravel now ships a JsonApiResource class. It extends the standard JsonResource and produces responses that comply with the JSON:API specification. The class handles resource object structure, relationship inclusion, sparse fieldsets and lazy attribute evaluation. It also sets the application/vnd.api+json content type.

php artisan make:resource PostResource --json-api

The generated class declares its attributes and includable relationships as properties. An attribute name reads straight from the model. A relationship name resolves the Eloquent relationship and finds the matching resource class, or you can name that class explicitly.

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class PostResource extends JsonApiResource
{
    public $attributes = [
        'title',
        'body',
        'created_at',
    ];

    public $relationships = [
        'author' => UserResource::class,
        'comments',
    ];
}

For anything conditional, override toAttributes and toRelationships instead. A closure defers the work until the response actually needs the value. That matters when an attribute costs a query, or when a relationship should differ per viewer.

/**
 * Get the resource's attributes.
 *
 * @return array<string, mixed>
 */
public function toAttributes(Request $request): array
{
    return [
        'title' => $this->title,
        'body' => $this->body,
        'is_published' => fn () => $this->published_at !== null,
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

Returning the resource works exactly as it does for ordinary API resources. That includes the toResource convenience method on the model.

Route::get('/api/posts/{post}', function (Post $post) {
    return $post->toResource();
});

Clients then shape the payload through the standard JSON:API query parameters. A relationship appears only when the request asks for it. Nested includes use dot notation, and sparse fieldsets trim attributes per resource type.

GET /api/posts/1?include=author,comments
GET /api/posts/1?include=comments.author
GET /api/posts?fields[posts]=title,created_at&fields[users]=name

Convention supplies the resource type and id. PostResource becomes the type posts, BlogPostResource becomes blog-posts, and the id comes from the primary key. Override toType and toId when a resource wraps a model with a different name, such as an AuthorResource over the User model. Overriding toLinks and toMeta adds links and meta. A configurable maximum caps nested include depth, and JsonApiResource::maxRelationshipDepth raises it.

One scoping note before you plan a migration onto this. The framework covers serialisation, not request parsing. It does not interpret filtering and sorting parameters for you, and the documentation points at a community package for that half of the specification. So an API contract that depends on JSON:API filters gets half a solution here, not all of it.

Request forgery protection is now origin-aware

This is the single high-impact change in the release, and the one most likely to touch your code. Laravel renamed the CSRF middleware from VerifyCsrfToken to PreventRequestForgery. It now verifies request origin as well as tokens.

The middleware works in two layers. First it inspects the browser Sec-Fetch-Site header, which modern browsers set automatically. That header states whether a request came from the same origin, the same site, or a cross-site source. A same-origin request passes immediately, with no token check.

When origin verification fails, the middleware falls back to the traditional CSRF token check. An older browser or an insecure connection will trigger that fallback. Browsers send Sec-Fetch-Site only over HTTPS, so a plain HTTP deployment gets token validation exactly as before.

Both configuration and exclusions now go through a preventRequestForgery method in bootstrap/app.php.

->withMiddleware(function (Middleware $middleware): void {
    // Rely solely on origin verification and disable the token fallback.
    // Failures return 403 instead of the usual 419.
    $middleware->preventRequestForgery(originOnly: true);

    // Accept same-site requests, e.g. example.com posting to dashboard.example.com.
    $middleware->preventRequestForgery(allowSameSite: true);

    // Exclude webhook endpoints that cannot send a token.
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})

The rename is what breaks. VerifyCsrfToken and ValidateCsrfToken survive as deprecated aliases, so most applications keep working. Still, update any code that names the class directly. The common offender is a test that disables the middleware.

use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;

// Laravel <= 12.x
->withoutMiddleware([VerifyCsrfToken::class]);

// Laravel >= 13.x
->withoutMiddleware([PreventRequestForgery::class]);

One behavioural detail deserves a word with whoever owns your error monitoring. In origin-only mode a rejected request returns 403, not the 419 that teams read as an expired session. Adjust alerting and user-facing copy keyed on 419 before you enable that mode.

Queue routing by class

Laravel 13 adds Queue::route. It declares the default connection and queue for a job class in one central place, rather than as properties on the job. Register it in the boot method of a service provider.

use App\Concerns\RequiresVideo;
use App\Jobs\ProcessPodcast;
use App\Jobs\ProcessVideo;
use Illuminate\Support\Facades\Queue;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');

    // An interface, trait or parent class routes every job that uses it.
    Queue::route(RequiresVideo::class, queue: 'video');

    // Several jobs at once.
    Queue::route([
        ProcessPodcast::class => ['podcasts', 'redis'],
        ProcessVideo::class => 'videos',
    ]);
}

A connection without a queue sends the job to that connection default queue. A job can still override the routing itself.

The second form is the interesting one. Routing by interface or trait means a new job picks up the right queue by declaring what it is. Nobody has to remember a property. That is the difference between a convention you can enforce and one you have to review.

Capacity isolation is the practical scenario. One slow job class starts starving the default queue. Previously you edited the job, or the dispatch sites, or both. Now you move it to a dedicated connection in the service provider, scale workers for that queue, and leave the job class untouched.

Expanded PHP attributes

Laravel 13 keeps pushing configuration into PHP attributes, so it sits beside the class it configures. Controllers gain Middleware, WithoutMiddleware and Authorize attributes.

<?php

namespace App\Http\Controllers;

use App\Models\Comment;
use App\Models\Post;
use Illuminate\Routing\Attributes\Controllers\Authorize;
use Illuminate\Routing\Attributes\Controllers\Middleware;

#[Middleware('auth')]
class CommentController
{
    #[Middleware('subscribed')]
    #[Authorize('create', [Comment::class, 'post'])]
    public function store(Post $post)
    {
        // ...
    }

    #[Authorize('delete', 'comment')]
    public function destroy(Comment $comment)
    {
        // ...
    }
}

Method-level middleware merges with class-level middleware. The attributes take the same only and except arguments as the static middleware method. Authorize shortcuts the can middleware: the first argument names the ability, and the second supplies the model class, route parameter or parameters for the policy. Child controllers inherit class-level WithoutMiddleware attributes, which strip route middleware but never global middleware.

Queued jobs gain equivalent controls. They replace properties and methods you would otherwise define on the job. A value on the job still wins over the same value passed on the worker command line.

<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\FailOnTimeout;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;

#[Tries(25)]
#[MaxExceptions(3)]
#[Timeout(120)]
#[FailOnTimeout]
class ProcessPodcast implements ShouldQueue
{
    use Queueable;

    public function handle(): void
    {
        // ...
    }
}

The semantics match the property-based equivalents exactly, which is the point. MaxExceptions still fails the job after three unhandled exceptions, even though Tries allows 25 attempts. FailOnTimeout still stops a timed-out job from retrying, whatever the attempt budget.

The release notes list further attributes across Eloquent, events, notifications, validation, testing and resource serialisation. None of this is mandatory. The old methods and properties still work. Mixing both styles across a large codebase is worse than picking one.

Extending a cache item without rewriting it

Cache::touch extends the TTL of an existing item without reading the value and writing it back. It returns true when the item exists and the expiry moves. It returns false when the item is absent.

Cache::touch('key', 3600);

Cache::touch('key', now()->addHours(2));

Small method, real use. Sliding expiry on a session-like cache entry previously meant a get and a put: two round trips, plus a race between them. Maintainers of custom cache stores should note the matching contract change below.

Passkeys ship in Fortify, not in the framework

Plenty of round-ups list passkey support as a Laravel 13 feature, so it is worth placing correctly. WebAuthn passkey authentication belongs to Laravel Fortify, a separately versioned first-party package, and Fortify itself wraps the laravel/passkeys package. It is not part of laravel/framework, and the upgrade does not bring it along.

Fortify users enable the feature in config, then implement a contract and use a trait on the user model.

use Laravel\Fortify\Features;

'features' => [
    // ...
    Features::passkeys([
        'confirmPassword' => true,
    ]),
],
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;

class User extends Authenticatable implements PasskeyUser
{
    use Notifiable, PasskeyAuthenticatable;
}

The passkeys configuration lives in config/fortify.php. It covers the relying party id, the browser origins that may complete a ceremony, the user handle secret behind opaque user identifiers, and a timeout. Fortify applies a dedicated rate limiter to its passkey routes. For custom front-ends, including Blade with browser scripts, the official @laravel/passkeys npm package drives the WebAuthn ceremonies against Fortify endpoints.

Upgrading from Laravel 12

The official estimate is ten minutes, and for a typical application that is honest. The work amounts to a dependency bump, one rename, and two configuration decisions.

Five step upgrade flow from Laravel 12 to 13: confirm PHP 8.3, update composer constraints, rename CSRF middleware references to PreventRequestForgery, review cache serializable_classes and upsert uniqueBy, then sweep the low impact renames
The Laravel 12 to 13 upgrade in five steps. Steps three and four are the changes most likely to affect an existing application.

Start with the dependency constraints in composer.json.

{
    "require": {
        "php": "^8.3",
        "laravel/framework": "^13.0",
        "laravel/tinker": "^3.0"
    },
    "require-dev": {
        "laravel/boost": "^2.0",
        "pestphp/pest": "^4.0",
        "phpunit/phpunit": "^12.0"
    }
}

The upgrade guide names laravel/framework, laravel/boost, laravel/tinker, phpunit/phpunit and pestphp/pest. Keep only the ones your application uses. Check your other first-party and community packages for Laravel 13 support before you run the update. Update the Laravel installer too if you scaffold applications with it, either through Composer or by updating Herd.

composer global update laravel/installer

An assisted path also exists. Laravel Boost, the first-party MCP server, adds an upgrade-laravel-v13 slash command for AI coding assistants once it sits inside a Laravel 12 application. It requires Boost version 2. Treat it like any automated refactor: useful for the mechanical parts, reviewed diff by diff.

Breaking changes worth reading twice

High impact

The request forgery middleware rename is the only high-impact code change, and the section above covers it. The other two high-impact items in the guide are the dependency update and the installer update.

Medium impact

First, the default cache configuration now sets serializable_classes to false. This hardens cache unserialisation against PHP deserialisation gadget chains if your APP_KEY leaks. An application that deliberately caches PHP objects must now list the classes it allows.

'serializable_classes' => [
    App\Data\CachedDashboardStats::class,
    App\Support\CachedPricingSnapshot::class,
],

Code that used to unserialise arbitrary cached objects needs one of two fixes: an explicit allow-list, or a move to non-object payloads such as arrays. Arrays are the better long-term answer. The allow-list is the faster one.

Second, upsert now validates uniqueBy. An empty value throws an InvalidArgumentException instead of generating invalid SQL. MySQL and MariaDB ignore that value in practice and rely on the primary and unique indexes to find existing rows. The validation still applies, so code that passed an empty array to satisfy the signature now fails loudly.

Low impact, but easy to miss

  • JobAttempted events now expose the exception object, or null, through an exception property. The boolean exceptionOccurred is gone.
  • The QueueBusy event renames its connection property to connectionName.
  • Bootstrap 3 pagination view names change from pagination::default and pagination::simple-default to pagination::bootstrap-3 and pagination::simple-bootstrap-3.
  • Route matching now prefers routes with an explicit domain over non-domain routes, whatever the registration order.
  • Container::call now respects nullable class parameter defaults when no binding exists, so such a parameter resolves to null rather than a fresh instance.
  • Eloquent collections restore their eager-loaded relations on deserialisation, for example inside queued jobs.
  • Laravel now pluralises polymorphic pivot table names inferred for custom pivot model classes.
  • Driver closures from a manager extend method now bind to the manager instance, so a previous $this may surprise you.
  • Test teardown now resets custom Str factories for UUIDs, ULIDs and random strings.
  • Default cache, Redis and session cookie name fallbacks now use hyphenated suffixes instead of underscored ones.
  • MySQL DELETE queries with JOIN now compile the ORDER BY and LIMIT clauses that earlier versions dropped.

Two of those deserve a comment. The cache prefix change affects only applications that lean on the framework fallback rather than defining CACHE_PREFIX, REDIS_PREFIX and SESSION_COOKIE themselves. Where it does apply, the practical effect is a cold cache and logged-out sessions at deploy time.

The DELETE change is the opposite kind of surprise. Clauses that earlier versions dropped now reach the database. A query that previously performed an unbounded delete may now throw a QueryException on engines that lack the syntax. Better failure mode, but still a new failure.

Contract additions for custom implementations

Several contracts gained methods. These rate as very low impact, unless you maintain your own implementations. In that case they are compile-time breaks.

  • Cache Store and Repository contracts add touch($key, $seconds).
  • Bus Dispatcher adds dispatchAfterResponse($command, $handler = null).
  • Routing ResponseFactory adds an eventStream signature.
  • Auth MustVerifyEmail adds markEmailAsUnverified().
  • Queue contracts add pendingSize, delayedSize, reservedSize and creationTimeOfOldestPendingJob, previously documented only in docblocks.
  • HTTP client response throw and throwIf now declare their callback parameters in the method signatures.

Behavioural changes worth a test run

A few changes will not break a build. They can still shift behaviour in ways your tests may or may not catch.

$container->call(function (?Carbon $date = null) {
    return $date;
});

// Laravel <= 12.x: Carbon instance
// Laravel >= 13.x: null

Laravel 13 also depends on symfony/polyfill-php85. Below PHP 8.5 that polyfill defines global functions including array_first and array_last, unless something defined them earlier during bootstrap. This collides with legacy helper packages. The historical array_first helper took a callback and returned the first matching element; the polyfilled function simply returns the first element. Prefer the Arr methods, which behave the same on every PHP version.

use Illuminate\Support\Arr;

Arr::first($array, function ($value) {
  return /* condition */;
});

The remaining items are small but real.

  • The default password reset mail subject moved from “Reset Password Notification” to “Reset your password”, so assertions and translation overrides keyed on the old string break.
  • Queued notifications now honour the DeleteWhenMissingModels attribute and property.
  • Js::from uses JSON_UNESCAPED_UNICODE by default, so snapshot tests expecting escaped sequences fail.
  • Schedules from withScheduling now register when the Schedule instance resolves, not during bootstrap.
  • Instantiating a model while that same model is still booting now throws a LogicException.

What has not changed

Knowing what stayed put is just as useful. The CSRF token workflow is intact. Your csrf_token helper, the @csrf Blade directive, the X-CSRF-TOKEN header and the encrypted XSRF-TOKEN cookie all behave as before. Token validation remains the default fallback, and the deprecated aliases mean the rename does not force an immediate sweep.

Application structure did not move either. The upgrade guide documents no changes to it, nor to the bootstrap/app.php configuration model from earlier releases. Both headline features stay opt-in. You install the AI SDK deliberately, and vector search needs PostgreSQL with pgvector. Upgrading to Laravel 13 pulls in neither by accident.

Who should upgrade now, and who should wait

Upgrade now if you run Laravel 12 on PHP 8.3 or later with a test suite you trust. The change surface is small, and the support clock on Laravel 12 is running. Wait longer and the upgrade competes with other work instead of standing alone as a ten-minute task.

Sequence the work more carefully in these cases:

  • Running PHP 8.2 or earlier: move to PHP 8.3 on Laravel 12 first, verify in production, then upgrade the framework. Two changes, two deploys.
  • Custom cache stores, queue drivers, dispatchers or response factories: add the new contract methods before the framework requires them.
  • Heavy caching of PHP objects: choose between an explicit serializable_classes allow-list and a move to array payloads before you upgrade, not during.
  • A dependency on laravel/helpers, or your own global array_first and array_last helpers: resolve the collision first.
  • Blocked by a community package with no Laravel 13 release: check the dependency tree before scheduling anything.

Migration checklist

  1. Confirm the application runs on PHP 8.3 or later. PHP 8.5 also works.
  2. Audit community packages for Laravel 13 compatible releases.
  3. Update the composer.json constraints for laravel/framework and the dev dependencies you use.
  4. Replace direct references to VerifyCsrfToken with PreventRequestForgery, especially in tests.
  5. Decide on the cache serializable_classes value and set it explicitly.
  6. Search for upsert calls that pass an empty uniqueBy argument.
  7. Set CACHE_PREFIX, REDIS_PREFIX and SESSION_COOKIE explicitly if you relied on the generated defaults.
  8. Update listeners for the JobAttempted and QueueBusy events.
  9. Run the full test suite. Review failures for the changed default strings and Unicode escaping.
  10. Exercise DELETE queries that combine joins with ORDER BY or LIMIT against your real database engine.
  11. Update the Laravel installer if you scaffold applications with it.

Conclusion

Laravel 13 is two releases wearing one version number. As an upgrade it is deliberately boring: one rename softened by a deprecated alias, two configuration decisions, and a list of low-impact renames you can clear in an afternoon. As a feature release it is ambitious. It moves AI agents, JSON:API serialisation and vector search from the ecosystem into first-party code.

Treat those as two separate pieces of work. Do the upgrade soon, because it is small and the Laravel 12 support window is closing. Then judge the AI SDK, JSON:API resources and vector search on their own merits, the way you would weigh any new dependency. Arriving in the same release is not a reason to adopt them together.

Further reading

  • Release notes for Laravel 13: laravel.com/docs/13.x/releases
  • Upgrade guide, 12 to 13: laravel.com/docs/13.x/upgrade
  • AI SDK documentation: laravel.com/docs/13.x/ai-sdk
  • JSON:API resources: laravel.com/docs/13.x/eloquent-resources
  • Vector similarity clauses: laravel.com/docs/13.x/queries
  • CSRF protection: laravel.com/docs/13.x/csrf
  • Fortify passkeys: laravel.com/docs/13.x/fortify

Add Comment

Click here to post a comment

12 − 4 =
Powered by MathCaptcha