
How can I make an AI assistant call my internal API (create tickets, update records) without writing a full agent framework from scratch?
Most teams who want an AI assistant to create tickets or update records hit the same wall: they don’t want to build and maintain a full-blown agent framework, but they still need the assistant to call internal APIs reliably and safely. The good news is you can get there with a thin, well-structured integration layer instead of a heavy agent system.
This guide walks through practical patterns, design choices, and implementation tips so your AI assistant can call your internal API (create tickets, update records, etc.) with minimal infrastructure, while still being robust, secure, and maintainable.
What you’re actually trying to build
Stripped to basics, the problem behind “how can I make an AI assistant call my internal API (create tickets, update records)” is this:
- A user describes what they want in natural language.
- The AI assistant decides which operation(s) match (e.g.,
create_ticket,update_ticket,add_comment). - The assistant calls your internal API with the right parameters.
- Your system executes the call and returns results.
- The assistant explains what it did back to the user.
You don’t need a complex agent framework for this. You need:
- A small “tool layer” exposing safe, well-defined operations.
- Good prompts so the model knows when and how to use them.
- A simple controller that:
- Receives tool calls from the AI,
- Calls your internal APIs, and
- Feeds results back into the AI.
Think of it less as “agents” and more as “function calls with guardrails.”
Core approach: tool calling instead of full agents
Most modern LLM platforms support a “tool calling” or “function calling” feature. Conceptually, they all work the same way:
- You define tools (functions) with names, descriptions, and parameter schemas.
- The model is instructed that it can call these tools instead of replying with plain text.
- When needed, the model responds with a structured tool request.
- Your backend executes the tool, then provides the result back in a new model turn.
This pattern is ideal if you want the AI assistant to call your internal API (create tickets, update records) without writing your own agent framework.
What tools should look like
For a ticketing scenario, useful tools might include:
create_ticketupdate_ticketget_ticketadd_ticket_commentlist_user_tickets
Each tool should be:
- High-level and business-oriented (e.g.,
create_ticket), not low-level HTTP wrappers. - Strongly typed via JSON schema or similar.
- Safe by default, with clear constraints and validations.
Step 1: Design a thin API layer just for the AI
Instead of exposing your entire internal API surface, design a minimal, AI-facing “facade”:
-
Wrap complex flows into one tool
Example: Instead of requiring multiple calls to create and then assign a ticket, offer a singlecreate_ticketthat supports optional assignment. -
Hide legacy complexity
Internally, you may have multiple microservices; externally, you just expose simple operations the AI can understand. -
Apply a permission model
- Only expose actions that are safe given what the AI can infer about the user.
- Enforce authorization in this layer, not in the model prompt.
This design makes it much easier to control what the AI can and cannot do without building an entire agent runtime.
Step 2: Define tools / functions for the model
Here’s a concrete example using a generic “tool calling” setup. Adjust to your LLM provider’s syntax.
Example tool schema for creating a ticket
{
"name": "create_ticket",
"description": "Create a new support ticket in the internal ticketing system.",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Short, descriptive title for the ticket."
},
"description": {
"type": "string",
"description": "Detailed description of the user issue."
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "Ticket priority, default is medium."
},
"requester_id": {
"type": "string",
"description": "Internal user or customer ID requesting support."
}
},
"required": ["title", "description", "requester_id"]
}
}
Example tool schema for updating a ticket
{
"name": "update_ticket",
"description": "Update fields on an existing ticket by ID.",
"parameters": {
"type": "object",
"properties": {
"ticket_id": {
"type": "string",
"description": "The unique ID of the ticket."
},
"status": {
"type": "string",
"enum": ["open", "pending", "resolved", "closed"],
"description": "New status for the ticket."
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "urgent"],
"description": "New priority (optional)."
},
"assignee_id": {
"type": "string",
"description": "User ID of the new assignee (optional)."
}
},
"required": ["ticket_id"]
}
}
These tools are purpose-built for “make an AI assistant call my internal API (create tickets, update records)” goals, without leaking internal implementation details.
Step 3: Build a simple controller instead of an agent framework
You still need some logic outside the model, but it can be very thin. The core responsibilities are:
- Send user messages + tool definitions to the model.
- Detect when the model wants to call a tool.
- Execute the tool by calling your internal API.
- Feed the result back into the model so it can respond to the user.
Example high-level flow (pseudo-code)
loop:
user_message = get_input_from_user()
# 1. Call LLM with tools enabled
llm_response = call_llm(
messages = conversation_history + [user_message],
tools = [create_ticket, update_ticket, get_ticket],
tool_choice = "auto"
)
if llm_response.type == "tool_call":
tool_name = llm_response.tool_name
tool_args = llm_response.tool_arguments
# 2. Dispatch to your internal API layer
tool_result = execute_tool(tool_name, tool_args)
# 3. Provide tool result back to the LLM
llm_followup = call_llm(
messages = conversation_history
+ [user_message]
+ [llm_response.as_message()]
+ [tool_result.as_tool_result_message()]
)
# 4. Send final assistant message to user
send_to_user(llm_followup.content)
conversation_history.append(user_message)
conversation_history.append(llm_followup)
else:
# Just a normal reply, no API call
send_to_user(llm_response.content)
conversation_history.append(user_message)
conversation_history.append(llm_response)
You’ve effectively built a “micro-agent” that’s specific to your use case, without a heavy framework.
Step 4: Use prompts to control when tools are called
You don’t want the model calling create_ticket for every little question. Use system and developer prompts to:
- Explain what tools are for.
- Describe when to call them vs. just answering.
- Impose boundaries (e.g., do not change ticket status without explicit user confirmation).
Example system prompt snippet
You are a support assistant that can:
- Answer questions using your general knowledge.
- Interact with the internal ticketing system via tools.
Use tools ONLY when the user explicitly requests actions like:
- Creating a new ticket
- Updating an existing ticket
- Checking the status or details of a ticket
Before using tools that change data (create, update, close), confirm the details with the user.
If the user is just asking for information or advice, respond normally without tools.
This prompt strategy keeps behavior predictable without complex agent policies.
Step 5: Implement the internal API calls safely
The “execute_tool” function is where your real internal logic lives. It should:
- Validate input.
- Check permissions.
- Call internal services.
- Sanitize outputs before returning them to the model.
Example implementation sketch (Node.js-style)
async function executeTool(toolName, args, userContext) {
switch (toolName) {
case "create_ticket":
return await createTicketForAI(args, userContext);
case "update_ticket":
return await updateTicketForAI(args, userContext);
case "get_ticket":
return await getTicketForAI(args, userContext);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
async function createTicketForAI(args, userContext) {
// Enforce that the ticket is created for the current user
const requesterId = userContext.userId;
const payload = {
title: args.title,
description: args.description,
priority: args.priority ?? "medium",
requesterId
};
const response = await fetch("https://internal.api/tickets", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-User-Id": requesterId
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.text();
return {
error: true,
message: "Failed to create ticket.",
details: errorBody.slice(0, 500)
};
}
const ticket = await response.json();
return {
error: false,
ticket_id: ticket.id,
status: ticket.status,
priority: ticket.priority
};
}
This pattern lets you enforce business rules even if the model suggests something invalid.
Step 6: Add confirmation and guardrails for critical changes
For operations that update records, close cases, or change status, add a two-step flow:
- Model proposes the change and asks the user for confirmation.
- Only once the user confirms does the model call the tool.
Prompt pattern
For any irreversible or high-impact actions (e.g., closing a ticket, deleting a record):
1. First summarize the requested change.
2. Ask the user to confirm.
3. Only after explicit confirmation, call the relevant tool.
When the user says “Yes, please close ticket 123,” the model then uses update_ticket with status: "closed".
This prevents overly aggressive automation and keeps users in control.
Step 7: Handle context: mapping natural language to real IDs
Users say things like:
- “Close my ticket about the VPN issue.”
- “Update the priority on the Jira bug you created earlier.”
To support this without a complex agent framework, add helper tools:
search_tickets_by_keywordlist_recent_user_tickets
Then your flow becomes:
- LLM calls
list_recent_user_ticketsto narrow options. - LLM disambiguates with the user if needed (e.g., “I found two tickets about VPN; which one?”).
- LLM calls
update_ticketon the chosen ID.
You’re still only using simple tool calls, but together they behave like a lightweight agent.
Step 8: Logging, observability, and safety checks
You don’t need agent orchestration, but you do need good logging:
- Log:
- User messages (redacted if needed),
- Tool calls + parameters,
- Internal API responses,
- Final assistant responses.
Use these logs to:
- Detect incorrect or over-broad updates.
- Refine tool descriptions and prompts.
- Add additional validation rules in your API layer.
Also consider:
- Rate limits (per user, per tool).
- Allow/deny lists (e.g., which users can close tickets).
- Hard limits on bulk updates or destructive changes.
When you might actually need a fuller agent framework
You can keep using this lightweight approach as long as:
- Workflows are relatively short (1–3 tool calls per task).
- You don’t need cross-session “plans” or long-running jobs.
- You’re okay with a single model deciding when/how to call tools.
You might consider a more advanced agent framework when:
- You need multi-step planning with dynamic branching.
- Different models or services must collaborate to complete a task.
- Tasks run asynchronously over minutes or hours with retries and state machines.
Even then, the tool layer you built for “make an AI assistant call my internal API (create tickets, update records)” will still be valuable—it becomes the capability layer your agents orchestrate.
Practical checklist to implement this in your stack
Here’s a concise action plan you can follow:
-
Identify operations
- List the core actions: create ticket, update ticket, get ticket, list tickets, add comment.
-
Design AI-facing tools
- Define 3–7 tools with clear names, descriptions, and JSON schemas.
- Keep them business-level, not low-level HTTP.
-
Build a small service layer
- Implement
execute_tool(toolName, args, userContext)that calls your internal API. - Enforce auth, validation, and safe defaults here.
- Implement
-
Integrate tool calling in your AI stack
- Configure your LLM client with these tools.
- Implement the basic tool-call loop (detect → execute → re-call model).
-
Tune prompts for behavior
- System prompt: roles, when to use tools, when not to.
- Developer prompt: specifics of your ticketing system, confirmation rules.
-
Add confirmations and guardrails
- Require explicit confirmation for updates/closings.
- Handle ambiguity (e.g., “which ticket?”) with follow-up questions.
-
Monitor and iterate
- Log tool usage and failures.
- Refine tool descriptions and schemas.
- Add missing tools (e.g., search by keyword) as patterns emerge.
Following this pattern, you can make an AI assistant call your internal API—create tickets, update records, and more—using a thin, maintainable integration layer, without committing to a complex agent framework that becomes another system you need to operate.