Skip to main content

Child Workflows

In LittleHorse, threads let you run multiple steps in parallel within a single WfRun. But what if you want to start an entirely separate WfRun from inside your workflow? That's where child workflows come in.

A child workflow is a separate WfRun (with its own WfSpec) that is launched by a parent WfRun. The parent can continue doing other work while the child executes, and later wait for the child to complete and retrieve its output.

Common use-cases for child workflows include:

  • Reusing a WfSpec as a building block across multiple parent workflows.
  • Isolating a sub-process so that it has its own WfRun lifecycle, variables, and visibility in the dashboard.
  • Delegating a piece of work to a different team's WfSpec.
  • Dynamically deciding which WfSpec to run based on runtime data.

Concepts

Child Workflows vs. Child Threads

Both child workflows and child threads let you run work in parallel, but there are important differences:

Child ThreadChild Workflow
Runs insideThe same WfRunA brand-new WfRun
Variable accessCan read/write parent variablesCan only access parent variables if INHERITED
Defined inThe same WfSpecA separate, independently registered WfSpec
LifecycleTied to the parent ThreadRunHas its own lifecycle and shows up as a separate WfRun in the dashboard
ReusabilityOnly within the same WfSpecAny WfSpec can launch it

Use child threads when the work is tightly coupled to the parent and shares state. Use child workflows when you want isolation, reusability, or independent lifecycle tracking.

How It Works

Running a child workflow involves two nodes in the parent WfSpec:

  1. RunChildWfNode: Launches a new WfRun of a specified WfSpec, passing in input variables. The node completes as soon as the child WfRun is started, and produces a handle (SpawnedChildWf) that you can use later.
  2. WaitForChildWfNode: Blocks the parent ThreadRun until the child WfRun completes, and returns the output of the child.

Because these are two separate nodes, the parent can continue doing other work (executing tasks, sleeping, etc.) between spawning the child and waiting for it.

info

The waitForChildWf() call must be made in the same ThreadSpec that called runWf(). This constraint is enforced at compile time by all SDKs.

Output from Child Workflows

A child WfSpec can produce an output using the complete() method. When the parent calls waitForChildWf(), the return value is a NodeOutput that contains whatever the child passed to complete(). If the child does not call complete(), the output will be null.

Failure Handling

If a child WfRun fails reaches the ERROR status, the WaitForChildWfNode in the parent will throw an ERROR of type CHILD_FAILURE. If the child WfRun throws an EXCEPTION, the WaitForChildWfNode will throw the same EXCEPTION thrown by the child. You can handle this using LittleHorse's exception handling mechanisms, just like you would handle a failed TaskRun or child thread.

In Practice

In this example, we will build two WfSpecs:

  1. A child WfSpec called greeting-child that takes a name as input, greets the person, and returns the greeting as output.
  2. A parent WfSpec called greeting-parent that takes a name, launches the child with that name, does some work of its own, and then waits for the child's output.
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 Kernel for local development.
  3. Add the dependency for your SDK of choice.

Building the WfSpec

Background: The TaskDef

We'll use a simple greet TaskDef that takes a name and returns a greeting string. Run the following code in a terminal to register the TaskDef and start the Task Worker. Keep it running for the duration of this exercise.

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();

LHTaskWorker worker = new LHTaskWorker(new Greeter(), "greet", config);
Runtime.getRuntime().addShutdownHook(new Thread(worker::close));

worker.registerTaskDef();
worker.start();
}
}

Registering the WfSpecs

The child WfSpec is a standalone workflow—there is nothing special about it. It declares a required input variable, executes a task, and returns a result using complete(). Notice how wf.complete(wf.execute("greet", name)) passes the task output as the workflow output. This value is what the parent will receive when it calls waitForChildWf().

The parent WfSpec uses two key methods:

  1. runWf() to start the child WfRun, passing in input variables as a map.
  2. waitForChildWf() to block until the child completes and retrieve its output.

We register the child first, then the parent (since the parent references the child's WfSpec):

package io.littlehorse.quickstart;

import io.littlehorse.sdk.common.config.LHConfig;
import io.littlehorse.sdk.wfsdk.SpawnedChildWf;
import io.littlehorse.sdk.wfsdk.WfRunVariable;
import io.littlehorse.sdk.wfsdk.Workflow;
import io.littlehorse.sdk.wfsdk.WorkflowThread;
import io.littlehorse.sdk.wfsdk.internal.WorkflowImpl;
import java.util.Map;

public class Main {

public static void childLogic(WorkflowThread wf) {
WfRunVariable name = wf.declareStr("name").required();
wf.complete(wf.execute("greet", name));
}

public static void parentLogic(WorkflowThread wf) {
WfRunVariable inputName = wf.declareStr("input-name").required();
WfRunVariable childOutput = wf.declareStr("child-output");

// Start the child WfRun
SpawnedChildWf child = wf.runWf("greeting-child", Map.of("name", inputName));

// The parent can do other work while the child is running
wf.execute("greet", "hi from parent");

// Wait for the child and capture its output
childOutput.assign(wf.waitForChildWf(child));
}

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

Workflow child = new WorkflowImpl("greeting-child", Main::childLogic);
child.registerWfSpec(config.getBlockingStub());

Workflow parent = new WorkflowImpl("greeting-parent", Main::parentLogic);
parent.registerWfSpec(config.getBlockingStub());
}
}

There are three key calls in the parent logic:

  • runWf("greeting-child", Map.of("name", inputName)) creates a new WfRun of the greeting-child WfSpec, passing inputName as the name input variable. The parent continues immediately—it does not block here.
  • wf.execute("greet", "hi from parent") runs in the parent while the child may still be executing. This demonstrates that the parent is free to do other work.
  • waitForChildWf(child) blocks the parent ThreadRun until the child WfRun completes, and returns the child's output (whatever was passed to complete() in the child).

Run the Workflow

Now that we've registered our TaskDef and both WfSpecs, let's run the parent and observe the child being spawned automatically.

Start a WfRun

Start the parent WfRun using lhctl:

lhctl run greeting-parent input-name "Obi-Wan"

Make note of the WfRunId from the output.

What Happens

When the parent WfRun starts:

  1. The parent reaches the RunChildWfNode and spawns a child WfRun of the greeting-child WfSpec, passing "Obi-Wan" as the name input.
  2. The parent immediately moves on and executes the greet task with "hi from parent".
  3. The parent then reaches the WaitForChildWfNode and blocks until the child completes.
  4. Meanwhile, the child WfRun executes the greet task with "Obi-Wan" and returns "Hello, Obi-Wan!" as its output via complete().
  5. The parent receives "Hello, Obi-Wan!" and stores it in child-output.

You can inspect the parent WfRun and see the child's WfRunId in the RunChildWfNode:

lhctl get wfRun <parentWfRunId>

You can also inspect the child WfRun directly—it's a fully independent WfRun in the system.

Further Resources

Congrats on learning how to use child workflows in LittleHorse! Here are some related topics to explore:

  • Threads for running work in parallel within a single WfRun.
  • Exception Handling for handling failures from child workflows.
  • Variables for understanding how input/output data flows between workflows.