User Tasks
User Tasks allow a WfRun
to stop and wait until a human user provides some input, and then resumes execution.
In order to use User Tasks in your workflows, you need to:
- Create a
UserTaskDef
, which defines the fields that our users need to provide. - Create a
WfSpec
that uses thatUserTaskDef
. - Run the
WfSpec
to create aWfRun
. - Complete the
UserTaskRun
.
For the sake of simplicity, every code sample in this document can be compiled and run on its own as a single file. You can even mix-and-match between different languages—try doing one step in Java and another in Python!
If you do want to follow along with the code here, we recommend checking out our installation docs to:
- Install
lhctl
. - Set up a LittleHorse Server for local development.
- Add the dependency for your SDK of choice.
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:
- We use a User Task to ask a workflow to provide their full name and favorite number.
- 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 TaskRun
s once you run your WfRun
.
- Java
- Go
- Python
- DotNet
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();
}
}
TODO
TODO
TODO
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
.
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 UserTaskDef
s works, please check out our Managing Metadata page.
- Java
- Go
- Python
- DotNet
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);
}
}
TODO
TODO
TODO
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:
- An
EntrypointNode
(allWfSpec
s have this). - A
UserTaskNode
which assigns a user-task to the specified user. - A
TaskNode
which executes aTaskRun
. - An
ExitNode
(allWfSpec
s have at least one of these).
We will define three variables in our WfSpec
:
- The
user-id
, which is the "user id" that we will assign theUserTaskRun
to. - The
favorite-team
, which is the user's favorite sports team. - 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.
- Java
- Go
- Python
- DotNet
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:

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
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"
}
]
}
In most real-world scenarios, if you are building an application that allows searching for UserTaskRun
s, you would likely want to use the rpc SearchUserTaskRun
. You can see examples of how to do that in our grpc user guide.
Check out the User Tasks Bridge, which removes most of the toil for finding and executing UserTaskRun
s!
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.
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!

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
UserTaskRun
s using the raw GRPC client. - Play around with
lhctl get userTaskRun <wfRunId> <userTaskGuid>
andlhctl list nodeRun <wfRunId>
and see if you can find out the relationship between them!