Making enterprise systems usable by AI agents at scale
A governed, robust and cost-efficient way to give agents access to thousands of API endpoints, grounded in how your business works.
| Client | Ntiva – $100M+ IT managed service provider, top 30 in the US |
|---|---|
| Business impact | Employees can ask data questions across the core enterprise system in natural language, get strategic analysis on demand, and have the agent carry out actions inside the system on their behalf. Work that once took hours of manual analysis and clicking through screens now takes minutes. |
| In the client’s words | “Sheila built us an agent system that understands how our business works and goes beyond data lookups to support strategic analysis and take action inside our systems.” – Steve Banke, CTO |
The problem
Ntiva runs IT operations for its clients, and one enterprise system sits at the centre of that work: ConnectWise Manage. It holds the company’s client records, service tickets, projects, billing data and more.
Ntiva wants employees to work across that data by asking natural-language questions instead of writing SQL or manually clicking through application screens. It also wants the agent to help employees carry out actions inside the system.
When I joined, the company already had ~30 curated agent tools, one for each API endpoint the team judged most useful. But this wasn’t enough to make the wider system available, because ConnectWise exposes thousands of endpoints.
What I built
I built a governed, robust and cost-efficient way for agents to access the complete surface of Ntiva’s core enterprise system, ConnectWise.
The considerations
These shaped the agent architecture I built.
One agent tool per API endpoint doesn’t scale across the long tail
Ntiva’s first ~30 curated agent tools were a sensible way to start, with each tool wrapping one API endpoint. Most teams’ first instinct would be to extend this pattern across the full API. However, this creates 2 problems:
- Thousands of system endpoints would require generating thousands of agent tool wrappers and keeping them in sync with the vendor API.
- Thousands of tool definitions would compete inside the agent’s context window, making tool selection slower and less accurate.
Teams commonly use AI coding agents to solve the first problem, as compared to writing every wrapper by hand. Some teams then use tool search to solve the second problem, which keeps tool definitions out of the model’s starting context, and lets the agent discover and load tool definitions on demand.
However, this still creates a permanent software artifact for every API endpoint, including the thousands that employees may rarely or never use. I call this the long tail. Each wrapper has to be generated, named, indexed, tested, and kept current for the agent to use it. Tool search reduces how many tool definitions enter the model’s context, but the search system still has thousands of tools to maintain.
This creates an ongoing maintenance problem. SaaS APIs change, sometimes frequently. If every endpoint is mirrored as an agent tool, an updated path, parameter or response schema can leave many wrappers stale.
Dependent tool calls add context noise, latency and cost
Enterprise tasks often need several API calls whose inputs depend on earlier calls’ outputs. Suppose someone asks which of a client’s service tickets are still open and which technician owns each one. The agent must first find the client company ID, use it to retrieve their open tickets, then use the owner IDs in those tickets to retrieve the right technician records.
The conventional tool call approach is that every tool call result returns to the agent’s context window before it can form the next tool call. But in this example, the model doesn’t need to reason over a long list of owner IDs. These IDs only need to feed the next technician API call. Returning every intermediate tool result into agent context adds noise, latency, and token costs.
Broad system access needs governed API requests
Giving an agent broad access to an enterprise system creates 3 governance problems:
- Data access. The agent should return only the data the employee is allowed to see. The agent system has to identify who’s asking and apply their permissions to the request.
- Request construction. The agent should supply only the business-specific inputs it needs. A deterministic backend should set the system API destination, validate the request, add credentials, then execute. The agent never sees credentials and can’t leak them accidentally.
- Action consequences. Write requests change production data. As such, a policy layer should determine which writes can run automatically and which must pause for human approval, taking approval fatigue into account.
What people saw
Ntiva wanted a custom chat interface for employees to interact with the agent. I thus built a familiar ChatGPT-style interface using OpenAI ChatKit. Employees could ask a business question, inspect the steps the agent took to reach the answer, and approve write actions before the agent executed them inside the system.
Importantly though, this access architecture isn’t tied to any chat interface. The same backend could be exposed through an MCP server and used from any existing chat product such as ChatGPT or Claude.
Worked for a while
Searched ConnectWise API6 relevant endpoints found
# Find endpoint definitions for sales opportunities, forecasts and sales activity.
grep -inE "opportunit|forecast|activit" connectwise-openapi.json
jq '.paths | with_entries(
select(.key | test("opportunit|activit"; "i"))
)' connectwise-openapi.json
... Queried ConnectWiseThrough programmatic tool calling
// Find open sales opportunities and recent sales activity in parallel.
const [opportunities, activities] = await Promise.all([
tools.connectwise_read({
path: "/sales/opportunities",
query: { conditions: 'status/name="Open"', fields: "id,name,primarySalesRep/name" }
}),
tools.connectwise_read({
path: "/sales/activities",
query: { conditions: 'status/name="Completed" and dateStart>[2026-06-17]', fields: "opportunity/id" }
})
]);
// Sales opportunities missing from the recent-activity results may be going cold.
const recentlyTouched = new Set(
activities.items.map(activity => activity.opportunity.id)
);
const coldOpportunities = opportunities.items.filter(
opportunity => !recentlyTouched.has(opportunity.id)
);
// Find the revenue forecast for each cold sales opportunity in parallel.
const valuedOpportunities = await Promise.all(
coldOpportunities.map(async opportunity => {
const forecast = await tools.connectwise_read({
path: `/sales/opportunities/${opportunity.id}/forecast`
});
return {
...opportunity,
revenue: forecast.items[0].forecastRevenueTotals.revenue
};
})
);
// Return only the high-value cold sales opportunities.
text(JSON.stringify(
valuedOpportunities.filter(opportunity => opportunity.revenue >= 50_000)
)); I found one high-value open opportunity at risk of going cold. Axiom Cloud Migration is worth $128,400 and is owned by Jordan Doe.
- Opportunity
- Axiom Cloud Migration
- Owner
- Jordan Doe
- Scheduled
- Tomorrow, 9am
- Endpoint
POST /sales/activities
I’ll use this sales opportunity example to explain the architecture behind the agent.
The agent architecture I built
An agent that answers data questions and takes actions inside the core enterprise system.
Inside an enterprise system, the API endpoints employees use for tasks follow a long tail. A small set appears in everyday work. Many more appear occasionally. Beyond those sits a much larger set needed only for niche tasks.
As such, I designed 3 access routes so the agent could reach the whole system without treating every API endpoint as equally common or important:
- Agent skills loaded business knowledge the API couldn’t explain.
- Curated agent tools kept frequently used endpoints easy to reach.
- Dynamic runtime discovery exposed the rest of the API only when a task needed it.
This kept the agent’s base context small, loading the right subset of information only when the task at hand needed it.
Skills teach the agent how the business works
An API contract can explain fields and response types, but it can’t explain what those fields mean inside a company.
I put this knowledge in agent skills that loaded only when a task needed it. For example, a service ticket skill could explain which priority categories Ntiva treated as an escalation. The API told the agent how to retrieve the ticket, while the skill told it how the business interprets the ticket.
In the sales opportunity example above, the sales skill supplied the business definitions of “high value”, “going cold” and “follow-up”. If the relevant definition didn’t yet exist, the agent stopped and asked the user instead of guessing. These new definitions could also be added into skills later on so future requests follow the same business rule.
Frequent API endpoints stay curated
After reviewing the tasks employees asked for most often, I kept curated agent tools for the endpoints the agent needed repeatedly. Common calls didn’t need to be rediscovered from the API contract each time, which made them faster and more robust.
As the curated set grew, I implemented tool search to preserve the agent’s context window. I grouped the tools by business domain, such as service tickets, projects and companies, then used tool search to defer their full definitions. The agent started with only each group’s name and short description. If a task needed company tools, it then loaded only that group’s detailed tool definitions at runtime, while the rest stayed out of context.
In the sales opportunity example, the required sales and activity endpoints weren’t in the agent’s curated tool set, so the agent skipped using any curated tools and searched the wider API contract to discover them instead (explained below).
The agent searches the long tail at runtime
Runtime discovery exposed the rest of the system’s API only when a task needed it. The closest public reference pattern is Cloudflare’s OpenAPI Code Mode wrapper: keep a large API contract outside the agent’s context, let the agent search it with code to obtain the endpoint definitions it needs. I implemented this pattern inside Ntiva’s existing OpenAI and Azure stack and added custom read, write and approval boundaries.
I gave the agent a dereferenced copy of the system’s complete OpenAPI specification in JSON. This spec file describes the API’s paths, parameters, request bodies, responses and data schemas. Dereferencing expanded the file’s internal links so the agent could inspect each relevant endpoint definition in one place.
The spec file lived in an OpenAI Hosted Shell sandbox, outside the agent’s context. The agent could write code inside the sandbox to search it for the endpoints needed. For example, it uses shell commands like grep to search for broad terms and jq to inspect matching paths and fields. If the user’s query words doesn’t match the API terminology, it writes a small Python fuzzy search program to look for similar terms. As a result, the agent never reads the whole spec. It uses code to retrieve only the parts it needs for the current task.
In the sales opportunity example, the agent found the sales opportunity, activity and forecast endpoints inside the spec file. It also found the IDs that connected them: an opportunity ID returned by one endpoint became the input used to retrieve the activity and revenue forecast for that sales opportunity from another.
This design reduces maintenance across the long tail. SaaS vendors regularly add endpoints and change paths, parameters or response schemas. Whenever the vendor publishes an updated API contract, the team could replace the spec file inside the sandbox. The agent would then use those changes immediately. This changes the unit of maintenance: keeping one OpenAPI spec current is far easier than maintaining one agent tool for every API endpoint.
This route could change with usage. If employees started asking for a once rarely-used endpoint more frequently, it could be promoted into the curated agent tool set. The next request would take the faster route instead of searching the spec.
Tool calls are orchestrated programmatically
Finding the correct endpoints is only the first step. The agent still has to call them, often in a dependent sequence.
In the sales opportunity example, one API endpoint returns open sales opportunities and another returns recent sales activity. The opportunity IDs from both results are compared to find open sales opportunities with no recent sales activity. Lastly, the revenue forecast endpoint is called for each remaining sales opportunity and filtered for high value.
This illustrates the earlier dependent tool calls problem mentioned. To solve this, I let the agent orchestrate tool calls in code. The agent writes JavaScript that coordinates several tool calls instead of asking the model to directly inspect every intermediate tool result. In code, the agent could run independent calls in parallel, use earlier results as inputs to later calls, and apply exact filters deterministically.
This makes the agent’s work more reliable. Comparing 2 sets of opportunity IDs and applying a revenue threshold are code problems, not judgement calls for an LLM. It also keeps the large intermediate results inside the code runtime. The agent receives only the final result that it needs to explain and act on, preserving its context window and reducing token costs.
One tool governs every read request
Since the agent writes code to call APIs, a natural approach would be to let it construct HTTP requests directly. However, this would give agent-generated code control over almost the entire API request: the destination host, method, path, headers, parameters and request body. A mistake in this generated code could therefore target the wrong host or send an invalid request, increasing task latency.
As such, I designed a generic connectwise_read() tool which the agent uses to make every read API request. It works across all GET endpoints, instead of creating one agent tool per endpoint. This provides one place to govern and validate each request before execution.
In the sales opportunity example, the agent reads both the /sales/opportunities and /sales/activities API endpoints through this tool:
tools.connectwise_read({
path: "/sales/opportunities",
query: {
conditions: 'status/name="Open"',
fields: "id,name,primarySalesRep/name"
}
})
tools.connectwise_read({
path: "/sales/activities",
query: {
conditions: 'status/name="Completed" and dateStart>[2026-06-17]',
fields: "opportunity/id"
}
})
Every connectwise_read() call passes through this same control flow before an HTTP request reaches ConnectWise:
- Confirm the exact GET path exists. This blocks invented, outdated or unapproved routes and prevents the agent from disguising a write endpoint as a read.
- Check access permissions. This uses the identity of the employee to restrict the data they’re allowed to see.
- Validate against the endpoint’s contract. This confirms that every required path value is present, rejects unknown path values and accepts only query parameters defined for this endpoint. Invented, outdated and malformed requests are stopped before execution.
- Add credentials and execute the request. Credentials are added in the background only after the request passes validation. The agent never sees the secrets and can’t leak them accidentally.
- Return a controlled result and log the request. The agent gets a consistent response shape, while the engineering team can inspect and diagnose every call.
This meant broad read access didn’t become uncontrolled read access. Every request went through the same checks before execution, and Ntiva’s engineering team had one place to update validation, protect credentials and govern live access.
Another tool governs every write request, pausing for human approval
Write requests change production data and thus need human approval. I designed a separate agent tool, connectwise_write(), that is used for any write API request.
It follows the same controlled request flow as connectwise_read(), with an extra boundary of human approval. The write couldn’t run until an employee approved it.
Whenever the agent’s generated code called connectwise_write(), the code paused and the chat interface rendered an approval widget. If the employee approved it, the write ran and the code resumed running from that point. The widget changed to show success with a link to the new data written to the system. If the employee rejected it, no write ran. This keeps a multi-step task inside one code program, even when the task includes a write.
The production stack
Outcomes
This engagement changed how Ntiva could interact with their core enterprise system, ConnectWise:
| Before | After |
|---|---|
| Complex data analysis takes hours to days | Agent provides insights in minutes |
| Employees manually clicked through multiple ConnectWise screens to find and join data | Employees could ask questions about ConnectWise data in natural language |
| Agent could use only the ~30 endpoints engineers had prebuilt as tools | Agent could reach the full ConnectWise API to execute any task |
| Each dependent API result returned to the agent before it could form the next call, adding context noise, latency, and token costs | Programmatic tool calling coordinated dependent API calls in code and returned only the final result to the agent |
The agent could check its own results, correct its approach and pursue analysis that had never been built explicitly as a feature. When the answer called for action, the same interaction could continue into a visible approval alert and a completed action inside the enterprise system.
Ntiva’s CTO described the impact this way:
“Sheila built us an agent that understands how our business works and that goes beyond data lookups to deliver analysis that’s useful for our strategic decisions. She also spotted multiple opportunities we hadn’t thought to look for. She’s a rare partner who goes beyond the statement of work to set us up for long-term success.”
The deeper result was architectural. Ntiva no longer had to decide in advance every question an employee might ask or every part of their enterprise system an agent might need. The agent system reached the right capability when the work called for it, while the company kept control of credentials, permissions and consequential actions.