Skip to content

ProConcepts Geoprocessing

UmaHarano edited this page Nov 6, 2024 · 26 revisions

The geoprocessing public APIs are exposed through the ArcGIS.Desktop.Core.Geoprocessing namespace in the ArcGIS Pro API Reference Guide. The core useful methods, enumerators, and interfaces for geoprocessing tasks are available in this namespace.

ArcGIS.Desktop.Core.Geoprocessing.dll

Language:      C#
Subject:       Geoprocessing
Contributor:   ArcGIS Pro SDK Team <arcgisprosdk@esri.com>
Organization:  Esri, http://www.esri.com
Date:          10/06/2024
ArcGIS Pro:    3.4
Visual Studio: 2022

In this topic


Performing analysis and geoprocessing

The Geoprocessing API available in the ArcGIS.Desktop.Core.Geoprocessing namespace is all about how to perform analysis in ArcGIS Pro using .NET. It exposes methods to execute any geoprocessing tool.

To use the Geoprocessing API, you first need to create an add-in that executes code involving ExecuteToolAsync in response to an event (for example, Button Click) triggered by interacting with the add-in.

Use the ExecuteToolAsync method to run any geoprocessing tool (including a Python script tool and models). The code of the add-in can use map layers, geometry created on the fly, or data stored on disk as input for analysis. You can provide paths to data to the geoprocessing tool, or open the tool dialog in the Geoprocessing pane of ArcGIS Pro and let the user provide the parameter values and execute the tool.

Each geoprocessing tool takes a set of input data of various types—such as feature class, table, geometry, workspace, spatial reference, and so on—and in most cases, creates new data as output. Refer to the tool's help page to learn about the tool's behavior and to get appropriate data types and acceptable values of input and output parameters.

ExecuteToolAsync has an overload; both methods require the path of the tool to be executed and all inputs and output packed in an array. The input-output parameter values are positional; you must maintain the same position and sequence as specified on the tool help page.

In ArcGIS Pro, you can no longer create your own custom geoprocessing tool in .NET. However, you can create a tool using Python script and call it in .NET code.

If you need to run a sequence of tools with inputs from the users between the calls, create a model with the sequence setting appropriate model parameters, and call this model tool in your code.

Before calling the ExecuteToolAsync method

Follow these preparatory steps before calling ExecuteToolAsync to avoid pitfalls and simple mistakes.

Refer to the tool documentation page

Note down the following:

  • Tool name and toolbox alias. Both are case sensitive.
  • Syntax for the correct parameter sequence. Note which parameters are required and which are optional.
  • Data types and acceptable values to be supplied for the parameters. In .Net, you can always pass a string for a parameter value. You can also pass native .Net objects for some Supported native objects as parameter values.
  • Names of the environments honored by the tool.
  • Example of using the tool by examining the Python code samples.

So, for referencing System toolbox tools, either specifying the full path to the Toolbox or using the convention "ToolboxName.ToolName" are equivalent. For example, these are both equivalent:

var path = @"C:\<ArcGIS Pro Install Path>\Resources\ArcToolBox\Toolboxes\Analysis Tools.tbx";
var long_tool_name = System.IO.Path.Combine(path, "Buffer");

var short_tool_name = "analysis.Buffer";

For custom tools, as they are not System toolbox tools, then the fully qualified path to the toolbox should always be provided. So, for example, given a custom toolbox called "DeepThought.tbx" containing a python script tool called 'Answer", installed to a folder C:\Data\DeepThought-ProAddin\Toolboxes\toolboxes on a particular machine, the tool name would be specified as:

var path = @"C:\Data\DeepThought-ProAddin\Toolboxes\toolboxes\DeepThought.tbx";
var long_tool_name = System.IO.Path.Combine(path, "Answer");

Create the array of parameter values

Use the MakeValueArray method to create an array to be passed to the ExecuteToolAsync method. Using MakeValueArray ensures that the parameter values are properly handled by ExecuteToolAsync.

Many parameters are specified as strings, including the paths to input and output datasets. The catalog path of any dataset or workspace can be specified like the following: C:/data/Redlands.gdb/Streets. Additionally you can use data stored in memory using a path like the following: memory/CensusBlocks. Learn more about memory data and geoprocessing

The following is a code example of creating an array of parameter values:

