How to make Library Activities available in coded workflows

Coded workflows are really useful, and becoming even more useful with the rapid rise of coding agents, however a gap (at least in knowledge) that has existed for a few years is how to make your custom library activities able to be used in a coded workflow.

In this tutorial I will explain how you do that, both with UiPath Studio libraries as well as Visual Studio libraries, as both are compatible with this.

I also have developed some coding agent skills to assist in doing this, which I will also share.

To understand the way this works, it’s important to know that activities are exposed to coded workflows via a ‘Workflow Service’.

For example, here is the System activity packages workflow service that exposes lots of the activities from that nuget.


To do the same with your library project, you’ll need to create a similar service with 3 files.

The first one is the interface, which defines what activities will be exposed. The activities need to have the same arguments and return types as the workflows.

For example

public interface IRazorService
    {
        /// <summary>
        /// Compiles and caches a Razor template under a key, if not already cached.
        /// </summary>
        /// <param name="templateKey">Key identifying the template. Keys cannot be reused for different templates.</param>
        /// <param name="templateSourceText">The cshtml source of the template.</param>
        void AddRazorTemplate(string templateKey, string templateSourceText);

        /// <summary>
        /// Runs a cached (or supplied) Razor template against a model and returns the populated text.
        /// </summary>
        /// <param name="templateKey">Key identifying the template.</param>
        /// <param name="inputObject">The model whose values are merged into the template.</param>
        /// <param name="modelType">Model type. Leave null for anonymous/dynamic models.</param>
        /// <param name="templateSourceText">The cshtml source, used when the key is not already cached.</param>
        /// <returns>The populated template text.</returns>
        string PopulateRazorTemplate(
            string templateKey,
            object inputObject,
            Type modelType = null,
            string templateSourceText = null);
    }

Next you need to make the service that implements that interface.
In this class we need a specific constructor that receives the workflow runner service, which we assign to a private field.

Then, for each activity we do some basic validation on the arguments, then simply use the workflow service to invoke the relevant workflow (this works for both xaml and cs workflows as both appear on the workflow runner service).

For example (note, the names of the activities here differ slightly and contain underscores, that’s because the original activities contain spaces in their names, which are converted to underscores).

/// <summary>
    /// Exposes this library's workflows to consuming coded workflows as `razor.*`.
    ///
    /// Both workflows are invoked through the runtime rather than constructed directly:
    /// a `new`'d CodedWorkflow has null services (Log, workflows, system all NRE), because
    /// CodedWorkflowBase.services is only populated by OnWorkflowInitialization when the
    /// runtime hosts the workflow.
    ///
    /// WorkflowRunnerService is generated by the Studio compiler from the project's workflow
    /// files. It resolves the correct target assembly via ILibraryAssemblyProvider — necessary
    /// because this class lives in Roboyo.Razor.Activities.Core.dll while the generated
    /// MapFilePathToActivity_... lives in Roboyo.Razor.Activities.dll.
    /// </summary>
    public class RazorService : IRazorService
    {
        private readonly WorkflowRunnerService _workflows;

        public RazorService(ICodedWorkflowServices services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            _workflows = new WorkflowRunnerService(services);
        }

        public void AddRazorTemplate(string templateKey, string templateSourceText)
        {
            if (string.IsNullOrEmpty(templateKey))
                throw new ArgumentException("templateKey is required.", nameof(templateKey));
            if (string.IsNullOrEmpty(templateSourceText))
                throw new ArgumentException("templateSourceText is required.", nameof(templateSourceText));

            _workflows.Add_Razor_Template(templateKey, templateSourceText);
        }

        public string PopulateRazorTemplate(
            string templateKey,
            object inputObject,
            Type modelType = null,
            string templateSourceText = null)
        {
            if (string.IsNullOrEmpty(templateKey))
                throw new ArgumentException("templateKey is required.", nameof(templateKey));
            if (inputObject == null)
                throw new ArgumentNullException(nameof(inputObject));

            return _workflows.Populate_Razor_Template(
                templateKey,
                inputObject,
                modelType,
                templateSourceText);
        }
    }

The last class you need is a registry, that is used to find and register any services to be used on a coded workflow, as well as making sure any relevant namespaces required for your activities are added to the new coded workflows that use the service.

For this you also need an assembly reference to make sure it gets compiled in the correct way. This is placed above the namespace, before the using statements.

This is also where you select the name of the service, try to make sure it’s unique. In this one the name is ‘razor’.

For example

[assembly: CodedWorkflowsServiceRegistry(typeof(RazorRegistry))]

namespace Roboyo.Razor.Activities.CodedWorkflowService
{
    public class RazorRegistry : ICodedWorkflowsServiceRegistry
    {
        
        internal const string ServiceName = "razor";
        
        public IEnumerable<string> AutoImportedNamespaces
        {
          get
          {
            return new string[1]
            {
                "Roboyo.Razor.Activities.CodedWorkflowService"
            };
          }
        }

        public IDictionary<string, Type> AutoImportedTypes
        {
          get
          {
            return (IDictionary<string, Type>) new Dictionary<string, Type>()
            {
              {
                "razor",
                typeof (IRazorService)
              }
            };
          }
        }

        public void Register(
          ICodedWorkflowsServiceLocator serviceLocator,
          ActivityContext initialActivityContext)
        {
          serviceLocator.RegisterType<IRazorService, RazorService>();
        }
    }
}

Do all this and publish the library.
When you then go into a coded workflow you should see your service.

and the activities exposed on that service.

To achieve this in a Visual Studio custom activity package you must do similar to the above, but there are a few differences, for example we don’t have the workflow runner service to invoke a workflow, and we require a few more classes to do the mapping.

This will be covered in one of the Coding Agent skills, but in order to keep this tutorial concise, I will cover the details in a different post if there is a request to explain it.

This is the skill for UiPath Studio

Here is the one for Visual Studio

Hygiene note, I’ll make pull requests to get these into the main repo of UiPath skills and update the links to point to them there if and when they get merged.

Interesting post. I have always wondered why there was not a built-in way for coded workflows to invoke activities from libraries. Especially considering you can invoke other workflows in your project and workflows and library activities built in Studio are really not that different.

Hi @Jon_Smith

Interesting content thanks for your ref

Thanks & cheers!