Hero image for When Drag-Drop Isn't Enough: Custom Code and Libraries in UiPath

When Drag-Drop Isn't Enough: Custom Code and Libraries in UiPath

RPA UiPath C# Python Custom Activities

UiPath is a low-code platform. That’s its greatest strength—and occasionally its limitation.

There comes a moment in every RPA developer’s journey when they think: “This would be three lines of code, but I need fifteen activities.”

That’s when you reach for custom code.


Why Write Code in UiPath?

UiPath’s activity library is vast. But there are gaps:

ScenarioWhy Custom Code Helps
Complex string manipulationRegEx in code is more readable than multiple activities
Mathematical algorithmsLoops and conditionals are cleaner in code
Custom encryption/hashingSecurity libraries need code integration
External DLL callsNo activity exists for your library
Performance-critical loopsCode executes faster than activity chains
Reusable team utilitiesPackage once, use everywhere

The goal isn’t to abandon low-code—it’s to use the right tool for each problem.

The Science Behind “Code is Faster”

Why does code outperform activity chains? It’s about overhead.

Let OactO_{act} = fixed overhead per activity execution (context switch, logging, type conversion)
Let TlogicT_{logic} = time to execute actual business logic
For a loop processing nn items:

Activity Chain (For Each → Multiple Activities): Ttotal=n×(Oact×k+Tlogic)T_{total} = n \times (O_{act} \times k + T_{logic}) where kk = number of activities inside the loop

Invoke Code (Single Activity → Loop in Code): Ttotal=Oact+(n×Tlogic)T_{total} = O_{act} + (n \times T_{logic})

The Math Matters:

Example: Processing 10,000 rows, 5 activities per row
O_act ? 5ms (activity overhead)
T_logic ? 1ms (actual work)

Activity Chain:  10,000 → (5ms → 5 + 1ms) = 260,000ms ? 4.3 minutes
Invoke Code:     5ms + (10,000 → 1ms) = 10,005ms ? 10 seconds

Speedup: 26x faster!

[!TIP] Rule of Thumb: If your loop runs 100+ iterations and contains multiple activities, consider moving the logic to Invoke Code or a Coded Workflow.


Invoke Code: Embedded Scripts

The Invoke Code activity lets you embed C# or VB.NET directly in your workflow.

Basic Structure

Activity: Invoke Code
├── Language: CSharp (or VBNet)
├── Code: [your script here]
├── Arguments (In): variables passed into the script
└── Arguments (Out): variables passed back to workflow

Example 1: Complex String Parsing

The Problem: Extract domain from various email formats

// Input: email (String)
// Output: domain (String)

try
{
    // Handle formats: user@domain.com, "Name" <user@domain.com>
    string emailPattern = @"[\w\.-]+@([\w\.-]+)";
    var match = System.Text.RegularExpressions.Regex.Match(email, emailPattern);
    
    if (match.Success)
    {
        domain = match.Groups[1].Value.ToLower();
    }
    else
    {
        domain = "INVALID";
    }
}
catch
{
    domain = "ERROR";
}

Example 2: Custom Hashing

// Input: inputText (String)
// Output: hashedValue (String)

using System.Security.Cryptography;
using System.Text;

using (SHA256 sha256 = SHA256.Create())
{
    byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(inputText));
    StringBuilder builder = new StringBuilder();
    foreach (byte b in bytes)
    {
        builder.Append(b.ToString("x2"));
    }
    hashedValue = builder.ToString();
}

Example 3: Date Calculation

// Input: startDate (DateTime), businessDays (Int32)
// Output: endDate (DateTime)

DateTime current = startDate;
int daysAdded = 0;

while (daysAdded < businessDays)
{
    current = current.AddDays(1);
    if (current.DayOfWeek != DayOfWeek.Saturday && 
        current.DayOfWeek != DayOfWeek.Sunday)
    {
        daysAdded++;
    }
}

endDate = current;

Invoke Code Best Practices

  1. Keep it short: If code exceeds ~50 lines, consider a custom activity
  2. Handle exceptions: Wrap in try-catch, return error state
  3. Avoid side effects: Don’t modify external state, just compute and return
  4. Document arguments: Use clear names like inputEmail not s1
  5. Test outside UiPath: Validate logic in Visual Studio first

Coded Workflows: The Bridge Between Low-Code and Pro-Code

2026 Update: Ignoring Coded Workflows in modern UiPath development is like discussing web development without mentioning TypeScript. This feature fundamentally changes how pro-code developers can work in UiPath.

