Want a live demo of our AI support agents? Join our next webinar on Wed 18 Nov, 9:30am (PT) / 5:30pm (UK)Webinar Wed 18 Nov · 9:30am PT / 5:30pm UK (opens in a new tab)

Build safe tools and APIs for an AI support agent

The short answer

A support tool should do one bounded job, check authority on the server and return a small, explicit result. Keep reads and writes separate. For every write, record the operation before you attempt it, and when the outcome is uncertain, check what actually happened before you try again.

The support case

Design each tool for a support task

A tool is a function the agent can ask your system to run, such as looking up an order or requesting a refund. Your existing APIs usually expose far more data and control than a support agent needs, because they were built for your own apps and staff. Rather than handing those over, create a small wrapper for each clear task, such as reading one order’s delivery status or requesting an address change.

Keep the destination host and credentials in server configuration, where the model can’t see or change them. Treat anything the customer supplies as data inside the request. A customer-supplied value should never be able to change which host is called or which authentication header is sent.

Declare the required arguments, validate their types and formats, and reject any field you didn’t define. Bind the customer’s trusted identity outside the schema the model fills in, so the model never chooses the account ID. The server then checks ownership, role and current business conditions, for example that the order hasn’t shipped yet. The model can propose a call, but your application decides whether it runs.

Adapt responses before the model sees them

A legacy order API might return internal notes, full addresses, payment details and years of history when the agent only needs the dispatch status. Put reviewed code between the API and the model, often called an adapter, that keeps only the fields the task needs and converts them into a stable format. Don’t rely on the model to ignore sensitive fields it has already been given.

Make the different kinds of “nothing” distinguishable: a valid empty result, missing required data, a partial list and an upstream error each mean something different to the customer. Keep a value as null when it is unknown. If the delivery date is missing, the agent should say it doesn’t have one, not invent a plausible estimate. If the adapter itself fails, return a controlled failure state, never the original raw response as a fallback.

When a response is too large, cut it at a structural boundary. Limit an array before converting it to JSON, keep the result valid and mark it with complete=false and a way to fetch more when that makes sense. Chopping the final JSON string can drop a closing bracket or the one condition that changes the answer. The downloadable adapter shows field filtering and a partial-result flag using synthetic data.

Model an action outcome explicitly

A write action does not simply succeed or fail. Networks drop responses, providers accept work and finish it later, and multi-step operations can stop halfway. Give every action one of these states, and let the state decide what the agent may do and say next.

StateMeaningPermitted next step
Not attemptedValidation or authorization stopped the callExplain or use the approved verification route
In progressAn accepted operation has no final resultObserve using the same operation identity
Confirmed successThe intended downstream change is verifiedReport only the confirmed result
Confirmed failureThe provider confirms no intended change occurredApply the documented retry or human route
Partial resultSome steps completed and others did notReconcile each step; do not repeat completed writes
UnknownThe request may have completed but evidence is missingHold and read back; do not replay blindly

Each state maps to something the agent can honestly tell the customer. “Your refund has been issued” belongs to confirmed success only. For an unknown outcome, the honest reply is closer to “I’ve sent the request and I’m checking that it went through.” Keep a receipt for every action, with the operation ID, the parameters and the final state, so a person can explain the result later without guessing.

Retry the operation, not the HTTP call

Before the first attempt, create a stable ID for the business operation (“refund $40 on order ORD-204”) and save the intended resource, parameters and policy decision. Many payment and order providers support idempotency keys: you send the same key with every attempt at the same operation, and the provider performs the operation only once within its documented key window. Use the same key only for the same operation. A changed amount or a different order is a new operation and needs a new key.

If the provider has no idempotency support, design a reconciliation path instead. Read the authoritative status from the provider, or hand the unresolved operation to a person who owns it. A generic HTTP retry library can’t decide whether a refund is safe to repeat, because it only sees a failed request. Also decide which layer owns retries. If the gateway, the background worker and the tool each retry three times on their own, one timeout can turn into many attempts.

Test the boundary and the customer result

Test the cases an attacker or a confused model would produce: a request for a resource the customer doesn’t own, a model-supplied account override, missing fields, an oversized response and an upstream error body. Plant a synthetic secret in an unexpected response field and check that it reaches neither the model nor your general logs.

For writes, simulate the worst moment. Stop the worker after the external action has happened but before your system has saved the success, then let it recover. The test passes only when the downstream state is correct, no duplicate action occurred and the receipt shows what happened.

Build decisions and checks

Use these decisions to turn the worked case into your own implementation. A check is complete only when you have evidence for the stated result.

Build small APIs for support work

When it applies. Existing APIs often expose database detail instead of the answer a support agent needs.

Build. Create narrow operations such as get_order_status or request_address_change. Validate schemas, dates, currencies, allowed values and required fields. Keep business rules in the service. Write clear tool descriptions and say what each failure means.

Check. Invalid arguments fail before any external action. A realistic request returns the facts needed for the next step.

Keep credentials and trusted fields outside the model

When it applies. If the model picks the credentials or the account ID, a tool can act with the wrong authority.

