Skip to main content

User Tasks

The purpose of a Workflow Engine is to coordinate processes that assign work. A TaskRun represents a unit of work assigned to a computer, but what about work assigned to a human? That's where User Tasks come in.

Motivation​

You might ask, why not just use an ExternalEvent? Technically, it is possible to implement similar functionality using just ExternalEvents rather than introducing a whole new concept into the API. The reason for this is that so many things about User Tasks are tied deeply into the logic of the WfRun itself, including assignment, reassignment, cancelling, lifecycle, and even simply scheduling a User Task.

For example, the Dashboard allows you to see a history of when a UserTaskRun was assigned, and to whom it was assigned:

A UserTask details modal showing the User Task's creation time, completion time, results submitted by the user, and the history of when it was assigned to each user.
A Screenshot of a User Task's details in LittleHorse Dashboard

In short, User tasks handle workflow use-cases which require the input, decision-making, or expertise of an actual person. Some common examples of user tasks include:

  • Workflow Approvals: Processes in which a specific person or group of people must review and authorize a business transaction.
  • KYC: Know-your-customer workflows in which a sales rep must input information about a customer (eg. billing information) before the business process can continue.
  • Data Input: Tasks involving filling out forms or providing specific information before the business process can continue.
  • Manual Calculations: Situations that require human intervention to perform calculations, analyses, or assessments that cannot be easily automated and wherein we can't trust Chat GPT πŸ˜‰.

The addition of the User Tasks feature allows LittleHorse to seamlessly automate workflows spanning humans and computers across multiple departments within an organization and beyond.

Concepts​

In the LittleHorse API, User Tasks are represented and controlled by three objects:

  1. The UserTaskDef object defines the schema of...
  2. The UserTaskRun object, which is created when a ThreadRun arrives at a...
  3. UserTaskNode object, which is a type of Node in a WfSpec.

User Task Lifecycle​

When a WfRun arrives at a UserTaskNode, LittleHorse creates a UserTaskRun. The WfRun will wait until the rpc CompleteUserTaskRun request is made for that UserTaskRun (in plain English, the WfRun blocks until a person executes the task).

The UserTaskDef is what defines the fields in the form that the user must execute.

info

User Tasks in LittleHorse are unopinionated. This means that the actual form that a user uses in order to execute a UserTaskRun can live anywhereβ€”it can be in LittleHorse Enterprises' User Tasks Bridge product, using lhctl, or in a homegrown UI that makes API calls to LittleHorse.

Users and Groups​

A UserTaskRun may be assigned to either a user_id or a user_group. Both user_id and user_group are just plain Strings in LittleHorse, and are not validated with any external third-party identity provider.

At creation time, UserTaskRun are assigned to the user id or group id that is specified in the WfSpec. User Tasks in LittleHorse support automatic reassignment, reminder TaskRuns, automatic cancellation after a configurable timeout, and are also searchable based on their owner.

A UserTaskRun is an instance of a UserTaskDef assigned to a human user or group of users. Just like a TaskRun, the UserTaskRun is an object that can be retrieved from the LittleHorse API using lhctl or the grpc clients.

Like TaskRuns, the output of the UserTaskRun is used as the output of the associated NodeRun. In other words, the output of a USER_TASK node is a Json Variable Value with a key for each field in the UserTaskDef.

UserTaskRun Status and Hooks​

A UserTaskRun can be in any of the following statuses:

  • UNASSIGNED, meaning that it isn't assigned to a specific user. If a UserTaskRun is UNASSIGNED, it is guaranteed to be associated with a user_group, and the user_id field will be un-set.

  • ASSIGNED means that a task is assigned to a specific user_id. The UserTaskRun may or may not have a user_group.

  • CANCELLED denotes that the UserTaskRun has been cancelled for some reason, either manually, due to timeout, or due to other conditions in the WfRun. CANCELLED is currently a terminal state.

  • DONE Once a user execute a user task, it moves to the terminal DONE state.

Another useful feature of LittleHorse User Tasks are hooks which allow you to automate certain lifecycle behaviors of User Tasks when certain time periods expire.

Use-cases include:

  • Re-assigning a User Task to a different user after a certain period of time expires.
  • Sending reminders to users or groups of users after a certain time period.
  • Releasing ownership of a User Task from a specific user to a group of users after a period of inactivity.
note

Check our WfSpec Development Docs to see how this works.

In Practice​

tip

If you want to follow along, all of the code snippets in this section can be copy-n-pasted and executed as standalone files. However, to do this, you'll need access to a LittleHorse Server and lhctl. You can do that via the instructions in our installation docs or by running the following commands:

  • brew install littlehorse-enterprises/lh/lhctl
  • docker run --rm -d --name littlehorse --pull always -p 2023:2023 -p 8080:8080 ghcr.io/littlehorse-enterprises/littlehorse/lh-standalone:latest

In order to use User Tasks in your workflows, you need to:

  1. Create a UserTaskDef, which defines the fields that our users need to provide.
  2. Create a WfSpec that uses that UserTaskDef.
  3. Run the WfSpec to create a WfRun.
  4. Complete the UserTaskRun.