Coded Workflows (introduced in UiPath 2023.4+) are .cs files that run alongside traditional XAML workflows. They solve the major pain points of Invoke Code:

Problem with Invoke CodeHow Coded Workflows Solve It
No IntelliSenseFull Visual Studio-level autocomplete
Can’t set breakpointsFull debugging with step-through
Hard to unit testStandard C# unit testing (NUnit, xUnit)
Code lives in activity propertyProper .cs files with version control
No code reuseImport and call from any workflow
Limited to ~50 linesWrite full classes with multiple methods

[!IMPORTANT] Why Senior Engineers Prefer Coded Workflows in 2026

1. Modularity via Inheritance: Create a BaseWorkflow.cs with common error handling, logging, or retry logic—then inherit:

public class InvoiceProcessor : BaseWorkflow
{
    // Automatically inherits logging, error handling, config loading
    [Workflow]
    public void ProcessInvoice(string invoiceId) { ... }
}

2. Git-Friendly Diffs:

# .cs file diff (readable)
- if (amount > 1000)
+ if (amount > 5000)

# .xaml file diff (nightmare)
- <Argument x:TypeArguments="x:Int32" Expression="[1000]" Name="threshold"/>
+ <Argument x:TypeArguments="x:Int32" Expression="[5000]" Name="threshold"/>
- <Activity ref="a7f3b2c1-..." DisplayName="If Amount Greater Than..."/>

Code reviewers can actually understand what changed!

What is a Coded Workflow?

A Coded Workflow is a C# class file that coexists with XAML in your UiPath project:

MyProject/
├── Main.xaml                    (Traditional low-code workflow)
├── ProcessTransaction.xaml      (Traditional low-code workflow)
├── Utilities/
│   ├── DataValidator.cs         → Coded Workflow!
│   ├── EmailBuilder.cs          → Coded Workflow!
│   └── CustomLogic.cs           → Coded Workflow!
└── project.json

Creating a Coded Workflow

In UiPath Studio: File → New → Coded Workflow

using System;
using System.Collections.Generic;
using UiPath.CodedWorkflows;

namespace MyProject.Utilities
{
    public class DataValidator : CodedWorkflow
    {
        [Workflow]
        public ValidationResult ValidateInvoice(
            string invoiceNumber,
            decimal amount,
            string vendorId,
            DateTime invoiceDate)
        {
            var errors = new List<string>();

            // Business Rule 1: Invoice number format
            if (!System.Text.RegularExpressions.Regex.IsMatch(
                invoiceNumber, @"^INV-\d{4}-\d{5}$"))
            {
                errors.Add($"Invalid invoice number format: {invoiceNumber}");
            }

            // Business Rule 2: Amount range
            if (amount <= 0 || amount > 1_000_000)
            {
                errors.Add($"Amount out of range: {amount}");
            }

            return new ValidationResult
            {
                IsValid = errors.Count == 0,
                Errors = errors
            };
        }
    }
}

Debugging Coded Workflows

This is where Coded Workflows truly shine:

╔═══════════════════════════════════════════════════════════════════╗
║               Debugging Comparison                                ║
╠═══════════════════════════════════════════════════════════════════╣
║                                                                   ║
║  INVOKE CODE (Old Way)           CODED WORKFLOWS (New Way)        ║
║  ─────────────────────           ─────────────────────────        ║
║  ✗ Set Log Messages everywhere   ✓ Set breakpoints on any line   ║
║  ✗ Check Locals after exception  ✓ Step Into / Step Over         ║
║  ✗ Guess which line failed       ✓ Inspect variables real-time   ║
║  ✗ No step-through debugging     ✓ Full IDE debugging!           ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

Unit Testing Coded Workflows

Another game-changer: real unit tests!

using NUnit.Framework;

[TestFixture]
public class DataValidatorTests
{
    [Test]
    public void ValidateInvoice_ValidData_ReturnsTrue()
    {
        var validator = new DataValidator();
        var result = validator.ValidateInvoice(
            "INV-2024-00001", 1000.00m, "V001", DateTime.Today);

        Assert.IsTrue(result.IsValid);
    }

    [Test]
    public void ValidateInvoice_InvalidFormat_ReturnsFalse()
    {
        var validator = new DataValidator();
        var result = validator.ValidateInvoice(
            "BAD-FORMAT", 100m, "V001", DateTime.Today);

        Assert.IsFalse(result.IsValid);
    }
}

Run tests with: dotnet test

Modern .NET Packaging with dotnet pack

