Variables
A WfSpec
in LittleHorse gives developers a distributed version of all of the primitives you use when writing code that runs on a single machine. This includes variables.
Concepts
In LittleHorse, a Variable
is defined by a WfSpec
and is created when a WfRun
is run. A Variable
can be used inside a WfRun
and it can also be fetched as a LittleHorse API Object.
Variables allow you to:
- Pass different inputs into different
WfRun
instances (via therpc RunWf
request). - Pass data between different nodes in the
WfSpec
(eg. pass data between tasks). - Search for specific
WfRun
instances based on variable values.
Variables are defined in the WfSpec
. Each variable definition has a name and a type. When you run your WfSpec
and create a WfRun
, LittleHorse creates a Variable
object for each variable definition in the WfSpec
. These Variable
s are then used by the WfRun
and can also be inspected and fetched from the LittleHorse API.
Declaring Variables
Just like in programming languages, LittleHorse supports different types of variables that allow you to store various kinds of data and handle a wide range of tasks. Before you can use a Variable, you need to declare it. The following shows how to define a variable of each type in your WfSpec
.
- Java
- Python
- Go
- C#
WfRunVariable itemId = wf.declareStr("item-id");
WfRunVariable quantity = wf.declareInt("quantity");
WfRunVariable price = wf.declareDouble("price");
WfRunVariable isPaid = wf.declareBool("is-paid");
WfRunVariable order = wf.declareJsonObj("order");
WfRunVariable items = wf.declareJsonArr("items");
TODO
TODO
TODO
Modifying Variables
When you execute a Task in LittleHorse, the SDK returns a NodeOutput
object. This object contains the output of the task and can be used to modify the value of a variable in the WfSpec
. You can use this handle to modify the value of a variable in the WfSpec
by using the WfRunVariable
's assign()
method.
- Java
- Python
- Go
- DotNet
// The .required() makes it a required input variable
WfRunVariable itemId = wf.declareStr("item-id").required();
WfRunVariable price = wf.declareDouble("price");
NodeOutput priceOutput = wf.execute("get-price", itemId);
price.assign(itemId);
TODO
TODO
TODO
You can form complex expressions with LittleHorse Variables. Check out the .add()
, .jsonPath()
, and other methods on the WfRunVariable
😃.
JSON Variables
LittleHorse's SDK has advanced support for JSON. The .jsonPath()
method in the WfRunVariable
allows you to access and modify sub-fields of a JSON_ARR
or JSON_OBJ
variable. You can also use .jsonPath()
on a NodeOutput
(i.e. the output of a Task that returns a JSON Object) to access fields within the output of a Task.
- Java
- Python
- Go
- DotNet
WfRunVariable itemId = wf.declareStr("item-id").required();
WfRunVariable item = wf.declareJsonObj("item");
NodeOutput itemOutput = wf.execute("fetch-item", itemId);
item.assign(itemOutput);
// Accessing a sub-field of a JSON Object
wf.execute("charge-credit-card", item.jsonPath("$.price"));
// Mutate a subfield of a JSON Object
item.jsonPath("$.isSold").assign(true);
TODO
TODO
TODO
JSON variables in LittleHorse do not have type information or schemas. This can make debugging with them quite difficult. In order to address this limitation, we are working on adding a new type of variable to LittleHorse, called a Struct
. For more information, follow this GitHub Issue.
Variable Modifiers
Just like Java (private
, public
, final
, static
, etc), variables in LittleHorse can have modifiers. Modifiers in LittleHorse include:
.required()
, which makes aVariable
a required input to the workflow (specifically, theThreadSpec
)..searchable()
, which allows you to search for instances of the variable by their value..public()
, which makes the Variable considered as part of theWfSpec
's public API. This is important for upcoming (very advanced) LittleHorse features such as the Output Topic.
In Practice
In order to use Variables in a workflow, you need to:
- Register a
WfSpec
that defines someVariable
s. - Run a
WfRun
, passing in those variables (if they are required as input variables).
After this, you can observe the result on the LittleHorse Dashboard.
Registering the WfSpec
In this example, we will build a WfSpec
that models a simple user notification workflow. The WfSpec
requires as input a user-id
and a message
. It then loads information about the user (this is a TaskRun
that most likely hits a user microservice) and uses that information to formulate a message to send via the email
TaskDef
.
Background: the Tasks
We'll create two TaskDef
s for this example. The first one will take in a String userId
and return a User
object (which will be serialized as a JSON_OBJ
variable value in LittleHorse). The second takes in an email address and a message and sends an email.
package io.littlehorse.quickstart;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.common.exception.LHTaskException;
import io.littlehorse.sdk.worker.LHTaskMethod;
import io.littlehorse.sdk.worker.LHTaskWorker;
class User {
public String email;
public String title;
public int age;
public User() {}
public User(String email, String title, int age) {
this.email = email;
this.title = title;
this.age = age;
}
}
class MyTasks {
// This Task Method returns a POJO, which is automatically serialized to a JSON_OBJ variable
// value in LittleHorse.
@LHTaskMethod("fetch-user")
public User fetchUser(String userId) {
if (userId.equals("obiwan")) {
return new User("obiwan@jedi.temple", "Master Kenobi", 37);
} else if (userId.equals("anakin")) {
return new User("anakin@jedi.temple", "Padawan Skywalker (not Master)", 22);
} else {
throw new LHTaskException("user-not-found", "Could not find specified user");
}
}
@LHTaskMethod("send-email")
public String sendEmail(String toAddress, String message) {
String result = "sent email " + message + " to address " + toAddress;
System.out.println(result);
return result;
}
}
public class Main {
public static void main(String[] args) throws Exception {
LHConfig config = new LHConfig();
MyTasks taskFuncs = new MyTasks();
LHTaskWorker emailer = new LHTaskWorker(taskFuncs, "send-email", config);
LHTaskWorker userService = new LHTaskWorker(taskFuncs, "fetch-user", config);
emailer.registerTaskDef();
userService.registerTaskDef();
Runtime.getRuntime().addShutdownHook(new Thread(emailer::close));
Runtime.getRuntime().addShutdownHook(new Thread(userService::close));
emailer.start();
userService.start();
}
}
At this point, you should have two new TaskDef
s available in your LittleHorse cluster.
The WfSpec
We'll create the following variables in our WfSpec
:
user-id
, of typeSTR
, which is required as input.email
, of typeSTR
, which is searchable.age
, of typeINT
, which is just used internally.
package io.littlehorse.quickstart;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.wfsdk.LHFormatString;
import io.littlehorse.sdk.wfsdk.NodeOutput;
import io.littlehorse.sdk.wfsdk.WfRunVariable;
import io.littlehorse.sdk.wfsdk.Workflow;
import io.littlehorse.sdk.wfsdk.WorkflowThread;
import io.littlehorse.sdk.common.proto.VariableType;
public class Main {
public static String WF_NAME = "variables-example";
public static void wfLogic(WorkflowThread wf) {
WfRunVariable userId = wf.declareStr("user-id").required().searchable();
WfRunVariable userObject = wf.declareJsonObj("user-obj");
WfRunVariable age = wf.declareInt("age");
// This task returns a json blob (the `User` class)
NodeOutput userOutput = wf.execute("fetch-user", userId);
// You can take a Json output from a task and just copy it into a JSON_OBJ variable
userObject.assign(userOutput);
// You can also use specific fields of the task output to edit a non-json variable
age.assign(userOutput.jsonPath("$.age"));
// This is somewhat advanced; we create an expression which combines a few
// strings together.
LHFormatString message = wf.format(
"Hello there, {0}! You are {1} years old",
userObject.jsonPath("$.title"),
age);
wf.execute("send-email", userObject.jsonPath("$.email"), message);
}
public static void main(String[] args) throws Exception {
LHConfig config = new LHConfig();
Workflow wfGenerator = Workflow.newWorkflow(WF_NAME, Main::wfLogic);
wfGenerator.registerWfSpec(config.getBlockingStub());
}
}
In the above example,
At this point, you should see the WfSpec
in the dashboard. It will look something like this:

