Open jwillmer opened 3 years ago
That's certainly an interesting feature request. Having done something like this before, my concern with this kind of functionality is that it creates activities which may only be executed in a specific context. One of Elsa's strengths is that each activity (at least, the logic in the activity class) is self-contained. Activities whose logic references the outcome of previous activities create a tight-coupling which can make the overall workflow harder to maintain.
Could the goals not be achieved via existing mechanisms?
In your example, you mention "reading data about the weather". I don't know if you have a different use-case in mind, but I'm going to imagine you intend to follow this up with an activity that either sets up or collapses some sun parasols/umbrellas. If it will not be windy then the parasols are set up, if it will be too windy then they will be collapsed (we don't want them to blow away or be damaged).
The "Get Weather" activity would return an Output model. This could be done using a CombinedResult using functionality that is already in Elsa 2.0. The next activity could be a SetVariable activity which receives the output of the "Get Weather" activity as an Input (again, this is already implemented) and sets one or more workflow-level variables using that data model.
Then, our set-up/collapse parasol activity looks simply at those workflow variables for information about whether or not it is windy. It does not actually know or care where the data for those variables came from. This means that there is no tight coupling between the set-up/collapse parasol activity and the get-weather activity. Indeed, later you might change the way you get the weather info to a totally different type of activity, and as long as you set the variables up in the same way, the parasol-controlling activity doesn't need to be touched.
What do you think?
I see two problems with this approach. First the workflow gets very long since I always need to set a variable between processing units. And the second problem is that for me a workflow consists of tightly coupled processes. I have an input and an output, in between I do something based on the input.
I still think that the variable store is a cool feature that I would use like this (step 1 and 2 are script activities in my example):
but my step 2 would still like to use the input data and maybe even the output data from step 1 (note that output data does not need to be equal to the data send to the output object
)
@craigfowler
Having done something like this before, my concern with this kind of functionality is that it creates activities which may only be executed in a specific context. One of Elsa's strengths is that each activity (at least, the logic in the activity class) is self-contained. Activities whose logic references the outcome of previous activities create a tight-coupling which can make the overall workflow harder to maintain
To be clear, no one is suggesting that an activity's implementation itself should have any knowledge about any other activity. All an activity knows about is input it receives and its own state (properties). It's up to the user to provide expressions that evaluate at runtime, of which the resulting value will be set to the property in question. Such expressions could be anything: JavaScript, C#, Liquid, or any other random expression evaluator. Just as long as the resulting value is compatible with the property type.
Could the goals not be achieved via existing mechanisms?
- Activity Input & Output
- Workflow variables
If the goal is defined as "I want to reference output from another activity", then absolutely yes for Activity Input & Output, and also with Workflow Variables, although that would be inconvenient and totally unnecessary.
But as I understand this feature request, what is asked, is to provide an easy way (a visual way) to pick the output from a given activity to use as an input.
Without a visual activity output picker, the user has to manually type in some expression to access an activity's output. In C#, it would be e.g. context => context.GetActivityOutputFrom<WeatherData>("RequestWeatherData").Wind
if we hade a strongly typed model, or some JObject
and dynamic
sourcery to conveniently access & traverse the output object from the "RequestWeatherData" activity. In JavaScript, it would be e.g. RequestWeatherData.output.wind
. And this would be perfectly fine (especially with the intellisense support I'm working on at the moment).
In addition to having C# and JavaScript, we might conceive of an output picker control that simplifies this.
The questions I have however are:
IExpressionHandler
, allowing the user to write simple object expressions like RequestWeatherData.Wind
? The implementation would simply split the expression into "activity name" and an object path into the output object. The picker would simply generate the appropriate expression.Many ways to skin this cat.
I think the simplest thing is to introduce an object that each activity has access to. The object hold all data about previous steps and could look something like this:
{
activities: [
http_request: {
input_data: { .. },
output_data: { .. },
options: { .. }
}
],
global_variables: [ .. ],
workflow_options: { .. }
}
The question about the "rolling log of previously executed activities, with their inputs/outputs" is how you deal with these when it gets more complex.
In the example, we've got an HTTP request activity with inputs, outputs & arbitrary data. Now, what if there is more than one HTTP request activity in the workflow? You'll have to also name your activities so that you can distinguish one from the other. OK - that's fine because we have a mechanism for that. Now what if you have an HTTP request activity which executes inside a loop? The same-named activity runs more than once and creates iterations of its input/output/other data. We don't have a good way to tell those apart right now except by array indexing.
Additionally, there's your accidental coupling, because whatever activity needs that data from the HTTP request needs to know that it was the HTTP request activity class which provided that data. If it were read from a variable, then all it needs to know is the name of the variable (controlled 100% by the designer of the workflow), and not which actual concrete activity class was responsible for setting the variable.
The reason I suggest the use of variables is that the person designing the workflow can be very specific as to what the semantics of their data is, and store it in a way that makes sense to them.
What it does remind me about though is a feature I was thinking about (might even already be implemented in Elsa 2) which is Activity Composition. It think that it would be totally legit to have a "Get Weather" activity which returns its response as "output", and to compose that with a "Set Variable" activity which sets "whatever the output of the previous activity was" into a named variable.
This composition creates us a single "Get weather and store as a variable" activity, which will shorten & simplify the overall workflow design, whilst avoiding altering the simplicity of the overall design.
We could even provide a "template composable activity" like StoreOutputInVariable<TComposed>
, which is essentially a decorator for an activity (of concrete type TComposed
). It behaves-as and does everything the wrapped activity does, except that after execution, it also takes whatever the output from the activity was and puts it into a named variable.
@craigfowler regarding your suggestion of the "Template composable Activity" which require a change in code (to add the decorator) why not having a simple property on the Base Class Activity that allow the user to store the output value in the Variable Context?
And advanced property (which appeard in a advanced tab of the designer) of type string, which allow, when filled to store the output of the Activity.
@jdevillard the reason I wouldn't suggest adding to the base class is that it adds complexity to the overall architecture, which I really believe we should be keeping as simple as we can. It's also my general opinion that "adding extra functionality to the base class" is a sort of development trap. It looks very appealing because one change will automatically add functionality everywhere, but it's a path that leads to bloated base classes with tightly-coupled functionality.
It's almost always better to split that new functionality out into either a different service, with a new interface or a decorator class which allows for decoupling between the original impl and that extra functionality. That said - the more I think about a template composable activity, I don't really like that idea so much either. It seems it would require quite a lot of change for no real reason right now.
Everything else aside, I'd like to go back and look at the original requirement for this:
As a user I like to be able to reference (use) data from the previous activities in my current activity.
At least in the core (let's ignore the UI for a second), we're already ticking all of the right boxes without changing anything at all. In the UI, this is also already possible right now, but it's a bit annoying because of the manual/multi-step process:
Well, we could certainly make all of that a lot easier by only changing the UI. How about a 'wizard' (but let's not call it that 😆) of sorts which can do the first three of those steps with just one logical action. You choose your data-getting/generating activity type and a variable name, and the UI creates all of that stuff for you in one shot. For the last step, perhaps offer autocomplete (or similar) functionality for names of variables that have been used in the workflow already?
@craigfowler totally agree on the fact that we've to avoid complexity on code backend and try to add feature on designer to simplify user tasks !
the idea might work but I don't like it because we just move complexity around and add some extra to the user. I can already see that the user will be annoyed or does not understand how he can reference output from a past activity. the UI should be as simple as possible in order to make Elsa accessable to a big user base.
As I've said asked and pointed out in https://github.com/elsa-workflows/elsa-core/issues/803, I think that usage of code complete in expressions could be the way to easily "reference previous activity data". And this approach works "on top" of all the functionality that already exists in Elsa that were mentioned by @craigfowler 3 posts above :).
Autocomplete is a great start, but I also agree that having an activity output picker makes it easier for the user.
The challenge here is not so much the complexity of the activity output picker itself, but rather how to mix it with Literal/JS/Liquid expressions.
For example, let's say we want to send an email to an email address that was provided as an output from a previously executed activity. If all we supported was selecting activity output, then the picker would be perfect. But since we can use any data source, such as a string literal, a workflow variable, or even the result of a method call on some service that was made available to the scripting context, we need more than a value picker.
An initial version might be a simple as adding another "format" to a property editor, e.g.:
Or perhaps we could think of a little toolbar containing buttons to open an activity output picker and a workflow variable picker, which would provide a user-friendly UI to select something, but insert the appropriate syntax into the code editor.
We might want to checkout other products such as n8n and code-red to see how they do it.
Slightly related to this topic: we need to come up with something intelligent too for other controls, like dropdowns. As you can see in the previous screenshot shared above, there is a "Language" and "Voice" property on the "Speak Text" activity. These dropdowns are very user friendly, but disable advanced use cases where these values should be determined dynamically (using e.g. JS).
If we figure out a good UX to solve that, we might solve the activity output UX thing as well.
Or perhaps we could think of a little toolbar containing buttons to open an activity output picker and a workflow variable picker, which would provide a user-friendly UI to select something, but insert the appropriate syntax into the code editor.
I definitely like this one. It would provide code complete feature I wanted with even better/user friendlier way to access it (a window hierarchical that would show you all activity outputs present in workflow with all their child elements I guess?).
I'm currently thinking along the lines of the following:
The idea is that a given property's type has a default control that makes sense (and is configurable from C#). For example, when a property allows the user to select one value from a set of options, we'd render a dropdown list with these options.
But when the user wants to use an expression, they can do so by switching from 'Dropdown' to e.g. 'JavaScript', which will replace the dropdown control with a code editor.
The
Literal
syntax would provide a plain text view of the literal value that would be stored by the default control.
This would work for any type of control, where the user can always switch to advanced mode and select a different mode of input.
This might even work for selecting an activity's output or a workflow variable. In fact, this list could potentially be extensible by application code itself (which would require adding StencilJS components).
As a user I like to be able to reference (use) data from the previous activities in my current activity.
{{ activity:Webhook.output.label }}