Skip to content

Tool-Backed Skills

What is a tool-backed skill?

A skill can be a purely language capability:

input → LLM → output

such as summarization or text classification.

Many useful agent skills, however, require external data or operations:

PR Review Skill
  ├── fetch diff
  └── read file

Deployment Health Skill
  ├── read deployment state
  └── query metrics

Support Account Skill
  ├── get account
  └── get invoices

These are tool-backed skills: the semantic capability depends on external tools.

The most important boundary: decision vs execution

The LLM/skill may decide which operation is needed.

Actual execution should remain under application control.

User request
    ↓
Skill / LLM
    ↓
Tool request
    ↓
Application validation
    ↓
Authorization / policy
    ↓
Tool execution
    ↓
Tool result
    ↓
Skill / LLM
    ↓
Result

This is the same principle introduced in Foundations Tool Calling, now viewed as a reusable skill boundary.

The model may request an action; the application decides whether and how that action is executed.

Tool selection vs tool execution

Suppose the user asks:

Why was my invoice rejected yesterday?

The skill may recognize:

Need current invoice data

and request:

{
  "tool": "get_invoice",
  "arguments": {
    "invoice_id": "INV-1842"
  }
}

But the LLM should not directly operate against production systems.

The application should:

1. validate arguments
2. check current user's permission
3. resolve tenant/account scope
4. execute API call
5. normalize result
6. return result to skill runtime

This separates probabilistic decision-making from the deterministic execution boundary.

Tool schema as an interface

A tool should have an explicit schema when possible.

For example:

{
  "name": "get_invoice",
  "description": "Read one invoice visible to the current authenticated customer.",
  "parameters": {
    "type": "object",
    "properties": {
      "invoice_id": {
        "type": "string"
      }
    },
    "required": ["invoice_id"]
  }
}

The schema helps the model create valid arguments, but the application still needs validation.

schema-valid arguments
        ≠
authorized operation

An invoice_id can be a valid string while referring to another customer's invoice.

Skill-level tool dependency

A skill contract may say:

requires:
- invoice_reader

and the runtime can map it to:

invoice_reader
     ↓
Billing API adapter

or in another environment:

invoice_reader
     ↓
Test fixture adapter

This is better coupling than making the skill depend on concrete REST endpoints.

Skill depends on capability
Runtime depends on implementation

Read-only and mutating tools

There is a major security difference between them.

Read-only

get_invoice
read_file
query_metrics
get_deployment
search_documentation

Side-effect risk is lower, but authorization and data isolation are still required.

Mutating

send_email
create_issue
restart_service
merge_pull_request
refund_payment
delete_resource

These produce real state changes.

The runtime should understand this as explicit metadata.

Tool
├── risk: read-only | mutating
├── approval: required | not-required
├── idempotent: true | false
└── authorization_scope

Example: GitHub Issue Creation Skill

The user asks:

Create a GitHub issue from this bug report.

The skill can produce:

{
  "title": "Retry path may duplicate payment charge",
  "body": "...",
  "labels": ["bug", "payments"]
}

and then request:

create_issue(...)

The correct runtime flow is:

Skill decides content
      ↓
App validates repository access
      ↓
App may request human confirmation
      ↓
Tool executes create_issue
      ↓
Tool returns issue URL/id

A weak boundary would be:

System prompt:
"Only create issues in repositories the user may access."

followed by blindly executing the model's request.

Authorization must be enforced deterministically from credentials and application policy.

Human approval before mutating actions

Not every write tool requires approval.

For higher-risk actions, however, a useful pattern is:

Plan / proposed action
        ↓
Human approval
        ↓
Execution

For example:

Agent wants to restart production payment-service.

The skill may return:

recommended_action = restart payment-service

while the runtime enforces:

production restart
      ↓
approval required

Approval is not merely a prompt instruction; it is a real execution gate.

Normalize tool output

Raw API output can be huge and noisy.

A GitHub PR API response may contain hundreds of fields while the skill needs only:

{
  "number": 184,
  "title": "Fix retry handling",
  "state": "open",
  "base_branch": "main",
  "head_branch": "retry-fix"
}

A useful architecture is:

External API
    ↓
Adapter / normalization
    ↓
Tool result contract
    ↓
LLM context

This:

  • reduces tokens,
  • reduces coupling,
  • reduces attack surface,
  • creates a more stable tool contract.

Tool error is not the same as model error

The runtime should distinguish failures such as:

TOOL_TIMEOUT
TOOL_UNAVAILABLE
NOT_AUTHORIZED
NOT_FOUND
RATE_LIMITED
INVALID_ARGUMENT
TRANSIENT_FAILURE

