Skip to main content
Version: Latest
Technical ManualLooking for step-by-step instructions? In the User Manual: Recipes and Shared Resources

CrestApps Recipes

Feature NameCrestApps Recipes
Feature IDCrestApps.OrchardCore.Recipes

Provides recipe steps for all modules.

In the admin​

Under Tools > Recipes, the feature adds a description and tags to every recipe, and a search box, so an administrator can read what a recipe does before running it. Under Tools > Deployments > JSON Import, the Recipe JSON editor uses the step schemas to suggest step names and settings as you type and to underline mistakes before anything is imported. To export the setup of a site as a recipe, build a deployment plan under Tools > Deployments > Plans and execute it to a file.

Overview​

CrestApps Recipes adds JSON-Schema support for Orchard Core recipes.

It exposes strongly-typed, per-step JSON schema definitions (one schema per recipe step) that match how Orchard Core actually parses and executes each step (i.e., the classes inheriting from NamedRecipeStepHandler).

This is especially useful when recipes are generated programmatically (e.g., by AI agents) because it enables:

  • Validation before execution: catch invalid or incomplete recipe payloads early.
  • Better AI guidance: provide the model with an explicit contract for each step.
  • Safer automation: reduce trial-and-error imports and unpredictable runtime failures.

At runtime the feature can compose all known step schemas into a single "recipe" schema for an object with a steps array.

The repository's CrestApps.OrchardCore.RecipeSchemaExporter utility now instantiates both parameterless and service-backed IRecipeStep implementations, including steps that depend on registered option snapshots such as AIDeployment and AIProviderConnections, so exported reference schemas stay aligned with the runtime schema providers.

AI profile creation from templates​

The AI module contributes a CreateAIProfileFromTemplate recipe step for creating or updating profiles from reusable AI templates whose source is Profile.

{
"name": "CreateAIProfileFromTemplate",
"Profiles": [
{
"TemplateId": "customer-support",
"Name": "customer-support-prod",
"DisplayText": "Customer Support",
"ChatDeploymentName": "gpt-4o"
}
]
}

Behavior:

  • TemplateId must point to an AI template whose source is Profile
  • the step loads that profile template first and uses it to generate the starting AIProfile
  • any fields you include in the recipe object then override the generated profile values
  • any fields you do not include keep the values copied from the selected profile template
  • if TemplateId points to a missing template, or to a template whose source is not Profile, the step adds a recipe error for that item and skips creating the profile

This means the step behaves like: create profile from profile template, then apply explicit recipe overrides.

The schema for this step intentionally mirrors the regular AIProfile step for profile fields such as Name, DisplayText, Description, Type, PromptTemplate, PromptSubject, OrchestratorName, ChatDeploymentName, UtilityDeploymentName, CreatedUtc, OwnerId, Author, Properties, and Settings, while adding the required TemplateId selector that must resolve to a Profile template.

For both AIProfile and CreateAIProfileFromTemplate, the Properties and Settings objects now enumerate the known nested objects while still allowing extra keys for feature-specific extensions. Known Properties entries include metadata such as AIProfileMetadata, FunctionInvocationMetadata, AgentInvocationMetadata, PromptTemplateMetadata, AnalyticsMetadata, DataSourceMetadata, AIDataSourceRagMetadata, DocumentsMetadata, MemoryMetadata, ClaudeSessionMetadata, CopilotSessionMetadata, AIProfileMcpMetadata, and AIProfileA2AMetadata. Known Settings entries include AIProfileSettings, ResponseHandlerProfileSettings, AIChatProfileSettings, AIProfileDataExtractionSettings, AIProfilePostSessionSettings, and ChatModeProfileSettings.

The AIProfileTemplate recipe step now extends that catalog with template-specific ProfileTemplateMetadata and SystemPromptTemplateMetadata objects, and it also documents the settings-style envelopes that templates persist in Properties. That makes profile templates expose the same shared AI metadata as profiles while still surfacing the template-only objects that the runtime understands.

When you import AIProfileTemplate items, each template entry should include:

  • Name: the unique template name
  • DisplayText: the human-readable title shown in Orchard
  • Source: the template-source identifier, such as Profile or SystemPrompt

If ItemId is omitted, the import step resolves an existing template by the combined Name and Source values before deciding whether to create a new record. That matches the runtime requirement that new template imports provide both the template name and source.

The Recipes feature now also exports dedicated step schemas for AIDataSource, McpConnection, McpPrompt, McpResource, and A2AConnection. Their Properties objects document the known built-in metadata envelopes such as SseMcpConnectionMetadata, StdioMcpConnectionMetadata, FtpConnectionMetadata, SftpConnectionMetadata, and A2AConnectionMetadata, while still allowing extra keys for future extensions.

