Modeling human tasks

I’ve modelled human approvals (e.g. waiting for 3 approvals) in a very simple way:

        var result2 = Async.function(activityStub::waitForApproval, "Approval 2");
        var result3 = Async.function(activityStub::waitForApproval, "Approval 3");
        var result4 = Async.function(activityStub::waitForApproval, "Approval 4");

        Promise.allOf(result2, result3, result4).get();

        logger.info("Got all approvals with messsage {} , {}, {}", 
            result2.get().message(), 
            result3.get().message(), 
            result3.get().message());

The waitForApproval functions is like:

    public Approval waitForApproval(String param) {
        
        ActivityExecutionContext executionContext = Activity.getExecutionContext();
        logger.info("Waiting for appoval {}, Task token: {}", param, executionContext.getTaskToken());

        executionContext.doNotCompleteOnReturn();

        return null;

    }

To avoid retries in a loop my activity options look like this

new HashMap<String, ActivityOptions>() {{
        put("WaitForApproval", ActivityOptions.newBuilder()
            .setStartToCloseTimeout(Duration.ofDays(2))
        .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).build())
            .build()
            );
    }};

It this approach safe? Does it consume some additional resources?

This is my first day with Temporal and I see that await + signals are recommended, but this is so much simpler.

It is simpler, but it doesn’t work because the activity task can be lost before the approval was sent to an external system. So it will take 2 days for it to be timed out.

So using activity + signal/update is the way to go.

@peperg I started a PR that takes the approach that Maxim mentions (and I realize now that I never merged it)

If you think this is useful let me know and I can work on it this week.

The example uses an external workflow to store the list of tasks and and their lifecycle.

Antonio

Hi! Thank you for your answers. Modeling human tasks is very important and as a new user I find it tricky to understand how to do it correctly in Temporal. Here are some of my thoughts so far:

  1. In my approach I was not thinking about sending the approval request but just use temporal API to check for workflow activities pending (like temporal workflow describe). Then I don’t have to have a separate system, just “in-temporal” approvals.
  2. The WaitForApproval task does not do anything in code, just waits. It’s return type describes what user input form must provide back.
  3. You are right that if I wanted to have 2 tasks - RequestForApproval and WaitForApproval i don’t have a way to pass required activityId or TaskToken
  4. In my approach the workflow status explicitly shows what the workflow is waiting for, right away in the UI. Operations team can quickly answer “Where is the process stuck?”

When considering the await + signal approach here are the drawbacks I see:

  1. For developers coming from BPM (Camunda, Flowable) or other tools (Kestra, Conductor), it is natural way that human activities are modeled as tasks/activities. They’ll look for HumanTask or something similar. Modelling as timer + flag + update function.
  2. The fact that Workflow.await timer is not completed when the condition is met and developer needs to wrap it into cancellation scope makes it quite complex.
  3. Some state/data “leaks” into the workflow code. I need to have a property to update in Signal and then something checks it. Looks like synchronization flags. Maybe I could just complete some Promise instead somehow in the signal function?
  4. The UI shows that the process is stuck on timer, or few timers. There are no pending activities, just timer.

@antonio.perez I’m looking into the sample from the PR and I see that this is super complex, hiding the complexity as sub-workflows but the WorkflowWithTasksImpl looks quite nice. I need to run it to see how it looks like in UI for troubleshooting.

Hi @peperg

my two cents,

In my approach I was not thinking about sending the approval request but just use temporal API to check for workflow activities pending (like temporal workflow describe). Then I don’t have to have a separate system, just “in-temporal” approvals.

This can be very inefficient if you have many workflows with pending activities. How the system knows how many pending tasks the user X has?

In my approach the workflow status explicitly shows what the workflow is waiting for, right away in the UI. Operations team can quickly answer “Where is the process stuck?”

I think you should use queries to check the internal state

For developers coming from BPM (Camunda, Flowable) or other tools (Kestra, Conductor), it is natural way that human activities are modeled as tasks/activities. They’ll look for HumanTask or something similar. Modelling as timer + flag + update function.

nothing stops you from creating your customs wrappers, similar to how it is done on the example I shared.

The fact that Workflow.await timer is not completed when the condition is met and developer needs to wrap it into cancellation scope makes it quite complex.

please refer to my previous comment

Some state/data “leaks” into the workflow code. I need to have a property to update in Signal and then something checks it. Looks like synchronization flags. Maybe I could just complete some Promise instead somehow in the signal function?

I am unsure why this is wrong, but same.. “taskService” helper class or something similar can help with this.

The UI shows that the process is stuck on timer, or few timers. There are no pending activities, just timer.

If that is the issue you could create a child workflow with the activity+signal implementation. To me, this feels like overkill, but it would work the same way.

Antonio

Thank you. I am trying to implement this using signal approach. This is my code for 3 approvals. It is quite complex and still timeout is not there (I know I need cancellation scope for this)

private final Map<String, Promise<Approval>> approvals = new HashMap<>();

    @Override
    public void startOnboarding(OnboaringData onboaringData) {

        activityStub.recordNewClient(onboaringData);
        logger.info("Waiting for approval");

        requestForApproval();
        requestForApproval();
        requestForApproval();

        var allApprovals = Promise.allOf(this.approvals.values());

        Workflow.await(() -> allApprovals.isCompleted()); // <-- Is this line needed?
        allApprovals.get();

        String msg = "";
        for (Promise<Approval> valPromise : this.approvals.values()) {
            msg += valPromise.get().message() + " , ";
        }

        logger.info("Got all approvals {}", msg);
    }

    private void requestForApproval() {
        var taskId = Workflow.randomUUID().toString();
        activityStub.requestForApproval(taskId);
        CompletablePromise<Approval> promise = Workflow.newPromise();
        this.approvals.put(taskId, promise);
    }

    @Override
    public void approve(String taskId, String message) {
        var promise = (CompletablePromise<Approval>) this.approvals.get(taskId);
        promise.complete(new Approval(message, Status.APPROVED));
    }

    @Override
    public String getWaitingStep() {
        String res = "";
        for (Entry<String, Promise<Approval>> e : this.approvals.entrySet()) {
            if (!((CompletablePromise<Approval>) e.getValue()).isCompleted()) {
                res += e.getKey() + ",";
            }
        }
        return res;
    }

The UI looks like this:

Comparing to the async task, which is more clear.

I’ll try a sub-workflow instead.

//EDIT:

Sub-workflows approach looks best.

I can list all manual tasks, as workflows, use memos, queries and signals and the main workflow clearly tells me where it is stuck.

Thank you for the great feedback, we certainly should consider providing a higher-level abstraction for human tasks.

1 Like