diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs
index 8e9b5f6faea..aed703f0887 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Program.cs
@@ -7,7 +7,12 @@
// 3. Read resources — reference files read via read_skill_resource tool
// 4. Run scripts — scripts executed via run_skill_script tool with a subprocess executor
//
-// This sample uses a unit-converter skill that converts between miles, kilometers, pounds, and kilograms.
+// This sample uses two skills:
+// - unit-converter: converts between miles, kilometers, pounds, and kilograms.
+// - temperature-converter: marked with 'advertise: false' in its frontmatter, so it is NOT listed
+// in the system prompt. It remains loadable by name via the load_skill tool, which the agent's
+// own instructions point it to. Use this to keep the system prompt small or to reserve skills
+// for explicit invocation.
using Azure.AI.Projects;
using Azure.Identity;
@@ -35,7 +40,9 @@
ChatOptions = new()
{
ModelId = deploymentName,
- Instructions = "You are a helpful assistant that can convert units.",
+ // The temperature-converter skill is not advertised (advertise: false), so the model
+ // only knows about it because these instructions reference it by name.
+ Instructions = "You are a helpful assistant that can convert units. For temperature conversions, load the 'temperature-converter' skill.",
},
AIContextProviders = [skillsProvider],
})
@@ -58,3 +65,14 @@
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
Console.WriteLine($"Agent: {response.Text}");
+
+// --- Example: Unadvertised skill ---
+// The temperature-converter skill is excluded from the system prompt's skill listing
+// (advertise: false), but the agent can still load it because its instructions mention it.
+Console.WriteLine();
+Console.WriteLine("Converting temperatures with an unadvertised skill");
+Console.WriteLine(new string('-', 60));
+
+response = await agent.RunAsync("What is 100 degrees Fahrenheit in Celsius?");
+
+Console.WriteLine($"Agent: {response.Text}");
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md
index 0c5b7f8416a..25dff26cd20 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/README.md
@@ -8,6 +8,7 @@ This sample demonstrates how to use **file-based Agent Skills** with a `ChatClie
- The progressive disclosure pattern: advertise → load → read resources → run scripts
- Using the `AgentSkillsProvider` constructor with a skill directory path and script runner
- Running file-based scripts (Python) via a subprocess-based executor
+- Opting a skill out of the system prompt listing with `advertise: false` in its frontmatter
## Skills Included
@@ -18,6 +19,24 @@ Converts between common units (miles↔km, pounds↔kg) using a multiplication f
- `references/conversion-table.md` — Conversion factor table
- `scripts/convert.py` — Python script that performs the conversion
+### temperature-converter
+
+Converts between Celsius and Fahrenheit. Its frontmatter sets `advertise: false`, so its name and
+description are **not** auto-listed in the system prompt:
+
+```yaml
+---
+name: temperature-converter
+description: Convert between Celsius and Fahrenheit temperatures.
+advertise: false
+---
+```
+
+The skill can still be loaded by name via the `load_skill` tool — in this sample the agent's
+instructions reference it explicitly. Use this to keep the system prompt small, or for skills that
+should only be used when the developer (or another skill) points the model at them. Omitting
+`advertise` defaults to `true`.
+
## Running the Sample
### Prerequisites
@@ -48,4 +67,8 @@ Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **75 kg → 165.35 lbs**
+
+Converting temperatures with an unadvertised skill
+------------------------------------------------------------
+Agent: 100°F is approximately **37.78°C**.
```
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/skills/temperature-converter/SKILL.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/skills/temperature-converter/SKILL.md
new file mode 100644
index 00000000000..1459cd802ab
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/skills/temperature-converter/SKILL.md
@@ -0,0 +1,12 @@
+---
+name: temperature-converter
+description: Convert between Celsius and Fahrenheit temperatures.
+advertise: false
+---
+
+## Usage
+
+When the user requests a temperature conversion:
+1. Celsius to Fahrenheit: multiply by 9/5, then add 32
+2. Fahrenheit to Celsius: subtract 32, then multiply by 5/9
+3. Present the converted value clearly with both units
diff --git a/dotnet/samples/02-agents/AgentSkills/README.md b/dotnet/samples/02-agents/AgentSkills/README.md
index 13232c5ab28..96bff539523 100644
--- a/dotnet/samples/02-agents/AgentSkills/README.md
+++ b/dotnet/samples/02-agents/AgentSkills/README.md
@@ -4,7 +4,7 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w
| Sample | Description |
|--------|-------------|
-| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
+| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill, plus an unadvertised (`advertise: false`) temperature-converter skill. |
| [Agent_Step02_CodeDefinedSkills](Agent_Step02_CodeDefinedSkills/) | Define skills entirely in C# code using `AgentInlineSkill`, with static/dynamic resources and scripts. |
| [Agent_Step03_ClassBasedSkills](Agent_Step03_ClassBasedSkills/) | Define skills as C# classes using `AgentClassSkill`. |
| [Agent_Step04_MixedSkills](Agent_Step04_MixedSkills/) | **(Advanced)** Combine file-based, code-defined, and class-based skills using `AgentSkillsProviderBuilder`. |
@@ -25,6 +25,15 @@ Samples demonstrating Agent Skills capabilities. Each sample shows a different w
| Sharing pattern | Copy skill directory | Inline or shared instances | Package in shared assemblies/NuGet |
| DI support | No | Yes (via `IServiceProvider` parameter) | Yes (via `IServiceProvider` parameter) |
+### Controlling skill advertisement
+
+By default, every skill's name and description are listed in the system prompt so the model can
+discover it. Set `advertise: false` in a skill's frontmatter (or `Advertise = false` on
+`AgentSkillFrontmatter` for code-defined skills) to exclude it from that listing while keeping it
+loadable by name via the `load_skill` tool. This is useful for skills that should only be used when
+referenced explicitly — e.g. from the agent's instructions or from another skill. To remove a skill
+entirely (tools included), use skill filtering instead (see below).
+
### `AgentSkillsProvider` vs `AgentSkillsProviderBuilder`
For single-source scenarios, use the `AgentSkillsProvider` constructors directly — they accept a skill directory path, a set of skills, or a custom source.
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillFrontmatter.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillFrontmatter.cs
index 136d4d6833a..0ce9f3e1950 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillFrontmatter.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillFrontmatter.cs
@@ -109,6 +109,18 @@ public string? Compatibility
///
public string? AllowedTools { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the skill's name and description are automatically
+ /// advertised in the system prompt. Defaults to .
+ ///
+ ///
+ /// When , the skill is omitted from the generated skills listing but remains
+ /// fully functional: it can still be loaded by name via the skill tools. Use this for skills that
+ /// should only be used when referenced explicitly (for example, from the agent's own instructions).
+ /// Corresponds to the advertise frontmatter key in SKILL.md files.
+ ///
+ public bool Advertise { get; set; } = true;
+
///
/// Gets or sets the arbitrary key-value metadata for this skill.
///
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
index 32750f1b8d9..8b60f8b417a 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
@@ -318,7 +318,11 @@ private AIFunction WrapWithApprovalIfRequired(AIFunction function, bool requireA
string promptTemplate = this._options?.SkillsInstructionPrompt ?? DefaultSkillsInstructionPrompt;
var sb = new StringBuilder();
- foreach (var skill in skills.OrderBy(s => s.Frontmatter.Name, StringComparer.Ordinal))
+
+ // Skills with Advertise = false are omitted from the listing but remain loadable by name via the skill tools.
+ foreach (var skill in skills
+ .Where(s => s.Frontmatter.Advertise)
+ .OrderBy(s => s.Frontmatter.Name, StringComparer.Ordinal))
{
sb.AppendLine(" ");
sb.AppendLine($" {SecurityElement.Escape(skill.Frontmatter.Name)}");
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs
index ae1714f237d..bcd5e941b57 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillsSource.cs
@@ -221,6 +221,7 @@ private bool TryParseFrontmatter(string content, string skillFilePath, [NotNullW
string? license = null;
string? compatibility = null;
string? allowedTools = null;
+ string? advertise = null;
foreach (Match kvMatch in s_yamlKeyValueRegex.Matches(yamlContent))
{
@@ -249,6 +250,10 @@ private bool TryParseFrontmatter(string content, string skillFilePath, [NotNullW
{
allowedTools = value;
}
+ else if (string.Equals(key, "advertise", StringComparison.OrdinalIgnoreCase))
+ {
+ advertise = value;
+ }
}
// Parse metadata block (indented key-value pairs under "metadata:").
@@ -274,6 +279,8 @@ private bool TryParseFrontmatter(string content, string skillFilePath, [NotNullW
{
License = license,
AllowedTools = allowedTools,
+ // Anything other than an explicit boolean "false" keeps the default of true.
+ Advertise = !bool.TryParse(advertise, out bool advertiseValue) || advertiseValue,
Metadata = metadata,
};
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs
index ed79909f9fc..33793f85b15 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs
@@ -93,6 +93,33 @@ public async Task InvokingCoreAsync_NullInputInstructions_SetsInstructionsAsync(
Assert.Contains("null-instr-skill", result.Instructions);
}
+ [Fact]
+ public async Task InvokingCoreAsync_NonAdvertisedSkill_ExcludedFromInstructionsButLoadableAsync()
+ {
+ // Arrange
+ this.CreateSkill("visible-skill", "Visible skill description", "Visible body.");
+ string hiddenSkillDir = Path.Combine(this._testRoot, "unadvertised-skill");
+ Directory.CreateDirectory(hiddenSkillDir);
+ await File.WriteAllTextAsync(
+ Path.Combine(hiddenSkillDir, "SKILL.md"),
+ "---\nname: unadvertised-skill\ndescription: Unadvertised skill description\nadvertise: false\n---\nUnadvertised body.");
+ var provider = new AgentSkillsProvider(new AgentFileSkillsSource(this._testRoot, s_noOpExecutor));
+ var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
+
+ // Act
+ var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
+
+ // Assert — the non-advertised skill is not listed in the instructions
+ Assert.NotNull(result.Instructions);
+ Assert.Contains("visible-skill", result.Instructions);
+ Assert.DoesNotContain("unadvertised-skill", result.Instructions);
+
+ // But it can still be loaded by name via the load_skill tool
+ var loadSkillTool = result.Tools!.First(t => t.Name == "load_skill") as AIFunction;
+ var content = await loadSkillTool!.InvokeAsync(new AIFunctionArguments(new Dictionary { ["skillName"] = "unadvertised-skill" }));
+ Assert.Contains("Unadvertised body.", content!.ToString());
+ }
+
[Fact]
public async Task InvokingCoreAsync_CustomPromptTemplate_UsesCustomTemplateAsync()
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs
index b185002f67f..fc3cd973aa1 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs
@@ -856,6 +856,42 @@ public async Task GetSkillsAsync_AllowedToolsField_ParsedCorrectlyAsync()
Assert.Equal("grep glob bash", skills[0].Frontmatter.AllowedTools);
}
+ [Fact]
+ public async Task GetSkillsAsync_AdvertiseFalse_ParsedCorrectlyAsync()
+ {
+ // Arrange
+ _ = this.CreateSkillDirectoryWithRawContent(
+ "unadvertised-skill",
+ "---\nname: unadvertised-skill\ndescription: A skill that is not advertised\nadvertise: false\n---\nBody.");
+ var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor);
+
+ // Act
+ var skills = await source.GetSkillsAsync(TestAgentSkillsSourceContextFactory.Create());
+
+ // Assert
+ Assert.Single(skills);
+ Assert.False(skills[0].Frontmatter.Advertise);
+ }
+
+ [Theory]
+ [InlineData("advertise: true")]
+ [InlineData("advertise: not-a-boolean")]
+ public async Task GetSkillsAsync_AdvertiseTrueOrInvalid_DefaultsToTrueAsync(string advertiseLine)
+ {
+ // Arrange
+ _ = this.CreateSkillDirectoryWithRawContent(
+ "advertised-skill",
+ $"---\nname: advertised-skill\ndescription: An advertised skill\n{advertiseLine}\n---\nBody.");
+ var source = new AgentFileSkillsSource(this._testRoot, s_noOpExecutor);
+
+ // Act
+ var skills = await source.GetSkillsAsync(TestAgentSkillsSourceContextFactory.Create());
+
+ // Assert
+ Assert.Single(skills);
+ Assert.True(skills[0].Frontmatter.Advertise);
+ }
+
[Fact]
public async Task GetSkillsAsync_MetadataField_ParsedCorrectlyAsync()
{
@@ -906,6 +942,7 @@ public async Task GetSkillsAsync_AllOptionalFields_ParsedCorrectlyAsync()
"license: Apache-2.0",
"compatibility: Requires Python 3.10+",
"allowed-tools: grep glob view",
+ "advertise: false",
"metadata:",
" org: contoso",
" tier: premium",
@@ -925,13 +962,14 @@ public async Task GetSkillsAsync_AllOptionalFields_ParsedCorrectlyAsync()
Assert.Equal("Apache-2.0", fm.License);
Assert.Equal("Requires Python 3.10+", fm.Compatibility);
Assert.Equal("grep glob view", fm.AllowedTools);
+ Assert.False(fm.Advertise);
Assert.NotNull(fm.Metadata);
Assert.Equal("contoso", fm.Metadata!["org"]?.ToString());
Assert.Equal("premium", fm.Metadata!["tier"]?.ToString());
}
[Fact]
- public async Task GetSkillsAsync_NoOptionalFields_DefaultsToNullAsync()
+ public async Task GetSkillsAsync_NoOptionalFields_UsesDefaultValuesAsync()
{
// Arrange
_ = this.CreateSkillDirectory("basic-skill", "A basic skill", "Body.");
@@ -946,6 +984,7 @@ public async Task GetSkillsAsync_NoOptionalFields_DefaultsToNullAsync()
Assert.Null(fm.License);
Assert.Null(fm.Compatibility);
Assert.Null(fm.AllowedTools);
+ Assert.True(fm.Advertise);
Assert.Null(fm.Metadata);
}