Build. Store secrets on the server. Take identity and the allowed resource scope from trusted context. Expose only the tool arguments the model is allowed to choose. Use allowlists for destinations and redirects.

Check. Prompts and tool outputs contain no tokens. The model-facing schema leaves out trusted account IDs and credentials. Attempts to override the account or call an internal-network URL are denied and can't change the identity the server supplied.

Shape API responses for the agent

When it applies. A raw API response can be large, incomplete or full of misleading internal fields.

Build. Return a small, stable schema with status, relevant facts, units, timestamps, source IDs and completeness. Keep no results, denied access, failure and unknown outcome distinct. Mark truncation and pagination, and never turn a missing value into zero. For an API you don't control, put a code-reviewed adapter in front of it that allows only the fields you need and returns explicit empty, partial, unavailable and error states. Trim arrays before serializing, keep the structure valid and mark incomplete results. If the adapter fails, never fall back to the raw payload.

Check. Test empty results, partial pages, null fields, stale data, oversized lists, missing fields and unexpected HTML errors, plus adapter and upstream errors. Raw secrets and error bodies never reach the model or general logs.

Prevent duplicate business actions

When it applies. A timeout can hit after a refund or update has already gone through, and a retry can repeat it.

Build. Use an operation ID and idempotency support (so a repeated request has no extra effect). Record the intent and the provider's receipt. Read back uncertain outcomes before retrying anything. Handle safe read retries separately from writes. After a model or worker restart, reuse results that already completed.

Check. Simulate a lost acknowledgment. The business action happens at most once, or the case stops for reconciliation when there's no proof either way.

Verify results and handle partial failure

When it applies. An HTTP success doesn't prove the right record changed. Two requested changes can have different outcomes.

Build. Read back important changes. Return confirmed, failed and unresolved items separately. Where a change can be reversed, define the compensation or human follow-up. Never let the model announce a success that hasn't been confirmed.

Check. Make one part fail after another succeeds. The reply describes what actually happened and doesn't repeat the part that worked.

Templates and examples

Every file uses synthetic data. Adapt it to your own policies and test it somewhere safe before it touches customers.

safe_tool_adapter.py
"""Synthetic, code-reviewed response adapter. Run: python3 safe_tool_adapter.py

Demonstrates allowlisted fields and explicit incomplete results. Authentication,
ownership checks, network timeouts and upstream size limits belong in the caller.
Never pass the raw payload to the model when this adapter returns an error.
"""


def adapt_orders(status_code, payload, limit=2):
    if status_code != 200:
        return {'status': 'unavailable'}
    if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1:
        raise ValueError('limit must be a positive integer')
    if not isinstance(payload, dict) or not isinstance(payload.get('orders'), list):
        return {'status': 'invalid_response'}
    orders = payload['orders']
    result = []
    for order in orders[:limit]:
        if not isinstance(order, dict):
            return {'status': 'invalid_response'}
        reference, state = order.get('reference'), order.get('state')
        if not isinstance(reference, str) or not reference or not isinstance(state, str):
            return {'status': 'invalid_response'}
        estimate = order.get('estimated_delivery')
        if estimate is not None and not isinstance(estimate, str):
            return {'status': 'invalid_response'}
        result.append({'reference': reference, 'state': state, 'estimated_delivery': estimate})
    return {'status': 'ok', 'orders': result, 'complete': len(orders) <= limit}

All templates and checklists

Keep it working

Launch is the start of the work, not the end. These are the recurring jobs this part of the agent needs, and who usually owns them.

CadenceOwnerWork
On changeEngineeringUpdate schemas and contract tests when upstream APIs change.
MonthlySecurityRotate credentials, review scopes and scan logs for leaks.
WeeklyEngineeringWatch response sizes and data the agent misreads.
DailyEngineeringReview pending and unknown outcomes, and reconcile them before resuming.
WeeklyEngineeringReview mismatches between tool receipts and what customers were told.

Test your decision

A timeout handler offers the model an automatic retry with a fresh request ID. What is missing?

Show the expected decision

The system has lost track of the original operation. It needs to keep the business-operation ID and first find out whether the first attempt completed, using the provider’s idempotency or status support. If neither exists, hold the action and reconcile it with an owner rather than letting the model try again.

Common questions

Can I expose an existing API directly to the agent?

Only if its permissions, parameters and output already match the narrow support task. Usually they don’t, so put a server-side adapter in front that enforces access and filters the data.

Is retrying a timed-out write safe?

Not by default. A timeout tells you the response didn’t arrive, not that the action failed. Either reuse a valid idempotency key for the same operation or check the original operation’s status before another attempt.

Should read-only tools get the same checks as write tools?

They need the same access checks, because a read can leak another customer’s data. They don’t need idempotency or reconciliation, since repeating a read changes nothing.

Sources

Official documentation backs the provider-specific facts; the worked cases and recommendations are teaching material. Check the current API and model version before you build.

Rather skip the build?

My AskAI is an AI support agent that already does all of this inside your helpdesk. Set it up in minutes and pay per AI reply: $0.05 on the Scale plan, with models, hosting and maintenance included.