Build Multi-Agent Workflows with the Laravel AI SDK

Build multi-agent workflows in the Laravel AI SDK: return a specialist agent from another agent's tools() method, hand off tasks, share context, and cap cost.

Steven Richardson
Steven Richardson
· 8 min read

I had a single support agent with eleven tools bolted on and a system prompt that had grown to three screens. It answered billing questions with the tone of a sysadmin and kept calling the refund tool for shipping queries. The prompt was doing too many jobs at once. The fix wasn't a better prompt — it was fewer responsibilities per agent. That's what a Laravel AI SDK multi-agent workflow buys you: a coordinator that routes, and specialists that each do one thing well.

Everything here uses laravel/ai. Install it with composer require laravel/ai.

When one Laravel AI SDK agent stops being enough#

A single agent() call is the right default. Most tasks never need more than one. The trouble starts when a single agent has to be a billing expert, a technical troubleshooter, and a policy lawyer in the same breath — each role wants different instructions, a different tool set, and sometimes a different model.

Here's the smell. When your tools() method looks like this, the prompt underneath it is almost certainly overloaded too:

public function tools(): iterable
{
    return [
        new LookupOrder,
        new IssueRefund,
        new CheckShippingStatus,
        new ResetPassword,
        new RunDiagnostics,
        new EscalateToHuman,
        // ...five more
    ];
}

The model has to hold every tool's purpose in working memory on every turn, and the more tools you give it, the more often it picks the wrong one. Splitting typed tools across focused agents is the same discipline as writing typed function tools with clear signatures — narrow the surface and the model stops guessing.

The coordinator and specialist pattern#

The Laravel AI SDK makes an agent a first-class tool. Return an Agent from another agent's tools() method and the parent can delegate to it, calling it in whatever order the task demands. That's the orchestrator-workers pattern: the coordinator understands the whole request, the specialists each own a slice.

Start with two specialists. A billing agent:

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\LookupInvoice;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class BillingAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You handle invoices, charges, and refund eligibility. Be precise about amounts and dates.';
    }

    public function tools(): iterable
    {
        return [
            new LookupInvoice,
        ];
    }
}

Then a coordinator that owns neither the billing tools nor the technical ones — it just delegates:

<?php

namespace App\Ai\Agents;

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

class SupportCoordinator implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You triage customer support requests. Route billing questions to the billing specialist and technical problems to the technical specialist. Answer general questions yourself.';
    }

    public function tools(): iterable
    {
        return [
            new BillingAgent,
            new TechnicalAgent,
        ];
    }
}

Prompt the coordinator like any other agent:

use App\Ai\Agents\SupportCoordinator;

$response = (new SupportCoordinator)->prompt(
    'I was charged twice for order #4471 this morning.'
);

echo $response->text;

The coordinator reads the request, sees it's a billing problem, and calls BillingAgent as a tool. You didn't hardcode that routing — the model chose the specialist from the description it was given.

Handing off between agents#

By default the parent sees a sub-agent by its class basename with a generic description. That's fine for a prototype, but the description is how the coordinator decides when to delegate — vague descriptions cause bad routing. Implement CanActAsTool to control exactly what the coordinator sees:

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\RunDiagnostics;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\CanActAsTool;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
class TechnicalAgent implements Agent, CanActAsTool, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You diagnose technical issues from error logs and configuration. Give one concrete next step.';
    }

    public function name(): string
    {
        return 'technical_specialist';
    }

    public function description(): string
    {
        return 'Diagnose login failures, errors, and broken features. Pass the full error message and what the user was doing.';
    }

    public function tools(): iterable
    {
        return [
            new RunDiagnostics,
        ];
    }
}

Two things are happening here. The #[Provider(Lab::Anthropic)] attribute pins this specialist to Anthropic even if the coordinator runs on OpenAI — each agent carries its own model and provider config. And the description() tells the coordinator not just when to hand off but what to include in the task.

That last part matters more than it looks. Each sub-agent invocation runs in isolation — it does not receive the coordinator's conversation history. The specialist only sees the task string the coordinator hands it. If the coordinator forwards "diagnose the login issue" without the error text, the specialist is blind. Writing the description() to demand a self-contained task is how you stop that.

Sharing memory and context across a multi-agent workflow#

Sub-agents are stateless between calls, which is a feature — you don't want a billing specialist inheriting three turns of unrelated chat. But you still need continuity somewhere, and that lives on the coordinator.

Give the coordinator a memory so it remembers the conversation, then let it compose the right task for each specialist. The AI SDK's conversation memory handles the coordinator's own history; I've covered the mechanics in persisting agent conversation history. The pattern is: coordinator holds the thread, specialists get a clean, explicit brief each time.