With Data Connectors enabled, the feature also exports the DataConnection and ConnectedDataSet step schemas: connections with their connector settings and clear-text Secrets, and data sets with their fields, access mode and refresh settings. See Data Connectors.

With AI Personas enabled, the feature also exports the AIPersonas step schema and the schema of the AI Personas deployment step (AIPersonaDeploymentStep), and the AIProfile step schema describes the top-level PersonaId and the AIPersonaMetadata property. See AI Personas.

For AIDataSource, the schema also derives the Source enum from the currently registered AIDataSourceSourceOptions entries, so recipe tooling can suggest only the source types that are actually enabled for the tenant. Recipe imports honor that same Source value and still default to SearchIndexProfile when older payloads omit it.

More broadly, the exported recipe-step schemas now attach descriptions to the known properties across the built-in CrestApps recipe steps so human authors and AI tools can discover what each property is for directly from the schema surface.

Contact Center, Omnichannel and phone system schemas​

The feature also exports dedicated schemas for the Contact Center, Omnichannel and phone system configuration steps, each gated on the feature that provides the step so tooling only suggests steps the tenant can actually import. Each schema describes every member the matching deployment step exports, including the Properties extension data an entry carries:

  • Contact Center (CrestApps.OrchardCore.ContactCenter): ContactCenterVoiceMedia, and the ContactCenterExternalTransferSettings site settings.
  • Contact Center queues (CrestApps.OrchardCore.ContactCenter.Queues): ContactCenterSkill, ContactCenterQueueGroup, and ContactCenterQueue.
  • Contact Center business hours (CrestApps.OrchardCore.ContactCenter.BusinessHours): ContactCenterBusinessHoursCalendar.
  • Contact Center agent entitlements (CrestApps.OrchardCore.ContactCenter.AgentEntitlements): ContactCenterAgentEntitlement.
  • Contact Center agents (CrestApps.OrchardCore.ContactCenter.Agents): AgentStateReasonCode.
  • Contact Center inbound voice (CrestApps.OrchardCore.ContactCenter.InboundVoice): ContactCenterEntryPoint.
  • Contact Center dialer (CrestApps.OrchardCore.ContactCenter.Dialer): ContactCenterDialerProfile.
  • Contact Center recording and secure capture: the ContactCenterRecordingSettings and SecureCaptureSettings site settings.
  • Omnichannel activities (CrestApps.OrchardCore.Omnichannel.Activities): OmnichannelCampaignGroup, OmnichannelCampaign, OmnichannelChannelEndpoint, OmnichannelDisposition, OmnichannelCadence, and OmnichannelSubjectAction.
  • Omnichannel messaging (CrestApps.OrchardCore.Omnichannel.Messaging): OmnichannelMessageTemplate.
  • Telephony (CrestApps.OrchardCore.Telephony): TelephonyExtension, and the TelephonySettings site settings; the soft phone widget feature adds the SoftPhoneWidgetSettings site settings.
  • Telnyx (CrestApps.OrchardCore.Telnyx, CrestApps.OrchardCore.Telnyx.Sms): TelnyxSettings and TelnyxSmsSettings, plus site settings schemas of the same names.
  • Asterisk (CrestApps.OrchardCore.Asterisk): AsteriskSettings, plus a site settings schema of the same name.

The provider settings steps take secrets in clear text and protect them on import, never export them, and keep the stored secret when a recipe omits it. Their site settings schemas describe the generic Settings step instead, which stores a value exactly as given, so a secret there must already be protected by the destination tenant.

The Contact Center workflows bridge (available automatically when OrchardCore.Workflows is enabled alongside Contact Center) additionally contributes workflow-activity schemas for the ContactCenterEvent event and the EnqueueActivityTask, ScheduleCallbackTask, SetAgentPresenceTask, StartCallRecordingTask, and StopCallRecordingTask tasks, so the WorkflowType step can describe their properties and outcomes.

Orchard Core 4.0 step schemas​