// the tool to be executed has workspace as first parameter
// feature class name as second and feature type as third parameter
string outpath = Project.Current.DefaultGeodatabasePath;
string fcname = "accident_sites";
var sr = await QueuedTask.Run(() => {
    return SpatialReferenceBuilder.CreateSpatialReference(3857);
});
// must maintain the sequence and position of each parameter value
var parameters = Geoprocessing.MakeValueArray(outpath, fcname, "POINT", "", "", "", sr);

Create the array of parameter values using Geometries

Many tools consume arbitrary geometries as part of their input parameters lists (eg a clip or buffer polygon). All geometries and Envelopes can be passed to Geoprocessing.MakeValueArray to create parameter values compatible with Geoprocessing.ExecuteToolAsync and Geoprocessing.OpenToolDialog.

Here are two examples of creating parameter value arrays that include arbitrary geometries. Note that the geometry/geometries to be parameterized must be added to a List before being parameterized by Geoprocessing.MakeValueArray:

 var mv = MapView.Active;
 var tool_name = "analysis.Buffer";

 var val_array = await QueuedTask.Run(() =>  {

    var center_pt = MapPointBuilderEx.CreateMapPoint(mv.Camera.X, mv.Camera.Y, mv.Camera.SpatialReference);
    var poly = GeometryEngine.Instance.Buffer(center_pt, 2000);
    //add the geometry to a list before parameterizing
    var geom_values = new List<object>() { poly };
     return Geoprocessing.MakeValueArray(new object[] { geom_values, null, @"1000 Meters" });

 });
 //execute the tool with ExecuteToolAsync
 //Geoprocessing.ExecuteToolAsync(tool_name, val_array,
 //                      null, null, null, GPExecuteToolFlags.InheritGPOptions);

 //Invoke on the UI - show the tool dialog pre-populated with the parameter values
 Geoprocessing.OpenToolDialog(tool_name, val_array, null, false);
 var tool_name = "analysis.Clip";
 var extent = MapView.Active.Extent;
 var sel_layer = MapView.Active.Map.GetLayersAsFlattenedList()
                        .OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "GreatLakes");
 if (sel_layer == null) return;

 var gdb = Project.Current.DefaultGeodatabasePath;
 var out_fc = System.IO.Path.Combine(gdb, "clipped_lakes_out");

 var val_array = await QueuedTask.Run(() => {
   var rect = GeometryEngine.Instance.Scale(extent, extent.Center, 0.5, 0.5) as Envelope;
   var clip_poly = PolygonBuilderEx.CreatePolygon(rect, rect.SpatialReference);

   //Add the geometry to a list before calling MakeValueArray
   //Envelope and Geometry types are supported
   var geom = new List<object>() { clip_poly };
   return Geoprocessing.MakeValueArray(new object[] { sel_layer, geom, out_fc });
 });
 Geoprocessing.ExecuteToolAsync(tool_name, val_array,
        null, null, null, GPExecuteToolFlags.InheritGPOptions);

Create an array of environment settings

To set one or more environments to be honored by the execution of the tool, use MakeEnvironmentArray as shown in the following code snippet:

// create the spatial reference object
var sp_ref = await QueuedTask.Run(() => {
    return SpatialReferenceBuilder.CreateSpatialReference(26911);  // UTM 83 11N: 26911
});

// create an extent object
var ext = "460532 3773964 525111 3827494";
// follow syntax env_name: env_value
// environment names will be available in the intellisense          
var environments = Geoprocessing.MakeEnvironmentArray(outputCoordinateSystem: sp_ref, extent: ext);

Note: Each geoprocessing tool honors specific environments. See the tool help page to find out which environments are honored by the tool. However, setting an environment that the tool does not honor will be ignored by the ExecuteToolAsync method.

Supported native objects as parameter values

In many scenarios, passing a Layer, geometry created while interacting with the map, Envelope or Extent, SpatialReference or a Table as an argument to a geoprocessing tool may be necessary. The following .Net types are supported and can be passed as parameter value:

  • Scalars – long, short, float, double, date, string
  • ArcGIS.Core.Geometry.SpatialReference
  • ArcGIS.Core.Geometry – point, line, polygon
  • ArcGIS.Core.Geometry.Envelope – supporting GP types of GPExtentEnv, GPExtent, GPEneveope
  • ArcGIS.Core.Data.Field – supporting GPField and list of fields for GPFieldList
  • ArcGIS.Desktop.Mapping – Layer, StandaloneTable
  • ArcGIS.Core.Data.Dataset – Table, FeatureClass
