Skip to main content

Conditionals

Programming languages conditional branching (like if/else statements) to allow your programs to respond to different inputs. In a LittleHorse WfSpec, conditional branching serves an identical purpose.

Basic Concepts

LittleHorse Conditionals work almost exactly like if does in most programming languages. Depending on whether an expression evaluates to true or false, a block of logic (code or WfSpec) is either executed or not.

Workflow Conditions

The WorkflowCondition in the LittleHorse DSL is analogous to the "thing" that goes between the parentheses in your if () statements (or just after the if in Python and GoLang). LittleHorse WorkflowConditions evaluate at runtime to true or false.

Under the hood (at the protobuf level), a WorkflowCondition consists of three parts:

  1. The Left-Hand-Side (LHS),
  2. The Comparator,
  3. and the Right-Hand-Side (RHS).

The LHS and the RHS can be anything: a Variable in a WfRun, a constant, a handle to the output of a previous node (eg. a TaskRun output or an ExternalEvent payload), or a complex Expression.

The Comparator compares the LHS and the RHS. LittleHorse supports the following comparisons:

  • LESS_THAN, which returns true if the LHS is < the RHS.
  • GREATER_THAN, which returns true if the LHS is > the RHS.
  • LESS_THAN_EQ, which returns true if the LHS is <= the RHS.
  • GREATER_THAN_EQ, which returns true if the LHS is >= the RHS.
  • EQUALS, which returns true if the LHS is == the RHS.
  • NOT_EQUALS, which returns true if the LHS is != the RHS.
  • IN, which returns true if the LHS is == one of the elements in the RHS (if the RHS is a JSON_ARR variable) or if the LHS is == one of the keys in the RHS (if the RHS is a JSON_OBJ variable).
  • NOT_IN, which is the opposite of IN.

You can find a detailed description of them in the protobuf documentation.

DoIf, DoIfElse, and DoWhile

In all of our SDK's, the WorkflowThread has a few methods that let you work with conditionals. Each method takes in a WorkflowCondition and one or more ThreadFuncs, which are lambda functions or function pointers that define workflow logic when given a WorkflowThread:

  1. WorkflowThread#doIf(), which takes in a WorkflowCondition and a workflow logic body (a lambda function). The resulting WfSpec executes the logic in the lambda if the WorkflowCondition evaluates to true at runtime.
  2. WorkflowThread#doIfElse(), which takes in a WorkflowCondition and two logic bodies. The resulting WfSpec executes the first body if the condition evaluates to true and the second one if the condition evaluates to false.
  3. WorkflowThread#doWhile(), which takes in a WorkflowCondition and a workflow logic body. It behaves exactly like while (someCondition) {/* */} in programming languaes: the logic body is executed repeatedly so long as the condition keeps evaluating to true.

In Practice

Let's see conditionals used in a practical example.

Creating the WfSpec

We'll write a WfSpec (similar to the example in the Variables Concepts page), which:

  1. Takes in a user-id and a message to send as input.
  2. Fetches the user's preferred contact method.
  3. If the user prefers contact via hologram, it sends them a hologram; otherwise, it sends them the message through their Comlink.

Background: The Tasks

This example will need three TaskDefs. There isn't any new magic here, so for the sake of brevity here is the code:

package io.littlehorse.quickstart;

import java.util.List;

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

