External Events

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 ExternalEvent
s 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:
ExternalEventDef
s, which are a piece of metadata that tells LittleHorse about a type of External Event.ExternalEvent
s, 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
.
You can also use ExternalEvent
s 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 ExternalEventDef
s
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.
As of now, the ExternalEventDef
does not carry any typing information about the payload for the associated ExternalEvent
s. We have an open issue to address this.
Posting ExternalEvent
s
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 ExternalEvent
s 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 ExternalEvent
s
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 ExternalEvent
s, check out our WfSpec
development guide or read on below.
In Practice
In order to use External Events in your workflows, you need to:
- Create an
ExternalEventDef
, which tells the LittleHorse Server about the type of callback. - Create a
WfSpec
that uses thatExternalEventDef
. - Run the
WfSpec
to create aWfRun
. - Post an
ExternalEvent
to theWfRun
to complete it.
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.
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:
- We wait for an
ExternalEvent
of typename-posted
. - We pass the payload from that
ExternalEvent
into agreet
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 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 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();
}
}
TODO
TODO
TODO
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.
- 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.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));
}
}
TODO
TODO
TODO
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:
- Block the
WfRun
until anExternalEvent
of thename-posted
type is sent to thisWfRun
. - Give us a handle that allows us to access the payload of the
ExternalEvent
(in this case, we assign thename
variable to the payload).
- Java
- Go
- Python
- DotNet
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());
}
}
TODO
TODO
TODO
After registering your WfSpec
with the above code, it should look like the following in the LittleHorse Dashboard:

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
.

Posting an Event
In LittleHorse, all ExternalEvent
s are associated with a WfRun
. Therefore, to post an ExternalEvent
, you need:
- The correlated
WfRunId
. - The
ExternalEventDefId
that tells LittleHorse what type of event we are posting. - 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:
- 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.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));
}
}
TODO
TODO
TODO
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 ExternalEvent
s in your LittleHorse workflows! There's a lot more you can do with ExternalEvent
s, though, so keep on reading:
- Timeouts on your
ExternalEventNode
s with the.timeoutSeconds()
method. - Idempotency when posting
ExternalEvent
s. - Viewing the
ExternalEvent
object vialhctl get externalEvent <wfRunId> <externalEventDefName> <guid>
- Play around with
lhctl get externalEvent <wfRunId> <externalEventDefName> <guid>
andlhctl list nodeRun <wfRunId>
and see if you can find out the relationship between them!