The schemas describe the Orchard Core 4.0 recipe steps, settings, parts, fields and workflow activities as the Orchard Core step handlers read them:

  • New steps: ReloadTenant, Secrets (with the OrchardCore.Secrets feature), DataPipelines (with OrchardCore.DataPipelines) and CreateOrUpdateRateLimitPolicies (with OrchardCore.RateLimits), plus the schemas of the Secrets, Data Pipelines and All Rate Limit Policies deployment steps.
  • Secrets in settings: the site settings that hold a credential (SMTP, Azure Email, Azure SMS, Twilio, Azure AI Search, OpenID Connect client, Facebook, GitHub, Google, Microsoft Account and Twitter) describe the ...SecretName property that references a secret of the Secrets module. A secret reference takes precedence over the protected value, which the generic Settings step stores exactly as given, so a clear value there does not work. With OrchardCore.OpenId.Secrets, the OpenIdSecretSettings site settings reference the signing and encryption keys of the OpenID Connect server.
  • Settings: AllowPageSizeSelection and PageSizeOptions at the root of the Settings step, where BaseUrl and CdnBaseUrl must be absolute URLs (or empty to clear them); AdminSettings adds DisplayQuickNavigation, ShowBreadcrumb, MenuBehavior, ListLayout, ActionsLayout and AllowUserListLayoutSelection; LoginSettings adds UsePersistentAuthenticationCookie; HttpsSettings uses StrictTransportSecurityMode; OpenIdServerSettings adds RequireEndSessionConfirmation.
  • Parts and fields: RenderLiquid is gone from the HTML and Markdown body parts and fields, TextFieldSettings adds MinLength and MaxLength, and the LiquidPart and LiquidField schemas describe the only places that execute Liquid.
  • Workflows: the WorkflowType step describes Variables, IsActivity, IsSingletonPerCorrelation, BranchingMode, FaultOnScriptErrors and RecordActivityData. Every expression accepts a Syntax (Literal, Liquid or JavaScript), which the control-flow and value activities honor; every activity accepts OutputBindings, and every task an ActivityRetryPolicy. The ExecuteWorkflowTask, SetVariableTask, StartedByWorkflowEvent, UserLoggedOutEvent, RunDataPipelineTask, DataPipelineRunCompletedEvent and MetaConversionsApiEventTask activities have schemas.

Creating a Recipe Step​

To define a recipe step, implement the IRecipeStep interface and register your implementation as a service.

Here's an example of a recipe step for a Settings step. This step requires a JSON object with a "name" property set to "settings" and can compose contributed site settings definitions.

internal sealed class SettingsSchemaStep : IRecipeStep
{
private readonly IEnumerable<ISiteSettingsSchemaDefinition> _schemaDefinitions;
private JsonSchema _schema;

public SettingsSchemaStep(IEnumerable<ISiteSettingsSchemaDefinition> schemaDefinitions)
{
_schemaDefinitions = schemaDefinitions;
}

public string Name => "Settings";

public ValueTask<JsonSchema> GetSchemaAsync(CancellationToken cancellationToken = default)
{
if (_schema != null)
{
return new ValueTask<JsonSchema>(_schema);
}

var builder = new JsonSchemaBuilder();
builder
.Type(SchemaValueType.Object)
.Properties(
("name", new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.Const("settings")
),
("HomeRoute", new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("Area", new JsonSchemaBuilder().Type(SchemaValueType.String)),
("Controller", new JsonSchemaBuilder().Type(SchemaValueType.String)),
("Action", new JsonSchemaBuilder().Type(SchemaValueType.String))
)
.AdditionalProperties(true)
)
)
.Required("name")
.AdditionalProperties(true);

_schema = builder.Build();

return new ValueTask<JsonSchema>(_schema);
}
}

Feature-specific site settings can contribute additional schema definitions by registering ISiteSettingsSchemaDefinition from feature-gated startups, which keeps the Settings step aligned with the actual SiteDisplayDriver<TSettings> registrations enabled for the tenant.

Registering Your Recipe Step​

Register your implementation as a scoped service in your Orchard Core module or startup:

services.AddScoped<IRecipeStep, SettingsSchemaStep>();

Using Recipe Steps​

  • Retrieve available recipe steps: Use the RecipeSchemaService to get a list of all registered recipe step names.
  • Validate and execute recipes: Use the RecipeExecutionService alongside the step schemas to validate a recipe payload before importing.

Extending content definition schemas​

IContentSchemaDefinition is the shared extension point that contributes schema fragments to the ContentDefinition and ReplaceContentDefinition recipe steps.

Use it when your feature adds a custom content part or content field and you want recipe validation, editor tooling, and AI-generated recipes to understand that feature's Settings object.

Use PartSchemaDefinitionBase when the schema belongs to a content part. It already marks the definition as ContentDefinitionSchemaType.Part, caches the part settings schema, and lets the implementation focus on the part-specific fragments.

Use FieldSchemaDefinitionBase when the schema belongs to a content field. It marks the definition as ContentDefinitionSchemaType.Field, caches the field settings schema, and lets the implementation describe the field-specific fragments.

Implement IContentSchemaDefinition directly only when the contribution does not fit either base class.

Why these types exist​