When several specialists need to look at the same input independently — say three reviewers scoring the same code — don't chain them. Run them at once. Laravel's Concurrency::run() fires them in parallel and collects the results, the same approach I use for parallel work with the Concurrency facade:

use Illuminate\Support\Facades\Concurrency;

[$security, $performance, $style] = Concurrency::run([
    fn () => (new SecurityReviewer)->prompt($diff)->text,
    fn () => (new PerformanceReviewer)->prompt($diff)->text,
    fn () => (new StyleReviewer)->prompt($diff)->text,
]);

Three isolated agents, three system prompts, one wall-clock cost instead of three.

Keeping a multi-agent workflow cheap and loop-free#

More agents means more model calls, and a coordinator that can call tools in a loop can, in the wrong conditions, keep calling them. Two guards keep that in check.

Cap the number of steps an agent may take when using tools with #[MaxSteps]. This is your hard stop against a coordinator that keeps re-delegating:

use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

#[MaxSteps(6)]
class SupportCoordinator implements Agent, HasTools
{
    use Promptable;

    // ...
}

Then push cheap work to cheap models. A coordinator whose only job is routing doesn't need your smartest model — tag it #[UseCheapestModel] and reserve #[UseSmartestModel] for the specialist that does the hard reasoning:

use Laravel\Ai\Attributes\UseCheapestModel;

#[UseCheapestModel]
#[MaxSteps(6)]
class SupportCoordinator implements Agent, HasTools
{
    use Promptable;

    // ...
}

Measure before you tune. Every response carries $response->usage, so log token counts per run and find out which specialist actually costs you money — I walk through capturing this in tracking token usage and cost. Guessing at the expensive step wastes more time than the metering does.

Gotchas and edge cases#

The isolation rule is the one that bites first. Because sub-agents never see the parent's history, a coordinator that under-specifies the task will get confident, wrong answers from a specialist that had no idea what it was really being asked. Treat every description() as a contract for what the task string must contain.

Nesting is tempting and usually a mistake. A sub-agent is a regular agent, so it can return its own sub-agents — but every level adds latency and another chance for context to get lost in translation. More than two levels deep and you're debugging a phone tree. Keep it flat: one coordinator, a handful of specialists.

Watch for routing thrash. If the coordinator keeps handing the same request back and forth between two specialists, the descriptions overlap. Sharpen them so each specialist owns a clearly disjoint slice, and let #[MaxSteps] catch anything that still slips through.

Finally, don't reach for this too early. A single agent with three or four well-named tools is easier to reason about than a coordinator plus four classes. Split only when one agent's prompt or tool set has genuinely become unwieldy.

Wrapping up#

Start with one agent() call. When its prompt is juggling roles and its tool list has crept past a handful, promote the roles into specialists and give a coordinator the job of routing to them — return each specialist from the parent's tools() method, implement CanActAsTool so the descriptions are sharp, and cap the whole thing with #[MaxSteps]. From here, wire the specialists up to real external capabilities with MCP server tools, and push long-running coordinators onto a queue so they run as background jobs instead of blocking a request.

FAQ#

What is a multi-agent workflow?

A multi-agent workflow splits a task across several cooperating AI agents instead of loading everything onto one. Each agent has a focused system prompt and its own tools, and a coordinator decides which specialist handles which part of the request. You reach for it when a single agent's prompt and tool set have grown too broad to stay reliable.

How do agents hand off to each other in the Laravel AI SDK?

You return one agent from another agent's tools() method. The Laravel AI SDK exposes the returned agent to the parent as a callable tool, so the parent can delegate a task to it and use the response while answering the original prompt. Implement the CanActAsTool interface on the sub-agent to control the name() and description() the parent sees when deciding whether to delegate.

When should I use multiple agents instead of one?

Use one agent by default — it's simpler to build and debug. Split into multiple agents only when a single agent is juggling distinct roles that each want different instructions, tools, or models, or when its tool list has grown large enough that the model starts picking the wrong tool. If a handful of well-named tools on one agent still works, stay with one.

How do multi-agent workflows manage shared memory and context?

Each sub-agent invocation runs in isolation and does not receive the coordinator's conversation history, so state lives on the coordinator. The coordinator keeps the conversation memory and passes a clear, self-contained task to each specialist on every call. When specialists need to see the same input independently, run them in parallel with Concurrency::run() rather than passing state between them.

How do I control cost in a multi-agent workflow?

Cap the number of tool-using steps with the #[MaxSteps] attribute so a coordinator can't loop indefinitely, and route low-effort work like classification to cheaper models with #[UseCheapestModel]. Read $response->usage after each run to see the real token cost per agent, then reserve your most capable model for the specialist that genuinely needs it.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.