For distributing Coded Workflows as NuGet packages:

# Create a class library with your coded workflows
dotnet new classlib -n MyCompany.RPA.Utilities
cd MyCompany.RPA.Utilities

# Add UiPath dependencies
dotnet add package UiPath.Workflow

# Build and package
dotnet build -c Release
dotnet pack -c Release -o ./packages

This creates a .nupkg file that can be:

  • Uploaded to Orchestrator Library Feed
  • Published to Azure Artifacts or private NuGet server
  • Installed in any UiPath project via Manage Packages

When to Use What?

ApproachBest ForAvoid When
XAML ActivitiesUI automation, simple logicComplex algorithms, heavy data processing
Invoke CodeQuick scripts, < 30 linesNeed debugging, reuse, or testing
Coded WorkflowsBusiness logic, validation, data transformationSimple UI clicks, non-developers
Custom ActivitiesOrg-wide reusable componentsProject-specific logic

AI-Assisted Coding in 2026

[!IMPORTANT] In 2026, nobody writes code from scratch. Leverage AI to accelerate development:

UiPath Autopilot (Built-in):

  • Right-click in Coded Workflow → “Generate with Autopilot”
  • Describe logic in natural language → Get C# implementation

GitHub Copilot (External):

  • Install Copilot extension in VS Code or Visual Studio
  • Write a comment describing your intent → Copilot completes the code

Prompting Patterns for RPA Code:

// Prompt: Validate Taiwan National ID format (A123456789)
// Copilot generates:
public bool ValidateTaiwanId(string id)
{
    if (string.IsNullOrEmpty(id) || id.Length != 10) return false;
    string pattern = @"^[A-Z][12]\d{8}$";
    return System.Text.RegularExpressions.Regex.IsMatch(id, pattern);
}

// Prompt: LINQ to group invoices by vendor and sum amounts
// Copilot generates:
var summary = invoices
    .GroupBy(i => i.VendorId)
    .Select(g => new {
        Vendor = g.Key,
        TotalAmount = g.Sum(i => i.Amount),
        Count = g.Count()
    })
    .OrderByDescending(x => x.TotalAmount);

[!TIP] Effective Prompting: Be specific about input/output types, edge cases, and error handling. “Validate email format and return reason if invalid” produces better code than just “validate email”.


Invoke Python: Machine Learning and Advanced Analytics

When C# isn’t enough—especially for data science—Python steps in.

Setting Up Python in UiPath

  1. Install Python on the robot machine
  2. Install UiPath.Python.Activities package
  3. Configure Python path in activity settings

Basic Python Invocation

Activity: Python Scope
├── Path: C:\Python39\python.exe
├── Target: x64
└── Activities:
    ├── Load Python Script: script.py
    ├── Invoke Python Method: method_name
    └── Get Python Object: result

Example 1: Pandas Data Processing

Python Script (process_data.py):

import pandas as pd

def process_invoice_data(file_path):
    # Read Excel
    df = pd.read_excel(file_path)
    
    # Clean data
    df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
    df = df.dropna(subset=['amount'])
    
    # Aggregate
    summary = df.groupby('vendor').agg({
        'amount': ['sum', 'count', 'mean']
    }).round(2)
    
    # Return as JSON (easily parsed in UiPath)
    return summary.to_json()

UiPath Workflow:

Python Scope
├── Load Python Script: "process_data.py"
├── Invoke Python Method
│   ├── Name: "process_invoice_data"
│   ├── Input: {excelFilePath}
│   └── Output: resultJson
└── (Parse JSON in UiPath)

Example 2: Machine Learning Classification

import joblib
import pandas as pd

# Load pre-trained model
model = joblib.load('invoice_classifier.pkl')

def classify_invoice(description, amount, vendor):
    # Create feature vector
    features = pd.DataFrame([{
        'description': description,
        'amount': float(amount),
        'vendor': vendor
    }])
    
    # Predict category
    prediction = model.predict(features)[0]
    confidence = max(model.predict_proba(features)[0])
    
    return {
        'category': prediction,
        'confidence': round(confidence, 3)
    }

When to Use Python vs C#

Use PythonUse C#
Data science (pandas, NumPy)General .NET integration
Machine learning (scikit-learn)Windows API calls
Natural language processingPerformance-critical code
Quick prototypingType-safe enterprise code
Existing Python codebase.NET library consumption

Calling External DLLs

Sometimes you need to call compiled libraries.

Method 1: Invoke Method Activity

For simple static methods:

