Creating Custom Dynamic Command (SDK)
Before starting, you can learn more about Dynamic Commands and their availability here: Dynamic Commands .
The Advanced Command Software Development Kit (SDK) provides support for writing custom advanced commands which can be embedded into Studio and Nintex Assistant.
This guide is primarily written for developers who are looking for guidance on authoring custom advanced commands and contains step-by-step tutorials. Also, it contains some advanced topics you can follow depending on your needs. You can create your own new Dynamic Commands.
Advanced Commands SDK Guidelines
Getting Started
The basic pattern for authoring custom advanced commands is:
-
Create and structure your solution and code.
-
It is recommended that each AC grouping (e.g. Folders) will have its own solution.
-
Add Advanced command SDK references.
-
Create a definition for your command.
-
Create an executor for your command.
-
Create an editor for your command.
This Getting Started Guide walks you through these steps for a simple advanced command.
Requirements
Advanced command SDK is written on .Net Standard and intended to be available on all .NET implementations, however, Studio and Nintex Assistant are designed to run only on Windows family operating system, so here we also use this restriction. Before you can use the AC SDK, the following must be installed:
-
Microsoft Visual Studio 2017 (or greater).
-
.NET framework 4.6.1 (or greater) and/or .NET Core 2.0 (or greater).
Structuring the solution
-
It’s recommended that the solution consists of all advanced commands which correspond to their group. (e.g. Folders) .
-
It’s also recommended that each advanced command will have its own solution folder.
-
Each advanced command consists of three parts:
-
Definition – represents the set of input parameters for the advanced command. Input parameters control the execution flow.
-
Execution – represents the actual business logic of the advanced command.
-
Editor - represents the UI editing component of a Definition object in Studio.
-
It’s recommended to create three separate projects for each advanced command part, but that is not mandatory. Following this recommendation allows us to isolate different aspects of the advanced command – definition, execution, and UI logic.
For our sample advanced command, we’ll create the following projects:
Project name |
Project template |
Description |
---|---|---|
Kryon.AdvancedCommands.Folders.Copy.Definition |
Class library (.NET Standard) |
Contains the class with a set of properties, aka definition for our ‘Copy a folder advanced command, e.g. path to the folder,target folder, and various errors |
Kryon.AdvancedCommands.Folders.Copy |
Class library (.NET Standard) |
Contains the actual business logic for our Copy a folder advanced command. |
Kryon.AdvancedCommands.Folders.Copy.Editor |
WPF User Control Library (.NET Core) |
Contains the UI editing component for our Copy a folder advanced command. |
Preparing projects
To correctly configure projects for our Copy a Folder command we have to add the corresponding AC SDK NuGet packages and do some additional steps for each project.
For extending Nintex advanced commands, AC SDK provides the following NuGet packages. The NuGets are available here:
https://public.Kryon.io/#RPA-Versions/Kryon-additional-solutions/DAC-SDK/
Definition
Let's configure Kryon.AdvancedCommands.Folders.Copy.Definition project.
-
Install Kryon.AdvancedCommands.Sdk.Definition NuGet package.
-
Create the Icons folder at the root of the project.
Executor
Let's configure Kryon.AdvancedCommands.Folders.Copy project.
-
Install Kryon.AdvancedCommands.Sdk.ExecutorPlugin NuGet package.
-
Add a project reference to Kryon.AdvancedCommands.Folders.Copy.Definition project.
-
Edit Kryon.AdvancedCommands.Folders.Copy.csproj file and add the following line to the <PropertyGroup> section
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
Editor
Let's configure Kryon.AdvancedCommands.Folders.Copy.Editor project.
-
Install Kryon.AdvancedCommands.Sdk.EditorAdapter NuGet package.
-
Add a project reference to Kryon.AdvancedCommands.Folders.Copy.Definition project.
-
Edit Kryon.AdvancedCommands.Folders.Copy.Editor.csproj file and add following line to the <PropertyGroup> section
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
-
Create the Resource Dictionary ‘Generic.xaml’ at the root of the project.
-
Re-adding *.deps.json and *.runtimeconfig.json files
”Kryon.AdvancedCommands.Sdk.EditorAdapter.runtimeconfig.json”
”Kryon.AdvancedCommands.Sdk.EditorAdapter.deps.json”
Steps:
-
Copy Full Path
-
Remove files from the project
-
Choose to add an existing file to the project option, paste copied path on step a and add *.deps.json and *.runtimeconfig.json files to the project.
-
Set ‘Copy To Output Directory’ property to 'Copy Always’ for both *.deps.json and *.runtimeconfig.json files.
-
Creating your first advanced command
Definition
To define a custom advanced command, you will start with creating a class that inherits from AdvancedCommandDefinition
.
public sealed class CopyFolderDefinition : AdvancedCommandDefinition
{
}
Now we have to create a manifest.json file to identify our advanced command and add descriptive information (see example below).
Warning: Command Input/Output interface should be in CamelCase.
For example:
"input": [ { "name": "folderPath", "type": "textWithVariable", "objectType": "string" } ]
{
"name": "CopyFolder",
"version": "2021.8.1",
"description": "Copy an existing folder to a new folder",
"publisher": "Kryon",
"executionType": "remote",
"extensionType": "activity",
"editor": {
"editorSchema": "./Kryon.AdvancedCommands.Folders.Copy.Editor.dll",
"displayName": "Copy a folder",
"icon": "FolderCopy.ico",
"base64ImageIcon": "AAABAAEAEBAAAAEAIABoBAAAF...ACsQQAArEEA/6xB//+sQQ==",
"groupName": "Folders",
"designerTemplate": "Copy folder with Name:<$.folderPath> to <$.targetFolder>"
},
"definition": {
"executorSchema": "./Kryon.AdvancedCommands.Folders.Copy.dll",
"definitionSchema": "./Kryon.AdvancedCommands.Folders.Copy.Definition.dll",
"input": [
{
"name": "folderPath",
"type": "textWithVariable",
"objectType": "string"
},
{
"name": "targetFolder",
"type": "textWithVariable",
"objectType": "string"
},
{
"name": "errorGeneral",
"type": "text",
"objectType": "string"
},
{
"name": "errorAccessDenied",
"type": "text",
"objectType": "string"
}
],
"output": [
{
"name": "errorVariable",
"searchable": false
}
]
}
}
The Build Action option must be set to Copy if newer for the manifest.json file.
Also, we need to add FolderCopy.ico to the Icons folder created earlier.
The Build Action option must be set to Embedded resource for the icon.
Now we continue to define our CopyFolderDefinition
class properties.
public sealed class CopyFolderDefinition : AdvancedCommandDefinition
{
private const string ErrorGeneralDefaultValue = "GeneralError";
private const string ErrorFolderNotFoundDefaultValue = "FolderNotFound";
private const string ErrorFolderExistsDefaultValue = "ErrorFolderExists";
private const string ErrorAccessDeniedDefaultValue = "AccessDenied";
public CopyFolderDefinition()
{
ErrorGeneral = ErrorGeneralDefaultValue;
ErrorFolderNotFound = ErrorFolderNotFoundDefaultValue;
ErrorAccessDenied = ErrorAccessDeniedDefaultValue;
ErrorFolderExists = ErrorFolderExistsDefaultValue;
}
public string FolderPath { get; set; }
public string TargetFolder { get; set; }
public string ErrorVariable { get; set; }
public string ErrorGeneral { get; set; }
public string ErrorFolderExists { get; set; }
public string ErrorFolderNotFound { get; set; }
public string ErrorAccessDenied { get; set; }
}
Executor
The next step is to add a corresponding executor for our CopyFolderDefinition
. To do this, you will need to add a class in Kryon.AdvancedCommands.Folders.Copy.Executor project that implements the IAdvancedCommand<T>
interface, where T
in our example is CopyFolderDefinition
.
public sealed class CopyFolderExecutor : IAdvancedCommand<CopyFolderDefinition>
{
public IExecutionResult Execute(CopyFolderDefinition definition, IExecutionContext context)
{
// Actual code goes here
}
}
Get and Set variables functionality is no longer available here. Assume that you will get actual values within the definition object.
Now let’s complete our example:
public sealed class CopyFolderExecutor : IAdvancedCommand<CopyFolderDefinition>
{
#region Members
private readonly IFileSystemWrapper _fileSystemWrapper;
private readonly ILogger<CopyFolderExecutor> _logger = null;
#endregion Members
#region C'tor
public CopyFolderExecutor(IFileSystemWrapper fileSystemWrapper, ILogger<CopyFolderExecutor> logger)
{
_fileSystemWrapper = fileSystemWrapper ?? throw new ArgumentNullException(nameof(fileSystemWrapper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
#endregion C'tor
#region Public Methods
public IExecutionResult Execute(CopyFolderDefinition definition, IExecutionContext context)
{
string folderPath = definition.FolderPath;
string targetFolder = definition.TargetFolder;
if (!_fileSystemWrapper.FileSystem.Directory.Exists(folderPath))
{
_logger.LogError($"Folder: {folderPath} not found.");
return new VariableResult(new Dictionary<string, string>
{{ definition.ErrorVariable, definition.ErrorFolderNotFound }}
);
}
try
{
_logger.LogDebug($"Copy folder: '{folderPath}' to '{targetFolder}'");
var diSource = _fileSystemWrapper.GetDirectoryInfo(folderPath);
var parentPath = _fileSystemWrapper.FileSystem.Directory.GetParent(folderPath).FullName;
if (_fileSystemWrapper.FileSystem.Path.GetFullPath(parentPath.ToLower()).
Equals(_fileSystemWrapper.FileSystem.Path.GetFullPath(targetFolder.ToLower())))
{
_logger.LogError("Source and target folders are the same.");
return new ErrorResult("Source and target folders are the same.");
}
// Check if target folder already exists
var newFolderPath = _fileSystemWrapper.FileSystem.Path.Combine(targetFolder, diSource.Name);
if (_fileSystemWrapper.FileSystem.Directory.Exists(newFolderPath))
{
_logger.LogDebug("Folder: '{newFolderPath}' already exists, merge source and target folders.");
var diTarget = _fileSystemWrapper.GetDirectoryInfo(newFolderPath);
_fileSystemWrapper.CopyDirectoryContent(diSource, diTarget);
}
else
{
_fileSystemWrapper.CopyDirectory(folderPath, targetFolder);
}
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Error Copy Folder!");
return new VariableResult(new Dictionary<string, string>
{{ definition.ErrorVariable, definition.ErrorAccessDenied }}
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Copy Folder!");
return new VariableResult(new Dictionary<string, string>
{{ definition.ErrorVariable, definition.ErrorGeneral }}
);
}
return new VariableResult(new Dictionary<string, string>());
}
#endregion Public Methods
}
IFileSystemWrapper
is a custom service to work with a File System engine and we skip the implementation details (see details on dependency injects in the appropriate section below)
Editor
The next step is to add a corresponding editor UI component for out CopyFolderDefinition
. To do this, you will need to add a class in Kryon.AdvancedCommands.Folders.Copy.Editor project that inherits from AdvancedCommandEditorBase<T>
abstract class, where T
in our example is CopyFolderDefinition
.
public sealed class CopyFolderEditor : AdvancedCommandEditorBase<CopyFolderDefinition>
{
#region Members
private readonly ILogger<CopyFolderEditor> _logger;
private string _folderPath;
private string _targetFolder;
private string _errorVariable;
private string _errorGeneral;
private string _errorFolderNotFound;
private string _errorAccessDenied;
#endregion Members
#region C'tor
public CopyFolderEditor(CopyFolderDefinition definition, IEditorContext context, ILogger<CopyFolderEditor> logger)
: base(definition, context)
{
FolderPath = definition.FolderPath;
TargetFolder = definition.TargetFolder;
ErrorVariable = definition.ErrorVariable;
ErrorGeneral = definition.ErrorGeneral;
ErrorFolderNotFound = definition.ErrorFolderNotFound;
ErrorAccessDenied = definition.ErrorAccessDenied;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
#endregion C'tor
#region Properties
public string FolderPath
{
get => _folderPath;
set => SetProperty(ref _folderPath, value);
}
public string TargetFolder
{
get => _targetFolder;
set => SetProperty(ref _targetFolder, value);
}
public string ErrorVariable
{
get => _errorVariable;
set => SetProperty(ref _errorVariable, value);
}
public string ErrorGeneral
{
get => _errorGeneral;
set => SetProperty(ref _errorGeneral, value);
}
public string ErrorFolderNotFound
{
get => _errorFolderNotFound;
set => SetProperty(ref _errorFolderNotFound, value);
}
public string ErrorAccessDenied
{
get => _errorAccessDenied;
set => SetProperty(ref _errorAccessDenied, value);
}
#endregion Properties
#region Public Methods
public override void AcceptChanges()
{
Definition.FolderPath = FolderPath.Trim();
Definition.TargetFolder = TargetFolder.Trim();
Definition.ErrorVariable = ErrorVariable;
Definition.ErrorGeneral = ErrorGeneral;
Definition.ErrorFolderNotFound = ErrorFolderNotFound;
Definition.ErrorAccessDenied = ErrorAccessDenied;
base.AcceptChanges();
}
public override bool Validate()
{
return !string.IsNullOrWhiteSpace(FolderPath) &&
!string.IsNullOrWhiteSpace(TargetFolder);
}
#endregion Public Methods
}
After that, we have to create a WPF UserControl CopyFolderEditorView
and edit the XAML file.
<UserControl x:Class="Kryon.AdvancedCommands.Folders.Copy.Editor.CopyFolderEditorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:krcv="http://schemas.Kryonsystems.com/xaml/controls/variables"
xmlns:local="clr-namespace:Kryon.AdvancedCommands.Folders.Copy.Editor"
xmlns:krci="http://schemas.Kryonsystems.com/xaml/controls/info"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:CopyFolderEditorView}">
<UserControl.Resources>
<ResourceDictionary>
</ResourceDictionary>
</UserControl.Resources>
<StackPanel>
<Label Content="Folder to copy" Padding="0,5"/>
<krcv:VariablesTextBox
Text="{Binding FolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Autofocus="True">
</krcv:VariablesTextBox>
<Label Content="Target folder" Padding="0,5"/>
<krcv:VariablesTextBox Text="{Binding TargetFolder, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Border BorderThickness="0,1,0,0" BorderBrush="LightGray" Margin="0,15,0,0" Padding="0,5,0,0" SnapsToDevicePixels="True">
<Expander IsExpanded="False" >
<Expander.Header>
<StackPanel Orientation="Horizontal">
<Label Content="Error handling" Padding="0"/>
<krci:FieldInfoIconControl VerticalAlignment="Bottom" Margin="5,0,0,1"
ToolTip=" Set an error value for your variable to get an indication if getting from clipboard fails."/>
</StackPanel>
</Expander.Header>
<Grid Margin="25,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Padding="0" Grid.Row="0" Content="Error variable" VerticalAlignment="Center" />
<krcv:VariablesComboBox Grid.Column="2" Grid.Row="0" Text="{Binding Path=ErrorVariable}"/>
<Label Padding="0" Grid.Row="2" Content="General error" VerticalAlignment="Center" />
<TextBox Grid.Row="2" Grid.Column="2" Text="{Binding Path=ErrorGeneral}"/>
<Label Padding="0" Grid.Row="4" Content="Folder not found" VerticalAlignment="Center" />
<TextBox Grid.Row="4" Grid.Column="2" Text="{Binding Path=ErrorFolderNotFound}"/>
<Label Padding="0" Grid.Row="6" Content="Access denied" VerticalAlignment="Center" />
<TextBox Grid.Row="6" Grid.Column="2" Text="{Binding Path=ErrorAccessDenied}"/>
</Grid>
</Expander>
</Border>
</StackPanel>
</UserControl>
The final step is to allow AC SDK to automatically find the corresponding editor for our advanced command. To do so, we need to add the following code to the Generic.xaml file created earlier.
<DataTemplate DataType="{x:Type local:CopyFolderEditor}">
<local:CopyFolderEditorView />
</DataTemplate>
Deployment
-
Make sure each solution that corresponds to an advanced command grouping will contain an artifacts.json file:
-
artifacts.Kryonjson example for the .AdvancedCommands.Folders solution:
{
"workflow": {
"build": "windows",
"publish": "windows"
},
"libraries": [
{
"name": "Kryon.AdvancedCommands.Folders.Move",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Move.Definition",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Move.Editor",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Copy",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Copy.Definition",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Copy.Editor",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Delete",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Delete.Definition",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Delete.Editor",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Rename",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Rename.Definition",
"build_flag": "win-x64"
},
{
"name": "Kryon.AdvancedCommands.Folders.Rename.Editor",
"build_flag": "win-x64"
}
],
"executables": [
{
"name": "Kryon.AdvancedCommands.Folders.Move",
"self_contained": false,
"build_flag": "win-x64",
"exe_dir_name": "kryon-ac-movefolder-1.0.0",
"zip_file_name": "kryon-ac-movefolder-1.0.0"
},
{
"name": "Kryon.AdvancedCommands.Folders.Copy",
"self_contained": false,
"build_flag": "win-x64",
"exe_dir_name": "kryon-ac-copyfolder-1.0.0",
"zip_file_name": "kryon-ac-copyfolder-1.0.0"
},
{
"name": "Kryon.AdvancedCommands.Folders.Delete",
"self_contained": false,
"build_flag": "win-x64",
"exe_dir_name": "kryon-ac-deletescpecificfolder-1.0.0",
"zip_file_name": "kryon-ac-deletescpecificfolder-1.0.0"
},
{
"name": "Kryon.AdvancedCommands.Folders.Rename",
"self_contained": false,
"build_flag": "win-x64",
"exe_dir_name": "kryon-ac-renamefolder-1.0.0",
"zip_file_name": "kryon-ac-renamefolder-1.0.0"
}
],
"files": [
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Move\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-movefolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Move.Definition\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-movefolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Move.Editor\\bin\\release\\netcoreapp3.1\\win-x64",
"dest_dir_name": "kryon-ac-movefolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Copy\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-copyfolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Copy.Definition\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-copyfolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Copy.Editor\\bin\\release\\netcoreapp3.1\\win-x64",
"dest_dir_name": "kryon-ac-copyfolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Delete\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-deletescpecificfolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Delete.Definition\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-deletescpecificfolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Delete.Editor\\bin\\release\\netcoreapp3.1\\win-x64",
"dest_dir_name": "kryon-ac-deletescpecificfolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Rename\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-renamefolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Rename.Definition\\bin\\release\\netstandard2.0\\win-x64",
"dest_dir_name": "kryon-ac-renamefolder-1.0.0"
},
{
"name": "*.dll",
"path": "Kryon.AdvancedCommands.Folders.Rename.Editor\\bin\\release\\netcoreapp3.1\\win-x64",
"dest_dir_name": "kryon-ac-renamefolder-1.0.0"
}
]
}
-
Checking advanced command in Studio locally.
-
Extract the zip file (e.g. kryon-ac-copyfolder-1.0.0) from your output folder (after running ci-publish command) to location stated by Kryon.Leo.Recorder.Client appsettings
AdvancedCommandsFolderPath
value. -
Set next feature toggle to true:
UseNewAcSdk
-
Run Studio or re-start Studio and check the advanced command locally.
-
Once you load your Dynamic Command into Studio, the commands appear under a new category named Customer Command / Unmapped Commands.
Use your Dynamic Commands as you would normally use any other command - simply drag and drop it into the Wizard.
In case you receive the following errors when editing or running a wizard, make sure that the command exists under the Advanced Commands folder. If needed, remove and re-add the command to the folder.
Possible error messages:
Make sure the command exists under the Advanced Commands folder
In case your Custom Dynamic Command was created using a previous RPA version and you are now using a newer RPA version, you need to remove the command from the current Wizard and re-drag it into the wizard in the newer RPA version.
If the Dynamic Advanced Command version doesn’t match the wizard's version as it was created in Studio, or if it doesn’t exist in the Advanced Command folder, then there is a fallback mechanism to run the old Advanced Command.
Therefore, it’s important to make sure that the DAC exists, and that its version matches that of the Studio wizard on the Robot VM. This will allow the published wizard, along with the specific DAC it was created with, to run correctly.