Give your agent domain actions, not database CRUD
Exposing raw CRUD endpoints to an AI agent leads to brittle multi-step plans and corrupted states. Design coarse-grained domain tools that enforce invariants in code.
On this page 5 sections
- Combine multi-step database mutations into atomic, business-level tools.
- Keep transaction safety, validation rules, and audit logging in deterministic code.
- Return compact evidence packets to the model rather than raw database payloads.
The natural temptation when giving an AI agent tool access is to point it at your existing backend API. Your team already maintains well-documented REST endpoints: GET /accounts, PATCH /accounts/{id}, POST /audit_events, and POST /notifications. Exposing those endpoints as tool definitions feels like the cleanest architecture. The assumption is that an agent with frontier reasoning capability can inspect the schema, choreograph the sequence, and handle edge cases across successive model turns.
In production, delegating database-level orchestration to a probabilistic model produces a brittle agent. It bloats the context window across chatty roundtrips, leaks internal database quirks into the prompt, and creates partial-execution failures where step two mutates state but step three crashes.
If you want an agent to act reliably across complex systems, stop giving it raw database verbs. Give it coarse-grained domain actions that enforce business invariants in code.
The illusion of granular flexibility#
Teams usually choose low-level CRUD tools for two reasons: convenience and the promise of autonomy. An engineer can generate twenty OpenAPI tool schemas in an afternoon by exporting existing route controllers. Product teams often encourage this because fine-grained tools seem to give the agent maximum flexibility to solve unforeseen user problems.
The math behind multi-step tool calls works against that flexibility. Suppose each individual tool selection, parameter extraction, and execution step has an optimistic 95% success rate. If completing a user request requires four sequential CRUD invocations—fetch the record, update status, append an audit entry, and dispatch an alert—the probability of the complete sequence succeeding without error drops to roughly 81% (0.95^4). At six steps, reliability falls to 73%.
Every unnecessary turn between the model and the host environment introduces compounding points of failure:
- The model might misread intermediate JSON output and invent an argument on step three.
- A transient network error on step four can leave the database in an invalid, half-committed state.
- The token cost multiplies because each subsequent turn resends the entire conversation history, including previous tool arguments and verbose payloads.
When you ask an agent to string together five low-level mutations, you are asking a language model to do determinism’s job. Deterministic pipelines belong in application code; probabilistic models belong at the decision boundary.
Why multi-step CRUD loops break in production#
When an agent interacts with granular APIs, three specific failure modes appear repeatedly in production logs.
1. Partial execution and orphaned state#
In standard web applications, a backend developer wraps related mutations inside a database transaction. If the payment record fails to save, the order status does not advance to paid.
When an agent calls granular endpoints across multiple chat turns, there is no shared database transaction across the turns. If the agent calls update_account_status(status="suspended") and then encounters a rate limit or a malformed response when attempting create_compliance_log(), the account remains suspended without an audit trail. The agent cannot reliably roll back previous HTTP calls on its own.
2. Parameter hallucination on wide schemas#
Backend CRUD schemas are notoriously wide. A typical update_user endpoint might accept thirty optional fields, from billing addresses to internal feature flags. When presented with twenty optional parameters, models frequently set defaults that clobber existing data, confuse deprecated fields with active ones, or pass null values for fields they were never meant to touch.
3. Context pollution from raw payloads#
A standard GET /customers/{id} response often returns hundreds of lines of JSON: internal UUIDs, created_at timestamps, hash digests, and nested metadata. Ninety percent of this payload is noise for the task at hand. This raw data consumes context tokens, degrades model attention, and increases latency on every subsequent turn.
| Architectural Dimension | Granular CRUD Tools | Domain Action Tools |
|---|---|---|
| Mutation scope | Single database table or field | Multi-table atomic business transaction |
| Error recovery | Relies on LLM to catch and rollback | Handled deterministically in application code |
| Parameter surface | 10–30 generic database fields | 2–4 explicit task-specific arguments |
| Context efficiency | High token overhead from chatty roundtrips | Single turn with targeted evidence output |
| Invariant guarantees | Probabilistic (agent must remember all steps) | Deterministic (code enforces business rules) |
The domain action pattern#
The solution is to design tools around domain intents rather than database tables. A domain action is a coarse-grained function that encapsulates a complete business workflow behind a single, tightly constrained interface.
Instead of exposing four separate tools:
- fetch_invoice_details(id)
- update_invoice_status(id, status)
- record_discrepancy_note(id, note)
- notify_finance_reviewer(id, email)
You expose one domain tool:
- resolve_invoice_discrepancy(invoice_id, resolution_type, explanation, notify_reviewer)
The backend implementation behind resolve_invoice_discrepancy executes inside a single application boundary. It validates whether the invoice is in an editable state, performs the database mutation, writes the audit log, triggers the notification, and wraps everything in a database transaction that rolls back if any step fails.
{
"name": "resolve_invoice_discrepancy",
"description": "Resolves an invoice hold by applying an approved resolution rule, updating the audit trail, and notifying the assigned reviewer.",
"parameters": {
"type": "object",
"properties": {
"invoice_id": {
"type": "string",
"description": "Unique identifier of the invoice on hold."
},
"resolution_type": {
"type": "string",
"enum": ["waive_fee", "rebill_line_item", "escalate_to_controller"],
"description": "The approved business resolution path."
},
"explanation": {
"type": "string",
"description": "The justification grounded in the customer contract or dispute history."
}
},
"required": ["invoice_id", "resolution_type", "explanation"]
}
}
Notice what disappears: internal status flags, foreign key IDs, user IDs, and timestamps. The tool schema only asks the model for decisions that genuinely require reasoning. Everything else is derived from the authenticated session and business logic.
How to evaluate tool schema boundaries#
When auditing your agent’s tool definitions, apply three criteria to decide whether a tool is at the right level of abstraction:
The atomicity test#
If step B must always happen when step A happens, step A and step B should never be two distinct tools in the agent’s catalog. If updating a customer tier must always write a regulatory audit record, build a single tool that takes the tier change and reason, then writes both records in one transaction.
The argument minimization test#
Inspect every parameter in your tool schema. If a parameter can be computed deterministically from the database, the current timestamp, or the user’s authentication context, remove it from the schema. An agent should never be asked to pass updated_at, modified_by_user_id, or organization_id.
The evidence-dense response test#
When a tool finishes execution, it should return an evidence summary, not an object dump. A response like:
{"status": "applied", "invoice_id": "INV-4021", "amount_waived": 45.00, "new_balance": 310.00} gives the model exact evidence to formulate its response without flooding the context window with raw schema baggage.
A checklist for your agent’s tool catalog#
Before shipping an agent feature that mutates system state, review your tool registry against these five questions:
- Are transactions handled by the code or the prompt? If an agent fails midway through a task, does the system stay in a consistent state without requiring cleanup?
- Are inputs constrained to enums and verified types? If a field accepts only three business states, are they defined as an enum in the schema rather than documented in the system prompt?
- Does any tool expose raw database IDs or internal flags that the model must coordinate? If the model needs to pass an ID from Tool A into Tool B just to mutate state, combine the two tools into a single workflow.
- Is destructive action decoupled from ambiguous discovery? Can the model query state and dry-run potential changes without triggering live side effects?
- Does the tool response provide only the evidence needed for the user-facing answer? Strip out unindexed metadata, nested relationships, and verbose system traces before the result enters the context.
If your agent is struggling with consistency, the problem is rarely that the underlying model lacks intelligence. More often, the agent is being asked to do the work of a backend framework. Move deterministic state transitions into your application code, keep business invariants safe, and let the agent focus on high-judgment decisions.