Build dynamic workflows with azure durable functions (NoCode style)

Photo by Pixabay on Pexels.com

This is part of a mini-series where we want to build low code product on the shoulders of azure durable functions:

  1. Build dynamic workflows with azure durable functions (NoCode style) – Part 1 (this)
  2. Build dynamic workflows with azure durable functions (Low-Code style) – Part 2
  3. Add branching logic to your dynamic workflows with azure durable functions – Part 3

Some of the hottest buzzwords nowadays are “no-code” and “low code” and to be honest, I’ve been looking into building some kind of platform like this for years. Don’t get me wrong, this is not me saying that I am a clairvoyant and I’ve seen the future in the past, but me saying that I am lazy. I have always been looking to find some ways of pushing some of the application’s responsibilities to BAs / POs and, why not, the actual person using the system. Why do you ask? Let’s be honest here, most of the knowledge transfer that usually happens between domain experts and developers is going through a “translation” process. From a business-focused view to a more tech-focused view, some things get lost in translation as it goes most of the time.


If we manage to write some software that gives people the options to customize at least parts of their daily workflows, I think we have a much greater chance of success. Apart from this, there is also the idea that the only constant is change, and let’s say when a flow must change slightly, in the classic approach, we would need to do a ticket, a redeploy, etc.


If you have read many articles on this blog, you know we will build a ridiculous example to show off this idea. Yes, you are right. To do this, we will be building probably the most complicated simple calculator made ( I am sure there is like an enterprise java edition somewhere that will take the prize, but never the less).


Now you need to be careful because once you get the taste for metaprogramming and dynamic stuff, you are done. It’s no turning back. It’s more or less like opening pandora’s box. You get hooked. Now that I warned you, let’s see what we are building today.

This article assumes you have some working knowledge / experience building azure functions and azure durable functions, if you don’t no biggie, you can find several articles on this blog (like this), or hack even google it. Also, I recommend reading “Dynamically select orchestrators in Azure Durable Functions“.

The usual disclaimer stuff:

Everything you see here is mostly theoretical and has an illustrative purpose and I tried to simplify it as possible and still keep some best practices (ish…), although I use this techniques in production, what you see here totally lacks security, logging, exception handling, tests.

The Intro

Let’s imagine we have a UI like this, think some kind of Angular / React / Blazor, what ever rocks your boat, doesn’t really matter, we will not build this now, it is just for context.

As you can see, we have a drop-down were we pick the action, we have input box for the value and a button to add it to the flow. Then we have a list with all the steps, and then we have a save button at the bottom, quite straight forward I’d say.

Now, what does this have to do with dynamic workflows ? Well, up there we actually are building the workflow. In our case, we actually provided the building blocks like “Add”, “Multiply”, but we are giving the user of the application the freedom to choose on how they interact, meaning the order of the operations.

One more thing before we jump in the code, in this case we assume that we will be always starting from 0.

Good, now that we have all this settled, let’s assume that this data is saved in a storage somewhere in a form like this:

{
    "Name": "Complex Calculation",
    "Version": "1.8",
    "Steps": [
        {
            "Action": "Add", 
            "Param": 5
        },
        {
            "Action": "Add", 
            "Param": 1
        }
...
    ]
}

The coding part

This article if focused on building the back-end for this, so before we do any kind of azure durable magic we first need to setup all the building blocks / capabilities that we want to offer our users. In order to this for this example we are using simple azure functions.

        [FunctionName("Add")]
        public static async Task<int> Add([ActivityTrigger] (int a, int b) numbers)
        {
            return numbers.a + numbers.b;
        }

        [FunctionName("Subtract")]
        public static async Task<int> Subtract([ActivityTrigger] (int a, int b) numbers)
        {
            return numbers.a - numbers.b;
        }

        [FunctionName("Multiply")]
        public static async Task<int> Multiply([ActivityTrigger] (int a, int b) numbers)
        {
            return numbers.a * numbers.b;
        }

        [FunctionName("Divide")]
        public static async Task<int> Divide([ActivityTrigger] (int a, int b) numbers)
        {
            return numbers.a / numbers.b;
        }

As you see, nothing fancy going on here, just some pure and simple math.

Now next, in order to make this at least a bit more professional looking and a bit dynamic we will need a few extra constructs:

    public class DynamicStep<T>
    {
        public string Action { get; private set; }
        public T param { get; private set; }

        [JsonConstructor]
        public DynamicStep(string action, T param)
        {
            Action = action;
            this.param = param;
        }
    }

    public class DynamicResult<T>
    {
        public T Result { get; set; }
    }

    public static class ActionName
    {
        public const string Add = "Add";
        public const string Subtract = "Subtract";
        public const string Multiply = "Multiply";
        public const string Divide = "Divide";
    }

Here we have three lovely classes, which hopefully have quite some descriptive names, and if not yet clear what each does, it will become quite clear in the next snippet.

Next we have the orchestrator that will get the flow data saved from the UI and call the our magic “dynamic” orchestrator.

        [FunctionName("FlowMaker")]
        public static async Task<int> Run([OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            // imagine this comes from an api call or cosmos or something
            var steps = new DynamicFlowContext
            {
                Steps = new List<DynamicStep<int>>
                {
                    new DynamicStep<int>(ActionName.Add, 1),
                    new DynamicStep<int>(ActionName.Add, 1),
                    new DynamicStep<int>(ActionName.Subtract, 1),
                    new DynamicStep<int>(ActionName.Multiply, 10)
                }
            };

            var result = await context.CallSubOrchestratorAsync<DynamicResult<int>>("DynamicOrchestrator", steps);
            return result.Result;
        }

As you can see, so far this looks fairly standard, but now let’s see this magic “DynamicOrchestrator”

        [FunctionName("DynamicOrchestrator")]
        public static async Task<DynamicResult<int>> RunInnerOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext ctx)
        {
            var input = ctx.GetInput<DynamicFlowContext>();
            int state = 0;

            foreach (var step in input.Steps)
            {
                state = await ctx.CallActivityAsync<int>(step.Action, (state, step.param));
            }

            return new DynamicResult<int>
            {
                Result = state
            };
        }

I know right ? Kinda disappointing, probably you were hoping to find something very complicated and quite “smart” here, and yet, all you get is this lousy “for each” …

Now, in all seriousness, this is what I actually like about the in process model of the durable function frameworks. It allows you to build quite interesting constructs quite easy.

The conclusion

I’m hope you found this quite an interesting idea, but before you go full bananas there are some small issues that you should know about, or at least on big issue. Right now, there is no support for generic azure functions, which does complicate things a bit. What this means is that you will need to actually create a “magic” orchestrator per return type. But other than that I think this is quite an awesome trick.

This time, I did not create a git repo, but if you want to see the code, I put everything together in a gist right here.

If you liked the content and would like to be notified …. well you know the drill…

Processing…
Success! You're on the list.