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
LocationOrchestration at top levelOrchestration inside a plugin
FlexibilityHigher - easy to modify flowLower - requires plugin changes
EncapsulationLower - flow is visibleHigher - flow is hidden
ReusabilityFunctions reusable individuallyEntire workflow reusable
Use CaseAd-hoc workflows, scriptsProduction workflows, APIs

The Scenario: Content Processing Pipeline

Our content pipeline will:

  1. Analyze the content (word count, reading level)
  2. Transform the content based on analysis (optimize for target audience)
  3. Validate the content meets publishing requirements
  4. 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
VisibilityEntire flow is visible at the top level
FlexibilityEasy to add/remove/reorder steps
DebuggingEach step can be inspected independently
Conditional LogicEasy to add branching based on results
Dynamic WorkflowsFlow can change based on runtime data

When to Use Chaining vs Nesting

Use Chaining when:

  1. Building ad-hoc or experimental workflows
  2. Flow needs to change frequently
  3. Debugging complex pipelines
  4. Adding conditional logic between steps

Use Nesting when:

  1. Creating reusable workflow components
  2. Building production-ready APIs
  3. Encapsulating complex business logic
  4. Hiding implementation details
Share this lesson: