tl;dr
Imagine an AI assistant in your company that can write emails, check expense reports, and run CI pipeline steps — but it has every capability baked into one giant prompt. It’s slow, expensive, brittle, and dangerously able to access systems it shouldn’t. Now imagine the same assistant composed from many small, well-documented, auditable “skills”: a policy-checker skill for expenses, an email-composer skill, a CI-invoker skill. Each skill advertises a compact summary the agent sees up-front and provides richer instructions or safe code only when needed. That’s the difference agent skills make: modularity, safety, and maintainability.

In this post you’ll get:
- A practical definition of agent skills and why they matter.
- The SKILL.md packaging convention and how agents discover and use skills.
- Runnable C# patterns for semantic (prompt) functions and native (C#) functions that agents can call.
- A small retrieval-augmented (RAG) example (embeddings + local vector store).
- Concrete operational guidance: progressive disclosure, security, testing, telemetry and CI.
What are agent skills?
A skill is a small, focused package that gives an AI agent a specific capability. A skill typically contains:
- Metadata (name, summary, inputs/outputs, examples).
- A description of what the skill is for
- One or more prompt templates (semantic functions).
- Optional native code (API wrappers, scripts) that the agent can call.
- Optional supporting assets (examples, test cases).
Why skills matter
- Modularity & reuse: Package common workflows once and share them across agents and projects.
- Progressive disclosure: Agents initially see only compact summaries (low-cost), fetching full prompts or code only when needed (reduced context bloat).
- Interoperability: Emerging conventions (SKILL.md / skill folders and plugin metadata) let skills be discovered and loaded by different frameworks.
- Testability & governance: Skills can be versioned, tested, audited, and signed independently from orchestration code.
SKILL.md: the portable skill package
A commonly adopted pattern is a small folder that contains a single SKILL.md file (YAML or structured text) that describes the skill plus optional prompt, code and example files. This single file is how registries, marketplaces and agent orchestrators discover and reason about a skill.
A minimal skill folder:
- DocumentSummarizer/
- SKILL.md
- prompts/
- summarize.txt
- examples/
- example1.txt
Example SKILL.md (YAML-like):
name: Document Summarizer
summary: Summarize documents into a single short paragraph.
description: |
The Document Summarizer skill produces a concise, factual paragraph that preserves
key facts and avoids hallucination. Use it for internal reports and documentation.
inputs:
- name: input
type: string
description: The text to summarize.
outputs: - name: summary
type: string
description: One-paragraph summary.
prompts: - file: prompts/summarize.txt
- prompts/summarize.txt:
- Summarize the text below in one short paragraph, focusing on main factual points. {{input}}
Two broad kinds of functions inside skills
- Semantic (prompt) functions: natural language templates packaged as callable functions. These send text to the LLM and return a string output.
- Native (plugin) functions: compiled code (C#, Python, HTTP endpoints) that expose controlled side-effecting operations (call an API, query a DB, create a ticket).
How agents use skills (discovery → execution)
- Discovery: Agent finds available skill folders (local, registry, or marketplace).
- Visibility: Agent sees compact metadata (name, summary, I/O).
- Planning: Agent decides if a given skill is relevant to the user request.
- Progressive disclosure: Agent loads the full prompt or code only on demand.
- Execution: Agent calls a semantic function (LLM) or a native function (HTTP or SDK call).
- Audit: Invocation logged and reviewed.
C# approach: two patterns
- Manual skill loader (framework-agnostic, runnable): show how to load SKILL.md, create prompt functions and call an LLM endpoint directly from C#. This is portable and doesn’t require a specific SDK.
- SDK-based integration (Microsoft Agent Framework): map skills to a Kernel and register C# methods as callable functions. I’ll explain the SDK approach conceptually and how to adapt the manual loader to it.
Runnable sample: a small console app that demonstrates semantic and native skills
This sample is framework-agnostic and uses HTTP calls to a standard LLM API (OpenAI-style). Files provided:
- AgentSkillsSample.csproj
- Program.cs
- SkillLoader.cs
- Models/SkillDefinition.cs
- Skills/DocumentSummarizer/SKILL.md
- Skills/DocumentSummarizer/prompts/summarize.txt
- Skills/FinanceSkill.cs (native skill)
Project file: AgentSkillsSample.csproj
Exe net7.0 latest
Notes: YamlDotNet reads SKILL.md, Serilog logs audit events, Newtonsoft.Json serializes HTTP payloads. You can substitute other libs (System.Text.Json) if you prefer.
Note: This code was generated by Copilot and has not been fully tested. Caveat Programmer!
using System.Collections.Generic;
public class SkillDefinition
{
public string Name { get; set; }
public string Summary { get; set; }
public string Description { get; set; }
public List<SkillIO> Inputs { get; set; } = new();
public List<SkillIO> Outputs { get; set; } = new();
public List<PromptFile> Prompts { get; set; } = new();
}
public class SkillIO
{
public string Name { get; set; }
public string Type { get; set; }
public string Description { get; set; }
}
public class PromptFile
{
public string File { get; set; }
}
Skill loader: SkillLoader.cs
using System;
using System.IO;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
public static class SkillLoader
{
// Loads SKILL.md (YAML) and returns SkillDefinition; progressive disclosure: never load prompt files here
public static SkillDefinition LoadSkillMetadata(string skillFolder)
{
var path = Path.Combine(skillFolder, "SKILL.md");
if (!File.Exists(path)) throw new FileNotFoundException("SKILL.md not found", path);
var yaml = File.ReadAllText(path);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var def = deserializer.Deserialize<SkillDefinition>(yaml);
return def;
}
// Load a prompt file only when invoked
public static string LoadPrompt(string skillFolder, string promptFile)
{
var path = Path.Combine(skillFolder, promptFile);
return File.Exists(path) ? File.ReadAllText(path) : throw new FileNotFoundException(promptFile);
}
}
using System;
[AttributeUsage(AttributeTargets.Method)]
public class SkillFunctionAttribute : Attribute
{
public string Name { get; set; }
public SkillFunctionAttribute(string name = null) { Name = name; }
}
using System.Threading.Tasks;
public class FinanceSkill
{
[SkillFunction("GetExchangeRate")]
public Task GetExchangeRateAsync(string currency)
{
// Replace with real API call; this is a stub for demo
return Task.FromResult(currency.ToUpper() switch
{
"EUR" => "1 USD = 0.92 EUR",
"JPY" => "1 USD = 150 JPY",
_ => "1 USD = 1 UNKNOWN"
});
}
}
Native skill attribute and example: Skills/SkillFunctionAttribute.cs and Skills/FinanceSkill.cs
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Serilog;
class Program
{
static readonly string OPENAI_API_KEY = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
static readonly HttpClient http = new HttpClient();
static async Task Main()
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
if (string.IsNullOrEmpty(OPENAI_API_KEY))
{
Console.WriteLine("Set OPENAI_API_KEY environment variable.");
return;
}
// 1. Discover skills (metadata only)
var skillFolder = Path.Combine(Directory.GetCurrentDirectory(), "Skills", "DocumentSummarizer");
var meta = SkillLoader.LoadSkillMetadata(skillFolder);
Console.WriteLine($"Discovered skill: {meta.Name} - {meta.Summary}");
// 2. Example: call semantic (prompt) function (progressive disclosure: load prompt on demand)
var promptTemplate = SkillLoader.LoadPrompt(skillFolder, meta.Prompts.First().File);
var inputText = File.ReadAllText(Path.Combine(skillFolder, "examples", "example1.txt"));
var prompt = promptTemplate.Replace("{{input}}", inputText);
Console.WriteLine("Calling LLM for summary...");
var summary = await CallChatCompletionAsync(prompt);
Console.WriteLine("Summary:\n" + summary);
Log.Information("SkillInvocation: {@skill} {@inputLength}", meta.Name, inputText.Length);
// 3. Example: call native skill directly via reflection
var finance = new FinanceSkill();
var method = typeof(FinanceSkill).GetMethods()
.FirstOrDefault(m => m.GetCustomAttributes(false).Any(a => a.GetType().Name == "SkillFunctionAttribute"));
if (method != null)
{
var resultTask = (Task<string>)method.Invoke(finance, new object[] { "EUR" });
var rate = await resultTask;
Console.WriteLine("Exchange rate (from native skill): " + rate);
Log.Information("NativeSkillInvocation: {@skill}", "Finance:GetExchangeRate");
}
}
// Minimal OpenAI Chat Completion HTTP call (simple completion flow). Adjust model as desired.
static async Task<string> CallChatCompletionAsync(string prompt)
{
var request = new
{
model = "gpt-4o-mini",
messages = new[] { new { role = "user", content = prompt } },
max_tokens = 400
};
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions");
req.Headers.Add("Authorization", $"Bearer {OPENAI_API_KEY}");
req.Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var resp = await http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
dynamic data = JsonConvert.DeserializeObject<dynamic>(json);
// Extract the assistant content safely
var text = (string)data.choices[0].message.content;
return text.Trim();
}
How this demonstrates key patterns
- Progressive disclosure: SKILL.md metadata is loaded up-front; prompt files only when the agent invokes a skill.
- Semantic function: the prompt template is a first-class asset loaded and passed to the LLM.
- Native function: a C# method annotated with SkillFunctionAttribute is discoverable and callable by the orchestrator (the agent’s planner) via reflection.
RAG + embeddings: a minimal in-memory example
Real RAG pipelines use a vector DB (Pinecone, Weaviate, FAISS) and an embedding endpoint. For a compact demo we’ll:
- Call an embedding endpoint for documents and queries.
- Store vectors in memory.
- Compute cosine similarity for top-k retrieval.
- Pass retrieved texts into a semantic skill.
Embedding call (OpenAI-style)
static async Task GetEmbeddingAsync(string text)
{
var request = new { model = "text-embedding-3-small", input = text };
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/embeddings");
req.Headers.Add("Authorization", $"Bearer {OPENAI_API_KEY}");
req.Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var resp = await http.SendAsync(req);
resp.EnsureSuccessStatusCode();
dynamic data = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync());
var vector = data.data[0].embedding.ToObject();
return vector;
}
Simple in-memory vector store (pseudo code)
- Store: List<(string id, string text, float[] vector)>
- Query: compute cosine similarities and return top-k texts.
RAG flow (high level)
- Index documents: embed and store vectors.
- On user query: embed query, retrieve top-k docs.
- Build a RAG context: join top-k texts with separators (truncation/careful length management).
- Pass context to the semantic skill prompt and call LLM.
Operational concerns: security, governance, and robustness
Skills are powerful and potentially dangerous. Here are concrete controls and practices.
1) Least privilege for native skills
- Native skills that perform sensitive actions (modify database, call billing APIs) must run under scoped credentials.
- Don’t give skills a “root” service principal; instead issue short-lived tokens scoped to exact operations.
- Example pattern: the orchestrator authenticates the agent using an identity token, then performs an OAuth token-exchange per invocation to obtain least-privilege credentials for the skill runtime.
2) Runtime isolation & sandboxing
- Execute native plugin code in a sandbox or separate process with tight ACLs.
- Use containerization (small docker container per plugin) or platform sandboxing (AppDomains are not sufficient) and restrict network access.
3) Audit logging
- Log: skill name, invocation timestamp, actor (user/session id), input hashes, output hash, status (success/fail), and duration. Redact secrets.
- Example Serilog event:
{
“Timestamp”:”2026-09-10T12:00:00Z”,
“Event”:”SkillInvocation”,
“Skill”:”DocumentSummarizer”,
“User”:”user-123″,
“InputHash”:”sha256:abcd…”,
“OutputHash”:”sha256:ef01…”,
“DurationMs”:312,
“Result”:”Success”
} - Retention: keep logs for the retention period required by compliance (e.g., 90 days, 1 year). Make them queryable.
4) Approval & registry
- Keep a registry of approved skills. New skills must pass code review, static analysis, and security review.
- Require signatures or checksums when installing third-party skills.
5) Limit function-calling from LLM
- When you advertise native functions to the LLM, provide explicit allow lists. Only expose the signatures the model truly needs.
- Consider requiring explicit operator approval for risky operations (e.g., “Do you want to run job X? Click approve.”).
Testing & CI for skills
Treat skills like code: add unit tests, integration tests and CI gates.
1) Unit tests
- Native functions: test logic with xUnit/MSTest, mocking dependencies.
- Semantic functions: unit test prompt templates by calling a local/mock LLM that returns deterministic output. Mock the HTTP client (HttpMessageHandler) so you can assert prompt content and expected result parsing.
Example xUnit test for FinanceSkill
[Fact]
public async Task GetExchangeRate_ReturnsExpected()
{
var svc = new FinanceSkill();
var res = await svc.GetExchangeRateAsync(“EUR”);
Assert.Contains(“EUR”, res);
}
2) Integration tests
- A test that runs the orchestration path: index a small set of docs, run RAG + semantic skill, assert returned summary contains a known fact.
- Use a staging LLM key with usage limits to avoid cost.
3) CI pipeline (GitHub Actions example)
name: CI
on: [push, pull_request]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
– run: dotnet restore
– run: dotnet build –configuration Release –no-restore
– run: dotnet test –no-build –verbosity normal
Add a step for SKILL.md schema validation (use a small script that parses SKILL.md using YamlDotNet and fails CI for missing fields).
Observability & telemetry
- Track metrics: per-skill invocation count, error rate, latency and model tokens consumed.
- Export traces: instrument the orchestrator to emit distributed traces (OpenTelemetry) for each skill invocation, so you can correlate LLM calls with native actions in traces.
- Use Serilog + sink to Application Insights or to a log platform.
Resilience patterns
- Retries and backoff for failing native calls (use exponential backoff with jitter).
- Circuit breakers for repeatedly failing or slow skills.
- Request batching for embedding calls (batch inputs to embedding endpoints to reduce cost).
Advanced patterns and mapping to SDKs
- Microsoft Agent Framework: these SDKs give built-in primitives that map to this model:
- Import prompt files as “semantic functions” and native C# methods as plugin functions using attributes like [KernelFunction]/[SKFunction] so the model can call them directly.
- The SDK advertises function signatures to the LLM (function-calling) and can automatically route decisions.
- SKILL.md mapping: when loading a skill folder you can:
- Parse SKILL.md to enumerate functions and prompts.
- For each prompt, call Kernel.CreateSemanticFunction or your equivalent helper to register it.
- For native code, reflect over types with attributes and register them as callable functions.
Common failure modes and mitigations
1) Hallucinations / incorrect output
- Mitigation: RAG (ground responses in retrieved docs), explicit instructions in prompts, ask agent to state uncertainty.
2) Incorrect skill selection - Mitigation: improve skill summaries and include disambiguating examples; add a planner policy that prefers small, well-defined skills.
3) Malicious or buggy skill - Mitigation: vet skills, require signatures, run native skills in sandbox and under restricted credentials.
4) Escalation loops between skills - Mitigation: set depth limits on planner calls, track call stack, and abort when loops detected.
5) Cost runaway (too many model calls) - Mitigation: enforce quotas, use cheaper models for routine tasks, cache results.
When to use semantic vs native skills
- Semantic skill (prompt) when: the task is pure language transformation or reasoning (summaries, analysis, paraphrase).
- Native skill when: you need to call external systems, perform deterministic computations, or run actions that must be auditable and auditable (DB writes, sending emails, creating tickets).
Packaging and governance checklist
- SKILL.md with schema-validated fields (name, summary, inputs, outputs, author, version).
- Example inputs and expected outputs (test vectors).
- Unit tests that run in CI.
- Security review & signing (PGP or similar).
- Runtime policy: allowed to run? who can install/update?
- Observability hooks: logging, metrics, tracing.
Key takeaways
- Agent skills let you compose agents from small, testable, auditable capabilities that are discoverable and loaded on demand.
- Use SKILL.md-like packaging, progressive disclosure, and clear input/output contracts.
- For C# developers, you can implement skills yourself (load SKILL.md, create prompt functions, expose native C# methods via reflection or attributes) or use SDKs like Semantic Kernel for structured integration.
- Secure native code, audit invocations, add tests and CI, instrument telemetry, and enforce governance.