string tool1 = "analysis.Buffer";

// create multipoints
List<MapPoint> list = new List<MapPoint>();
list.Add(MapPointBuilderEx.CreateMapPoint(1.0, 1.0));
list.Add(MapPointBuilderEx.CreateMapPoint(1.0, 2.0));
Multipoint multiPoint = MultipointBuilderEx.CreateMultipoint(list);

var args = await QueuedTask.Run(() =>
{
  return Geoprocessing.MakeValueArray(multiPoint, "memory\\Buffers", "800 meters");
});

// example of using ArcGIS.Core.Geometry.SpatialReference object
var spatial_ref = await QueuedTask.Run(() => {
    return SpatialReferenceBuilder.CreateSpatialReference(3857);
});
// call MakeEnvironmentArray on spatial_ref so that ExecuteToolAsync can internally use the object
var env = Geoprocessing.MakeEnvironmentArray(outputCoordinateSystem: spatial_ref);

Geoprocessing.ExecuteToolAsync(tool1, args, env);

Decide which ExecuteToolAsync to use

ExecuteToolAsync is overloaded; there are two methods having the same name with different parameter signature. To handle geoprocessing events, use ExecuteToolAsync with a parameter of type GPToolExecuteEventHandler. To show the progressor dialog, use the one with a parameter of type CancelableProgressor.

Executing geoprocessing tools

Executing a tool with event handling

One of the overloaded ExecuteToolAsync methods has a delegate parameter of type GPToolExecuteEventHandler. To handle various events—such as OnValidate, OnProgressMessage, OnBeginExecute, OnProgressPos, OnBeginExecute, and OnEndExecute—pass a callback (for the fifth parameter) as shown in the following code example:

// code in the implementing method that Button's OnClick can call
string ozone_points = @"C:\data\ca_ozone.gdb\MarchValues";

var args = Geoprocessing.MakeValueArray(ozone_points, "OZONE", "", "in_memory\\raster", "300", "EMPIRICAL", 
               "300", "5", "5000",
               "NBRTYPE=StandardCircular RADIUS=310833.272442914 ANGLE=0 NBR_MAX=10 SECTOR_TYPE=ONE_SECTOR",
               "PREDICTION", "0.5", "EXCEED", "", "K_BESSEL" );

string tool_path = "ga.EmpiricalBayesianKriging";

// cancellation token variable is declared as a class member
System.Threading.CancellationTokenSource _cts = new System.Threading.CancellationTokenSource();

var result = Geoprocessing.ExecuteToolAsync(tool_path, args, null, _cts.Token,
    (event_name, o) =>  // implement delegate and handle events, o is message object.
    {
        switch (event_name){     
            case "OnValidate": // stop execute if any warnings
                if ((o as IGPMessage[]).Any(it => it.Type == GPMessageType.Warning))
                    _cts.Cancel();
                break;
            case "OnProgressMessage":
                string msg = string.Format("{0}: {1}", new object[] { event_name, (string)o });
                System.Windows.MessageBox.Show(msg);
                _cts.Cancel();
                break;
            case "OnProgressPos":
                string msg2 = string.Format("{0}: {1} %", new object[] { event_name, (int)o });
                System.Windows.MessageBox.Show(msg2);
                _cts.Cancel();
                break;
        }
    });

Executing a tool with the progress dialog

This example uses CancelableProgressorSource with a Cancel option so that a user can terminate the execution before it completes.

protected override async void OnClick()
{
    var progDlg = new ProgressDialog("Running Geoprocessing Tool", "Cancel", 100, true);
    progDlg.Show();

    var progSrc = new CancelableProgressorSource(progDlg);
    string infc = @"D:\data\california\california.gdb\ca_highways";
    string outfc = System.IO.Path.Combine(ArcGIS.Desktop.Core.Project.Current.DefaultGeodatabasePath, 
                                                                        "Clipped_points");
    var parameters = Geoprocessing.MakeValueArray(infc, outfc);
    await Geoprocessing.ExecuteToolAsync("management.CopyFeatures", parameters,
        null, new CancelableProgressorSource(progDlg).Progressor, GPExecuteToolFlags.Default);

    progDlg.Hide();
}

