Runtype
GuidesGuide

How to turn an internal workflow into a tool an agent can call

Turn a CRM lookup or an internal service into an agent tool: task-shaped granularity, typed inputs, server-side credentials, timeouts and a read-only rollout.

Last updated 6 min read

Wrap the workflow, not the endpoint. Pick one task with a clear input and a clear output, put a typed interface in front of it, hold the credentials on the server side of that interface, and return a small structured result. Expose it over HTTP or an MCP server, and ship the read-only half of the surface before the writes.

An agent earns its place only when it can reach the systems the work lives in: the CRM, the ticket queue, a billing service behind a VPN, a reporting database nobody has touched since the migration. Each of those arrives as a separate integration with its own auth, its own pagination and its own way of failing.

How do I connect my AI agent to our internal systems?

The usual first attempt is a thin wrapper per route. An order service has fourteen REST endpoints, so the agent gets fourteen tools, and answering "can this customer get a refund on A-10482" becomes six model turns: fetch the order, fetch the line items, fetch the shipment, fetch the account plan, fetch the refund history, then reason about a policy the model half-remembers from the system prompt.

Every one of those turns is a chance to pass a wrong id, to stop early, or to invent the policy rule. The same question as one tool, check_refund_eligibility, is one turn, one audited call, and one place where the policy is written down as code. The routes are still there. They stopped being the agent's interface.

A tool has three consumers with different demands: a model that needs a name and a description precise enough to choose correctly, a runtime that needs a schema it can validate and a timeout it can enforce, and an internal system that needs a caller with credentials and a tenant scope. Designing for one and hoping the other two work out is where most of the failure modes come from.

Wrap the workflow in seven steps

1. Pick a workflow with one input and one output

Look for a task somebody on your team already does end to end with a name they use out loud: check refund eligibility, look up a customer's current plan, open a P2 ticket, reconcile an invoice. Write the tool description in one sentence. If the sentence needs an "and", you have two tools.

The other test is the output. A workflow with a clear output has an answer that fits in a paragraph, not a page of records for the model to summarize. When you can only describe the output as "the data", the boundary is in the wrong place.

2. Give it a typed interface before you give it to a model

Your schema is the contract, and the descriptions inside it are prompt text the model reads on every turn. Constrain what you can: enums instead of free strings, identifiers in the format your system actually prints, additionalProperties: false so a hallucinated field is rejected at the boundary instead of ignored downstream.

{
  "name": "check_refund_eligibility",
  "description": "Decide whether one order can be refunded under the account's refund policy. Returns the decision, the policy rule that produced it, and the refundable amount in cents.",
  "input_schema": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "pattern": "^[A-Z]-[0-9]{5}$",
        "description": "Order identifier as printed on the receipt, for example A-10482"
      },
      "reason_code": {
        "type": "string",
        "enum": ["damaged", "late", "wrong_item", "changed_mind"]
      }
    },
    "required": ["order_id", "reason_code"],
    "additionalProperties": false
  }
}

Note what is absent: no tenant_id, no api_key, no override_policy. Anything the model can put in an argument is something an injected instruction inside a support ticket can put there too.

3. Decide synchronous or asynchronous before you write the handler

Tool calls run inside a turn somebody is waiting on, and runtimes cap them. Thirty seconds is a common default ceiling, and a call that blows past it is cancelled with no result rather than finishing quietly in the background. Measure the workflow's slow tail, not its median, and design against that number.

If the tail exceeds the ceiling, split the tool. One call starts the job and returns a handle, a second reads its status, and the agent tells the user the work is queued. Work that runs on a clock rather than inside a conversation belongs in a scheduled run instead, with the agent reading its result later.

4. Put authorization and tenant scoping on the server side

The tenant identity comes from the request that started the agent turn, and the handler reads it from there. It is never a model argument, never a value the system prompt carries, and never something a user message can influence. Same rule for credentials: the handler attaches the service account or API key when it calls the internal system, so no secret is ever in a prompt, an argument or a stored trace.

Then check ownership inside the handler. Given A-10482 and a tenant, the query is scoped by tenant and returns nothing for an order belonging to somebody else, which surfaces as a clean not-found rather than a leak. Assume every id the model supplies was read out of a document an attacker could write.

