Note
This feature is available starting from FactoryTalk Optix 1.7.X and should not be confused with legacy recipe features.
The RecipeX module provides a new way to handle recipes in FactoryTalk Optix applications. Recipes are structured data templates used to store, retrieve, and transfer configuration parameters and data items. The RecipeX system supports:
- Recipe creation, duplication, renaming, and deletion
- Data item management and value manipulation
- Metadata handling with custom fields
- Recipe transfers between store and target locations
- Edit model management for recipe editing workflows
Before reading this chapter, please check the Recipe Schemas Documentation for an introduction to recipe schemas and their configuration in the FactoryTalk Optix HMI project.
Note
If a recipe schema is edited, the Runtime will require a Refactor to update the database table structure accordingly. It is advised to backup your project before making changes to recipe schemas.
Uniquely identifies a recipe using name and version information.
[MapDataType(NamespaceUri = Module.Uri, Number = 10)]
public class RecipeId : TypedStruct
{
public string Name { get; set; } // Recipe name
public string Version { get; set; } // Recipe version
}Usage Example:
var recipeId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1", Version = "1.0" };Represents a complete recipe with metadata and timestamps.
[MapDataType(NamespaceUri = Module.Uri, Number = 14)]
public classRecipe : TypedStruct
{
public FTOptix.RecipeX.RecipeId FTOptix.RecipeX.RecipeId { get; set; } // Recipe identifier
public DateTime RecipeSchemaTimestamp { get; set; } // Schema timestamp
public DateTime CreatedAt { get; set; } // Creation timestamp
public DateTime ModifiedAt { get; set; } // Last modification timestamp
public FTOptix.RecipeX.RecipeMetadata[] MetadataValues { get; set; } // Custom metadata fields
}Represents custom metadata associated with a recipe.
[MapDataType(NamespaceUri = Module.Uri, Number = 12)]
public classRecipeMetadata : TypedStruct
{
public string Name { get; set; } // Metadata field name
public LocalizedText DisplayName { get; set; } // Localized display name
public NodeId DataTypeId { get; set; } // OPC UA data type reference
public object Value { get; set; } // Metadata value
}Describes a data item within a recipe, including path information for accessing nested properties.
[MapDataType(NamespaceUri = Module.Uri, Number = 171)]
public classRecipeDataItem : TypedStruct
{
public string[] ItemRelativeBrowsePath { get; set; } // Path to the recipe item, relative to the schema's target node
public string[] DataItemRelativeBrowsePath { get; set; } // Path to data item, relative to the item node reached by ItemRelativeBrowsePath
public FTOptix.Core.ElementAccessStruct ElementAccess { get; set; } // Array element access (index/range)
public NodeId DataTypeId { get; set; } // Data type identifier
}Defines error handling behavior for recipe operations.
public enum FTOptix.RecipeX.ErrorPolicy : int
{
Strict = 0, // Stop on first error
BestEffort = 1 // Continue despite errors
}An Edit Model is a temporary, session-exclusive working copy of a recipe used to support editing workflows. Key characteristics:
- Session-exclusive: only one session can hold an Edit Model at a time; concurrent access is not allowed. Each session can have a dedicated Edit Model instance if needed.
- Non-destructive: changes made inside the Edit Model are isolated from the persisted recipe until explicitly committed back to the store.
- Queryable: the Edit Model supports filtering and sorting, making it suitable as the data source for recipe-editing UIs (e.g. grids, lists).
- Lifecycle: the Edit Model is created by loading a recipe via
TransferFromStoreToEditModeland is automatically destroyed when released or whenTransferFromEditModelToStorecompletes. TherequesterNodeIdparameter identifies the session owner and is used to enforce the exclusivity constraint. AnEditModelcan also be created programmatically viaCreateEditModelfor use cases that require more direct control over its lifecycle and interactions or retrieved from an existing interactive session where aListViewis being used by the user to interact with a recipe.
The FTOptix.RecipeX.EditModel class exposes the following members. Obtain an instance by resolving the EditModelNodeId returned by CreateEditModel and casting it:
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);Properties:
| Member | Type | Description |
|---|---|---|
RecipeSchemaNodeId |
NodeId |
NodeId of the FTOptix.RecipeX.RecipeSchema that owns this Edit Model |
RequesterNodeId |
NodeId |
NodeId of the session owner; used to enforce exclusivity |
TargetNodeId |
NodeId |
NodeId of the target node the Edit Model was created against |
FTOptix.RecipeX.RecipeId |
FTOptix.RecipeX.RecipeId |
Identifies which recipe is loaded in the Edit Model |
PendingChanges |
bool |
true if the Edit Model contains unsaved changes relative to the store; set to false after a successful commit |
Methods:
| Method | Description |
|---|---|
TransferFromStore(FTOptix.RecipeX.RecipeId) |
Loads a recipe from the store into this Edit Model |
TransferToStore(FTOptix.RecipeX.RecipeId) |
Commits Edit Model changes back to the store |
TransferToTarget(NodeId targetNodeId, FTOptix.RecipeX.ErrorPolicy) |
Applies Edit Model values to a target node |
TransferFromTarget(NodeId targetNodeId, FTOptix.RecipeX.ErrorPolicy) |
Overwrites Edit Model values with the current state of a target node |
Edit Model instances can be programmatically created and/or interacted with as shown in the examples below.
A data item is the fundamental building block of a recipe. It represents a single value that can be stored, retrieved, and transferred as part of a recipe. Data items map to variables or properties in your application model and provide a structured way to capture and apply configurations.
When you define a recipe schema in the FactoryTalk Optix designer, you select variables and properties to include in the recipe. Each selected variable becomes a data item. The recipe system then:
- Captures current values from the target location when saving
- Stores values in the database as part of the recipe
- Restores values to the target location when applying
Data items are hierarchically organized using browse paths - arrays of strings that represent the navigation path through your object model:
ItemRelativeBrowsePath = ["Settings", "Motor"]
DataItemRelativeBrowsePath = ["Speed", "MaxRPM"]Please note: Both paths are relative, never absolute.
ItemRelativeBrowsePathis relative to the target node configured on theFTOptix.RecipeX.RecipeSchema(e.g.TargetNode = Section1).DataItemRelativeBrowsePathis relative to the item node that is reached after navigatingItemRelativeBrowsePathfrom the target node.
This structure allows you to:
- Navigate through nested objects and folders
- Access properties within objects
- Handle array subscripts using
ElementAccess
Visual Example:
Target Object (Settings)
|- Motor (Item)
| |- Speed (DataItem) <- Gets/Sets here
| |- MaxRPM (DataItem) <- Gets/Sets here
|- Conveyor (Item)
|- Enable (DataItem)var itemPath = new string[] { "Temperature" };
var dataItemPath = new string[] { }; // Empty = direct variable
var elementAccess = new FTOptix.Core.ElementAccessStruct();
// Gets/sets "Temperature" variable directly
var result = schema.GetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
elementAccess);var itemPath = new string[] { "MotorSettings" };
var dataItemPath = new string[] { "Speed" };
var elementAccess = new FTOptix.Core.ElementAccessStruct();
// Gets/sets MotorSettings.Speed
var result = schema.GetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
elementAccess);var itemPath = new string[] { "Parameters", "Industrial", "Motor1" };
var dataItemPath = new string[] { "Configuration", "MaxSpeed" };
var elementAccess = new FTOptix.Core.ElementAccessStruct();
// Gets/sets Parameters -> Industrial -> Motor1 -> Configuration -> MaxSpeed
var result = schema.GetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
elementAccess);var itemPath = new string[] { "Sensors" };
var dataItemPath = new string[] { "Readings" };
var elementAccess = new FTOptix.Core.ElementAccessStruct
{
Index = 0 // Get first array element
};
// Gets/sets Sensors.Readings[0]
var result = schema.GetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
elementAccess);[ExportMethod]
public void InspectDataItems(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
// Get the structure of data items in this recipe
var result = schema.GetDataItems(recipeId);
if (result.ResultCode == GetDataItemsResultCode.Success)
{
Log.Info($"Recipe contains {result.DataItems.Length} data items:");
foreach (var dataItem in result.DataItems)
{
// Build readable path
var itemPath = string.Join("/", dataItem.ItemRelativeBrowsePath);
var dataPath = string.Join("/", dataItem.DataItemRelativeBrowsePath);
if (!string.IsNullOrEmpty(dataPath))
{
Log.Info($" {itemPath} -> {dataPath}");
}
else
{
Log.Info($" {itemPath}");
}
}
}
}[ExportMethod]
public object ReadDataItemValue(string recipeName, string[] itemPath,
string[] dataItemPath)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
var result = schema.GetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
new FTOptix.Core.ElementAccessStruct());
if (result.ResultCode == GetRecipeDataItemValueResultCode.Success)
{
return result.DataItemValue;
}
else
{
Log.Error($"Failed to read data item: {result.ResultCode}");
return null;
}
}[ExportMethod]
public void WriteDataItemValue(string recipeName, string[] itemPath,
string[] dataItemPath, object value)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
var result = schema.SetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
new FTOptix.Core.ElementAccessStruct(),
value);
if (result == FTOptix.RecipeX.SetRecipeDataItemValueResultCode.Success)
{
Log.Info("Data item value updated");
}
else
{
Log.Error($"Failed to update data item: {result}");
}
}[ExportMethod]
public bool CompareDataItemWithTarget(string recipeName, string[] itemPath,
string[] dataItemPath, string targetVariablePath)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
var target = LogicObject.Owner.GetVariable(targetVariablePath);
// Get stored recipe value
var recipeResult = schema.GetRecipeDataItemValue(
recipeId,
itemPath,
dataItemPath,
new FTOptix.Core.ElementAccessStruct());
if (recipeResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
return false;
// Get current target value
var targetValue = target?.Value.Value;
// Compare
bool valuesMatch = Equals(recipeResult.DataItemValue, targetValue);
Log.Info($"Recipe value: {recipeResult.DataItemValue}");
Log.Info($"Target value: {targetValue}");
Log.Info($"Match: {valuesMatch}");
return valuesMatch;
}- Path Ordering Matters: The order of path segments must exactly match your object hierarchy
- Empty Data Item Path: If the item itself is the variable, use an empty array for
dataItemRelativeBrowsePath - Type Consistency: Ensure the value type matches the data item's data type
- Array Indices: Use
FTOptix.Core.ElementAccessStructonly for array subscripts; regular object properties go in the data item path - Performance: Reading/writing individual data items has overhead; consider batch operations when possible
Wrong: Using the whole path as item path
// Incorrect
var itemPath = new string[] { "Settings", "Motor", "Speed" };
var dataItemPath = new string[] { };Right: Splitting item and property paths
// Correct
var itemPath = new string[] { "Settings", "Motor" };
var dataItemPath = new string[] { "Speed" };All methods return result types containing both a result code and relevant data. These are used to handle success and error conditions consistently.
Result of retrieving all recipes from a recipe schema.
[MapDataType(NamespaceUri = Module.Uri, Number = 182)]
public class GetRecipesResult : TypedStruct
{
public Recipe[] Recipes { get; set; } // Array of recipes
public GetRecipesResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Recipes retrieved successfullyStoreError = 1- Error accessing the storeGenericError = 2- General error occurred
Result of retrieving data items from a recipe.
[MapDataType(NamespaceUri = Module.Uri, Number = 178)]
public class GetDataItemsResult : TypedStruct
{
public RecipeDataItem[] DataItems { get; set; } // Array of data items
public GetDataItemsResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Data items retrieved successfullyRecipeNotFound = 1- Specified recipe not foundStoreError = 2- Error accessing the storeGenericError = 3- General error occurred
Result of retrieving a specific data item value from a recipe.
[MapDataType(NamespaceUri = Module.Uri, Number = 127)]
public class GetRecipeDataItemValueResult : TypedStruct
{
public object DataItemValue { get; set; } // Retrieved value (scalar or array)
public GetRecipeDataItemValueResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Value retrieved successfullyGenericError = 1- General error occurredStoreError = 2- Error accessing the storeDataItemNotFound = 3- Data item not found in recipeUnexpectedEmptyRecipeName = 4- Recipe name is empty/nullRecipeNotFound = 5- Recipe not found in store
Result of retrieving a single metadata value.
[MapDataType(NamespaceUri = Module.Uri, Number = 134)]
public classGetRecipeMetadataValueResult : TypedStruct
{
public RecipeMetadata MetadataValue { get; set; } // Retrieved metadata
public GetRecipeMetadataValueResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Metadata retrieved successfullyGenericError = 1- General error occurredStoreError = 2- Error accessing the storeUnexpectedEmptyRecipeName = 3- Recipe name is empty/nullRecipeNotFound = 4- Recipe not found in storeMetadataNotFound = 5- Metadata field not found
Result of retrieving all metadata for a recipe.
[MapDataType(NamespaceUri = Module.Uri, Number = 138)]
public class GetRecipeMetadataValuesResult : TypedStruct
{
public RecipeMetadata[] MetadataValues { get; set; } // Array of metadata
public GetRecipeMetadataValuesResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Metadata values retrieved successfullyGenericError = 1- General error occurredStoreError = 2- Error accessing the storeUnexpectedEmptyRecipeName = 3- Recipe name is empty/nullRecipeNotFound = 4- Recipe not found in store
Result of renaming a recipe.
[MapDataType(NamespaceUri = Module.Uri, Number = 115)]
public class RenameRecipeResult : TypedStruct
{
public FTOptix.RecipeX.RecipeId NewRecipeId { get; set; } // New recipe identifier (after rename)
public RenameRecipeResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Recipe renamed successfullyGenericError = 1- General error occurredStoreError = 2- Error accessing the storeUnexpectedEmptyRecipeName = 3- Recipe name is empty/nullSourceRecipeDoesNotExist = 4- Recipe not foundTargetRecipeAlreadyExist = 5- Target recipe name already in use
Result of creating an edit model.
[MapDataType(NamespaceUri = Module.Uri, Number = 142)]
public classCreateEditModelResult : TypedStruct
{
public NodeId EditModelNodeId { get; set; } // NodeId of created edit model
public CreateEditModelResultCode ResultCode { get; set; } // Result code
}Result Codes:
Success = 0- Edit model created successfullyGenericError = 1- General error occurredStoreError = 2- Error accessing the storeParentNodeNotFound = 3- Parent node for edit model not foundRequesterNodeNotFound = 4- Requester node not found
The main class for recipe operations in FTOptix. Inherit from UAObject and provides methods for all recipe management tasks.
[MapType(NamespaceUri = Module.Uri, Number = 1)]
public classRecipeSchema : UAObject
{
// Properties
public NodeId TargetNode { get; set; } // Target node for recipe operations
public NodeId Store { get; set; } // Store node reference
public RecipeSchemaStatus Status { get; } // Current status
public UInt32 RemoteReadTimeout { get; set; } // Remote read timeout in ms
public PlaceholderChildNodeCollection<UAVariable> MetaData { get; } // Metadata variables
// Methods...
}Creates a new empty recipe in the store.
public CreateRecipeResultCode CreateRecipe(FTOptix.RecipeX.RecipeId recipeId)Parameters:
recipeId- Identifier with name and version
Returns: CreateRecipeResultCode
Success- Recipe created successfullyGenericError- General error occurredStoreError- Error accessing the storeUnexpectedEmptyRecipeName- Recipe name is emptyRecipeAlreadyExist- Recipe with same name/version existsTargetNotFound- Target node not found
Example:
var schema = (FTOptix.RecipeX.RecipeSchema)hmiProject.GetVariable("RecipeSchema1").Value;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = "MyRecipe", Version = "1.0" };
var result = schema.CreateRecipe(recipeId);
if (result == CreateRecipeResultCode.Success)
{
Log.Info("Recipe created successfully");
}Deletes a recipe from the store.
public DeleteRecipeResultCode DeleteRecipe(FTOptix.RecipeX.RecipeId recipeId)Parameters:
recipeId- Recipe to delete
Returns: DeleteRecipeResultCode
Success- Recipe deleted successfullyGenericError- General error occurredStoreError- Error accessing the storeUnexpectedEmptyRecipeName- Recipe name is emptyRecipeDoesNotExist- Recipe not found
Example:
var result = schema.DeleteRecipe(recipeId);Creates a copy of an existing recipe with a new identifier.
public DuplicateRecipeResultCode DuplicateRecipe(FTOptix.RecipeX.RecipeId recipeId, FTOptix.RecipeX.RecipeId newRecipeId)Parameters:
recipeId- Source recipe to duplicatenewRecipeId- Identifier for the new recipe
Returns: DuplicateRecipeResultCode
Success- Recipe duplicated successfullyGenericError- General error during duplicationStoreError- Error accessing the storeUnexpectedSourceEmptyRecipeName- Source recipe name is emptyUnexpectedTargetEmptyRecipeName- Target recipe name is emptySourceRecipeDoesNotExist- Source recipe not foundTargetRecipeAlreadyExist- Target recipe name already existsTargetNotFound- Target node not found
Example:
var sourceId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1", Version = "1.0" };
var targetId = new FTOptix.RecipeX.RecipeId { Name = "Recipe1_Copy", Version = "1.0" };
var result = schema.DuplicateRecipe(sourceId, targetId);Renames an existing recipe.
public RenameRecipeResult RenameRecipe(FTOptix.RecipeX.RecipeId recipeId, string newName)Parameters:
recipeId- Recipe to renamenewName- New recipe name
Returns: RenameRecipeResult
NewRecipeId- Updated recipe identifierResultCode- Operation result
Example:
var result = schema.RenameRecipe(recipeId, "NewRecipeName");
if (result.ResultCode == RenameRecipeResultCode.Success)
{
Log.Info($"Recipe renamed to: {result.NewRecipeId.Name}");
}Retrieves all recipes from the schema.
public GetRecipesResult GetRecipes()Returns: GetRecipesResult
Recipes- Array of all recipesResultCode- Operation result
Example:
var result = schema.GetRecipes();
if (result.ResultCode == GetRecipesResultCode.Success)
{
foreach (var recipe in result.Recipes)
{
Log.Info($"Recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version}");
}
}Retrieves the data item structure for a recipe.
public GetDataItemsResult GetDataItems(FTOptix.RecipeX.RecipeId recipeId)Parameters:
recipeId- Recipe identifier
Returns: GetDataItemsResult
DataItems- Array describing data items in the recipeResultCode- Operation result
Example:
var result = schema.GetDataItems(recipeId);
if (result.ResultCode == GetDataItemsResultCode.Success)
{
foreach (var item in result.DataItems)
{
var itemPath = string.Join("/", item.ItemRelativeBrowsePath);
Log.Info($"Data Item: {itemPath}");
}
}Retrieves a specific data item value from a recipe.
public GetRecipeDataItemValueResult GetRecipeDataItemValue(
FTOptix.RecipeX.RecipeId recipeId,
string[] itemRelativeBrowsePath,
string[] dataItemRelativeBrowsePath,
FTOptix.Core.ElementAccessStruct elementAccess)Parameters:
recipeId- Recipe identifieritemRelativeBrowsePath- Browse path to item (array of path segments)dataItemRelativeBrowsePath- Browse path to data item within the itemelementAccess- Array element access for subscripts (if applicable)
Returns: GetRecipeDataItemValueResult
DataItemValue- Retrieved value (scalar or array)ResultCode- Operation result
Example:
var itemPath = new string[] { "Item1" };
var dataItemPath = new string[] { "Property1" };
var elementAccess = new FTOptix.Core.ElementAccessStruct(); // No array index
var result = schema.GetRecipeDataItemValue(recipeId, itemPath, dataItemPath, elementAccess);
if (result.ResultCode == GetRecipeDataItemValueResultCode.Success)
{
Log.Info($"Value: {result.DataItemValue}");
}Retrieves a single metadata value.
public FTOptix.RecipeX.GetRecipeMetadataValueResult GetRecipeMetadataValue(FTOptix.RecipeX.RecipeId recipeId, string metadataName)Parameters:
recipeId- Recipe identifiermetadataName- Name of the metadata field
Returns: FTOptix.RecipeX.GetRecipeMetadataValueResult
MetadataValue- Retrieved metadataResultCode- Operation result
Example:
var result = schema.GetRecipeMetadataValue(recipeId, "Author");
if (result.ResultCode == GetRecipeMetadataValueResultCode.Success)
{
Log.Info($"Author: {result.MetadataValue.Value}");
}Retrieves all metadata for a recipe.
public GetRecipeMetadataValuesResult GetRecipeMetadataValues(FTOptix.RecipeX.RecipeId recipeId)Parameters:
recipeId- Recipe identifier
Returns: GetRecipeMetadataValuesResult
MetadataValues- Array of all metadata fieldsResultCode- Operation result
Example:
var result = schema.GetRecipeMetadataValues(recipeId);
if (result.ResultCode == GetRecipeMetadataValuesResultCode.Success)
{
foreach (var metadata in result.MetadataValues)
{
Log.Info($"{metadata.Name}: {metadata.Value}");
}
}Updates a data item value in a recipe.
public FTOptix.RecipeX.SetRecipeDataItemValueResultCode SetRecipeDataItemValue(
FTOptix.RecipeX.RecipeId recipeId,
string[] itemRelativeBrowsePath,
string[] dataItemRelativeBrowsePath,
FTOptix.Core.ElementAccessStruct elementAccess,
object value)Parameters:
recipeId- Recipe identifieritemRelativeBrowsePath- Browse path to itemdataItemRelativeBrowsePath- Browse path to data itemelementAccess- Array element accessvalue- New value to set
Returns: FTOptix.RecipeX.SetRecipeDataItemValueResultCode
Success- Value set successfullyGenericError- General error occurredStoreError- Error accessing the storeDataItemNotFound- Data item not foundUnexpectedEmptyRecipeName- Recipe name is emptyRecipeNotFound- Recipe not found
Example:
var result = schema.SetRecipeDataItemValue(
recipeId,
new string[] { "Item1" },
new string[] { "Property1" },
new FTOptix.Core.ElementAccessStruct(),
42);Updates a metadata value.
public FTOptix.RecipeX.SetRecipeMetadataValueResultCode SetRecipeMetadataValue(
FTOptix.RecipeX.RecipeId recipeId,
string metadataName,
object value)Parameters:
recipeId- Recipe identifiermetadataName- Metadata field namevalue- New metadata value
Returns: FTOptix.RecipeX.SetRecipeMetadataValueResultCode
Success- Metadata set successfullyGenericError- General error occurredStoreError- Error accessing the storeUnexpectedEmptyRecipeName- Recipe name is emptyRecipeNotFound- Recipe not foundMetadataNotFound- Metadata field not found
Example:
var result = schema.SetRecipeMetadataValue(recipeId, "Author", "John Doe");Transfer methods move recipes between three locations: Store (persistent database), Edit Model (temporary working copy), and Target (active runtime location).
Loads a recipe from the store and applies it to a target location.
public FTOptix.RecipeX.TransferFromStoreToTargetResultCode TransferFromStoreToTarget(
FTOptix.RecipeX.RecipeId recipeId,
NodeId targetNodeId,
FTOptix.RecipeX.ErrorPolicy errorPolicy)Parameters:
recipeId- Recipe to transfertargetNodeId- Destination nodeerrorPolicy- Error handling mode
Returns: FTOptix.RecipeX.TransferFromStoreToTargetResultCode
Success- Transfer successfulGenericError- General error occurredStoreError- Error accessing the storeUnexpectedEmptyRecipeName- Recipe name is emptyRecipeNotFound- Recipe not foundTargetNotFound- Target node not foundDataMismatch- Data structure mismatch
Example:
var targetNode = hmiProject.GetVariable("TargetObject").Value;
var result = schema.TransferFromStoreToTarget(recipeId, targetNode, FTOptix.RecipeX.ErrorPolicy.Strict);Captures the current state of a target and saves it as a recipe.
public FTOptix.RecipeX.TransferFromTargetToStoreResultCode TransferFromTargetToStore(
FTOptix.RecipeX.RecipeId recipeId,
NodeId targetNodeId,
bool overwrite,
FTOptix.RecipeX.ErrorPolicy errorPolicy)Parameters:
recipeId- Recipe identifier to create/updatetargetNodeId- Source location to captureoverwrite- Whether to overwrite existing recipeerrorPolicy- Error handling mode
Returns: FTOptix.RecipeX.TransferFromTargetToStoreResultCode
SuccessRecipeCreated- New recipe createdSuccessRecipeUpdated- Existing recipe updatedGenericError- General errorStoreError- Store access errorUnexpectedEmptyRecipeName- Recipe name is emptyRecipeAlreadyExists- Recipe exists and overwrite is falseTargetNotFound- Target node not foundDataMismatch- Data structure mismatch
Example:
var result = schema.TransferFromTargetToStore(
recipeId,
targetNode,
overwrite: true,
FTOptix.RecipeX.ErrorPolicy.BestEffort);Loads a recipe from the store into the session's Edit Model.
public FTOptix.RecipeX.TransferFromStoreToEditModelResultCode TransferFromStoreToEditModel(
NodeId requesterNodeId,
FTOptix.RecipeX.RecipeId recipeId)Parameters:
requesterNodeId- Node requesting the operationrecipeId- Recipe to load
Returns: FTOptix.RecipeX.TransferFromStoreToEditModelResultCode
Success- Transfer successfulGenericError- General errorStoreError- Store access errorRequesterNodeNotFound- Requester node not foundUnexpectedEmptyRecipeName- Recipe name is emptyRecipeNotFound- Recipe not foundEditModelNodeNotFound- Edit model not found
Example:
var requester = hmiProject.GetVariable("LogicObject").Value;
var result = schema.TransferFromStoreToEditModel(requester, recipeId);Saves the edit model changes back to the store.
public FTOptix.RecipeX.TransferFromEditModelToStoreResultCode TransferFromEditModelToStore(
NodeId requesterNodeId,
FTOptix.RecipeX.RecipeId recipeId)Parameters:
requesterNodeId- Node requesting the operationrecipeId- Recipe identifier (for new recipes)
Returns: FTOptix.RecipeX.TransferFromEditModelToStoreResultCode
SuccessRecipeCreated- New recipe createdSuccessRecipeUpdated- Existing recipe updatedGenericError- General errorStoreError- Store access errorRequesterNodeNotFound- Requester node not foundUnexpectedEmptyRecipeName- Recipe name is emptyTargetNotFound- Target not foundEditModelNodeNotFound- Edit model not found
Example:
var result = schema.TransferFromEditModelToStore(requester, recipeId);Applies edit model changes to a target location.
public FTOptix.RecipeX.TransferFromEditModelToTargetResultCode TransferFromEditModelToTarget(
NodeId requesterNodeId,
NodeId targetNodeId,
FTOptix.RecipeX.ErrorPolicy errorPolicy)Parameters:
requesterNodeId- Node requesting the operationtargetNodeId- Destination nodeerrorPolicy- Error handling mode
Returns: FTOptix.RecipeX.TransferFromEditModelToTargetResultCode
Success- Transfer successfulGenericError- General errorStoreError- Store access errorRequesterNodeNotFound- Requester node not foundTargetNotFound- Target node not foundEditModelNodeNotFound- Edit model not foundDataMismatch- Data structure mismatch
Loads target state into edit model for editing.
public FTOptix.RecipeX.TransferFromTargetToEditModelResultCode TransferFromTargetToEditModel(
NodeId requesterNodeId,
NodeId targetNodeId,
FTOptix.RecipeX.ErrorPolicy errorPolicy)Parameters:
requesterNodeId- Node requesting the operationtargetNodeId- Source locationerrorPolicy- Error handling mode
Returns: FTOptix.RecipeX.TransferFromTargetToEditModelResultCode
Success- Transfer successfulGenericError- General errorStoreError- Store access errorRequesterNodeNotFound- Requester node not foundTargetNotFound- Target node not foundEditModelNodeNotFound- Edit model not foundDataMismatch- Data structure mismatch
Creates a working copy of a recipe for editing.
public FTOptix.RecipeX.CreateEditModelResult CreateEditModel(
NodeId parentNodeId,
NodeId requesterNodeId,
NodeId targetNodeId,
FTOptix.RecipeX.RecipeId recipeId)Parameters:
parentNodeId- Parent node for the edit modelrequesterNodeId- Node requesting the operationtargetNodeId- Target structure referencerecipeId- Recipe to edit
Returns: FTOptix.RecipeX.CreateEditModelResult
EditModelNodeId- NodeId of created edit modelResultCode- Operation result
Tip
Once you have the EditModelNodeId, you can resolve it to the actual FTOptix.RecipeX.EditModel node and cast it to FTOptix.RecipeX.EditModel. The cast exposes additional members not available through FTOptix.RecipeX.RecipeSchema, including:
PendingChanges— indicates whether the edit model has unsaved changes relative to the recipe in the store.- Transfer methods directly on the
FTOptix.RecipeX.EditModelinstance (e.g. applying to target, committing to store) that operate implicitly on this edit model without having to pass the requester/node again.
Note
CreateEditModel behaves as get-or-create: if an edit model already exists for the given parentNodeId/requesterNodeId pair, it returns the existing instance instead of creating a new one. This is the correct way to access an edit model that was created and is managed by a ListView widget — pass the ListView.NodeId as both parentNodeId and requesterNodeId, and you will get back the live edit model the ListView is currently using.
Example:
var parent = hmiProject.GetVariable("Parent").Value;
var requester = hmiProject.GetVariable("Requester").Value;
var target = hmiProject.GetVariable("Target").Value;
var result = schema.CreateEditModel(parent, requester, target, recipeId);
if (result.ResultCode == CreateEditModelResultCode.Success)
{
// Cast to FTOptix.RecipeX.EditModel to access PendingChanges and FTOptix.RecipeX.EditModel-level transfer methods
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(result.EditModelNodeId);
Log.Info($"Pending changes: {editModel.PendingChanges}");
}Asynchronously refactors the recipe schema. This is a long-running operation that completes asynchronously. When finished, a RefactorCompleted event is fired.
public void RefactorRecipes()Notes:
- This is the only async method
- Does not block the calling thread
- Listen for
RefactorCompletedevent for completion notification
Example:
schema.RefactorRecipes();
// Method returns immediately
// Listen for RefactorCompleted event to know when completeFired when the RefactorRecipes() async method completes.
Example:
public class RecipeRefactorLogic : BaseNetLogic
{
public override void Start()
{
var schema = LogicObject.Owner.GetObject("RecipeSchema1");
if (schema != null)
{
// Subscribe to event
schema.EventOccurred += OnRefactorCompleted;
}
}
private void OnRefactorCompleted(IUANode sender, UAEventArgs args)
{
if (args.Event.BrowseName.Name == "RefactorCompleted")
{
Log.Info("Recipe refactoring completed");
}
}
}Fired when TransferFromStoreToTarget or TransferFromEditModelToTarget operations complete successfully.
RecipeX uses database tables to persist recipe data. Understanding this structure is useful for direct database queries.
CREATE TABLE Recipes (
Id, -- PRIMARY KEY [UInt32]
Name, -- [String] Recipe name
Version, -- [String] Recipe version
RecipeSchemaName, -- [String] Schema name
RecipeSchemaNamespaceUri, -- [String] Namespace URI
RecipeSchemaTimestamp, -- [DateTime] Schema timestamp
CreatedAt, -- [DateTime] Creation time
ModifiedAt -- [DateTime] Last modification
)CREATE TABLE RecipeItems (
Id, -- PRIMARY KEY [UInt32]
FTOptix.RecipeX.RecipeId, -- [UInt32] Foreign key to Recipes
RelativeBrowsePath, -- [String] Item path
TypeId -- [String] OPC UA type identifier
)CREATE TABLE RecipeDataItems (
Id, -- PRIMARY KEY [UInt32]
RecipeItemId, -- [UInt32] Foreign key to RecipeItems
RelativeBrowsePath, -- [String] Data item path
ElementAccess, -- [String] Array element access
DataTypeId, -- [String] OPC UA data type
Value -- [String] Stored value
)Dynamically created as RecipeMetadata_<RecipeSchemaName> per schema.
-- Example: RecipeMetadata_RecipeSchema1
CREATE TABLE RecipeMetadata_RecipeSchema1 (
FTOptix.RecipeX.RecipeId, -- Foreign key to Recipes
Metadata1, -- [UserDefined] Custom metadata
Metadata2, -- [UserDefined] Custom metadata
-- ... additional metadata columns as defined in schema
)Sample Query:
SELECT R.*, M.*
FROM Recipes AS R
LEFT JOIN "RecipeMetadata_RecipeSchema1" AS M
ON R.Id = M.FTOptix.RecipeX.RecipeId
WHERE R.RecipeSchemaName = 'RecipeSchema1'
ORDER BY R.Namepublic class RecipeManager : BaseNetLogic
{
public override void Start()
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
// Create recipe
var recipeId = new FTOptix.RecipeX.RecipeId { Name = "Configuration1", Version = "1.0" };
var createResult = schema.CreateRecipe(recipeId);
if (createResult == CreateRecipeResultCode.Success)
{
// Set metadata
schema.SetRecipeMetadataValue(recipeId, "Author", "Engineering");
schema.SetRecipeMetadataValue(recipeId, "Department", "Manufacturing");
// Set data values
var itemPath = new string[] { "Settings" };
var dataItemPath = new string[] { "Parameter1" };
var elementAccess = new FTOptix.Core.ElementAccessStruct();
schema.SetRecipeDataItemValue(recipeId, itemPath, dataItemPath, elementAccess, 100);
Log.Info($"Recipe '{recipeId.Name}' created and configured");
}
}
}public class RecipeLoader : BaseNetLogic
{
[ExportMethod]
public void ApplyRecipe(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var targetNode = LogicObject.Owner.GetVariable("TargetObject").Value;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
var result = schema.TransferFromStoreToTarget(
recipeId,
targetNode,
FTOptix.RecipeX.ErrorPolicy.Strict);
if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
{
Log.Info($"Recipe '{recipeName}' applied to target");
}
else
{
Log.Error($"Failed to apply recipe: {result}");
}
}
}public class ConfigurationCapture : BaseNetLogic
{
[ExportMethod]
public void SaveCurrentConfiguration(string recipeName)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var targetNode = LogicObject.Owner.GetVariable("SourceObject").Value;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = "1.0" };
var result = schema.TransferFromTargetToStore(
recipeId,
targetNode,
overwrite: true,
FTOptix.RecipeX.ErrorPolicy.BestEffort);
if (result == FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeUpdated ||
result == FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeCreated)
{
Log.Info($"Configuration saved as '{recipeName}'");
}
}
}public class RecipeList : BaseNetLogic
{
[ExportMethod]
public void ListAllRecipes()
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var result = schema.GetRecipes();
if (result.ResultCode == GetRecipesResultCode.Success)
{
foreach (var recipe in result.Recipes)
{
var metadata = schema.GetRecipeMetadataValues(recipe.FTOptix.RecipeX.RecipeId);
Log.Info($"Recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version}");
Log.Info($" Created: {recipe.CreatedAt}");
Log.Info($" Modified: {recipe.ModifiedAt}");
if (metadata.ResultCode == GetRecipeMetadataValuesResultCode.Success)
{
foreach (var meta in metadata.MetadataValues)
{
Log.Info($" {meta.Name}: {meta.Value}");
}
}
}
}
}
}Verify whether a recipe exists before attempting operations:
[ExportMethod]
public bool RecipeExists(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null || string.IsNullOrEmpty(recipeName))
return false;
var result = schema.GetRecipes();
if (result.ResultCode != GetRecipesResultCode.Success)
return false;
// Check if recipe matches
foreach (var recipe in result.Recipes)
{
if (recipe.FTOptix.RecipeX.RecipeId.Name == recipeName &&
recipe.FTOptix.RecipeX.RecipeId.Version == version)
return true;
}
return false;
}Apply a recipe with comprehensive error handling and logging:
[ExportMethod]
public void SafeApplyRecipe(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
{
Log.Error("Recipe schema not found");
return;
}
var target = LogicObject.Owner.GetVariable("TargetObject").Value;
if (target == null)
{
Log.Error("Target object not found");
return;
}
var recipeId = new FTOptix.RecipeX.RecipeId
{
Name = recipeName,
Version = version
};
// Check if recipe exists first
var recipesResult = schema.GetRecipes();
bool recipeExists = false;
if (recipesResult.ResultCode == GetRecipesResultCode.Success)
{
recipeExists = recipesResult.Recipes.Any(r =>
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
r.FTOptix.RecipeX.RecipeId.Version == version);
}
if (!recipeExists)
{
Log.Error($"Recipe '{recipeName}' v{version} not found");
return;
}
// Apply the recipe
var result = schema.TransferFromStoreToTarget(
recipeId,
target,
FTOptix.RecipeX.ErrorPolicy.Strict);
if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
{
Log.Info($"Recipe '{recipeName}' applied successfully");
}
else
{
switch (result)
{
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.RecipeNotFound:
Log.Error("Recipe not found in store");
break;
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.TargetNotFound:
Log.Error("Target node not found");
break;
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.DataMismatch:
Log.Error("Recipe structure does not match target");
break;
case FTOptix.RecipeX.TransferFromStoreToTargetResultCode.StoreError:
Log.Error("Error accessing recipe store");
break;
default:
Log.Error($"Failed to apply recipe: {result}");
break;
}
}
}Capture current configuration as a recipe:
[ExportMethod]
public void CaptureAsRecipe(string recipeName)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var source = LogicObject.Owner.GetVariable("SourceObject").Value;
if (schema == null || source == null)
{
Log.Error("Recipe schema or source not found");
return;
}
var recipeId = new FTOptix.RecipeX.RecipeId
{
Name = recipeName,
Version = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss")
};
// Capture and save
var result = schema.TransferFromTargetToStore(
recipeId,
source,
overwrite: false, // Don't overwrite existing
FTOptix.RecipeX.ErrorPolicy.Strict);
switch (result)
{
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeCreated:
Log.Info($"Recipe '{recipeName}' created and saved");
// Optionally set metadata
schema.SetRecipeMetadataValue(recipeId, "CaptureTime", DateTime.UtcNow);
schema.SetRecipeMetadataValue(recipeId, "CapturedBy", "AutoCapture");
break;
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.SuccessRecipeUpdated:
Log.Info($"Recipe '{recipeName}' updated");
break;
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.RecipeAlreadyExists:
Log.Warning($"Recipe '{recipeName}' already exists");
break;
case FTOptix.RecipeX.TransferFromTargetToStoreResultCode.DataMismatch:
Log.Error("Source structure does not match recipe schema");
break;
default:
Log.Error($"Failed to capture recipe: {result}");
break;
}
}Duplicate a recipe while updating metadata:
[ExportMethod]
public void DuplicateRecipeWithMetadata(string sourceRecipe, string targetRecipe)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return;
// Get source metadata
var sourceId = new FTOptix.RecipeX.RecipeId { Name = sourceRecipe, Version = "1.0" };
var metadataResult = schema.GetRecipeMetadataValues(sourceId);
if (metadataResult.ResultCode != GetRecipeMetadataValuesResultCode.Success)
{
Log.Error("Failed to retrieve source metadata");
return;
}
// Duplicate the recipe
var targetId = new FTOptix.RecipeX.RecipeId { Name = targetRecipe, Version = "1.0" };
var dupResult = schema.DuplicateRecipe(sourceId, targetId);
if (dupResult != DuplicateRecipeResultCode.Success)
{
Log.Error($"Failed to duplicate recipe: {dupResult}");
return;
}
// Copy metadata from source
foreach (var metadata in metadataResult.MetadataValues)
{
schema.SetRecipeMetadataValue(targetId, metadata.Name, metadata.Value);
}
// Update some fields
schema.SetRecipeMetadataValue(targetId, "DuplicatedFrom", sourceRecipe);
schema.SetRecipeMetadataValue(targetId, "DuplicatedAt", DateTime.UtcNow);
Log.Info($"Recipe '{targetRecipe}' created as copy of '{sourceRecipe}'");
}Create an interactive experience for selecting and applying recipes:
[ExportMethod]
public string[] GetAvailableRecipes()
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return new string[] { };
var result = schema.GetRecipes();
if (result.ResultCode != GetRecipesResultCode.Success)
return new string[] { };
return result.Recipes
.Select(r => $"{r.FTOptix.RecipeX.RecipeId.Name} (v{r.FTOptix.RecipeX.RecipeId.Version})")
.ToArray();
}
[ExportMethod]
public void ApplySelectedRecipe(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var target = LogicObject.Owner.GetVariable("TargetObject").Value;
if (schema == null || target == null)
return;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
var result = schema.TransferFromStoreToTarget(recipeId, target, FTOptix.RecipeX.ErrorPolicy.Strict);
if (result == FTOptix.RecipeX.TransferFromStoreToTargetResultCode.Success)
{
Log.Info($"Applied recipe: {recipeName}");
// Update UI display variable
LogicObject.Owner.GetVariable("CurrentRecipe").Value = recipeName;
}
else
{
Log.Error($"Application failed: {result}");
}
}
[ExportMethod]
public Dictionary<string, object> GetRecipeDetails(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var details = new Dictionary<string, object>();
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
// Get general recipe info
var recipesResult = schema.GetRecipes();
if (recipesResult.ResultCode == GetRecipesResultCode.Success)
{
var recipe = recipesResult.Recipes.FirstOrDefault(r =>
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
r.FTOptix.RecipeX.RecipeId.Version == version);
if (recipe != null)
{
details["Name"] = recipe.FTOptix.RecipeX.RecipeId.Name;
details["Version"] = recipe.FTOptix.RecipeX.RecipeId.Version;
details["CreatedAt"] = recipe.CreatedAt;
details["ModifiedAt"] = recipe.ModifiedAt;
}
}
// Get metadata
var metaResult = schema.GetRecipeMetadataValues(recipeId);
if (metaResult.ResultCode == GetRecipeMetadataValuesResultCode.Success)
{
var metadata = new Dictionary<string, object>();
foreach (var meta in metaResult.MetadataValues)
{
metadata[meta.Name] = meta.Value;
}
details["Metadata"] = metadata;
}
// Get data items count
var itemsResult = schema.GetDataItems(recipeId);
if (itemsResult.ResultCode == GetDataItemsResultCode.Success)
{
details["DataItemCount"] = itemsResult.DataItems.Length;
}
return details;
}Verify recipe consistency and values:
[ExportMethod]
public bool ValidateRecipe(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return false;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
// Check recipe exists
var recipesResult = schema.GetRecipes();
if (recipesResult.ResultCode != GetRecipesResultCode.Success)
{
Log.Error("Failed to retrieve recipes");
return false;
}
var recipeExists = recipesResult.Recipes.Any(r =>
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
r.FTOptix.RecipeX.RecipeId.Version == version);
if (!recipeExists)
{
Log.Error($"Recipe '{recipeName}' not found");
return false;
}
// Validate data items
var itemsResult = schema.GetDataItems(recipeId);
if (itemsResult.ResultCode != GetDataItemsResultCode.Success)
{
Log.Error("Failed to retrieve data items");
return false;
}
if (itemsResult.DataItems.Length == 0)
{
Log.Warning("Recipe contains no data items");
}
// Validate individual values
bool allValid = true;
foreach (var item in itemsResult.DataItems)
{
var valueResult = schema.GetRecipeDataItemValue(
recipeId,
item.ItemRelativeBrowsePath,
item.DataItemRelativeBrowsePath,
item.ElementAccess);
if (valueResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
{
Log.Warning($"Could not retrieve value for {string.Join("/", item.ItemRelativeBrowsePath)}");
allValid = false;
}
}
// Validate metadata
var metaResult = schema.GetRecipeMetadataValues(recipeId);
if (metaResult.ResultCode != GetRecipeMetadataValuesResultCode.Success)
{
Log.Warning("Could not retrieve metadata");
allValid = false;
}
return allValid;
}This method implements a two-step confirmation pattern to prevent accidental deletion. It is designed to be called from a UI button via ExportMethod:
- First call (
confirmed = false, the default): performs a dry run. It looks up the recipe and logs the details of what would be deleted, then returns without making any changes. This gives the operator a chance to review before committing. - Second call (
confirmed = true): performs the actual deletion.
This pattern is useful in HMI screens where a "Delete" button can first show a confirmation prompt (confirmed parameter set to false) and only proceed when the operator explicitly confirms the action (confirmed parameter set to true).
[ExportMethod]
public void DeleteRecipeWithConfirmation(string recipeName, string version, bool confirmed)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
if (!confirmed)
{
// First call: log what would be deleted
var result = schema.GetRecipes();
if (result.ResultCode == GetRecipesResultCode.Success)
{
var recipe = result.Recipes.FirstOrDefault(r =>
r.FTOptix.RecipeX.RecipeId.Name == recipeName &&
r.FTOptix.RecipeX.RecipeId.Version == version);
if (recipe != null)
{
Log.Info($"Would delete recipe: {recipe.FTOptix.RecipeX.RecipeId.Name} v{recipe.FTOptix.RecipeX.RecipeId.Version} " +
$"(modified: {recipe.ModifiedAt})");
return;
}
}
Log.Warning("Recipe not found");
return;
}
// Second call with confirmed=true: actually delete
var deleteResult = schema.DeleteRecipe(recipeId);
if (deleteResult == DeleteRecipeResultCode.Success)
{
Log.Info($"Recipe '{recipeName}' deleted successfully");
}
else
{
Log.Error($"Failed to delete recipe: {deleteResult}");
}
}Complete workflow for editing a recipe using edit models:
Note
The Edit Model session is tied to the lifecycle of its parent node (parentNodeId passed to CreateEditModel). If the parent node is deleted, the Edit Model node created underneath it is automatically deleted as well. This means you can scope Edit Model cleanup to session or screen lifecycle by controlling the parent node's lifetime rather than managing the Edit Model node directly.
[ExportMethod]
public NodeId StartEditingRecipe(string recipeName, string version)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
var parentNode = LogicObject.Owner.GetVariable("EditModelsParent").Value;
var requester = LogicObject.GetNodeId();
var target = LogicObject.Owner.GetVariable("TargetObject").Value;
if (schema == null)
return NodeId.Empty;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
// Create edit model
var createResult = schema.CreateEditModel(parentNode, requester, target, recipeId);
if (createResult.ResultCode != CreateEditModelResultCode.Success)
{
Log.Error($"Failed to create edit model: {createResult.ResultCode}");
return NodeId.Empty;
}
// Load recipe into edit model
var transferResult = schema.TransferFromStoreToEditModel(requester, recipeId);
if (transferResult != FTOptix.RecipeX.TransferFromStoreToEditModelResultCode.Success)
{
Log.Error($"Failed to load recipe into edit model: {transferResult}");
return NodeId.Empty;
}
// Cast to FTOptix.RecipeX.EditModel to access additional members such as PendingChanges
// and transfer methods that operate directly on this edit model instance.
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
Log.Info($"Recipe '{recipeName}' loaded for editing. Pending changes: {editModel.PendingChanges}");
return createResult.EditModelNodeId;
}
[ExportMethod]
public void CancelEditingRecipe(NodeId editModelId)
{
// Simply delete the edit model
var editModel = InformationModel.Get(editModelId);
if (editModel != null)
{
editModel.Delete();
Log.Info("Edit model discarded");
}
}
[ExportMethod]
public void SaveEditedRecipe(NodeId editModelId, string recipeName, string version,
NodeId requesterNodeId)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return;
var recipeId = new FTOptix.RecipeX.RecipeId { Name = recipeName, Version = version };
// Save changes back to store
var result = schema.TransferFromEditModelToStore(requesterNodeId, recipeId);
if (result == FTOptix.RecipeX.TransferFromEditModelToStoreResultCode.SuccessRecipeUpdated ||
result == FTOptix.RecipeX.TransferFromEditModelToStoreResultCode.SuccessRecipeCreated)
{
Log.Info($"Recipe '{recipeName}' saved successfully");
// Clean up edit model
var editModel = InformationModel.Get(editModelId);
if (editModel != null)
editModel.Delete();
}
else
{
Log.Error($"Failed to save recipe: {result}");
}
}
[ExportMethod]
public void ApplyAndClose(NodeId editModelId, NodeId targetNodeId, string recipeName,
NodeId requesterNodeId, FTOptix.RecipeX.ErrorPolicy errorPolicy = FTOptix.RecipeX.ErrorPolicy.Strict)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return;
// Apply edit model directly to target
var applyResult = schema.TransferFromEditModelToTarget(requesterNodeId, targetNodeId, errorPolicy);
if (applyResult == FTOptix.RecipeX.TransferFromEditModelToTargetResultCode.Success)
{
Log.Info($"Recipe '{recipeName}' applied to target");
// Clean up
var editModel = InformationModel.Get(editModelId);
if (editModel != null)
editModel.Delete();
}
else
{
Log.Error($"Failed to apply recipe to target: {applyResult}");
}
}When the Recipes Editor widget from the TemplateLibrary is used, FactoryTalk Optix automatically creates an FTOptix.RecipeX.EditModel to back the ListView. This edit model is not exposed as a visible project node, it exists internally and is not directly accessible as children of the ListView instance. To programmatically obtain a reference to it, call CreateEditModel passing the ListView.NodeId as both parentNodeId and requesterNodeId. Because the edit model already exists for that parent/requester pair, the call returns the existing instance rather than creating a new one.
This pattern is useful when you want to run server-side logic against the values the user is currently editing, for example to validate ranges or cross-field constraints before the recipe is saved.
[ExportMethod]
public void ValidateListViewRecipe(NodeId listViewNodeId, out bool isValidRecipe)
{
isValidRecipe = true;
var listView = InformationModel.Get<ListView>(listViewNodeId);
if (listView == null)
{
Log.Error("ListView not found");
isValidRecipe = false;
return;
}
var recipeSchema = Project.Current.Get<FTOptix.RecipeX.RecipeSchema>("Recipes/RecipeSchema1");
if (recipeSchema == null)
{
Log.Error("FTOptix.RecipeX.RecipeSchema not found");
isValidRecipe = false;
return;
}
// Use get-or-create: if the ListView already owns an edit model this returns it,
// otherwise a new one is created. Pass NodeId.Empty and an empty FTOptix.RecipeX.RecipeId so the
// call does not load a different recipe on top of the one already active.
var createResult = recipeSchema.CreateEditModel(
listView.NodeId, // parentNodeId — same as the ListView used when it created the edit model
listView.NodeId, // requesterNodeId — same as the ListView used when it created the edit model
NodeId.Empty, // targetNodeId — not needed; we are accessing an existing model
new FTOptix.RecipeX.RecipeId()); // recipeId — empty; the loaded recipe is already in the model
if (createResult.ResultCode != CreateEditModelResultCode.Success)
{
Log.Error($"Failed to access edit model: {createResult.ResultCode}");
isValidRecipe = false;
return;
}
var editModel = (FTOptix.RecipeX.EditModel)InformationModel.Get(createResult.EditModelNodeId);
if (editModel == null)
{
Log.Error("FTOptix.RecipeX.EditModel not found");
isValidRecipe = false;
return;
}
// Retrieve the data items defined in the recipe that is currently loaded
var dataItemsResult = recipeSchema.GetDataItems(editModel.FTOptix.RecipeX.RecipeId);
if (dataItemsResult.ResultCode != GetDataItemsResultCode.Success)
{
Log.Error($"Failed to retrieve data items: {dataItemsResult.ResultCode}");
isValidRecipe = false;
return;
}
// Iterate every field and apply validation rules
foreach (var dataItem in dataItemsResult.DataItems)
{
var valueResult = recipeSchema.GetRecipeDataItemValue(
editModel.FTOptix.RecipeX.RecipeId,
dataItem.ItemRelativeBrowsePath,
dataItem.DataItemRelativeBrowsePath,
dataItem.ElementAccess);
if (valueResult.ResultCode != GetRecipeDataItemValueResultCode.Success)
{
Log.Warning($"Could not read '{string.Join("/", dataItem.ItemRelativeBrowsePath)}': {valueResult.ResultCode}");
isValidRecipe = false;
continue;
}
var value = valueResult.DataItemValue;
var fieldName = string.Join("/", dataItem.ItemRelativeBrowsePath);
if (value == null)
{
Log.Warning($"Field '{fieldName}' has a null value");
isValidRecipe = false;
continue;
}
// Add your custom validation logic here.
// Examples:
// if (value is double d && (d < 0 || d > 100)) { ... isValidRecipe = false; }
// if (fieldName == "Motor/MaxSpeed" && (int)value > 3000) { ... isValidRecipe = false; }
Log.Info($"Field '{fieldName}' = {value} — OK");
}
Log.Info(isValidRecipe
? $"Recipe '{editModel.FTOptix.RecipeX.RecipeId.Name}' passed validation"
: $"Recipe '{editModel.FTOptix.RecipeX.RecipeId.Name}' failed validation");
}Note
Do not delete or commit the edit model inside this method. Lifecycle management of the edit model (saving, discarding) belongs to the ListView. This method is read-only with respect to the edit model — it only inspects the current values.
Perform operations on multiple recipes:
[ExportMethod]
public void RenameAllRecipesWithPrefix(string oldPrefix, string newPrefix)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return;
var recipesResult = schema.GetRecipes();
if (recipesResult.ResultCode != GetRecipesResultCode.Success)
{
Log.Error("Failed to retrieve recipes");
return;
}
int renamed = 0;
foreach (var recipe in recipesResult.Recipes)
{
if (recipe.FTOptix.RecipeX.RecipeId.Name.StartsWith(oldPrefix))
{
var newName = recipe.FTOptix.RecipeX.RecipeId.Name.Replace(oldPrefix, newPrefix);
var result = schema.RenameRecipe(recipe.FTOptix.RecipeX.RecipeId, newName);
if (result.ResultCode == RenameRecipeResultCode.Success)
{
renamed++;
Log.Info($"Renamed: {recipe.FTOptix.RecipeX.RecipeId.Name} -> {newName}");
}
}
}
Log.Info($"Renamed {renamed} recipes");
}
[ExportMethod]
public void DeleteRecipesOlderThan(int daysOld)
{
var schema = (FTOptix.RecipeX.RecipeSchema)LogicObject.Owner.GetObject("RecipeSchema1");
if (schema == null)
return;
var recipesResult = schema.GetRecipes();
if (recipesResult.ResultCode != GetRecipesResultCode.Success)
return;
var cutoffDate = DateTime.UtcNow.AddDays(-daysOld);
int deleted = 0;
foreach (var recipe in recipesResult.Recipes)
{
if (recipe.ModifiedAt < cutoffDate)
{
var result = schema.DeleteRecipe(recipe.FTOptix.RecipeX.RecipeId);
if (result == DeleteRecipeResultCode.Success)
{
deleted++;
Log.Info($"Deleted old recipe: {recipe.FTOptix.RecipeX.RecipeId.Name}");
}
}
}
Log.Info($"Deleted {deleted} old recipes");
}- Legacy recipe schemas and new RecipeX schemas can coexist in the same project but they have different structures, so they cannot easily interoperate.
- Recipe parameters represent Items or DataItems selected in the FTOptix.RecipeX.RecipeSchema editor.
- Recipe parameters displayed in a ListView are always presented as read-only.
- Always check result codes after operations; they indicate specific error conditions.
- Use
FTOptix.RecipeX.ErrorPolicy.Strictfor critical operations andFTOptix.RecipeX.ErrorPolicy.BestEffortfor optional operations. - Log failures with the specific result code for easier diagnosis.