class MyTasks {

// This Task Method returns a POJO, which is automatically serialized to a JSON_OBJ variable
// value in LittleHorse.
@LHTaskMethod("fetch-contact-method")
public String fetchUser(String userId) {
if (List.of("obiwan", "padme", "satine").contains(userId)) {
return "COMLINK";
} else {
return "HOLOGRAM";
}
}

@LHTaskMethod("send-comlink-message")
public String sendComlink(String userId, String message) {
String result = "sent comlink " + message + " to user " + userId;
System.out.println(result);
return result;
}

@LHTaskMethod("send-hologram")
public String sendHologram(String userId, String message) {
String result = "sent hologram " + message + " to user " + userId;
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 contactMethodService = new LHTaskWorker(taskFuncs, "fetch-contact-method", config);
LHTaskWorker comlinkService = new LHTaskWorker(taskFuncs, "send-comlink-message", config);
LHTaskWorker hologramService = new LHTaskWorker(taskFuncs, "send-hologram", config);
hologramService.registerTaskDef();
contactMethodService.registerTaskDef();
comlinkService.registerTaskDef();

Runtime.getRuntime().addShutdownHook(new Thread(hologramService::close));
Runtime.getRuntime().addShutdownHook(new Thread(contactMethodService::close));
Runtime.getRuntime().addShutdownHook(new Thread(comlinkService::close));

hologramService.start();
contactMethodService.start();
comlinkService.start();
}
}

The WfSpec

This is where the magic happens. We will use the doIfElse() method.

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.wfsdk.WfRunVariable;
import io.littlehorse.sdk.wfsdk.Workflow;
import io.littlehorse.sdk.wfsdk.WorkflowThread;

public class Main {

public static final String WFSPEC_NAME = "send-message";

public static void wfLogic(WorkflowThread wf) {
WfRunVariable userId = wf.declareStr("user-id").required();
WfRunVariable message = wf.declareStr("message").required();
WfRunVariable preferredContact = wf.declareStr("contact-method");

// Fetch + save the preferred contact method
preferredContact.assign(wf.execute("fetch-contact-method", userId));

wf.doIfElse(preferredContact.isEqualTo("COMLINK"), ifHandler -> {
// It is CRUCIAL here to use `ifHandler`, not `wf`.
ifHandler.execute("send-comlink-message", userId, message);
// Feel free to add more logic in here.

}, elseHandler -> {
elseHandler.execute("send-hologram", userId, message);
});
}

public static void main(String[] args) throws Exception {
LHConfig config = new LHConfig();
Workflow wfGenerator = Workflow.newWorkflow(WFSPEC_NAME, Main::wfLogic);
wfGenerator.registerWfSpec(config.getBlockingStub());
}
}

We should be able to see the WfSpec in our dashboard now:

A WfSpec diagram with a conditional branch in it
The Send-Message Example WfSpec

Running the WfSpec

Let's run the WfSpec, and send anakin the message You're on this council, but we do not grant you the rank of master.:

lhctl run send-message user-id anakin message "You're on this council, but we do not grant you the rank of master."

If we look at the WfRun on the dashboard, you can see that it turns out Anakin prefers Holograms rather than Comlink messages (but if you run it for padme, you'll find out that our favorite Senator prefers Holograms).

The WfRun dashboard showing which conditional branch was executed.
The Completed WfRun
note

If you pay attention, obiwan and satine have the same preferred contact method.

Gotchas

When building your IfBody, it is important to NOT use the WorkflowThread of the base. If you do that, you'll get an error saying that you need to use the handler.

Here's an example of doing it wrong:

public void wfLogic(WorkflowThread wf) {
var myVariable = wf.declareStr("input");
wf.doIf(myVariable.isEqualTo("something"), handler -> {
wf.execute("some-task");
});
}

And here is the right way:

public void wfLogic(WorkflowThread wf) {
var myVariable = wf.declareStr("input");
wf.doIf(myVariable.isEqualTo("something"), handler -> {
handler.execute("some-task");
});
}

Don't worry, LittleHorse won't let you mess it up; we'll give you an error message rather than fail silently. But keep that in mind!

Further Resources

Congrats on learning how conditionals work in LittleHorse! If you want to dive further, check out the following:

  • How to build a workflow that uses loops!
  • The Node and Edge protobuf structures in our WfSpec schema.
info

We are considering enhancements to the Dashboard which will allow you to visualize the comparisons that occurred at each conditional branch. The goal is let you easily determine why a certain WfRun went down a certain path.

If you have any suggestions on how this should look, drop us a note in Slack!