The ContentDefinition and ReplaceContentDefinition recipe steps have two nested settings surfaces:

  • ContentTypePartDefinitionRecords[].Settings for settings that apply when a part is attached to a content type
  • ContentParts[].Settings for the reusable content part definition itself

The Recipes module gathers all registered IContentSchemaDefinition services and merges their schema fragments into those nested Settings objects only when the owning feature is enabled. Definitions are grouped by Name, so if multiple contributors target the same part or field type, their schema fragments are combined into the final schema for that single Orchard name instead of replacing each other. That keeps validation aligned with the actual Orchard Core features available to the tenant.

The separate IContentPartSchemaDefinition and IContentFieldSchemaDefinition interfaces are still used by the content recipe step. They now receive ContentPartSchemaContext and ContentFieldSchemaContext, so payload contributors can inspect the concrete ContentTypePartDefinition and ContentPartFieldDefinition for the exact definition instance being rendered. IContentSchemaDefinition remains the shared contract for content-definition settings schemas.

In this repository, feature-gated contributors now cover custom field payloads such as PhoneField and custom part payloads/settings such as AIProfilePart, RolePickerPart, UserFullNamePart, OmnichannelContactPart, and RolePickerPartContentAccessControlSettings.

If a part only contains content fields, do not add a dedicated part payload contributor just to restate those field shapes. The content-item schema already composes each attached field through its registered field schema definitions, so dedicated part contributors are only needed when the part also exposes non-field C# properties or other part-level payload that fields alone cannot describe.

Implementing a part schema​

This example adds schema support for a fictional ContactCardPart, including both the part settings envelope used by ContentDefinition and the content item payload used by the Content recipe step:

using CrestApps.OrchardCore.Recipes.Core.Schemas;
using CrestApps.OrchardCore.Recipes.Core.Schemas.Parts;
using Json.Schema;

namespace MyModule.Schemas;

public sealed class ContactCardPartSchema : PartSchemaDefinitionBase
{
public override string Name { get; } = "ContactCardPart";

protected override JsonSchemaBuilder BuildSettingsCore()
{
return new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("ContactCardPartSettings", new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("ShowPhoneNumber", new JsonSchemaBuilder().Type(SchemaValueType.Boolean)),
("Layout", new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.Enum("Compact", "Full")
.Default("Compact")))
.AdditionalProperties(false)))
.AdditionalProperties(true);
}

protected override JsonSchemaBuilder BuildPartSchemaCore()
{
return new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("Heading", new JsonSchemaBuilder().Type(SchemaValueType.String)),
("PhoneNumber", new JsonSchemaBuilder().Type(SchemaValueType.String)))
.AdditionalProperties(true);
}
}

Register it from the feature startup so it only contributes schema when the part is actually available:

services.AddScoped<IContentSchemaDefinition, ContactCardPartSchema>();

Defining the reusable part and attaching it to a type​

ContentParts defines the reusable part definition. ContentTypes[].ContentTypePartDefinitionRecords attaches that part to a content type and adds placement or per-type part settings.

{
"name": "ContentDefinition",
"ContentParts": [
{
"Name": "ContactCardPart",
"Settings": {
"ContactCardPartSettings": {
"ShowPhoneNumber": true,
"Layout": "Compact"
}
},
"ContentPartFieldDefinitionRecords": []
}
],
"ContentTypes": [
{
"Name": "PersonPage",
"DisplayName": "Person Page",
"Settings": {
"ContentTypeSettings": {
"Creatable": true,
"Listable": true,
"Draftable": true,
"Versionable": true,
"Securable": false
}
},
"ContentTypePartDefinitionRecords": [
{
"PartName": "ContactCardPart",
"Name": "ContactCardPart",
"Settings": {
"ContentTypePartSettings": {
"Position": "3"
},
"ContactCardPartSettings": {
"ShowPhoneNumber": true,
"Layout": "Full"
}
}
}
]
}
]
}

In that example:

  • ContentParts[].Settings.ContactCardPartSettings describes the reusable part definition
  • ContentTypePartDefinitionRecords[].Settings.ContentTypePartSettings controls Orchard placement metadata for that type attachment
  • ContentTypePartDefinitionRecords[].Settings.ContactCardPartSettings contributes the part-specific settings object validated by ContactCardPartSchema

The same schema definition also shapes the Content recipe step payload for the part itself:

{
"name": "Content",
"data": [
{
"ContentType": "PersonPage",
"DisplayText": "Jane Doe",
"Published": true,
"Latest": true,
"ContactCardPart": {
"Heading": "Primary contact",
"PhoneNumber": "+1-555-0100"
}
}
]
}

