Chapter 1 of 7 · 10 min read
A well-formed call is not a correct decision
On this page
Two weeks into the pilot, the finance team's month-end reconciliation caught it. The support agent had issued 1,240 refunds against the live queue, and 38 of them were against orders delivered more than 30 days earlier, worth $4,110 in total. Another 61 refunded the shipping charge on items the customer reported as damaged, where the published policy refunds shipping only when the parcel never arrived. None of these were malformed calls. Every one carried a real order_id, an amount_cents that matched a line total in the orders table, and a reason_code drawn from the permitted enum. The tool's own dashboards showed a 100% validation rate and zero errors for the fortnight, which is why nobody between the agent and finance had anything to look at.
Three different things could have prevented those 99 refunds, and because they are not variations on a single idea it is worth naming all three before going further. A tool is a callable interface the model is permitted to invoke: a name, a description, a schema for the arguments, and code behind it that does something in the world, such as moving money at a payment provider or writing a row in the orders table. The model never runs that code itself. It emits a request to call the tool, the agent loop executes it, and the result comes back to the model as text. issue_refund is a tool, and tools exist because a language model produces only text, so every effect it has on a real system has to pass through an interface that somebody owns, permissions and holds a credential for.
A skill is durable procedural knowledge, written down for the model to read, describing how a competent person handles a class of situation — the refund policy, in this case, with its 30-day window and its rule about shipping. Topic 12.4 covered how one is written and packaged. It is text that gets loaded into the model's context when the situation calls for it, so it changes what the model decides and causes nothing at all by itself.
An MCP server is a tool provider that runs as its own process, generally operated by a different team or by an outside vendor, and speaks the Model Context Protocol (MCP) — an open protocol, published by Anthropic at the end of 2024 and since implemented by most agent frameworks and many SaaS vendors, that standardises how a client discovers the tools a server offers and calls them, so one integration can be consumed by any agent instead of being rewritten per client. Topic 6.16 covered where it is the wrong answer. From the model's seat, a server's tools look exactly like tools defined inside your own process; what differs is who writes the code, who holds the credential, and who gets paged at three in the morning.
You meet the choice between the three every time a capability is missing and somebody proposes the obvious fix, which is usually another tool when the honest answer is a paragraph of policy, or a server when a function in your own process would have done. The refund queue is what a misclassification looks like from the outside, and this chapter is about how to tell the first two apart before finance does it for you.
The engineer who built the tool had done the job carefully. The schema was tight, the arguments were named for what they meant, the description said what the tool did and when to reach for it, and the whole thing had been reviewed by someone who had shipped tools before.
ISSUE_REFUND = {
"name": "issue_refund",
"description": (
"Issue a refund against a completed order. Amount is in cents and "
"must not exceed the order's refundable balance. Returns the "
"provider's refund id and the new refundable balance."
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "pattern": "^ord_[0-9a-f]{16}$"},
"amount_cents": {"type": "integer", "minimum": 1},
"reason_code": {
"type": "string",
"enum": ["damaged", "not_received", "wrong_item",
"changed_mind", "duplicate_charge"],
},
},
"required": ["order_id", "amount_cents", "reason_code"],
},
}What a tool contract is actually promising
A schema is a statement about the shape of a call. It says which arguments exist, what types they take, which values are permitted for an enum, and which of them may be omitted. It is evaluated against a call that has already been decided on, which means it can reject a call that is unrepresentable and has nothing to offer about a call that is representable and wrong. Refunding $89.00 against ord_3f2a... with reason_code: "damaged" is a perfectly representable call. Whether it is the right thing to do depends on the delivery date, on whether the customer has already been refunded for this order, on whether the damage claim included a photograph, and on the fact that shipping is not refundable under damaged — none of which is a property of the call's shape.
The 30-day window and the shipping rule are decisions, and the model was making them with nothing to make them from. It knew that refunds exist, that this tool issues them, and that the customer in front of it was unhappy about a broken lamp. From that position the calls it produced were reasonable. They were also, on 99 occasions out of 1,240, against policy, and the policy was in a Confluence page that no part of the system had ever read.
There is a version of this fix that people reach for first, which is to move the rules into the tool's server side and reject the bad calls. That is a real improvement and it is worth being honest about how far it goes. The team did exactly this as an emergency patch: issue_refund began checking the delivery date and returning {"error": "outside_refund_window", "delivered_days_ago": 44}. The money stopped leaving, and the agent's behaviour got worse in a way that was easy to miss. Over the following week the traces showed the model receiving the rejection and retrying — a smaller amount, then a different reason_code, then the same call again with the order id it had used two turns earlier. In 23 of 31 rejected cases it made at least one further attempt before giving up, and in 9 of them it told the customer that the refund had been processed and would appear in three to five business days. An error returned at call time arrives after the decision has been made, so all the model can do with it is guess again.
Two capabilities, and what happens when you remove one
The shape that works gives the model the policy before it chooses, in a form it can reason over, and keeps the callable interface for the part that has to actually move money. The refund rules are durable procedural knowledge: they change a few times a year, they apply to a class of situations rather than to one call, and reading them changes what a competent agent decides to do. That is a skill, in the sense established in 12.4. The transfer to the payment provider is an action with a credential behind it and a permission attached, which is a tool.
## Deciding a refund
Check the delivery date on the order before anything else.
- Delivered 30 days ago or less → refund is available.
- Delivered 31–60 days ago → offer store credit, and do not call
`issue_refund`. Store credit is issued by the human queue.
- Delivered more than 60 days ago → decline, and say why.
What the amount covers, by reason code:
| reason_code | item total | shipping | restocking fee |
| -------------- | ---------- | -------- | -------------- |
| not_received | yes | yes | n/a |
| damaged | yes | no | n/a |
| wrong_item | yes | yes | n/a |
| changed_mind | yes | no | deduct 15% |
| duplicate_charge | full charge | n/a | n/a |
`damaged` does not refund shipping: the parcel arrived, so the carrier
was paid for work that was performed. Customers argue this one. The
answer is that we replace the item free of charge if they would rather
have that, which you can offer without approval.The interesting part of the deployment was the ablation the team ran before they trusted it, meaning that they ran the same set of conversations three times with one half of the capability removed each time, because the two halves fail in ways that look nothing alike. They took 200 replayed conversations from the pilot queue and ran them through three configurations.
| Configuration | Calls made | Well-formed | Policy-correct | Failure signature |
|---|---|---|---|---|
| Tool only | 194 | 194 | 178 | Well-formed refunds outside the window; shipping refunded on damaged. Nothing in any log looks wrong. |
| Skill only | 0 | n/a | n/a | Correct reasoning, no effect. In 14 of 200 the agent told the customer the refund was done. |
| Skill + tool | 161 | 161 | 159 | 33 conversations correctly routed to store credit or declined instead of refunded. |
The tool-only row is the pilot, reproduced. The skill-only row is the one people find surprising in the moment and obvious in retrospect: the model reads the policy, reaches the correct decision, describes the refund it is going to issue in fluent detail, and nothing happens, because describing a transfer is not a transfer. Worse, the description is convincing enough that in fourteen conversations the model reported the refund as complete. Guidance shapes what gets invoked and cannot substitute for the invocation.
The 161-versus-194 gap is the whole point of the exercise. Thirty-three conversations that had been producing a refund stopped producing one, and every one of them is a decision the tool schema had no way to express and the error path could only punish after the fact.
Sorting a requirement into the three shapes
The classification question in front of you when a capability is missing is which of three things is absent, and they are not alternatives competing for the same slot. Durable guidance is knowledge that changes what a decision looks like and is inert on its own. An executable interface is a permissioned way to cause an effect or read a system of record, and it is silent about when causing that effect is right. An externally operated integration is an executable interface whose code, credentials, releases and incidents belong to somebody else, which makes it a question about lifecycle ownership rather than about capability. A requirement usually needs more than one of them, and asking which one it is — singular — is how teams end up with the pilot.
The lifecycle question is the one most often skipped, so it is worth making explicit while sorting. For each piece, name who edits it, who reviews the edit, whose deploy carries it to production, and who is paged when it misbehaves. For the refund skill the answer was the support operations lead, reviewed by legal once a year, shipped in the agent repository. For issue_refund it was the payments team's library inside the agent's own process, with the provider key held by the agent service. Those are different answers, and the next chapter is about what follows from that.
Retention has the same shape and a harder deadline
The pattern generalises to anything where an action is cheap to perform and expensive to perform in the wrong case. A data-retention requirement is the clearest example, because the categories are genuinely intricate: some record classes are purged the day the account closes, some are anonymised so that aggregate reporting survives, and some are retained for seven years because a legal hold or a regulatory obligation says so, and the third category overrides the first two whenever they collide.
The temptation is to express all of this in the tool signature — delete_customer_record(customer_id, mode) with mode as an enum of purge, anonymise, retain — and consider the policy encoded. The enum encodes that three modes exist. It does not encode that support transcripts from a closed account go to anonymise while the payment instruments on the same account go to purge, that a chargeback opened in the last 24 months puts the entire order history under retain regardless of account state, or that anonymising a record with an open legal hold is itself a compliance failure rather than a conservative fallback. Those are the category rules, they run to a couple of pages, they are amended when a regulator publishes guidance, and they need to reach the model before it picks a mode.
So the correct shape is the same as refunds: the category rules as a skill, deletion and anonymisation as tools. The tools stay narrow — purge_records(customer_id, record_classes) and anonymise_records(customer_id, record_classes), each with its own permission and its own audit line — and the skill decides which record class goes where. Legal-hold checking belongs on the tool side as a hard refusal as well, because a purge is irreversible and the cost of being wrong is not symmetric, but the refusal is a backstop under a decision that has already been made correctly rather than the mechanism that makes it correct.
What a skill does not buy you
A skill is not a guardrail, and treating it as one is the most expensive misreading available here. Guidance the model reads is guidance the model can be argued out of, by a customer who is persistent, by a conversation long enough that the relevant paragraph has fallen out of attention, or by a genuinely ambiguous case where two rules point different ways. The refund skill moved policy-correct calls from 178 to 159 out of a smaller total, which is a large improvement and not a proof. Two of the 161 calls in the third configuration were still wrong, and the team kept the server-side window check underneath precisely because 99% is a good hit rate for a decision and a poor one for a payment.
The division is between what shapes a decision and what enforces a limit. Skills do the first well and cannot do the second at all, because nothing about a paragraph of text prevents a call from being made. Tools do the second and are structurally incapable of the first, since they only ever see a decision that has already happened. A requirement that needs both gets both, and the ablation is how you find out which half you are missing rather than arguing about it.
Everything so far has assumed the same team writes the skill, owns the tool, and holds the credential the tool uses. That assumption is what makes the refund case simple, and it is false for most capabilities of any size. The next chapter traces one capability through all three shapes and follows the credential, because where the secret lives turns out to decide more about the architecture than where the code does.
Skill vs tool vs MCP server — read the current version at https://daviddominguez.dev/learn/en/skill-vs-tool-vs-mcp-server/a-well-formed-call-is-not-a-correct-decision