Activity: Invoke Method
├── Target Type: MyCompany.Utilities.StringHelper
├── Method Name: SanitizeInput
├── Parameters: 
│   └── [0]: inputString (String)
└── Result: sanitizedString

Method 2: Invoke Code with Assembly Reference

// Add assembly reference in Invoke Code settings
// References: MyCompany.Utilities.dll

using MyCompany.Utilities;

var helper = new DataProcessor();
var result = helper.ProcessComplexData(inputData, options);
outputData = result.ToString();

Method 3: Load Assembly Dynamically

// Input: dllPath (String), inputData (String)
// Output: result (String)

var assembly = System.Reflection.Assembly.LoadFrom(dllPath);
var type = assembly.GetType("MyCompany.Utilities.Processor");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("Process");

result = (string)method.Invoke(instance, new object[] { inputData });

Building Custom Activities

For code you’ll reuse across projects, package it as a Custom Activity.

Why Custom Activities?

ApproachProsCons
Invoke CodeQuick, inline, no setupNot reusable, no IntelliSense
Workflow (XAML)Visual, reusableSlower, can’t use all .NET features
Custom ActivityReusable, IntelliSense, fastRequires C# knowledge, build pipeline

Creating a Custom Activity

Step 1: Create Class Library Project

dotnet new classlib -n MyCompany.RPA.Activities
cd MyCompany.RPA.Activities
dotnet add package UiPath.Workflow

Step 2: Implement Activity Class

using System;
using System.Activities;
using System.ComponentModel;

namespace MyCompany.RPA.Activities
{
    [DisplayName("Calculate Business Days")]
    [Description("Calculates a date N business days in the future")]
    public class CalculateBusinessDays : CodeActivity<DateTime>
    {
        [Category("Input")]
        [RequiredArgument]
        [DisplayName("Start Date")]
        [Description("The starting date for calculation")]
        public InArgument<DateTime> StartDate { get; set; }

        [Category("Input")]
        [RequiredArgument]
        [DisplayName("Business Days")]
        [Description("Number of business days to add")]
        public InArgument<int> BusinessDays { get; set; }

        [Category("Input")]
        [DisplayName("Exclude Holidays")]
        [Description("Optional list of holiday dates to skip")]
        public InArgument<DateTime[]> Holidays { get; set; }

        protected override DateTime Execute(CodeActivityContext context)
        {
            DateTime start = StartDate.Get(context);
            int days = BusinessDays.Get(context);
            DateTime[] holidays = Holidays.Get(context) ?? Array.Empty<DateTime>();

            DateTime current = start;
            int added = 0;

            while (added < days)
            {
                current = current.AddDays(1);
                
                if (current.DayOfWeek != DayOfWeek.Saturday &&
                    current.DayOfWeek != DayOfWeek.Sunday &&
                    !Array.Exists(holidays, h => h.Date == current.Date))
                {
                    added++;
                }
            }

            return current;
        }
    }
}

Step 3: Build NuGet Package

Create MyCompany.RPA.Activities.nuspec:

<?xml version="1.0"?>
<package>
  <metadata>
    <id>MyCompany.RPA.Activities</id>
    <version>1.0.0</version>
    <title>MyCompany RPA Utilities</title>
    <authors>RPA Team</authors>
    <description>Common utilities for RPA development</description>
    <tags>uipath activities</tags>
  </metadata>
  <files>
    <file src="bin\Release\net461\*.dll" target="lib\net461" />
  </files>
</package>

Build and pack:

dotnet build -c Release
nuget pack MyCompany.RPA.Activities.nuspec

Step 4: Publish to Company Feed

Upload .nupkg to:

  • Orchestrator Library Feed
  • Azure Artifacts
  • Internal NuGet server

Step 5: Use in UiPath

Install package in UiPath Studio. Activity appears in Activities panel with IntelliSense!

NuGet Version Conflicts: The Hidden Pain Point

[!CAUTION] The Most Frustrating Bug: Your Custom Activity works in Visual Studio but crashes in UiPath with FileLoadException or MissingMethodException.

Why This Happens:

UiPath uses specific versions of common libraries (e.g., Newtonsoft.Json 12.0.3). If your Activity references a different version (e.g., 13.0.1), you get version conflicts.

Solutions:

ApproachWhen to Use
Match UiPath’s versionBest option—use same version as UiPath
Binding RedirectsWhen you must use a specific version
ILMerge/ILRepackEmbed dependency into your DLL

Solution 1: Check UiPath’s Dependencies

Look at the installed packages in a UiPath project to see which versions are used, then match them in your .csproj:

