AI Agents · Ai

Function Calling Patterns with Acumatica

Function Calling Patterns with Acumatica is the work that defines the next phase of enterprise software. ERP systems hold the most valuable business data in the company —.

John Kihiu12 min read

Function calling is what turns an LLM from a chatbot that talks about your ERP into something that can actually read and write records in it. The API shape looks simple — you send tool definitions, the model returns a name and arguments, you execute and send back the result — but almost all of the real engineering is in what happens around that exchange: how the tools are scoped, how arguments are validated before they touch production data, and what happens when the model calls something wrong.

Designing tool schemas

Both the OpenAI and Anthropic tool-use APIs want the same basic shape: a name, a description, and a JSON Schema for the parameters. The description is doing more work than it looks like — it's the only signal the model has for when to call this tool versus another, so vague descriptions produce vague tool selection. Be specific about what the tool does and doesn't do: "Retrieves a single AP bill by reference number. Does not search by vendor or date range — use search_ap_bills for that."

JSON · TOOL DEFINITION
{
  "name": "get_ap_bill",
  "description": "Retrieve a single AP bill by its reference number. Returns status, amount, vendor, and approval state. Does not search — use search_ap_bills for that.",
  "input_schema": {
    "type": "object",
    "properties": {
      "reference_nbr": {
        "type": "string",
        "description": "The AP bill reference number, e.g. 'INV-88213'."
      }
    },
    "required": ["reference_nbr"],
    "additionalProperties": false
  }
}

Enums beat free-text wherever the valid values are known — a status field should be an enum of the actual Acumatica status codes, not an open string the model has to guess the spelling of. Every constraint you can express in the schema is a validation the model handles correctly before you ever see the call, instead of a runtime error you have to catch after.

Narrow, composable tools vs. one mega-tool

The instinct to build a single generic call_acumatica_api(endpoint, method, body) tool is understandable — it's less code to maintain — but it pushes all the hard work of knowing which endpoint and payload shape to use onto the model, which is exactly where you don't want uncertainty. Narrow, single-purpose tools (get_ap_bill, approve_ap_bill, search_vendors) constrain what the model can attempt at each step and make each call's blast radius small and reviewable. The cost is more tool definitions to write and keep in sync with your API, which is a fair trade against a model that occasionally invents a plausible-looking endpoint that doesn't exist.

A middle ground that works well in practice: narrow read tools (cheap, safe, can be liberal) and narrow, explicitly-named write tools with tight schemas and mandatory confirmation steps — never a generic "write" tool that takes an endpoint and body as arguments.

Validating LLM-generated arguments before execution

Treat every tool call the model makes as untrusted input, the same way you'd treat a value from an HTTP request body. Validate types and ranges against the JSON Schema, but also validate business rules the schema can't express — does this vendor ID actually exist, is this amount within the range the requesting user is authorized for, does the reference number match an existing record. A model that hallucinates a vendor ID that doesn't exist should get a clean error back, not a call that silently no-ops or, worse, creates a new record because your write path assumes valid input.

Never execute against production on schema validity alone

Passing JSON Schema validation only means the shape is right — it says nothing about whether the values make business sense. A syntactically valid call to approve_ap_bill with an amount ten times the actual invoice is still a disaster. Layer business-rule validation in front of every write tool.

Idempotency for tools that mutate state

Retries happen — a network blip, a timeout on your side while the write actually succeeded, an agent framework that resends a tool call after an ambiguous response. Any tool that creates or modifies a record needs an idempotency key, the same pattern you'd use for a payment API: the caller (or the agent framework) generates a unique key per logical operation, and the handler checks whether that key was already processed before executing again. Without this, a timed-out "approve this bill" call that actually succeeded server-side gets retried and you've double-approved something. For Acumatica specifically, this often means checking current record state before applying a transition — don't approve a bill that's already approved, return the existing result instead.

When the model calls a tool wrong

Malformed arguments, a tool called with a nonexistent ID, a call that violates a business rule — these need to come back to the model as a clear, actionable error message, not a raw exception stack trace. "vendor_id 'V-9982' not found; did you mean to search first with search_vendors?" gives the model something it can act on in the next turn. A bare 500 error or a Python traceback gives it nothing, and you'll often see the model retry the exact same bad call in a loop. Cap retries per tool call — two or three attempts — and fall back to asking the user for clarification rather than looping indefinitely.

Parallel vs. sequential tool calls

Both OpenAI and Anthropic's APIs support the model requesting multiple tool calls in a single turn. Parallel calls are safe and worth encouraging for independent reads — fetching a vendor record and a PO record at the same time cuts latency roughly in half versus doing them sequentially. They are not safe for writes with any ordering dependency or shared state: approving a bill and then paying it must happen sequentially, and forcing that sequencing usually means splitting it across two turns rather than trusting the model to order parallel calls correctly within one.

Let reads run parallel, force writes sequential

A simple rule that covers most cases: mark read-only tools as safe for parallel execution in your tool-calling loop, and structure write tools so that a write's tool definition or system prompt makes clear it depends on a preceding read's result, encouraging the model to sequence naturally rather than fire both at once.

Wrapping up

The API shape for function calling is the easy 20%. The other 80% is schema design tight enough to constrain the model, validation that treats every argument as untrusted, idempotency on anything that mutates a record, and error messages the model can actually recover from. Get those right and tool calling against a real ERP becomes boring and reliable — which, for anything touching production financial data, is exactly the property you want.

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.