Laravel AI SDK: Building Production-Safe Database Tools for Agents

Laravel AI SDK: Building Production-Safe Database Tools for Agents

Your AI agent can chain multi-step workflows together, but it still can't answer "What happened to my order last week?" until you connect it to your database.

We hit this wall while building a support agent for Laravel Nova and Spark, our customer-facing SaaS. The agent could reason, route between specialists, and hold a conversation. But the moment a customer asked about their own data, it had nothing. Couldn't pull an order, check a subscription, or look up a help article. So we wired it up.

The fix was a Laravel AI SDK Tool, a PHP class that tells the agent what data it can ask for and how to get it. We started with individual tools, then pushed further into a single query builder tool. Here are both approaches.

If you haven't set up the SDK yet, read the introduction first.

Building AI SDK Tools: One Tool, One Query

Each Laravel AI SDK's Tool defines three methods: when to use it (description), what inputs it takes (schema), and how to run the query (handle).

Generate one with the Artisan command:

Here's a tool that searches a user's orders by status:

Notice how $userId comes from the constructor, not from the session inside handle(). The tool can't work without it, and it's set by your application code, not by the agent or the user's prompt. A prompt injection can't change which user's data comes back.

The select() call limits which columns the agent sees, and limit(10) keeps result sets small enough that they won't eat the agent's context window.

Registering Database Tools with a Laravel AI Agent

Attach the tool to an agent by implementing HasTools:

Then prompt it:

The agent reads the tool description, picks the tool, and calls it. No routing logic is required.

For agents that cover multiple areas, register several scoped tools together:

Each tool queries only what it needs, picks only the columns the agent should see, and caps result sizes. If your infrastructure supports it, you can go a step further and use a read-only database connection:

Even if some clever prompt tricks the agent into attempting a write, the connection physically can't do it.

Adding Vector Search with Laravel SimilaritySearch

The first time a customer typed "How can I get a refund?", the agent just sat there. There was no status to filter on, and no order ID. It needed a completely different kind of search: semantic search.

The Laravel AI SDK ships a built-in SimilaritySearch tool for exactly this. Add an embedding column to your table:

Then register it:

To scope results to the current user, use the closure form:

You can also set a minimum similarity threshold:

A minSimilarity of 0.7 means the agent only gets results that are at least 70% semantically similar to the question. Less noise, better answers.

Individual tools plus SimilaritySearch carried us pretty far. Each tool was focused, testable, and safe by construction. Then the scope grew.

When Individual Tools Stop Scaling

As we kept building out the Nova and Spark support agent, the tool count kept climbing. Every new customer question meant another PHP class. Asks like "I'm on the Pro plan, but I can't see the analytics dashboard" needed a tool to check feature entitlements. "I got charged twice last month" needed one to pull the invoice history. "My subscription says expired, but I just renewed yesterday" needed another to cross-reference payment records with subscription status.

We hit a dozen tools, and two problems started compounding:

  • First, every tool's description and schema is sent to the model as part of the prompt. A dozen tools means a significant chunk of your context window is just tool definitions before the customer even asks a question.
  • Second, the more tools the agent has to choose from, the more often it picks the wrong one. We saw it call FetchInvoiceTool when it should have called CheckSubscriptionTool, or hallucinate parameters that didn't exist on the schema. More tools means more room for the model to get confused.

On top of that, the gaps kept showing up. The agent would fail on a perfectly reasonable question, not because the data wasn't there, but because nobody had written that particular tool yet.

So we asked a different question: what if we stopped trying to predict every query the agent would need and just gave it read access?

The Middle Ground: Building a Database Query Tool for Laravel AI Agents

Instead of a dozen individual tools, we built a single tool backed by Laravel's query builder. The agent sends structured parameters (table, columns, filters) and the tool builds a safe query with DB::table().

Generate the tool with Artisan:

Defining Database Schema for your AI Agent

The description() is the agent's map of your database. It tells the model which tables and columns exist and how they relate to each other. The agent never discovers your schema on its own.

Defining the Inputs

The schema() tells the model what parameters it can send. The enum() on table restricts the agent to your approved tables right at the schema level. The where parameter uses a typed object array so the model sends structured filters instead of freeform text.

When the agent receives "What's the status of my subscription?", it sends something like:

Building the Query Safely

The handle() method is where the query builder earns its keep. PHP Data Objects (PDO) automatically binds every value the agent passes, so SQL injection through filter values is handled for you. But PDO can't bind column names or operators. Those get interpolated as-is. So we validate them against allowlists before they touch the query.

The core of handle() validates columns, builds the query, applies filters, and returns results:

The Laravl AI SDK's Request class gives you typed accessors like $request->string(), $request->array(), and $request->integer() through Laravel's InteractsWithData trait.

Redacting Sensitive Data

Even with column allowlists, you might accidentally expose a sensitive column as your schema grows. The redact() method catches columns ending in _token, _secret, _password, or _key and replaces their values before they reach the agent:

Capping Output Size

One query returning 10,000 rows would eat the agent's entire context window. The formatOutput() method caps the response to fit within MAX_OUTPUT_LENGTH characters.

