This is the final part of the series, where we move into laravel ai agents style workflows. The goal is to combine tool calling and queued jobs so your app can run multi-step operations safely.
Why tool calling plus queues is the right pattern
Single-call prompts are great for drafting text, but operations workflows need more:
- fetch data from internal systems,
- execute side effects (tickets, alerts, updates),
- retry safely,
- audit every action.
That is exactly where tool calling + Laravel queues shines.
Define safe tool schemas and a tool registry
Treat tool definitions as contracts and keep execution in explicit Laravel classes.
<?php
namespace App\AI;
class ToolRegistry
{
public static function definitions(): array
{
return [
[
'type' => 'function',
'name' => 'create_jira_ticket',
'description' => 'Create a Jira ticket for an incident',
'parameters' => [
'type' => 'object',
'properties' => [
'title' => ['type' => 'string'],
'priority' => ['type' => 'string', 'enum' => ['low', 'medium', 'high', 'critical']],
'summary' => ['type' => 'string'],
],
'required' => ['title', 'priority', 'summary'],
],
],
[
'type' => 'function',
'name' => 'post_slack_update',
'description' => 'Post an incident update to Slack',
'parameters' => [
'type' => 'object',
'properties' => [
'channel' => ['type' => 'string'],
'message' => ['type' => 'string'],
],
'required' => ['channel', 'message'],
],
],
];
}
}Run the workflow in a queued job
<?php
namespace App\Jobs;
use App\AI\ToolRegistry;
use App\Services\OpsToolExecutor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class RunOpsCopilotJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public string $incidentSummary, public string $actorId)
{
}
public function handle(OpsToolExecutor $executor): void
{
$response = Http::withToken(config('services.openai.key'))
->timeout(120)
->post('https://api.openai.com/v1/responses', [
'model' => config('services.openai.model'),
'instructions' => 'Use tools only when required and keep actions minimal.',
'input' => $this->incidentSummary,
'tools' => ToolRegistry::definitions(),
])
->throw()
->json();
$toolCalls = collect(data_get($response, 'output', []))
->filter(fn ($item) => data_get($item, 'type') === 'function_call')
->values();
foreach ($toolCalls as $call) {
$executor->execute(
name: (string) data_get($call, 'name'),
arguments: (array) data_get($call, 'arguments', []),
actorId: $this->actorId,
traceId: (string) data_get($response, 'id', ''),
);
}
}
}Execute tools with authorization and idempotency
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use RuntimeException;
class OpsToolExecutor
{
public function execute(string $name, array $arguments, string $actorId, string $traceId): void
{
$key = "tool_exec:{$traceId}:{$name}:" . md5(json_encode($arguments));
if (! Cache::add($key, true, now()->addMinutes(10))) {
return; // Idempotent skip
}
if (! $this->isAuthorized($actorId, $name)) {
throw new RuntimeException('Unauthorized tool execution attempt.');
}
match ($name) {
'create_jira_ticket' => $this->createJiraTicket($arguments),
'post_slack_update' => $this->postSlackUpdate($arguments),
default => throw new RuntimeException("Unsupported tool: {$name}"),
};
logger()->info('tool_executed', [
'tool' => $name,
'actor_id' => $actorId,
'trace_id' => $traceId,
]);
}
private function isAuthorized(string $actorId, string $toolName): bool
{
// Replace with policy/permission checks
return ! empty($actorId) && in_array($toolName, ['create_jira_ticket', 'post_slack_update'], true);
}
private function createJiraTicket(array $args): void
{
// Call your Jira integration
}
private function postSlackUpdate(array $args): void
{
// Call your Slack integration
}
}Real-world scenario: operations incident copilot
During an outage, your on-call engineer submits an incident summary. The workflow can:
- gather structured details,
- create a Jira ticket,
- post an internal Slack update,
- return a short action summary.
Because everything runs in queue workers, your dashboard stays fast and reliable under pressure.
Common mistakes in advanced Laravel AI workflows
- Letting model call unrestricted tools.
- Executing side effects without authorization checks.
- Running long tool loops in web request cycle.
- Missing idempotency, causing duplicate external actions.
- Skipping audit logs for sensitive operations.
Production checklist
- Restrict tools to a hard allowlist.
- Validate tool arguments before execution.
- Add policy checks per tool and actor role.
- Use queue retries + dead-letter handling.
- Add idempotency keys for side-effectful calls.
- Record trace IDs for every tool execution.
FAQ
1) Are AI agents deterministic enough for enterprise workflows?
They can be reliable with strict schemas, bounded tool access, and robust audit controls.
2) Should I run tool workflows synchronously?
No. Use queue jobs for anything with network calls or side effects.
3) How many tools should I expose initially?
Start with 1 to 3 high-value tools and expand only after observing behavior.
Series navigation and references
- Previous: Production AI in PHP: Guardrails, Cost Control, and Evals
- Foundation: AI for PHP and Web Developers: Complete 6-Part Series
- Next: All articles
That completes the 6-part AI series for PHP and web developers.
- Newsletter: Get practical build notes
- Secondary contact: Start a conversation
Official references: