AI Templates
| Feature Name | AI Prompt Templates |
| Feature ID | CrestApps.OrchardCore.AI.Prompting |
Overview
The AI Templates module provides a centralized system for managing AI system prompts. Instead of scattering hardcoded prompt strings across your codebase, you define prompts as reusable .md files with metadata and Liquid template support.
This module is built on top of the standalone CrestApps.AI.Prompting library, which can be used in any .NET project — not just Orchard Core.
Key Benefits
- Centralized Management — All prompts live in dedicated
AITemplates/Prompts/directories, easy to find and maintain. - Liquid Templates — Use Liquid syntax (via Fluid) for dynamic prompt generation with variables, conditionals, and loops.
- Metadata — Add title, description, category, and custom properties to each prompt via front matter.
- JSON Compaction — Fenced
```json ```blocks are automatically compacted during parsing to reduce token usage while keeping source files readable. - Caching — Parsed templates are cached in memory and invalidated when the tenant shell is released or the application restarts.
- Composition — Merge multiple prompts together, use the
AITemplateBuilderfor efficient assembly, or compose templates with shared scope using therender_ai_templatetag. - Feature-Aware — In Orchard Core, prompts are automatically tied to module features and only available when those features are enabled.
- Extensible Parsers — Markdown front matter parsing ships by default; add YAML, JSON, or other formats by implementing
IAITemplateParser. - Extensible — Register prompts via code, files, or custom providers.
Defining Prompt Templates
File-Based Prompts
Create a .md file in the AITemplates/Prompts/ directory of any module or project:
MyModule/
└── AITemplates/
└── Prompts/
├── my-prompt.md # Module-level prompt
└── MyModule.FeatureId/
└── feature-prompt.md # Feature-specific prompt
The filename (without extension) becomes the prompt ID. For example, my-prompt.md registers a prompt with ID my-prompt.
Front Matter Metadata
Add YAML-style front matter at the top of the file to provide metadata. A blank line after the closing --- is recommended for readability:
---
Title: Generate Chart Configuration
Description: Instructs AI to produce Chart.js JSON configuration
IsListable: false
Category: Data Visualization
CustomKey: CustomValue
---
You are a data visualization expert. Generate a Chart.js configuration...
Supported Metadata Fields
| Field | Type | Default | Description |
|---|---|---|---|
Title | string | Derived from filename | Display title for UI |
Description | string | null | Description shown in prompt selection UI |
IsListable | bool | true | Whether this prompt appears in selection dropdowns |
Category | string | null | Category for grouping prompts in the UI |
Parameters | list | [] | Describes expected template arguments (see below) |
Any additional Key: Value pairs are stored in AdditionalProperties for custom use.
Parameter Descriptors
Use the Parameters metadata field to document the arguments a template expects. Each entry uses tab-indented - name: description format:
---
Title: Document Availability Instructions
Description: Instructs the AI about uploaded documents and available tools.
Parameters:
- tools: array of AIToolDefinitionEntry objects for document processing.
- knowledgeBaseDocuments: array of profile-level document objects used as hidden background knowledge.
- userSuppliedDocuments: array of user/session document objects with DocumentId, FileName, ContentType, and FileSize.
IsListable: false
Category: Documents
---
{% if tools.size > 0 %}
Available document tools:
{% for tool in tools %}
- {{ tool.Name }}: {{ tool.Description }}
{% endfor %}
{% endif %}
Parameter descriptors are informational — they appear in the UI when a user selects a template, helping them understand what arguments are available. The template still renders even if no arguments are supplied (missing variables render as empty strings).
JSON Compaction
Fenced ```json ``` code blocks in template files are automatically compacted during parsing. This lets you write readable, pretty-printed JSON in your source files while keeping the actual system prompt token-efficient at runtime:
Source file:
[Output Format]
```json
{
"type": "bar",
"data": {
"labels": ["Jan", "Feb", "Mar"]
}
}
```
Parsed output:
[Output Format]
```json
{"type":"bar","data":{"labels":["Jan","Feb","Mar"]}}
```
Non-JSON fenced blocks and invalid JSON are left unchanged.
Tip: Feel free to format JSON with indentation and line breaks in your template files for readability — the parser automatically optimizes it into compact form when the templates are loaded, reducing token usage without sacrificing source clarity.
Code-Based Registration
Register prompts programmatically via AITemplateOptions:
services.Configure<AITemplateOptions>(options =>
{
options.Templates.Add(new AITemplate
{
Id = "my-code-prompt",
Content = "You are a helpful {{ role }} assistant.",
Metadata = new AITemplateMetadata
{
Title = "Code-Registered Prompt",
Description = "Registered via C# code. Parameters - role (string): the assistant role.",
Category = "General",
},
});
});
Embedded Resource Registration
For class libraries (non-module projects), embed prompt .md files as assembly resources and register them:
<!-- In your .csproj -->
<ItemGroup>
<EmbeddedResource Include="AITemplates\Prompts\*.md" />
</ItemGroup>
// In your service registration
services.AddAITemplatesFromAssembly(typeof(MyService).Assembly);
Using Liquid Templates
Prompt bodies support full Liquid syntax. Use camelCase for all template variable names:
---
Title: Task Planning
---
You are a planning assistant.
{% if userTools.size > 0 %}
## Available User Tools
{{ userTools }}
{% endif %}
{% if systemTools.size > 0 %}
## System Tools
{{ systemTools }}
{% endif %}
Including Other Templates
Use the render_ai_template tag to render another template while sharing the current Liquid scope:
{% render_ai_template "template-id" %}
The sub-template is rendered in a child scope that inherits all variables from the calling template. This makes it ideal for template composition where the included template needs access to parent variables.
Example — Reusing an agent listing template:
{% assign agents = tools | where: "Source", "Agent" %}
{% if agents.size > 0 %}
{% render_ai_template "agent-availability" %}
{% endif %}
In this example, the agent-availability template can access the agents variable defined in the parent template.
Key features:
- Variables from the parent scope are inherited by the sub-template.
- Variables defined in the sub-template do not leak back to the parent.
- A recursion guard prevents infinite nesting (max depth: 10).
Dynamic template IDs:
The template ID can be a variable:
{% assign templateName = "my-custom-template" %}
{% render_ai_template templateName %}
The render_ai_template tag is available in the standalone CrestApps.AI.Prompting library and can be used in any .NET project — not just Orchard Core.
Using the Service
IAITemplateService
The main service interface for working with prompts:
public interface IAITemplateService
{
Task<IReadOnlyList<AITemplate>> ListAsync();
Task<AITemplate> GetAsync(string id);
// Throws KeyNotFoundException if the template ID is not found.
Task<string> RenderAsync(string id, IDictionary<string, object> arguments = null);
Task<string> MergeAsync(IEnumerable<string> ids, IDictionary<string, object> arguments = null, string separator = "\n\n");
}
RenderAsync throws a KeyNotFoundException if the specified template ID does not exist. This ensures that missing templates are caught early during development rather than silently returning null. Always make sure your template files are properly registered before calling RenderAsync.
Examples
// Inject the service
public class MyService
{
private readonly IAITemplateService _templateService;
public MyService(IAITemplateService templateService)
{
_templateService = templateService;
}
public async Task DoWorkAsync()
{
// Render a simple prompt
var prompt = await _templateService.RenderAsync("my-prompt");
// Render with arguments
var dynamicPrompt = await _templateService.RenderAsync("task-planning", new Dictionary<string, object>
{
["userTools"] = "search, calculator",
["systemTools"] = "file-reader",
});
// Merge multiple prompts
var combined = await _templateService.MergeAsync(
["use-markdown-syntax", "my-prompt"],
separator: "\n\n");
// List all available prompts
var allPrompts = await _templateService.ListAsync();
}
}
AITemplateBuilder
For composing prompts from multiple sources (raw strings, AITemplate objects, and template IDs), use the AITemplateBuilder. It uses pooled buffers to minimize allocations:
// Build from raw strings and AITemplate objects (synchronous)
var result = new AITemplateBuilder()
.WithSeparator("\n\n")
.Append("You are a helpful assistant.")
.Append(someAITemplate)
.Append("Always respond in English.")
.Build();
// Build with template ID resolution (asynchronous, requires IAITemplateService)
var result = await new AITemplateBuilder()
.WithSeparator("\n\n")
.Append("Base instructions.")
.AppendTemplate("rag-response-guidelines")
.AppendTemplate("use-markdown-syntax")
.BuildAsync(templateService);
Extending Parsers
The library ships with a Markdown front matter parser (DefaultMarkdownAITemplateParser) which handles .md files. To add support for other formats (e.g., YAML, JSON), implement IAITemplateParser:
public class YamlAITemplateParser : IAITemplateParser
{
public IReadOnlyList<string> SupportedExtensions => [".yaml", ".yml"];
public AITemplateParseResult Parse(string content) { /* ... */ }
}
Register your parser in DI:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IAITemplateParser, YamlAITemplateParser>());
Providers (FileSystemAITemplateProvider, EmbeddedResourceAITemplateProvider, ModuleAITemplateProvider) automatically select the correct parser based on file extension.
Orchard Core Integration
When the CrestApps.OrchardCore.AI.Prompting feature is enabled:
- Module Discovery — Prompts in
AITemplates/Prompts/directories of all Orchard Core modules are automatically discovered. - Feature Filtering — Prompts placed in
AITemplates/Prompts/{featureId}/subdirectories are only available when that feature is enabled. - Caching — Templates are parsed once and cached in memory. The cache is invalidated when the tenant shell is released or the application restarts.
- Runtime Template Consumption — When a prompt template is selected on an AI Profile or Chat Interaction, the template is rendered at runtime during orchestration. The rendered output replaces the custom system message.
- UI Integration — A prompt selection dropdown is added to AI Profile and Chat Interaction editors:
- Prompts are grouped by category in the dropdown using
<optgroup>elements. - Selecting a template shows its description and lists any parameter descriptors.
- When a template is selected, the system message textarea is hidden since the template provides the system message.
- A Template Parameters field accepts JSON key-value pairs for passing arguments to Liquid templates.
- Choosing Custom Instructions (the default) allows free-form system message input.
- Prompts are grouped by category in the dropdown using
Using in Non-OrchardCore Projects
The standalone CrestApps.AI.Prompting library can be used in any .NET project:
// In your Program.cs or Startup.cs
services.AddAIPrompting();
// Optionally configure discovery paths
services.Configure<AITemplateOptions>(options =>
{
options.DiscoveryPaths.Add(Path.Combine(AppContext.BaseDirectory));
});
This registers:
IAITemplateParser— Parses front matter metadata (markdown by default; extensible). JSON blocks inside fenced code blocks are automatically compacted.IAITemplateEngine— Processes Liquid templates (rendering and validation)IAITemplateService— Main service for listing, getting, and rendering promptsIAITemplateProvider— File system and options-based providers
Validation
The CI pipeline validates all prompt template files on every pull request:
- Checks for valid front matter structure (matching
---delimiters) - Verifies files are not empty
- Validates fenced
```json ```blocks — If a fenced JSON block contains invalid JSON and doesn't appear to be a schema description (e.g., containingtrue | falseor<placeholder>patterns), the CI fails. This catches accidental JSON typos that could cause the AI model to produce malformed output. - Validates
Parameters:entries — Each parameter entry must use the- name: descriptionformat with a colon separator. Malformed entries fail the CI.
To validate locally:
var engine = serviceProvider.GetRequiredService<IAITemplateEngine>();
bool isValid = engine.TryValidate(templateBody, out var errors);