When to read this: You are building activities that need dependency injection, automatic telemetry, project settings integration, connection/binding support, built-in retry logic, or governance. If your activity is a simple synchronous operation (string manipulation, math), the classic
CodeActivity<T>pattern is sufficient and you do not need the SDK.
Cross-references:
- Activity code and CacheMetadata — classic activity patterns (CodeActivity, AsyncCodeActivity, NativeActivity)
- ViewModel fundamentals — classic DesignPropertiesViewModel
- Architecture overview — project structure, naming conventions
- Advanced patterns — NativeActivity composites, bookmarks, interface contracts
Use the SDK when your activities need:
- Dependency injection -- register and resolve services via
Microsoft.Extensions.DependencyInjection - Automatic telemetry -- execution metrics tracked without manual instrumentation
- Project settings integration --
[ArgumentSetting]attribute auto-reads values from Studio project settings - Connection/binding support --
[ConnectionBinding],[PropertyBinding],[BindingContract]attributes - Built-in retry logic --
SdkNativeActivity<T>providesShouldRetry()andPrepareForRetry() - Governance -- runtime governance checks applied automatically
For simple synchronous activities (e.g., string manipulation, math), the classic CodeActivity<T> pattern is sufficient -- the SDK adds unnecessary complexity.
The SDK is added as a git submodule and integrated via MSBuild targets:
repo/
+-- Activities.SDK/ <-- git submodule
| +-- Sdk.imports.build.targets
| +-- Sdk.dependencies.build.targets
| +-- Sdk.constants.build.targets
+-- MyActivityPack/
| +-- Directory.build.targets <-- imports SDK targets
| +-- Activities/
| | +-- Activities.csproj
| | +-- MyActivity.cs
| +-- Activities.UnitTest/
| +-- Activities.UnitTest.csproj
Directory.build.targets (in the activity pack directory):
<Project>
<Import Project="..\Activities.SDK\Sdk.dependencies.build.targets"/>
<Import Project="..\Activities.SDK\Sdk.imports.build.targets"/>
</Project>Activity .csproj -- enable SDK shared project imports:
<PropertyGroup>
<UiPathActivityProject>True</UiPathActivityProject> <!-- imports UiPath.Sdk.Activities.shproj -->
<UiPathActivityDesignProject>True</UiPathActivityDesignProject> <!-- imports UiPath.Sdk.Activities.Design.shproj -->
</PropertyGroup>Package versions are centrally managed by Sdk.dependencies.build.targets -- do NOT specify versions for SDK-managed packages in individual .csproj files.
The SDK defines conditional compilation constants in Sdk.constants.build.targets:
| Constant | Condition |
|---|---|
NETCORE_UIPATH |
net5.0+ (any .NET Core target) |
WINDOWS_UIPATH |
net461/net462 or *-windows |
NETPORTABLE_UIPATH |
net5.0/net6.0/net7.0/net8.0 (non-Windows) |
INTEGRATION_SERVICE |
<UseIntegrationService>True</UseIntegrationService> |
GOVERNANCE_SERVICE |
<UseGovernanceService>True</UseGovernanceService> |
Use these constants for platform-specific code:
#if NETCORE_UIPATH
// .NET Core-specific implementation
#elif WINDOWS_UIPATH
// .NET Framework / Windows-specific implementation
#endifThe primary SDK base class for async activities. Derives from AsyncCodeActivity<T> and wraps the APM pattern into a clean async/await API with automatic service scope management.
public abstract class SdkActivity<T> : SdkActivity<T, DefaultRuntimeServicePolicy> { }
public abstract class SdkActivity<T, TPolicy> : AsyncCodeActivity<T>, IDisposable
where TPolicy : class, IRuntimeServicePolicy, new()
{
protected IServiceProvider Services { get; }
protected IRuntimeServices RuntimeServices { get; }
protected abstract Task<T> ExecuteAsync(
AsyncCodeActivityContext context,
IServiceProvider serviceProvider, // scoped to this execution
CancellationToken cancellationToken);
protected virtual void OnCompleted(
AsyncCodeActivityContext context,
IServiceProvider serviceProvider) { } // runs with fresh context after await
}- SDK creates a DI scope tied to the execution context.
- Applies project settings via
RuntimeServices.ProjectSettings?.Apply(context, this). - Applies bindings via
RuntimeServices.BindingService?.Apply(this). - Calls
ExecuteAsync()-- read all inputs before the firstawait(context disposed after). - Stores the result in
Result. - Creates a new DI scope and calls
OnCompleted()with a fresh context.
public partial class OpenAccount : SdkActivity<IAccount>
{
[RequiredArgument]
public string AccountHolder { get; set; }
public OpenAccount() : base() { }
internal OpenAccount(IServiceProvider provider) : base(provider) { }
protected override async Task<IAccount> ExecuteAsync(
AsyncCodeActivityContext context,
IServiceProvider serviceProvider,
CancellationToken cancellationToken)
{
var bank = serviceProvider.GetService<IBank>();
var client = await bank.GetClientAsync(AccountHolder, cancellationToken);
RuntimeServices.ExecutorRuntime?.LogMessage(new LogMessage
{
EventType = TraceEventType.Information,
Message = $"Account opened for {AccountHolder}"
});
return client.Accounts[0];
}
}Note the dual constructor pattern: the parameterless constructor is for the workflow runtime; the IServiceProvider constructor is for unit testing.
For activities that need WF4 native features: child activity scheduling, bookmarks, or built-in retry.
public abstract class SdkNativeActivity<T> : NativeActivity<T>
{
protected abstract Task<T> ExecuteAsync(
NativeActivityContext context,
IServiceProvider serviceProvider,
CancellationToken cancellationToken);
protected virtual bool ShouldRetry(NativeActivityContext context, object executionValue, int retryCount) => false;
protected virtual void PrepareForRetry(NativeActivityContext context, int retryCount) { }
protected int GetRetryCount(NativeActivityContext context);
protected virtual void OnCompleted(NativeActivityContext context, IServiceProvider serviceProvider) { }
}Override ShouldRetry and PrepareForRetry for built-in retry logic:
protected override bool ShouldRetry(NativeActivityContext context, object executionValue, int retryCount)
{
// Retry up to 3 times on transient failures
return retryCount < 3 && executionValue is TransientException;
}
protected override void PrepareForRetry(NativeActivityContext context, int retryCount)
{
// Reset state before retry
_cachedResult = null;
}Schedules child activities via OnCompleted:
public partial class MyContainer : SdkNativeActivity<int>
{
[Browsable(false)]
public ActivityAction<object> Body { get; set; } = new()
{
Argument = new DelegateInArgument<object>("Service"),
Handler = new Sequence() { DisplayName = "Do" }
};
protected override Task<int> ExecuteAsync(
NativeActivityContext context, IServiceProvider sp, CancellationToken ct)
=> Task.FromResult(42);
protected override void OnCompleted(NativeActivityContext context, IServiceProvider sp)
{
base.OnCompleted(context, sp);
context.ScheduleAction(Body, new MyService());
}
}Override DefaultRuntimeServicePolicy to register custom services in the DI container:
public class CustomServicePolicy : DefaultRuntimeServicePolicy
{
public override IServicePolicy Register(Action<IServiceCollection> collection = null)
{
_services.TryAddSingleton<IMyService, MyService>();
return base.Register(collection);
}
}
// Use as second type parameter
public partial class MyActivity : SdkActivity<string, CustomServicePolicy> { ... }TryAddSingleton ensures the service is only registered once, even if multiple activities use the same policy.
SDK ViewModels inherit from BaseViewModel (or BaseViewModel<TPolicy>) and are linked to activities via the [ViewModelClass] attribute on a partial class.
// ViewModels/MyActivity.Design.cs
[ViewModelClass(typeof(MyActivityViewModel))]
public partial class MyActivity { }internal class MyActivityViewModel : BaseViewModel
{
public DesignInArgument<string> Input { get; set; } = new();
public DesignOutArgument<string> Result { get; set; } = new();
public MyActivityViewModel(IDesignServices services) : base(services) { }
internal MyActivityViewModel(IDesignServices services, IServiceProvider sp) : base(services, sp) { }
protected override void InitializeModel()
{
base.InitializeModel();
Input.IsPrincipal = true;
Input.OrderIndex = PropertyOrderIndex++; // built-in counter
Result.OrderIndex = PropertyOrderIndex++;
}
}| Aspect | Classic (DesignPropertiesViewModel) |
SDK (BaseViewModel) |
|---|---|---|
| Order counter | Manual var order = 0 |
Built-in PropertyOrderIndex |
| Class visibility | public |
internal |
| Activity linking | viewModelType in metadata JSON |
[ViewModelClass] attribute |
| DI in ViewModel | Not available | ActivityServices.GetService<T>() |
| Design-time services | Via IDesignServices |
Via DesignServices property |
| Init call | base.InitializeModel() + PersistValuesChangedDuringInit() |
base.InitializeModel() only |
| Testability | Mock IDesignServices |
Mock IDesignServices + inject IServiceProvider |
| Attribute | Purpose |
|---|---|
[ConnectionBinding] |
Marks a property as an Integration Service connection |
[PropertyBinding(nameof(X))] |
Declares a dependency on another property for binding |
[BindingContract("Name")] |
Defines a contract name for the binding system |
[ArgumentSetting(section, property)] |
Auto-reads value from Studio project settings |
[Sensitive] |
Excludes property data from telemetry |
[ViewModelClass(typeof(VM))] |
Links activity to its ViewModel (on partial class) |
// Automatically reads from project settings at runtime
[ArgumentSetting("OrchestratorSettings", "DefaultFolderPath")]
public InArgument<string> FolderPath { get; set; }[ConnectionBinding]
[BindingContract("SalesforceConnection")]
public InArgument<string> ConnectionId { get; set; }Available via the RuntimeServices property on any SDK activity:
RuntimeServices.ExecutorRuntime // IExecutorRuntime -- logging, settings, OAuth
RuntimeServices.WorkflowRuntime // IWorkflowRuntime -- workflow services
RuntimeServices.ProjectSettings // Read/apply project settings
RuntimeServices.Telemetry // Automatic telemetry tracking
RuntimeServices.BindingService // Apply connection/property bindings
RuntimeServices.Governance // Runtime governance (if GOVERNANCE_SERVICE defined)
RuntimeServices.ConnectionClient // Integration Service client (if INTEGRATION_SERVICE defined)All properties are nullable -- the SDK gracefully handles missing services (e.g., when running outside Studio or in unit tests). Always use null-conditional access:
RuntimeServices.ExecutorRuntime?.LogMessage(new LogMessage { ... });
RuntimeServices.Governance?.ValidateExecution(this);