Executing model and script tools

Creating your own geoprocessing custom tool using .NET is no longer available. However, you can create your custom tool in Python leveraging the power of an ArcPy package. You can also create a model by knitting together a series of systems tools and calling the model tool in .NET.

Pass the full path or the relative path of the script tool or model. Follow the preparatory steps previously described. Be sure to get the tool's syntax from its documentation.

// get the model tool's parameter syntax from the model's help 
string input_roads = @"C:\data\Input.gdb\PlanA_Roads";
string buff_dist_field = "Distance";   // use the distance values from a field named Distance
string input_vegetation = @"C:\data\Input.gdb\vegtype";
string output_data = @"C:\data\Output.gdb\ClippedFC2";

// the tool path can also be a relative path such as tool_path = @"Default.atbx\Script"
string tool_path = @"C:\data\CompletedModels.tbx\ExtractVegetationforProposedRoads";

var args = Geoprocessing.MakeValueArray(input_roads, buff_dist_field, input_vegetation, output_data);

return Geoprocessing.ExecuteToolAsync(tool_path, args);

Executing a geoprocessing service

First create an *.ags file in ArcGIS Pro, and pass the full path of the service. Use the GPThread execute flag as best practice.

// pass the full path of the service
string tool_path = @"C:\data\agsonline.ags\Network/ESRI_DriveTime_US\CreateDriveTimePolygons";
string in_point = @"C:\MyProject\MyProject.gdb\apoint";

var result = await Geoprocessing.ExecuteToolAsync(tool_path, Geoprocessing.MakeValueArray(in_point, "3 9 12"),null,CancelableProgressor.None, GPExecuteToolFlags.GPThread);

Adding results to Map and Project history

The type of the last parameter of the ExecuteToolAsync method is GPExecuteToolFlags. Use one of the following values as needed. Using GPExecuteToolFlags.Default adds outputs to map and refreshes project items.

GPExecuteToolFlags.AddOutputsToMap
GPExecuteToolFlags.AddToHistory
GPExecuteToolFlags.RefreshProjectItems
GPExecuteToolFlags.Default

Open the tool dialog in the Geoprocessing pane

The OpenToolDialog method opens the tool dialog in the Geoprocessing pane, allowing the user to enter the input values and run the tool manually. You can fill in the parameter values calculated within the add-in code before opening the tool dialog, open the dialog, and let the users fill in the other values. OpenToolDialog should always be called on the UI thread. The following are three examples:

This code opens the Buffer dialog with all three required values filled in:

//On the UI thread
string input_points = @"C:\data\ca_ozone.gdb\ozone_points";
string output_polys = @"C:\data\ca_ozone.gdb\ozone_buff";
string buffer_dist = "2000 Meters";
var param_values = Geoprocessing.MakeValueArray(input_points, output_polys, buffer_dist);

Geoprocessing.OpenToolDialog("analysis.Buffer", param_values);

This code opens the Buffer dialog with the first two parameter values filled in:

//On the UI thread
string input_points = @"C:\data\ca_ozone.gdb\ozone_points";
string output_polys = @"C:\data\ca_ozone.gdb\ozone_buff";
string buffer_dist = "";
var param_values = Geoprocessing.MakeValueArray(input_points, output_polys, buffer_dist);
 
Geoprocessing.OpenToolDialog("analysis.Buffer", param_values);

This code opens the Buffer dialog with no value for any of the parameters, letting the user have full control over what values to use:

//On the UI
string input_points = "";
string output_polys = "";
string buffer_dist = "";
var param_values = Geoprocessing.MakeValueArray(input_points, output_polys, buffer_dist);
 
Geoprocessing.OpenToolDialog("analysis.Buffer", param_values);

