> ## Documentation Index
> Fetch the complete documentation index at: https://lab.pollack.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# DSL Primitives

> 10+ composable primitives for building agentic pipelines — sequential, branch, loop, parallel, decision, gate, supervisor, and more

## Two Entry Points

```java theme={null}
// Named shortcuts — intent obvious from the first word
Workflow.sequential("pipeline").step(a).then(b).then(c).run(input);
Workflow.loop("refine").step(refineStep).times(5).run(draft);
Workflow.parallel("gather").step(review, audit, ciCheck).run(event);

// Full DSL — for gates, branches, decisions, error paths
Workflow.define("complex-flow")
    .step(fetch)
    .then(analyze)
    .gate(judgeGate).onPass(approve).onFail(revise).end()
    .run(event);

// Supervisor — LLM autonomously delegates to sub-agents
Workflow.supervisor("delegate", routingClient)
    .agents(codeReview, securityAudit, docUpdate)
    .until(result -> result.score() >= 0.9)
    .run(event);
```

Both surfaces produce a `WorkflowGraph` and support `.run(input)`.

## The Primitives

### Sequential

Chain steps — output flows forward:

```java theme={null}
Workflow.define("pipeline")
    .step(write)
    .then(editForAudience)
    .then(editForStyle)
    .run(input);
```

### Branch (predicate routing)

Route based on output:

```java theme={null}
Workflow.define("router")
    .step(classify)
    .branch(output -> "medical".equals(output))
        .then(medicalExpert)
        .otherwise(legalExpert)
    .run("I broke my leg");
```

### Loop (while-do)

Exit condition checked before body:

```java theme={null}
Workflow.loop("refine")
    .step(refineStep)
    .until(result -> result.score() >= 0.8)
    .run(draft);
```

### Loop (do-while / repeatUntilOutput)

Body runs first, exit condition reads actual output:

```java theme={null}
Workflow.define("score-loop")
    .repeatUntilOutput(score -> score instanceof Double d && d >= 0.8)
        .step(editor)
        .step(scorer)
    .end()
    .run(initialInput);
```

### Gather (homogeneous fan-out)

Run steps concurrently, collect all results into a `List<Object>` that becomes the input to the next step. Use when all branches produce the same type and downstream needs everything together:

```java theme={null}
List<Object> results = (List<Object>) Workflow.define("gather")
    .gather(findMeals, findMovies)
    .run("romantic");
```

Chain directly into a join step:

```java theme={null}
String summary = (String) Workflow.<String, Object>define("parallel-review")
    .gather(reviewStep, auditStep)
    .then(Step.named("summarize", (ctx, in) -> {
        List<Object> parts = (List<Object>) in;
        return String.join("+", parts.stream().map(Object::toString).toList());
    }))
    .run("input");
```

### Parallel (enrichment fan-out)

Run steps concurrently; the join passes the **fork input through unchanged**. Each branch writes its output to a named context key (`Steps.outputOf(branchName)`). Use when branches produce different types and downstream reads from context:

```java theme={null}
Step<String, String> reviewStep = Step.named("review", (ctx, in) -> "review-done");
Step<String, String> auditStep  = Step.named("audit",  (ctx, in) -> "audit-done");

Workflow.<String, Object>define("parallel-review")
    .parallel(reviewStep, auditStep)
    .then(Step.named("report", (ctx, in) -> {
        // in == fork input ("pr-123"), unchanged
        String reviewResult = ctx.get(Steps.outputOf("review")).orElseThrow();
        String auditResult  = ctx.get(Steps.outputOf("audit")).orElseThrow();
        return reviewResult + " | " + auditResult;
    }))
    .run("pr-123");
```

### Parallel (dynamic fan-out)

Fan-out determined at runtime:

```java theme={null}
Workflow.define("batch")
    .parallel(
        ctx -> loadItems(),                    // items supplier
        item -> Step.named("process", (c, i) -> processItem(i))  // step factory
    )
    .run(null);
```

### Decision (LLM-routed)

LLM picks which step to run:

```java theme={null}
Workflow.define("decision-router")
    .decision(chat)
        .option("summarize", summarizeStep)
        .option("translate", translateStep)
    .end()
    .run("The quick brown fox...");
```

### Gate (quality checkpoint)

Approve, reject, or retry with feedback:

```java theme={null}
Workflow.define("gated-pipeline")
    .step(generate)
    .gate(new JudgeGate(jury, 0.8))
        .onPass(approve)
        .onFail(revise)
        .withReflector(reflectorStep)   // transforms verdict → feedback
        .maxRetries(3)
    .end()
    .run(input);
```