The skill can respond differently to each.

NOT_FOUND

Invoice not found.

Do not guess the invoice state.

NOT_AUTHORIZED

User cannot access this repository.

Do not search for an alternative route to bypass the permission.

TRANSIENT_FAILURE

A controlled retry may be appropriate.

Retry and idempotency

Retrying reads is often simple:

query metrics
 ↓
timeout
 ↓
retry

Retrying writes can be dangerous:

create refund
 ↓
timeout
 ↓
retry
 ↓
duplicate refund?

This is why idempotency matters for mutating tools.

For example:

refund_payment(
  payment_id,
  amount,
  idempotency_key
)

The skill does not necessarily implement the idempotency mechanism; that is typically an application/tool boundary responsibility.

Tool results and hallucination

Using a tool does not automatically eliminate hallucination.

For example, a tool returns:

{
  "status": "REJECTED",
  "reason_code": "DUPLICATE_REFERENCE"
}

The model might incorrectly explain:

"The bank rejected it because of insufficient funds."

even though no such evidence exists.

A useful instruction is:

Explain the rejection using only returned reason codes and supplied policy knowledge.
If the code meaning is unknown, say that it is unknown.

Tool = grounded data source.

Tool ≠ guaranteed correct interpretation.

Tool granularity

Very low-level tools can increase agent-runtime complexity.

For example:

open_tcp_connection
send_http_bytes
parse_json

instead of a task-oriented tool such as:

get_customer_invoice

The opposite extreme is an overly powerful tool:

do_everything_in_billing_system(command: string)

This creates a large attack surface.

A good tool typically has:

  • a clear responsibility,
  • typed input,
  • typed output,
  • limited permissions,
  • predictable side effects.

Skill vs tool responsibility

Example: Deployment Health Skill.

Skill responsibility

- interpret user's diagnostic goal
- decide which observations matter
- compare signals
- explain likely issue
- recommend next diagnostic action

Tool responsibility

get_deployment_state
query_error_rate
query_latency
read_recent_events

The tools provide facts.

The skill combines them into meaning and decisions where semantic reasoning is useful.

A tool-backed skill can use multiple tools

For incident diagnosis:

Incident Diagnosis Skill
         │
         ├── get_deployment_state
         ├── query_metrics
         ├── query_logs
         └── read_recent_changes

Execution can be iterative:

read incident
 ↓
query error rate
 ↓
high error rate after deployment
 ↓
read recent deployment
 ↓
query affected endpoint logs
 ↓
produce diagnosis

This approaches the agentic loop topic.

The skill defines the capability we want; the loop defines how execution proceeds across multiple observation/action iterations.

Relationship to MCP

A skill may require a capability such as:

repository_reader

The runtime can provide it through:

native function call
REST adapter
connector
MCP server

So:

Skill = what capability is needed and how it is used
MCP   = one possible protocol/runtime mechanism to expose tools/resources

MCP should not be conflated with the skill concept itself.

Anti-pattern: giving credentials directly to the model

Do not put values such as these into skill context:

AWS_SECRET_ACCESS_KEY=...
GitHub token=...
DB password=...

The model usually does not need the credential itself.

LLM
 ↓
tool request
 ↓
trusted runtime owns credential
 ↓
external system

This is the safer boundary.

Anti-pattern: unrestricted shell tool

A generic tool such as:

shell(command: string)

is extremely powerful.

A deployment-health skill may only need:

get_deployment
get_pods
get_recent_events

Domain-specific tools can provide much better least privilege.

A generic shell may still have a place inside a sandboxed coding agent, but its security model is very different.

Anti-pattern: guessing after a tool error

Weak:

get_balance → timeout
LLM → "Your balance is probably around 500 EUR."

Better:

get_balance → timeout
↓
controlled retry / fallback
↓
if unavailable:
CURRENT_BALANCE_UNAVAILABLE

Tool failure should become explicit system state.

Takeaways

  • A tool-backed skill uses external data or actions as part of a reusable capability.
  • The LLM may request a tool call; the application controls actual execution.
  • Tool argument schemas do not replace authorization and business validation.
  • Treat read-only and mutating tools as different risk categories.
  • High-risk write actions may require human approval as a real execution gate.
  • Normalize and minimize tool results before placing them into context.
  • Consider idempotency before retrying, especially for writes.
  • Tool results provide grounding, but model interpretation can still be wrong.
  • Skills should depend on capabilities; the runtime maps them to concrete provider/tool implementations.
  • MCP is one tool-exposure mechanism; it is not the skill itself.