Semantic Kernel
Plugins
Created: 03 Feb 2026
Updated: 03 Feb 2026
Chaining Kernel Functions in Semantic Kernel
Function chaining in Semantic Kernel involves executing functions in a specific sequence, where the output of one function becomes the input of the next. Unlike function nesting (where the flow is encapsulated in a parent plugin), chaining keeps the orchestration logic at the top level, providing more flexibility and visibility.
In this article, we'll build a Content Processing Pipeline that demonstrates function chaining through a practical content management scenario.
Chaining vs Nesting
| Aspect - Chaining - Nesting | ||
| Location | Orchestration at top level | Orchestration inside a plugin |
| Flexibility | Higher - easy to modify flow | Lower - requires plugin changes |
| Encapsulation | Lower - flow is visible | Higher - flow is hidden |
| Reusability | Functions reusable individually | Entire workflow reusable |
| Use Case | Ad-hoc workflows, scripts | Production workflows, APIs |
The Scenario: Content Processing Pipeline
Our content pipeline will:
- Analyze the content (word count, reading level)
- Transform the content based on analysis (optimize for target audience)
- Validate the content meets publishing requirements
- Publish the final content
[Raw Content]
|
v
+------------------+
| ContentAnalyzer | --> Analysis Result (word count, reading level)
+------------------+
|
v
+------------------+
| TextTransformer | --> Transformed Content (based on analysis)
+------------------+
|
v
+------------------+
| ContentValidator | --> Validation Result (pass/fail)
+------------------+
|
v
+------------------+
| ContentPublisher | --> Published Content (with metadata)
+------------------+
Implementation
Step 1: Content Analyzer Plugin
public class ContentAnalyzerPlugin
{
[KernelFunction("analyze_content")]
[Description("Analyzes content and returns metrics")]
public AnalysisResult AnalyzeContent(string content)
{
var words = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
int wordCount = words.Length;
double avgWordLength = words.Average(w => w.Length);
// Simple reading level calculation
string readingLevel = avgWordLength switch
{
< 4 => "Easy",
< 6 => "Medium",
_ => "Advanced"
};
return new AnalysisResult(wordCount, avgWordLength, readingLevel);
}
}
Step 2: Text Transformer Plugin
public class TextTransformerPlugin
{
[KernelFunction("optimize_for_audience")]
[Description("Optimizes content based on target reading level")]
public string OptimizeForAudience(string content, string targetLevel)
{
return targetLevel.ToLower() switch
{
"easy" => SimplifyContent(content),
"advanced" => EnrichContent(content),
_ => content
};
}
private string SimplifyContent(string content)
{
// Simulate simplification
return content
.Replace("utilize", "use")
.Replace("implement", "do")
.Replace("functionality", "feature");
}
private string EnrichContent(string content)
{
// Simulate enrichment
return $"[Enhanced] {content}";
}
}
Step 3: Content Validator Plugin
public class ContentValidatorPlugin
{
[KernelFunction("validate_content")]
[Description("Validates content meets publishing requirements")]
public ValidationResult ValidateContent(string content, int minWords, int maxWords)
{
var wordCount = content.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
var errors = new List<string>();
if (wordCount < minWords)
errors.Add($"Content too short: {wordCount} words (minimum: {minWords})");
if (wordCount > maxWords)
errors.Add($"Content too long: {wordCount} words (maximum: {maxWords})");
if (string.IsNullOrWhiteSpace(content))
errors.Add("Content cannot be empty");
return new ValidationResult(errors.Count == 0, wordCount, errors);
}
}
Step 4: Content Publisher Plugin
public class ContentPublisherPlugin
{
[KernelFunction("publish_content")]
[Description("Publishes content with metadata")]
public PublishResult PublishContent(string content, string author, string category)
{
var publishId = Guid.NewGuid().ToString("N")[..8];
var publishedAt = DateTime.UtcNow;
return new PublishResult(publishId, content, author, category, publishedAt);
}
}
Step 5: Chaining the Functions
// Register all plugins
kernel.ImportPluginFromType<ContentAnalyzerPlugin>();
kernel.ImportPluginFromType<TextTransformerPlugin>();
kernel.ImportPluginFromType<ContentValidatorPlugin>();
kernel.ImportPluginFromType<ContentPublisherPlugin>();
var rawContent = "We need to utilize advanced functionality to implement this feature.";
// CHAIN STEP 1: Analyze content
var analysis = await kernel.InvokeAsync<AnalysisResult>(
nameof(ContentAnalyzerPlugin), "analyze_content",
new() { ["content"] = rawContent });
Console.WriteLine($"Analysis: {analysis.WordCount} words, Level: {analysis.ReadingLevel}");
// CHAIN STEP 2: Transform based on analysis (chaining output to input)
var targetLevel = analysis.ReadingLevel == "Advanced" ? "easy" : analysis.ReadingLevel;
var transformed = await kernel.InvokeAsync<string>(
nameof(TextTransformerPlugin), "optimize_for_audience",
new() { ["content"] = rawContent, ["targetLevel"] = targetLevel });
// CHAIN STEP 3: Validate transformed content
var validation = await kernel.InvokeAsync<ValidationResult>(
nameof(ContentValidatorPlugin), "validate_content",
new() { ["content"] = transformed, ["minWords"] = 5, ["maxWords"] = 100 });
// CHAIN STEP 4: Publish if valid
if (validation.IsValid)
{
var published = await kernel.InvokeAsync<PublishResult>(
nameof(ContentPublisherPlugin), "publish_content",
new() { ["content"] = transformed, ["author"] = "System", ["category"] = "Tech" });
Console.WriteLine($"Published: {published.PublishId}");
}
Key Differences from Nesting
In Nesting (previous article):
// Single call - flow is hidden inside ProcessOrder
var result = await kernel.InvokeAsync<string>(
nameof(ShoppingAssistantPlugin), "process_order", arguments);
In Chaining (this article):
// Multiple calls - flow is visible and controllable
var analysis = await kernel.InvokeAsync<AnalysisResult>(...);
var transformed = await kernel.InvokeAsync<string>(...); // Uses analysis result
var validation = await kernel.InvokeAsync<ValidationResult>(...); // Uses transformed
var published = await kernel.InvokeAsync<PublishResult>(...); // Uses validation
Benefits of Function Chaining
| Benefit - Description | |
| Visibility | Entire flow is visible at the top level |
| Flexibility | Easy to add/remove/reorder steps |
| Debugging | Each step can be inspected independently |
| Conditional Logic | Easy to add branching based on results |
| Dynamic Workflows | Flow can change based on runtime data |
When to Use Chaining vs Nesting
Use Chaining when:
- Building ad-hoc or experimental workflows
- Flow needs to change frequently
- Debugging complex pipelines
- Adding conditional logic between steps
Use Nesting when:
- Creating reusable workflow components
- Building production-ready APIs
- Encapsulating complex business logic
- Hiding implementation details