Build a task planner for AI in Java

When an agent takes on a task like "help me make a market-research report," the hard part isn't execution but decomposing the task and managing state. This records how I used langchain4j to implement a task tree, dependency checks, retries, and reasoning traces.

Build a task planner for AI in Java

Most agent demos are single-step tasks: ask one question, call one tool, answer one line.

Real needs aren’t like that. A task like “help me compile a competitor research report” can’t be done by the model in one step — it has to break it into several things, do them in order, and if some step in the middle fails, be able to retry or take another path. This step is task planning.

I recently built one in Java + langchain4j, and I’m recording the design points that actually affected usability.

First, model the task as a tree

My first version modeled the task as a List, and hit a wall fast: a parent task hangs several child tasks, and the children have dependencies among themselves — a List can’t express that structure.

After switching to a tree, the node definition looked roughly like this:

@Data
public class TaskNode {
    private String id;
    private String name;
    private String description;
    private TaskStatus status;
    private String parentId;
    private List<TaskNode> children;
    private List<String> dependencies;
    private List<String> requiredCapabilities;
    private Integer estimatedDuration;
    private Integer retryCount;
    private Integer maxRetries;
    private Object result;
    private String error;
    private Map<String, Object> metadata;
    private Double confidence;
    private List<String> reasoningTrace;
}

Several fields were added later, each corresponding to a pit I’d fallen into; I’ll go through them one by one.

The state machine matters more than expected

I started with only four states: pending, running, success, failed. In practice it wasn’t enough, and it eventually grew to seven:

public enum TaskStatus {
    PENDING("pending"),
    RUNNING("running"),
    SUCCESS("success"),
    FAILED("failed"),
    SKIPPED("skipped"),
    RETRYING("retrying"),
    BLOCKED("blocked");
}

The three extra ones each have their use:

  • BLOCKED — the upstream dependency task failed; this task isn’t failed, it simply never got a chance to run. Distinguishing it from FAILED matters, otherwise error stats get distorted: one upstream failure drags a dozen downstream tasks all showing FAILED, and you can’t tell where the root cause is.
  • SKIPPED — the model judges this step unnecessary in the current context. Common in conditional branches like “if XX then YY.”
  • RETRYING — currently retrying. Pulling it out separately matters because a task that’s retrying shouldn’t be picked up and executed again by the scheduler.

Dependency check: one method solves scheduling order

With the tree and states in place, the scheduling logic is actually simple. The core is one method:

public boolean canExecute(Map<String, Object> context) {
    // check dependencies
    for (String depId : dependencies) {
        if (!context.containsKey(depId)) {
            return false;
        }
    }
    return status == TaskStatus.PENDING;
}

After the upstream dependency task finishes, write its result into context (key is the task id), and the downstream task’s canExecute naturally passes. The scheduler scans each round and throws every task with canExecute == true into the thread pool.

This “write dependency results into a shared context” approach has an extra benefit: downstream tasks can directly get the upstream’s output, without another round of passing.

Retries must have a cap

The retryCount and maxRetries fields default to 0 and 3.

Why not let it retry infinitely? Because model failures have two causes:

  1. Transient (interface jitter, wrong return format) — retries fix it
  2. Fundamental (tool doesn’t exist, task description itself is wrong) — no number of retries helps

Without a cap, the second kind of failure makes the agent spin in the same place, burning huge token counts before you even notice.

Setting it to 3 is a rule of thumb. Past that it goes to FAILED, fills in the error field, and lets the upper layer decide whether to skip or abandon entirely.

reasoningTrace: the most useful field when debugging

This is the design I most want to recommend.

reasoningTrace is a List recording each step of the model’s reasoning while planning and executing this task. I started it just to store logs, then found it solves the most annoying thing in agent development: you don’t know why it made that decision.

After the task tree finishes, a child task failed — what you see isn’t just “failed,” but how it thought through each step before:

[plan] user wants competitor research → first need to determine competitor scope
[plan] competitor scope depends on industry classification → split out subtask "determine industry classification"
[exec] called search tool, returned 8 results
[judge] 3 of the results low relevance → filter out
[fail] remaining results insufficient to support analysis

Without this field you only see “task failed” and have to guess from scratch. With it, most problems are obvious at a glance: was the decomposition wrong in the planning phase, or did the tool return badly in the execution phase.

The cost is it eats tokens and storage. My approach: in production keep the full trace only for failed tasks; for successful ones keep only the last step.

confidence: let the model say it’s unsure

confidence is a 0–1 Double, default 1.0. Have the model give a confidence score alongside completing the task.

This field alone isn’t very useful, but paired with a threshold it gets interesting: results below a certain threshold skip the automatic flow and go to manual confirmation.

For example, an agent helping you compile a competitor list judges one company with only 0.4 confidence — that item gets flagged yellow for you to confirm, instead of being mixed into the final report. In scenarios needing reliability, having the model admit uncertainty is more valuable than having it answer hard.

To be clear, the model’s self-rated confidence isn’t strictly calibrated — when it says 0.8 it isn’t necessarily really 80%. But as a relative indicator (A is more uncertain than B) it’s good enough.

Looking back

The hardest part of this wasn’t the code, it was figuring out how to decompose the task tree.

The same need, decomposed into three levels versus five, gives very different result quality. Too coarse, and inside each subtask the model still can’t think it through in one step; too fine, and the task count explodes, token cost doubles, and errors propagate down the tree.

My current approach: have the model produce a first-pass decomposition, use reasoningTrace to see if its thinking is reasonable, and add constraints in the prompt where it isn’t. There’s no shortcut to this prompt-tuning process — just round after round of reading traces and tuning.

If you’re also building agents in Java, feel free to reach out; my WeChat ID is on the about page.

Qimiaogan This work is licensed under CC BY-NC-SA. Please credit the source and author when reposting.

Contents