Setting up Metadata​

In this section, we will create a TaskDef, UserTaskDef, and WfSpec for our example. The example we will build involves a workflow in which:

  1. We use a User Task to ask a workflow to provide their full name and favorite number.
  2. We send that person a fictious greeting with the provided information.

Background: The TaskDef​

Let's use a TaskDef called report-favorite-player, and deploy a task worker for it all at the same time. This example focuses on User Tasks, so we won't go into detail about how the Task Worker works.

Run the following code in a terminal to register the TaskDef and run your Task Worker. Keep it running for the duration of this exercise so that the worker can execute TaskRuns once you run your WfRun.

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.worker.LHTaskMethod;
import io.littlehorse.sdk.worker.LHTaskWorker;

class PlayerReporter {

@LHTaskMethod("report-favorite-player")
public String reportFavoritePlayer(String user, String team, int player) {
String result = user + "'s favorite player is #" + player + " on the " + team + " team!";
System.out.println(result);
return result;
}
}

public class Main {

public static void main(String[] args) {
LHConfig config = new LHConfig();

// Create a Task Worker
LHTaskWorker worker = new LHTaskWorker(new PlayerReporter(), "report-favorite-player", config);
Runtime.getRuntime().addShutdownHook(new Thread(worker::close));

// Register the TaskDef
worker.registerTaskDef();

// Start the Worker
worker.start();
}
}

Create the UserTaskDef​

A UserTaskDef defines the fields on the form that must be completed by a human user in order to advance the WfRun.

In order to create a UserTaskDef, you must execute the rpc PutUserTaskDef using a PutUserTaskDefRequest.

warning

Before you can create a WfSpec that has a User Task in it, you must first create a UserTaskDef. This is similar to how you must first register a TaskDef before you can use it inside a WfSpec.

For more details on how the creation of UserTaskDefs works, please check out our Managing Metadata page.

In Java, our SDK provides the UserTaskSchema and UserTaskField utilities which make it easy to generate a PutUserTaskDefRequest from a POJO.

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.common.proto.PutUserTaskDefRequest;
import io.littlehorse.sdk.usertask.UserTaskSchema;
import io.littlehorse.sdk.usertask.annotations.UserTaskField;

class FavoritePlayerForm {

@UserTaskField(displayName = "Favorite Team", required = true)
public String favoriteTeam;

@UserTaskField(displayName = "Favorite Player's Number", required = true)
public int favoritePlayerNumber;
}

public class Main {

public static final String USER_TASKDEF_NAME = "report-favorite-player";

public static void main(String[] args) {
LHConfig config = new LHConfig();

// Compile the UserTaskDef using the UserTaskSchema utility.
UserTaskSchema schema = new UserTaskSchema(new FavoritePlayerForm(), USER_TASKDEF_NAME);
PutUserTaskDefRequest request = schema.compile();

// Register the UserTaskDef using the client
config.getBlockingStub().putUserTaskDef(request);
}
}

Regardless of which language you use, the above will create a UserTaskDef with the name favorite-number that looks as follows:

->lhctl get userTaskDef favorite-player
{
"name": "favorite-player",
"version": 0,
"fields": [
{
"name": "favoriteTeam",
"type": "STR",
"displayName": "Favorite Team",
"required": true
},
{
"name": "favoritePlayerNumber",
"type": "INT",
"displayName": "Favorite Player's Number",
"required": true
}
],
"createdAt": "2025-01-29T05:26:09.532Z"
}

Define the WfSpec​

After we have created our UserTaskDef and TaskDef (and we have the Task Worker running in another terminal), it's time to register our WfSpec.

No matter which language you use, the WfSpec will be the same, and it will have four nodes:

  1. An EntrypointNode (all WfSpecs have this).
  2. A UserTaskNode which assigns a user-task to the specified user.
  3. A TaskNode which executes a TaskRun.
  4. An ExitNode (all WfSpecs have at least one of these).

We will define three variables in our WfSpec:

  1. The user-id, which is the "user id" that we will assign the UserTaskRun to.
  2. The favorite-team, which is the user's favorite sports team.
  3. The favorite-player-number, which is the jersey number of the user's favorite player.

We will use a UserTaskRun to set the values of favorite-team and favorite-player-number, and then we will pass those variables into the report-favorite-player task.

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.LHLibUtil;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.common.proto.PutWfSpecRequest;
import io.littlehorse.sdk.wfsdk.UserTaskOutput;
import io.littlehorse.sdk.wfsdk.WfRunVariable;
import io.littlehorse.sdk.wfsdk.Workflow;
import io.littlehorse.sdk.wfsdk.WorkflowThread;