In this example, the tool parameters are configured on the QueuedTask and passed to OpenToolDialog on the UI thread:

 var mv = MapView.Active;

 //configure parameters on the QueuedTask
 var val_array = await QueuedTask.Run(() =>  {

    var center_pt = MapPointBuilderEx.CreateMapPoint(mv.Camera.X, mv.Camera.Y, mv.Camera.SpatialReference);
    var poly = GeometryEngine.Instance.Buffer(center_pt, 2000);
    //add the geometry to a list before parameterizing
    var geom_values = new List<object>() { poly };
     return Geoprocessing.MakeValueArray(new object[] { geom_values, null, @"1000 Meters" });

 });

 //show the tool dialog pre-populated with the parameter values on the UI thread
 Geoprocessing.OpenToolDialog("analysis.Buffer", val_array, null, false);

This code opens a script tool using a relative tool path

string tool_path = @"Default.atbx\Script";
Geoprocessing.OpenToolDialogAsync(tool_path );

After execution of the geoprocessing tools is complete

You can determine whether the execution was successful or not by examining the IGPResult object. The following code checks whether the execution failed by checking if gp_result.IsFailed is true or not.

Using result and message objects

You can get the geoprocessing messages from the result object as shown below:

// gp_result is the returned result object from a call to ExecuteToolAsync
// displays error messages if the tool fails, otherwise shows the default messages
Geoprocessing.ShowMessageBox(gp_result.Messages, "GP Messages", 
                gp_result.IsFailed ? GPMessageBoxStyle.Error : GPMessageBoxStyle.Default);

Accessing geoprocessing history item

IGPHistoryItem is used to access Geoprocessing History in a project. There are three properties can be obtained: ToolPath, GPResult, TimeStamp for the Geoprocessing History.

// Open a project with data inside
string openProjectPath = @"D\DATA"\IGPHistoryItemTestProject\IGPHistoryItemTestProject.aprx";
await Project.OpenAsync(openProjectPath); 

// Run  the Geoprocessing tool using the data in the project, remember to use GPExecuteToolFlags.AddToHistory to add the execution messages to History
MapProjectItem mapProjItem = Project.Current.GetItems<MapProjectItem>().FirstOrDefault(item => item.Name.Equals("Map", StringComparison.CurrentCultureIgnoreCase));
var map = await QueuedTask.Run(() => mapProjItem.GetMap());
var ftrLayer = map.Layers[0] as FeatureLayer;
string tool1 = "management.GetCount";     
var args1 = Geoprocessing.MakeValueArray(ftrLayer);
var env = Geoprocessing.MakeEnvironmentArray(overwriteoutput: true);          
GPExecuteToolFlags executeFlags = GPExecuteToolFlags.AddToHistory;
var t = await Geoprocessing.ExecuteToolAsync(tool1, args1,env,null,null,executeFlags);

// Get the IGPHistoryItem and its properties
String hitemID="";
String hitemToolPath="" ;
IGPResult hitemGPResult=null;
DateTime hitemTimeStamp;

foreach (var hitem in hisItems)
{
    // common IGPHistoryItem and Item properties
    hitemID = (hitem as Item).ID;
    hitemToolPath = hitem.ToolPath;
    hitemGPResult = hitem.GPResult;
    hitemTimeStamp = hitem.TimeStamp;
}

## Handle Geoprocessing event