tenant  <- execution context   (server, trusted)
user    <- execution context   (server, trusted)
args    <- model output        (untrusted, validated against schema)
secret  <- secret store        (server, never serialized into the call)

5. Return a result the model can act on

A raw payload of forty fields costs tokens and invites the model to reason about the wrong ones. Return the decision, the reason for it, the numbers the next step needs, and a stable shape for failure. Field naming carries more weight here than in an ordinary API, since the names are the only documentation the model gets. The general practice is covered in structured output from an LLM.

{ "ok": true, "eligible": false, "rule": "return_window_expired", "window_days": 30, "days_since_delivery": 44, "refundable_cents": 0 }
{ "ok": false, "error": { "code": "upstream_timeout", "retryable": true, "message": "The billing service did not respond. Tell the customer you will follow up, and do not promise a refund." } }

The failure shape matters as much as the success shape. A handler that swallows a timeout and returns [] teaches the agent that the customer has no orders, and the agent will say so with complete confidence.

6. Expose it as an HTTP tool or over MCP

Registering an HTTP endpoint as a tool is the shorter path when one team owns both the agent and the workflow. You already have a URL, a schema and a handler, and the runtime needs a name, a description, the schema and where to send the call. An MCP server is worth the extra piece when several agents or several clients need the same tools, or when the team that owns the internal system wants to own its tool surface too and hand you a URL. The tradeoff is laid out in MCP versus a plain API, and the operational side of running one is covered under MCP server.

Either way, keep the set small. Selection accuracy falls as the catalog grows, and a set assembled by wrapping every route is the fastest way to a large catalog. Ten well-named task tools beat sixty route tools, which is the argument in how many tools an agent can handle.

7. Test the tool alone, then inside the agent

Two harnesses, because two things can break. The first calls the handler directly with fixed arguments and asserts the contract: valid input returns the documented shape, an id from another tenant returns not-found, an upstream 500 returns retryable: true, an unknown field is rejected. None of that needs a model.

The second runs the agent against realistic requests and asserts that it chose this tool, passed sane arguments, and used the result. Agent-level failures usually trace back to the description rather than the code, so fix the wording and rerun before touching the handler. Keep both suites, since a passing contract test with a failing selection test is a completely different bug from the reverse.

Ship the read-only half first

Read tools can be wrong. Write tools can be expensive. Ship the lookups, watch two weeks of real traffic, and read the calls the agent actually made: the arguments it invented, the tools it reached for when it should have asked a question, the cases where it called nothing at all. That log is the design review you cannot get any other way.

Add writes after that, one at a time, each with an idempotency key so a retried call cannot double-charge and a scope narrow enough that the worst outcome is recoverable. Put an approval in front of the first few and read what the agent was about to do. Approval is also the right long-term home for the small number of actions where a wrong call costs real money.

Where this gets easier

Most of the list above is plumbing that has nothing to do with your workflow: a registration format, somewhere safe to keep a credential, a timeout, a retry rule, a record of what the tool returned. Runtype registers HTTP endpoints, MCP servers and internal flows as tools, with secrets written as {{secret:NAME}} references that are resolved server-side at call time so the value never reaches the model or the trace, and a deterministic multi-step flow can be published as a single tool an agent calls. Every call is recorded with its arguments and its result, which is what turns the two test suites above into something you can check against production traffic.

Frequently asked questions

Should I give the agent one tool per API endpoint?
No. An endpoint is shaped for a programmer who already knows the sequence, and a model has to rediscover that sequence on every turn. Write one tool per decision the agent makes, and let the handler behind it call as many endpoints as the decision needs. The exception is a genuinely single-call task, where the endpoint and the decision happen to be the same thing.
Where do the credentials for an internal system live?
On the server side of the tool boundary, never in the system prompt, a tool argument or a default value in the schema. The handler holds the API key or the service account and attaches it when it calls the internal system. Anything the model can read, it can also be persuaded to repeat into a message, and anything it can write, it can be persuaded to change.
What should a tool return when the internal system is down?
A structured failure the model can act on, rather than a raw exception or an empty array. Return a machine-readable code, a flag saying whether retrying is worthwhile, and one sentence the agent can pass to the user. An empty result reads to a model as a valid answer, which is how a broken lookup turns into a confident wrong reply.