AI Agents · Ai

AI Agent Architecture for ERP Systems — A Complete Guide

AI Agent Architecture for ERP Systems — A Complete Guide is the work that defines the next phase of enterprise software.

John Kihiu12 min read

Most "AI agent for the ERP" projects fail for the same structural reason: someone lets the model call write endpoints directly, with no layer in between to say no. The fix is not a smarter prompt. It is a boring architectural split — a deterministic API layer that owns every side effect, and an orchestration layer that only ever proposes actions through that API. Everything else in this article is detail on top of that one decision.

The two-layer split

Layer one is ordinary application code: a service layer in front of the Acumatica REST/contract-based API (or a Laravel middleware app if you're fronting it) that exposes a small, deliberately narrow set of operations — "create AP bill," "apply payment," "look up vendor balance." This layer enforces every business rule you already enforce for human users: required fields, GL period status, approval thresholds, branch restrictions. It has no idea an LLM exists.

Layer two is the LLM orchestration loop — the part that takes a user's natural-language request, decides which layer-one operations to call, in what order, and with what arguments. The LLM never touches the Acumatica database or API directly. It only ever emits a proposed function call, which layer one then validates and executes (or rejects) exactly as it would for a REST client. If layer one would reject the call from a Postman request, it rejects it from the agent too — that symmetry is the whole point.

Designing the tool contract

Each tool the agent can call should map to one ERP business operation, not one API endpoint. "Create an AR invoice" is a tool; "PUT /entity/Default/23.200.001/Invoice" is not — the raw endpoint has too many optional fields and too many ways to end up in an inconsistent state. Wrap it so the tool signature only exposes what a well-formed request needs: customer ID, line items, a reference to source documents. Push defaulting, tax calculation, and numbering back into the wrapper, not into the prompt.

C# · TOOL WRAPPER (SIMPLIFIED)
// Layer 1: deterministic wrapper around the Acumatica contract API.
// The agent never sees raw endpoints, only this method signature.
public class ApBillTool
{
    public ToolResult CreateBill(CreateBillRequest req, AgentContext ctx)
    {
        // Same authorization check a human user would hit
        if (!ctx.User.HasAccess("AP301000", AccessRight.Insert))
            return ToolResult.Denied("User lacks AP bill entry rights");

        if (req.Amount > ctx.User.ApprovalLimit)
            return ToolResult.RequiresApproval(req);

        var bill = _billService.CreateFromVendorInvoice(req.VendorId,
            req.LineItems, req.SourceDocumentId);

        _auditLog.Record(ctx.User, "agent.ap_bill.create", bill.RefNbr,
            confidence: req.ExtractionConfidence);

        return ToolResult.Ok(bill.RefNbr);
    }
}

Auth and scoping — the agent inherits the user, never exceeds it

The single most common security mistake in ERP agent projects is running the agent under a service account with broad permissions "to keep things simple," then relying on the prompt to behave. Don't. The agent session should carry the same Acumatica user context — same role, same branch restrictions, same approval limits — as whoever is chatting with it. Every tool call resolves permissions against that user, not against the agent process. If a warehouse clerk asks the agent to void an invoice they couldn't void through the UI, the tool call fails for the same reason the UI button would be greyed out.

Don't let the agent assume elevated identity

If the agent needs to act across multiple users' data (e.g., a nightly reconciliation agent), that's a distinct, explicitly-scoped service identity with its own audit trail — not a fallback the interactive agent quietly uses when a permission check fails.

State management: keep it out of the LLM's head

Don't ask the model to remember what it did three turns ago and infer the current state of a multi-step workflow from conversation history — that's how you get double-submitted bills. Keep a small, explicit state machine (in your own database, not in the prompt) tracking what step a multi-step task is on: "extracted," "pending approval," "posted." The LLM reads current state from a tool call at the start of each turn and writes state transitions through tools, the same as any other side effect. Conversation history is context for language generation, not your source of truth for what happened.

Sync vs. async execution

Interactive requests ("show me open AP bills over $10k") should run synchronously and return in a few seconds — a single tool call or two, no reason to make the user wait. Anything that fans out across many records (matching 200 receipts to card transactions, re-coding a quarter's worth of misclassified GL entries) belongs in an async job queue: the agent enqueues the work, a background worker executes tool calls with retries and rate limiting, and the user gets a notification or a status the next time they check. Trying to hold an HTTP connection open for a long agent loop is a reliability problem you don't need to have.

Where guardrails actually belong

Put validation in layer one, not in the system prompt. A system prompt instruction like "never approve invoices over $5,000 without human review" is a suggestion the model can be talked out of by a cleverly worded user message; a hard check in CreateBill that routes anything over the threshold to a pending-approval queue is not. Use the prompt to shape behavior and tone — use code to enforce anything where being wrong costs money or violates a control.

ConcernBelongs inWhy
Field validation, required dataDeterministic API layerMust hold regardless of how the call was generated
Approval thresholdsDeterministic API layerPrompt instructions are not enforcement
User permission checksDeterministic API layerAgent must inherit the caller's actual rights
Which tool to call, in what orderLLM orchestration layerThis is the reasoning the model is good at
Explaining results to the userLLM orchestration layerNatural language generation, low stakes if imperfect

Wrapping up

None of this is exotic — it's the same client/server discipline you'd apply to any integration, with the LLM treated as one more untrusted caller of your API rather than a special case. Get the deterministic layer right first, with the exact permission and validation rules you'd want even without an agent in the picture, and the "AI" part becomes a thin, replaceable orchestration loop on top. That's also what makes it survivable when you swap models or providers eighteen months from now — the business logic didn't move.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.