Running the WfRun
Now let's run the workflow!
->lhctl run variables-example
2025/03/08 11:32:31 rpc error: code = InvalidArgument desc = Must provide required input variable user-id of type STR
Oops, that didn't go too well. The LittleHorse Server got grumpy with us because our WfSpec
has a required variable user-id
and we didn't pass it in. Let's fix that:
lhctl run variables-example user-id anakin
Now, if we go to the Dashboard, we can see the WfRun
is COMPLETED
. When we click on the WfRun
, we can easily inspect the ending state of the variables after all of the mutations happened:

If you inspect the user-object
variable, you'll notice it exactly matches the output of the fetch-user
task, which is just the User
object serialized to json.
{
"email": "anakin@jedi.temple",
"title": "Padawan Skywalker (not Master)",
"age": 22
}
Have some fun and explore! Look at what was passed into each of your TaskRun
s and compare that to our WfSpec
code. This will help you understand the parallels between LittleHorse Variable
s + WfSpec
s and Java variables + programs.
Searching by Variables
Before we start searching, let's run one more WfRun
:
lhctl run variables-example user-id obiwan
You can search for a Variable
using rpc SearchVariable
, or via lhctl
as follows:
lhctl search variable --wfSpecName variables-example --name user-id --varType STR --value anakin
The output looks like:
{
"results": [
{
"wfRunId": {
"id": "52357bdec63241ed9b3dfef2b5f4a1d4"
},
"threadRunNumber": 0,
"name": "user-id"
}
]
}
The command above searches for Variables with a specific type. The VariableId
includes a WfRunId
, so you can easily find the associated WfRun
.
Let's look at the actual Variable
:
lhctl get variable 52357bdec63241ed9b3dfef2b5f4a1d4 0 user-id
The output shows that the value is indeed anakin
!
{
"id": {
"wfRunId": {
"id": "52357bdec63241ed9b3dfef2b5f4a1d4"
},
"threadRunNumber": 0,
"name": "user-id"
},
"value": {
"str": "anakin"
},
"createdAt": "2025-03-08T19:33:45.431Z",
"wfSpecId": {
"name": "variables-example",
"majorVersion": 0,
"revision": 0
},
"masked": false
}
Lastly, you can search for variables on the dashboard, by going to the WfSpec
page and clicking Search By Variable
.
Please note that, while Obi-Wan Kenobi is indeed a Jedi Master, Anakin Skywalker is not.
Gotchas
LittleHorse Variables are designed to be as close to programming language variables as possible. However, there are some key differences to be aware of.
Why WfRunVariable
is Special
It's important to note that your Workflow Function (using the WorkflowThread
) is not executed when you run a WfRun
. Rather, it is executed once to compile a WfSpec
using the LittleHorse DSL, and gets translated into a WfSpec
protobuf object which is a directed graph of nodes and edges.
The WorkflowThread#declareStr()
method tells LittleHorse to create a LittleHorse Variable inside the WfSpec
. At runtime, when an instance of the workflow (WfRun
) is started, LittleHorse schedules and executes the tasks defined in the WfSpec
, and the LittleHorse variables in the WfSpec
will also be created and stored in the LittleHorse server.
Variable Payload Size
Variables are great for passing small pieces of data around (and even works for JSON files up to a few dozen KB). However, if you need to pass around large files or data, you should consider using a different mechanism, you should store the data payload in an external system and use a LittleHorse Variable to pass around a reference to the data (eg. an S3 URL). There is a hard limit to LittleHorse Variables of size 250KB.
Further Resources
Congrats on learning how to use Variable
s in your LittleHorse workflows! There's a lot more you can do with Variable
s, though, so keep on reading:
- Check out our
WfSpec
Development Guide. - Join the LittleHorse Slack Community
- Give us a star on GitHub