The trick is avoiding a brute-force loop. Instead of halving the array over and over (re-encoding thousands of rows each time), we encode once, estimate how many rows actually fit, and slice directly:

This typically fits in a single pass. The halving fallback is a safety net that rarely fires, but guarantees the output always fits.

Why the Query Builder Makes This Simpler

You don't need keyword blocklists, comment stripping, or multi-statement prevention. The query builder physically can't produce a DELETE, DROP, or any write operation. Safety comes from the API's structure, not from parsing SQL strings.

Securing your Laravel AI SDK Database Tools

Code-level allowlists are one half of the picture. The other half is what you set up around the tool.

Use a read-only database user: Create a dedicated MySQL or Postgres user with SELECT-only grants on the tables you expose. Point the tool at that connection with DB::connection('readonly'). Even if every application-level check somehow fails, the database itself refuses writes.

Log tool invocations: The Laravel AI SDK dispatches events around every tool call. Log the table name, requested columns, and filter conditions so you can audit what the agent actually queries over time. If the same patterns keep showing up, that's a signal you might want a dedicated Eloquent tool for that specific query.

Test adversarial prompts: Write prompts that try to trick the agent into querying tables outside the allowlist, selecting columns you didn't approve, or using operators you blocked. Run these as part of your test suite. The allowlists should catch everything, but verifying it builds confidence before you ship.

It handled most of our support questions well. "What's the status of my subscription?" "Show me my recent invoices." Simple filters and lookups worked great.

What the Agent Could Do

Without writing individual tool classes for every question, the agent started handling user queries on its own:

  • "I upgraded to Pro last week but I'm still seeing the free tier limits." The agent queried subscriptions filtered by user and status, found the active record, and confirmed the plan field matched what the user expected.
  • "Can you tell me how much I've spent in the last three months?" It queried invoices filtered by user and date range, then summed the amounts in its response.
  • "I submitted a support ticket about this two days ago and haven't heard back." The agent queried tickets filtered by the user and date, found the ticket, and pulled its current status and assignment.

Choosing Between Eloquent Tools and Query Builder Tools

We tried both approaches, and they solve different problems.

Individual Eloquent tools are the safest and most predictable. Each tool does one thing, the schema constrains inputs tightly, and you control exactly how the query is built. Easier to test, easier to reason about. If you know your queries up front and the domain is narrow, start here.

The query builder tool gives you one tool instead of a dozen. PDO handles value binding, the column and table allowlists prevent the agent from touching anything you haven't approved, and the operator allowlist keeps comparisons safe. You don't need keyword blocklists or comment stripping because the query builder can't produce a DELETE or DROP, no matter what the agent sends. The tradeoff is that complex joins and aggregates are harder to express as structured parameters, but for most support and lookup use cases, it covers the ground well.

Individual Eloquent tools SimilaritySearch Query builder tool
Best for Known, predictable queries Fuzzy/semantic questions Unpredictable query patterns
Setup effort One class per query One tool + vector column One tool + allowlists
Context window cost Grows with tool count Single tool Single tool
Security model Constructor-scoped user ID Closure-scoped or model-scoped Allowlists + read-only connection
Joins/aggregates Full Eloquent power N/A Limited to flat queries

For most applications, start with individual Eloquent tools and add SimilaritySearch for fuzzy questions. If you find yourself writing a new tool every week, move to the query builder approach.

The query builder path worked for Nova and Spark because the support domain spans five tables, and the questions users ask are unpredictable. We wouldn't have reached the same coverage writing tools one at a time.

What Comes Next

We're exploring how to bring this pattern into the Laravel AI SDK itself, with built-in safety guardrails so you don't have to wire them up yourself.

In the meantime, both the individual tool pattern and SimilaritySearch are production-ready today. Start with a single Eloquent tool scoped to the authenticated user, return a small JSON payload, and limit your column selection to what the agent actually needs. The article on multi-agent workflow patterns shows how these tools compose into larger pipelines.

If you want to try the query builder approach, everything in this post will get you there. Read the Laravel AI SDK tools documentation for the full Tool interface reference."

Frequently Asked Questions

How do I connect the Laravel AI SDK to my database?

Implement the Tool interface with a handle() method that runs Eloquent or query builder queries. Pass the authenticated user ID through the constructor, not the session.

How do I add vector search to a Laravel AI agent?

Use the built-in SimilaritySearch tool. Add a vector embedding column to your table using $table->vector('embedding', dimensions: 1536), then register the tool with SimilaritySearch::usingModel(). Requires Postgres with pgvector.

How do I prevent prompt injection in Laravel AI SDK tools?

Scope all queries by user ID passed through the constructor (not from the prompt). Use column allowlists, operator allowlists, and a read-only database connection. The query builder physically cannot produce DELETE or DROP statements.

Should I use individual Eloquent tools or a single query builder tool?

Start with individual Eloquent tools when you know your queries up front. Move to the query builder approach when you hit a dozen tools and the context window overhead becomes a problem.

Keep reading