On failure, the full `Verdict` (score, reasoning, per-judge judgments) is written to `AgentContext` under `JUDGE_VERDICT` for the retry step to consume.

### Supervisor (autonomous delegation)

LLM picks sub-agents each iteration:

```java theme={null}
Workflow.supervisor("text-improver", chat)
    .agents(review, edit, format)
    .until(ctx -> ctx.get(AgentContext.ITERATION_COUNT).orElse(0) >= 3)
    .run(roughDraft);
```

### Error Recovery

Route exceptions to recovery steps:

```java theme={null}
Workflow.define("resilient")
    .step(riskyStep)
        .onError(TimeoutException.class, retryWithBackoff)
    .then(finalStep)
    .run(input);
```

### BackTo (cyclic back-edge)

Jump back to an earlier step when a condition is met — a lightweight escape hatch for retry patterns:

```java theme={null}
Workflow.define("rebase-loop")
    .step(rebase)
    .step(runTests)
    .backTo("rebase", result -> !"PASS".equals(result))
    .step(merge)
    .run(branch);
```

`backTo` creates an `EdgeCondition.BackEdge` in the graph IR — a real edge, visible in traces. The next `.step()` chains from the same node (not the back-edge target), so `merge` runs when the predicate returns `false`.

Use `RunOptions.maxIterations()` as a circuit breaker to prevent infinite loops:

```java theme={null}
.run(input, RunOptions.maxIterations(10));
```

**When to use `backTo` vs `repeatUntil`**: `repeatUntil` creates structured loop nodes (entry → body → check → exit). `backTo` is a single edge — no extra nodes, no loop wrapper. Use it when you need a quick "retry this section" without the overhead of a full loop construct.

### Terminate

Exit early from anywhere:

```java theme={null}
Steps.terminate(WorkflowStatus.FAILED, "Quality below threshold")
```

## Step Types

Steps are the atomic unit. Each wraps a different kind of execution:

| Step Type                              | What It Wraps                 | Execution Time |
| -------------------------------------- | ----------------------------- | -------------- |
| `Step.named("n", lambda)`              | Any lambda                    | Depends        |
| `ChatClientStep.of(chat, template)`    | Single Spring AI call         | Seconds        |
| `ClaudeStep.of(template)`              | Full Claude CLI agent session | Minutes        |
| `AgentClientStep.of(client, template)` | AgentClient abstraction       | Minutes        |
| `A2AStep.of(url)`                      | Remote A2A agent              | Minutes        |
| `Steps.of(fn)`                         | Pure deterministic function   | Milliseconds   |
| `Steps.retrying(n, step)`              | Retry wrapper                 | Per attempt    |
| `Steps.outputOf("step-name")`          | Read prior step's output      | Instant        |

<Note>
  The key differentiator: `ClaudeStep` runs a full agentic loop internally — many turns, many tool calls, minutes of execution. The workflow sees it as one step. This is the opposite of frameworks where each LLM API call is a separate workflow activity.
</Note>

## Composability

Workflows implement `Step<I, O>`. Nest them freely:

```java theme={null}
var inner = Workflow.<String, String>define("inner")
    .step(analyze).then(classify).build();

Workflow.define("outer")
    .step(inner)        // workflow-as-step
    .then(report)
    .run(input);
```

## RunOptions

Runtime constraints — not DSL verbs, hints to the executor:

```java theme={null}
Workflow.define("bounded")
    .step(expensiveStep)
    .run(input, RunOptions.maxCost(5.0)
        .withMaxIterations(50)
        .withMaxDuration(Duration.ofMinutes(10)));
```

## The Graph IR

Every primitive compiles to real nodes and typed edges — not opaque lambdas:

* A **branch** is 4 nodes (gateway → then-step, else-step → join) + 4 edges
* A **loop** has a back-edge from body to entry
* A **parallel** has fork and join nodes with `BranchIndex` edges

This makes workflows inspectable, traceable, and replayable. The `TraceRecorder` sees every transition; Markov analysis works on the edge data.

```java theme={null}
WorkflowGraph<String, String> graph = Workflow.define("my-flow")
    .step(a).then(b).compile();

graph.nodes()     // List<WorkflowNode> — sealed interface
graph.edges()     // List<WorkflowEdge> — typed conditions
```

## Related

<CardGroup cols={2}>
  <Card title="Getting Started" icon="play" href="/docs/agent-workflow/getting-started">
    Install, first workflow, four-layer architecture
  </Card>

  <Card title="Complete Examples" icon="flask" href="/docs/agent-workflow/examples">
    8 runnable integration tests validated against GPT-4.1
  </Card>
</CardGroup>
