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.