public class Main {

public static void wfLogic(WorkflowThread wf) {
WfRunVariable userId = wf.declareStr("user-id").searchable().required();
WfRunVariable favoriteTeam = wf.declareStr("favorite-team");
WfRunVariable favoriteNumber = wf.declareInt("favorite-player-number");

String userTaskDefName = "report-favorite-player";
String userGroup = null; // We aren't using groups in this example
UserTaskOutput formResult = wf.assignUserTask(userTaskDefName, userId, userGroup);

// formResult can be treated like a JSON_OBJ variable with a key for each
// field in the `report-favorite-player` UserTaskDef
favoriteTeam.assign(formResult.jsonPath("$.favoriteTeam"));
favoriteNumber.assign(formResult.jsonPath("$.favoritePlayerNumber"));

// Execute a TaskRun
wf.execute("report-favorite-player", userId, favoriteTeam, favoriteNumber);
}

public static void main(String[] args) {
LHConfig config = new LHConfig();

Workflow wfGenerator = Workflow.newWorkflow("favorite-player-demo", Main::wfLogic);

// Just register the WfSpec.
wfGenerator.registerWfSpec(config.getBlockingStub());

// BELOW IS JUST FOR ILLUSTRATIVE PURPOSES
// If you want to see the actual PutWfSpecRequest:
PutWfSpecRequest rawRequest = wfGenerator.compileWorkflow();
// Print it out in json format
System.out.println(LHLibUtil.protoToJson(rawRequest));
}
}

After running the code above (in a language of your choice), you should see the a WfSpec called favorite-player-demo which looks like the following:

A WfSpec on the LittleHorse Dashboard with a UserTaskNode and a TaskNode.
The favorite-player-demo WfSpec

Run the Workflow​

Now that we've registered the necessary metadata for our User Tasks workflow, all that remains is to run the WfSpec (and thus create a WfRun) and execute the UserTaskRun.

Start a WfRun​

You can start a WfRun using lhctl as follows. We will use obiwan as our user id.

lhctl run favorite-player-demo user-id obiwan
tip

In a real-world scenario, you would most likely want to start the WfRun programmatically (eg. in a handler for a REST API). You can do that following this documentation.

Find the UserTaskRun​

You can find the UserTaskRun by searching for entries in the report-favorite-player UserTaskDef which are assigned to obiwan.

lhctl search userTaskRun --userTaskDefName report-favorite-player --userId obiwan

The results show a wfRunId and a userTaskGuid. Make a note of these values, as we'll use them in the next step (which is )

{
"results": [
{
"wfRunId": {
"id": "9bb4d2077d984acba1c0eb2f8d3ff54d"
},
"userTaskGuid": "61d3ea725e37484491a8660cfe1e9b20"
}
]
}
warning

In most real-world scenarios, if you are building an application that allows searching for UserTaskRuns, you would likely want to use the rpc SearchUserTaskRun. You can see examples of how to do that in our grpc user guide.

tip

Check out the User Tasks Bridge, which removes most of the toil for finding and executing UserTaskRuns!

Complete the UserTaskRun​

The last thing we need to do is to complete the UserTaskRun. You can do that with the lhctl complete userTaskRun <wfRunId> <userTaskGuid> command, or you can do it using the GRPC API.

tip

For information about how to complete a UserTaskRun using GRPC, check out the relevant grpc user guide docs.

Here's the console output from going through the process of completing the UserTaskRun from the previous step using lhctl:

->lhctl execute userTaskRun 9bb4d2077d984acba1c0eb2f8d3ff54d 61d3ea725e37484491a8660cfe1e9b20
Executing UserTaskRun 9bb4d2077d984acba1c0eb2f8d3ff54d 61d3ea725e37484491a8660cfe1e9b20
Enter the userId of the person completing the task: obiwan

Field: Favorite Team
Please enter the response for this field (STR): San Jose Sharks

Field: Favorite Player's Number
Please enter the response for this field (INT): 19
Saving userTaskRun progress!
{}

Wrapping Up​

Next, check the terminal that we started in the first step that has the Task Worker running. You can see that the task was executed:

obiwan's favorite player is #19 on the San Jose Sharks team!
22:24:16 DEBUG [LH] ScheduledTaskExecutor - Task executed for: report-favorite-player

And on the dashboard, our WfRun is completed!

A completed `WfRun` on the LittleHorse Dashboard from the example on this page.
The completed WfRun.

Further Resources​

Hopefully, this recipe de-mystifies the process of working with User Tasks in LittleHorse. However, it just scratches the surface of what you can do with this powerful feature.

To take your User Tasks skills to the next level, check out these resources in our documentation:

  • Reminder Tasks, which let you execute a TaskRun to remind someone about a pending task.
  • Re-Assigning UserTaskRuns, which allows you to change the person to whom the UserTaskRun is assigned.
  • Automatic Reassignment, which allows you to reassign a task if it is not completed within a certain time frame.
  • Groups and Users, how to assign to users and groups, and how they are related.
  • The User Tasks Bridge product, which acts as a bridge between the LittleHorse Server and your SSO provider, simplifying the process of finding and executing User Tasks.
  • The Managing User Tasks, which goes into detail about how to find, assign, cancel, and complete UserTaskRuns using the raw GRPC client.
  • Play around with lhctl get userTaskRun <wfRunId> <userTaskGuid> and lhctl list nodeRun <wfRunId> and see if you can find out the relationship between them!