Skip to main content

External Events

The LittleHorse Dashboard depicting a WfRun waiting for an ExternalEvent.
A WfRun waiting at an ExternalEvent Node

Workflows must be able to respond to inputs from the outside world in order to be truly useful and dynamic. One of the tools that LittleHorse provides which allows you to do that is External Events. You can think of an ExternalEvent as a callback or webhook which signals something to your WfRun.

Common use-cases for ExternalEvents include:

  • Integrate with asynchronous third-party API's.
  • Wait for a person to sign a document in DocuSign.
  • Wait for a customer to respond to a text message using a callback from the Twilio API.
  • Wait for a GitHub build to be successfully completed or to fail.

Concepts

When dealing with External Events, you must know about the following two API Objects in LittleHorse:

  1. ExternalEventDefs, which are a piece of metadata that tells LittleHorse about a type of External Event.
  2. ExternalEvents, which are an actual instance of a callback or webhook (External Event) that occurred in Littlehorse.

In your WfSpec, you can block the execution of your WfRun until an External Event occurs using an ExternalEventNode.

tip

You can also use ExternalEvents to interrupt the execution of a WfRun and cause special handler logic to be run. For information about how to do that, please check out the

Registering ExternalEventDefs

Before you can use an External Event, you must register the ExternalEventDef. You can do so using the request rpc PutExternalEventDef, with detailed instructions in our grpc user guide.

info

As of now, the ExternalEventDef does not carry any typing information about the payload for the associated ExternalEvents. We have an open issue to address this.

Posting ExternalEvents

An External Event can be recorded through the rpc PutExternalEvent gRPC call. This can be accomplished using clients in any of our SDK's or through the use of lhctl. You can find detailed instructions for posting ExternalEvents in our grpc user guide.

Crucially, every ExternalEvent must be associated with 1) specific WfRun, and 2) an ExternalEventDef. Therefore, the PutExternalEventRequest protobuf has a required wf_run_id and external_event_id field.

You may optionally specify content (of type VariableValue) for the request. In a WfSpec, when you wait for an ExternalEvent, you can access the content that was passed in.

Lastly, for the sake of idempotence when retrying, you may specify a guid field. Only one ExternalEvent with a given combination of wf_run_id, external_event_id, and guid may exist. Subsequent calls to rpc PutExternalEvent with the same id's will result in the ALREADY_EXISTS error.

Waiting for ExternalEvents

When developing a WfSpec using our DSL's, you can use the waitForEvent() method (or wait_for_event() in python) to create an ExternalEventNode. When a ThreadRun arrives at an ExternalEventNode, it will wait (without using any resources) until that event arrives.

For detailed information about waiting for ExternalEvents, check out our WfSpec development guide or read on below.

In Practice

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.

Write the Workflow

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!