<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

Solution 2: Binding Redirects (app.config)

If you must use a newer version, add to your .nuspec or include an app.config that the robot loads:

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" 
                          publicKeyToken="30ad4fe6b2a6aeed" />
        <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" 
                         newVersion="13.0.1.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

[!WARNING] Test thoroughly: Binding redirects can cause subtle runtime bugs. Always test your Activity in a real UiPath project before publishing.


Building Shared Libraries (Object Repository Alternative)

For sharing XAML workflows across projects:

Library Project Structure

MyCompany.RPA.Core/
├── project.json
├── README.md
├── Workflows/
│   ├── SAP/
│   │   ├── SAP_Login.xaml
│   │   ├── SAP_Logout.xaml
│   │   └── SAP_Create_PO.xaml
│   ├── Email/
│   │   ├── Email_SendWithAttachment.xaml
│   │   └── Email_ReadInbox.xaml
│   └── Utilities/
│       ├── Util_RetryMechanism.xaml
│       └── Util_LogTransaction.xaml
└── Tests/
    └── (Test workflows)

Publishing a Library

  1. Set project type to Library in project.json
  2. Mark workflows as callable (not just invokable)
  3. Document Input/Output arguments clearly
  4. Publish to Orchestrator or shared feed

Consuming a Library

Activity: Invoke Workflow File
├── File Name: MyCompany.RPA.Core\SAP_Login.xaml
└── Arguments:
    ├── in_Username: sapUser
    ├── in_Password: sapPassword
    └── out_SessionId: (captured)

Or better—after publishing as package:

Activity: SAP Login (from installed library)
├── Username: sapUser
├── Password: sapPassword
└── Output SessionId: sessionId

Code Organization Patterns

Pattern 1: Utility Class

Group related functions:

public static class StringUtilities
{
    public static string Sanitize(string input) { ... }
    public static string ExtractNumbers(string input) { ... }
    public static bool IsValidEmail(string email) { ... }
    public static string MaskPII(string text) { ... }
}

Pattern 2: Configuration Helper

public class ConfigHelper
{
    private Dictionary<string, object> _config;
    
    public ConfigHelper(string configPath)
    {
        _config = LoadFromExcel(configPath);
    }
    
    public string GetString(string key) => _config[key]?.ToString();
    public int GetInt(string key) => Convert.ToInt32(_config[key]);
    public bool GetBool(string key) => Convert.ToBoolean(_config[key]);
}

Pattern 3: Service Wrapper

Encapsulate external system interaction:

public class SalesforceService
{
    private string _accessToken;
    private string _instanceUrl;
    
    public void Authenticate(string clientId, string clientSecret) { ... }
    public JObject Query(string soql) { ... }
    public string CreateRecord(string objectType, JObject data) { ... }
    public void UpdateRecord(string objectType, string id, JObject data) { ... }
}

Debugging Custom Code

In Invoke Code

Add logging:

// Use System.Diagnostics.Debug for UiPath logs
System.Diagnostics.Debug.WriteLine($"Processing: {inputValue}");

try
{
    // Your code
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine($"Error: {ex.Message}");
    System.Diagnostics.Debug.WriteLine(ex.StackTrace);
    throw;
}

In Custom Activities

protected override DateTime Execute(CodeActivityContext context)
{
    // Log to UiPath
    context.GetExtension<System.Activities.Tracking.TrackingParticipant>();
    
    // Or write to trace
    System.Diagnostics.Trace.WriteLine("Executing CalculateBusinessDays");
    
    // Your logic
}

Testing Outside UiPath

Always test complex logic in pure .NET first:

// Test project
[TestMethod]
public void CalculateBusinessDays_SkipsWeekends()
{
    var friday = new DateTime(2024, 1, 12); // Friday
    var result = BusinessDayCalculator.AddDays(friday, 1);
    
    Assert.AreEqual(new DateTime(2024, 1, 15), result); // Monday
}

Key Takeaways

  1. Invoke Code is for small scripts (< 50 lines of focused logic).
  2. Coded Workflows are the future for complex logic with full IDE debugging and unit testing.
  3. Python fills the data science gap when C# isn’t enough.
  4. Custom Activities scale across projects with full IDE support.
  5. Libraries enable team reuse without copy-paste nightmares.
  6. Use dotnet pack to distribute your code as NuGet packages.
  7. Test code outside UiPath before integrating.

The best RPA developers know when to drag-drop and when to write code. Mastering both is what makes you indispensable.