In that payload, ContactCardPart.Heading and ContactCardPart.PhoneNumber come from BuildPartSchemaCore(), while ContactCardPartSettings still belongs only to the ContentDefinition and ReplaceContentDefinition recipe steps.

For simple parts, the context can be ignored.

For container-style parts such as BagPart or FlowPart, implement the optional IContainedContentPartSchemaDefinition interface alongside PartSchemaDefinitionBase. That secondary interface lets the contributor declare which payload property contains nested items and which content types are allowed for the current attachment, while ContentItemSchemaService stays responsible for recursive schema composition and cycle protection.

Implementing a field schema​

Use FieldSchemaDefinitionBase when you need both:

  • a field settings envelope under ContentPartFieldDefinitionRecords[].Settings
  • a field value schema for the field payload inside a content item

This example adds schema support for a fictional SocialLinkField:

using CrestApps.OrchardCore.Recipes.Core.Schemas;
using CrestApps.OrchardCore.Recipes.Core.Schemas.Fields;
using Json.Schema;

namespace MyModule.Schemas;

public sealed class SocialLinkFieldSchema : FieldSchemaDefinitionBase
{
public override string Name { get; } = "SocialLinkField";

protected override JsonSchemaBuilder BuildSettingsCore()
{
return new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("SocialLinkFieldSettings", new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("AllowedHost", new JsonSchemaBuilder().Type(SchemaValueType.String)),
("OpenInNewTab", new JsonSchemaBuilder().Type(SchemaValueType.Boolean)))
.AdditionalProperties(false)))
.AdditionalProperties(true);
}

protected override JsonSchemaBuilder BuildFieldSchemaCore()
{
return new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(
("Text", new JsonSchemaBuilder().Type(SchemaValueType.String)),
("Url", new JsonSchemaBuilder().Type(SchemaValueType.String)))
.AdditionalProperties(true);
}
}

Register it the same way:

services.AddScoped<IContentSchemaDefinition, SocialLinkFieldSchema>();

Then the field settings and content payload can be expressed like this:

{
"name": "ContentDefinition",
"ContentParts": [
{
"Name": "ProfilePart",
"ContentPartFieldDefinitionRecords": [
{
"Name": "SupportLink",
"DisplayName": "Support link",
"ContentFieldDefinition": {
"Name": "SocialLinkField"
},
"Settings": {
"SocialLinkFieldSettings": {
"AllowedHost": "example.com",
"OpenInNewTab": true
}
}
}
]
}
]
}
{
"name": "Content",
"data": [
{
"ContentType": "PersonPage",
"ProfilePart": {
"SupportLink": {
"Text": "Contact support",
"Url": "https://example.com/support"
}
}
}
]
}

Extending workflow activity schemas​

The WorkflowType step composes its schema from per-activity contributions, so every workflow event and task can describe its own category, outcomes and properties. See Workflow Activity Schemas for the full contract and for how to describe a custom event or task.

Extending rule condition schemas​

The Layers step composes its LayerRule schema from per-condition and per-operator contributions, so every rule condition and operator can describe its own members. See Rule Condition Schemas for the full contract and for how to describe a custom condition or operator.

Extending sitemap source schemas​

The Sitemaps step composes its schema from per-source contributions, so every sitemap source can describe its own members. See Sitemap Source Schemas for the full contract and for how to describe a custom source.

Extending deployment step schemas​

The deployment step composes its schema from per-step contributions, so every deployment step can describe its own Step payload. See Deployment Step Schemas for the full contract and for how to describe a custom deployment step.

Extending admin menu node schemas​

The AdminMenu step composes its schema from per-node contributions, so every admin menu node can describe its own members. See Admin Menu Node Schemas for the full contract and for how to describe a custom admin menu node.

Extending query source schemas​

The Queries step composes its schema from per-source contributions, so every query source can describe its own members. See Query Source Schemas for the full contract and for how to describe a custom query source.

Extending URL rewrite rule schemas​

The UrlRewriting step composes its schema from per-source contributions, so every rewrite rule source can describe its own members. See URL Rewrite Rule Schemas for the full contract and for how to describe a custom rewrite rule source.

Extending placement node filter schemas​

The Placements step composes its placement node schema from per-filter contributions, so every placement node filter can describe its own value. See Placement Node Filter Schemas for the full contract and for how to describe a custom placement node filter.

How schemas stay accurate​

Orchard Core recipe steps are implemented by classes inheriting from NamedRecipeStepHandler. Each handler converts the incoming JSON into a specific model (for example ContentStepModel) and then processes it.

This module mirrors that contract by ensuring each IRecipeStep schema includes the known properties that Orchard Core expects (including correct property names and types).