Skip to main content

External Events

External Events allow your workflows to react to input from the outside world. You can think of an ExternalEvent as a callback or webhook which signals something to your WfRun.

In order to use External Events in your workflows, you need to:

  1. Create an ExternalEventDef, which tells the LittleHorse Server about the type of callback.
  2. Create a WfSpec that uses that ExternalEventDef.
  3. Run the WfSpec to create a WfRun.
  4. Post an ExternalEvent to the WfRun to complete it.
tip

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:

  1. Install lhctl.
  2. Set up a LittleHorse Server for local development.
  3. Add the dependency for your SDK of choice.

Setting up Metadata

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

  1. We wait for an ExternalEvent of type name-posted.
  2. We pass the payload from that ExternalEvent into a greet TaskRun.

Background: The TaskDef

Let's use a TaskDef called greet, 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 Greeter {

@LHTaskMethod("greet")
public String greet(String name) {
String result = "Hello, " + name + "!";
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 Greeter(), "greet", config);
Runtime.getRuntime().addShutdownHook(new Thread(worker::close));

// Register the TaskDef
worker.registerTaskDef();

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

Create the ExternalEventDef

An ExternalEventDef registers a type of callback with the LittleHorse Server. Before you can use an ExternalEvent in your workflows, you need to create the ExternalEventDef. You can do it as follows.

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.LHLibUtil;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.common.proto.ExternalEventDef;
import io.littlehorse.sdk.common.proto.PutExternalEventDefRequest;
import io.littlehorse.sdk.common.proto.LittleHorseGrpc.LittleHorseBlockingStub;

public class Main {

public static final String EXTERNAL_EVENTDEF_NAME = "name-posted";

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

PutExternalEventDefRequest request = PutExternalEventDefRequest.newBuilder()
.setName(EXTERNAL_EVENTDEF_NAME)
.build();

ExternalEventDef result = client.putExternalEventDef(request);
System.out.println(LHLibUtil.protoToJson(result));
}
}

For more information about registering an ExternalEventDef, check out the Managing Metadata docs.

Once you run the code, you should have some output like the following:

{
"id": {
"name": "name-posted"
},
"createdAt": "2025-01-29T07:29:54.223Z",
"retentionPolicy": {
}
}

Registering the WfSpec

Let's register our WfSpec! The key method here is waitForEvent, which does two things:

  1. Block the WfRun until an ExternalEvent of the name-posted type is sent to this WfRun.
  2. Give us a handle that allows us to access the payload of the ExternalEvent (in this case, we assign the name variable to the payload).
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 EXTERNAL_EVENTDEF_NAME = "name-posted";

public static void wfLogic(WorkflowThread wf) {
WfRunVariable name = wf.declareStr("name");

// Wait for the ExternalEvent
NodeOutput eventPayload = wf.waitForEvent(EXTERNAL_EVENTDEF_NAME);

// Save the payload into the `name` variable
name.assign(eventPayload);

// Pass the `name` variable (which has our event's payload) into the
// greet task
wf.execute("greet", name);
}

public static void main(String[] args) {
LHConfig config = new LHConfig();
Workflow wfGenerator = Workflow.newWorkflow("greet-event", Main::wfLogic);

wfGenerator.registerWfSpec(config.getBlockingStub());
}
}

After registering your WfSpec with the above code, it should look like the following in the LittleHorse Dashboard:

A WfSpec on the LittleHorse Dashboard with an ExternalEventNode and a TaskNode.
The greet-event WfSpec

Run the Workflow

Now that we've registered our TaskDef, ExternalEventDef, and WfSpec, all that remains for us is to run the WfSpec (and start a WfRun) and then complete the WfRun by posting an ExternalEvent to it.

Start a WfRun

You can start the WfRun using lhctl as follows:

lhctl run greet-event

Make note of the WfRunId (this comes from theid.id field) as we'll need it in the next step!

If we look at our WfRun in the dashboard, we'll see that it's waiting on the ExternalEventNode.

A WfRun on the LittleHorse Dashboard that is waiting for the ExternalEventNode step.
Our WfRun waiting for the ExternalEvent

Posting an Event

In LittleHorse, all ExternalEvents are associated with a WfRun. Therefore, to post an ExternalEvent, you need:

  1. The correlated WfRunId.
  2. The ExternalEventDefId that tells LittleHorse what type of event we are posting.
  3. An optional payload that we want to send to the WfRun.

You can post an ExternalEvent using our SDK's or by using lhctl. With lhctl, it's simple:

lhctl postEvent <wfRunId> name-posted STR "Obi-Wan Kenobi"

We first provide the WfRun Id, then the name of the ExternalEventDef, then the type of the payload, and then finally the actual payload.

Using the SDK's, we can do the above in code as follows:

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.LHLibUtil;
import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.common.proto.ExternalEvent;
import io.littlehorse.sdk.common.proto.ExternalEventDefId;
import io.littlehorse.sdk.common.proto.PutExternalEventRequest;
import io.littlehorse.sdk.common.proto.WfRunId;

public class Main {

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

WfRunId wfRunId = WfRunId.newBuilder()
.setId("e27af9ea516749fda454202101c734f8")
.build();
ExternalEventDefId eventDefId = ExternalEventDefId.newBuilder()
.setName("name-posted")
.build();

PutExternalEventRequest request = PutExternalEventRequest.newBuilder()
.setWfRunId(wfRunId)
.setExternalEventDefId(eventDefId)
.setContent(LHLibUtil.objToVarVal("Obi-Wan Kenobi"))
.build();

ExternalEvent result = config.getBlockingStub().putExternalEvent(request);
System.out.println(LHLibUtil.protoToJson(result));
}
}

The output of either the lhctl command or the code will look something like:

{
"id": {
"wfRunId": {
"id": "e27af9ea516749fda454202101c734f8"
},
"externalEventDefId": {
"name": "name-posted"
},
"guid": "9062b812ecb54328812ec32273791ecd"
},
"createdAt": "2025-01-29T08:01:57.348Z",
"content": {
"str": "Obi-Wan Kenobi"
},
"threadRunNumber": 0,
"nodeRunPosition": 1,
"claimed": true
}

That output is the actual ExternalEvent object in the LittleHorse API. Cool!

You'll also notice that the WfRun has completed, and the Task Worker said hello to Obi-Wan Kenobi:

Hello, Obi-Wan Kenobi!

Further Resources

Congrats on learning how to use ExternalEvents in your LittleHorse workflows! There's a lot more you can do with ExternalEvents, though, so keep on reading:

  • Timeouts on your ExternalEventNodes with the .timeoutSeconds() method.
  • Idempotency when posting ExternalEvents.
  • Viewing the ExternalEvent object via lhctl get externalEvent <wfRunId> <externalEventDefName> <guid>
  • Play around with lhctl get externalEvent <wfRunId> <externalEventDefName> <guid> and lhctl list nodeRun <wfRunId> and see if you can find out the relationship between them!