ArcGIS.Desktop.Core.Events.GPExecuteToolEvent and GPExecuteToolEventArgs can be used to handle Geoprocessing event. 
```csharp
ArcGIS.Desktop.Core.Events.GPExecuteToolEvent.Subscribe(e =>
            {
                string id = e.ID;                   // Same as history ID
                if (e.IsStarting == false)  // Execute completed
                    _ = e.GPResult.ReturnValue;
                System.Windows.MessageBox.Show("event triggered.");
            });
var t = await Geoprocessing.ExecuteToolAsync("management.GetCount", Core.Geoprocessing.Geoprocessing.MakeValueArray(@"c:\shape_file.shp"));

Running ArcPy Python from Pro Addin

The test1.py script (in C:\scripts folder) runs a geoprocessing tool and returns messages as a string. In .NET, you can intercept that value and use it. This mechanism offers a way to implement workflows in a simpler, yet powerful ArcPy module's methods and functions, and use them in .NET.

object myCommand = "python " + @"C:\scripts\test1.py";
System.Diagnostics.ProcessStartInfo procStartInfo = 
               new System.Diagnostics.ProcessStartInfo("cmd", "/c" + myCommand );

procStartInfo.RedirectStandardOutput = true;
procStartInfo.UseShellExecute = false;
procStartInfo.CreateNoWindow = true;

System.Diagnostics.Process proc = new System.Diagnostics.Process();
proc.StartInfo = procStartInfo;
proc.Start();

string result = proc.StandardOutput.ReadToEnd();

System.Windows.MessageBox.Show(result);

Geoprocessing Options

Geoprocessing options corresponding to a subset of the options on the Pro options UI are available in the api via the static ApplicationOptions.GeoprocessingOptions property - refer to ProConcepts Content - ApplicationOptions for more information. The available options include:

 //ArcGIS.Desktop.Core.GeoprocessingOptions 
 //Gets and sets the available application geoprocessing options
 public class GeoprocessingOptions {
   //Gets whether geoprocessing tools can overwrite existing data, layers, or files when run.
   public bool OverwriteExistingDatasets { get; }
   //Gets whether layers in a map are removed if their source datasets are deleted
   //by a geoprocessing tool that overwrites output.
   public bool RemoveOverwrittenLayers { get; }
   //Gets whether output datasets created by geoprocessing tools should be automatically
   //added to an open map.
   public bool AddOutputDatasetsToOpenMap { get; }
   //Gets whether the tools being run are added to the current project's geoprocessing history.
   public bool WriteGPOperationsToHistory { get; }

   //Sets whether geoprocessing tools can overwrite existing data, layers, or files
   //when run. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetOverwriteExistingDatasets(bool overwriteExistingDatasets);

   //Sets whether layers in a map are removed if their source datasets are deleted by a geoprocessing 
   //tool that overwrites output. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetRemoveOverwrittenLayers(bool removeOverwrittenLayers);

   //Sets whether output datasets created by geoprocessing tools should be automatically
   //added to an open map. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetAddOutputDatasetsToOpenMap(bool addOutputDatasetsToOpenMap);

   //Sets whether the tools being run are added to the current project's geoprocessing
   //history. This method must be called on the MCT. Use QueuedTask.Run.
   public void SetWriteGPOperationsToHistory(bool writeGPOperationsToHistory);	
}

Note the use of explicit "Set" methods rather than "Setter" properties. This is because setting the GP option values requires use of the Pro Main CIM Thread, MCT, which is accessed via QueuedTask.Run(...). It should also be noted that changing these option values modifies behavior of interactive GP tools not the behavior of Geoprocessing.ExecuteToolAsync(...). To modify the behavior of ExecuteToolAsync, add-in developers should use its ArcGIS.Desktop.Core.Geoprocessing.GPExecuteToolFlags parameter.

 //Get the current option values
 var overwrite_gp = ApplicationOptions.GeoprocessingOptions.OverwriteExistingDatasets;
 var remove_gp = ApplicationOptions.GeoprocessingOptions.RemoveOverwrittenLayers;
 var addoutput_gp = ApplicationOptions.GeoprocessingOptions.AddOutputDatasetsToOpenMap;
 var history_gp = ApplicationOptions.GeoprocessingOptions.WriteGPOperationsToHistory;

 //Setting GeoprocessingOptions requires the QueuedTask. We are modifying the behavior
 //of interactive GP tools _only_.
 QueuedTask.Run(() => {
   ApplicationOptions.GeoprocessingOptions.SetOverwriteExistingDatasets(true);
   ApplicationOptions.GeoprocessingOptions.SetRemoveOverwrittenLayers(false);
   ApplicationOptions.GeoprocessingOptions.SetAddOutputDatasetsToOpenMap(true);
   ApplicationOptions.GeoprocessingOptions.SetWriteGPOperationsToHistory(false);
 });

Developing with ArcGIS Pro

    Migration


Framework

    Add-ins

    Configurations

    Customization

    Styling


Arcade


Content


CoreHost


DataReviewer


Editing


Geodatabase

    3D Analyst Data

    Plugin Datasources

    Topology

    Linear Referencing

    Object Model Diagram


Geometry

    Relational Operations


Geoprocessing


Knowledge Graph


Layouts

    Reports


Map Authoring

    3D Analyst

    CIM

    Graphics

    Scene

    Stream

    Voxel


Map Exploration

    Map Tools


Networks

    Network Diagrams


Parcel Fabric


Raster


Sharing


Tasks


Workflow Manager Classic


Workflow Manager


Reference

Clone this wiki locally