Deterministic Tasks
Overview
Section titled “Overview”Not every task in an ensemble requires AI reasoning. Sometimes you need to:
- Call a REST API and pass the raw response downstream
- Read a file or database and forward the contents
- Transform or aggregate outputs from prior AI tasks
- Run a
ToolPipelinewithout LLM round-trips
For these cases, routing through the LLM wastes tokens, adds latency, and introduces non-determinism where none is needed. Deterministic tasks let you execute any Java function directly as a task step, bypassing the agent and the ReAct tool-calling loop.
API Design
Section titled “API Design”TaskHandler Interface
Section titled “TaskHandler Interface”@FunctionalInterfacepublic interface TaskHandler { ToolResult execute(TaskHandlerContext context);}A TaskHandler is a functional interface that receives a TaskHandlerContext and returns
a ToolResult. Use ToolResult.success(String) for normal output and
ToolResult.failure(String) to signal an error.
TaskHandlerContext Record
Section titled “TaskHandlerContext Record”public record TaskHandlerContext( String description, String expectedOutput, List<TaskOutput> contextOutputs) {}The context carries:
- The task’s resolved description and expected output (with
{variable}placeholders already substituted). - The outputs of all tasks declared in
Task.context()that completed before this task.
Builder Overloads on Task
Section titled “Builder Overloads on Task”Two builder overloads configure a handler:
// Lambda overload -- full context accesstask.builder().handler(TaskHandler handler)
// AgentTool overload -- wraps an existing tooltask.builder().handler(AgentTool tool)The AgentTool overload resolves the tool input as:
- Last context output’s raw text, if context outputs are present
- The task description otherwise
Level 1: Lambda Handler
Section titled “Level 1: Lambda Handler”Task fetchPrices = Task.builder() .description("Fetch current stock prices") .expectedOutput("JSON with stock prices") .handler(ctx -> ToolResult.success(httpClient.get("https://api.example.com/prices"))) .build();Level 2: Wrap an Existing AgentTool
Section titled “Level 2: Wrap an Existing AgentTool”// httpTool.execute() is called with the task description as inputTask fetch = Task.builder() .description("https://api.example.com/prices") .expectedOutput("HTTP response body") .handler(httpTool) // AgentTool overload .build();Level 3: Use a ToolPipeline
Section titled “Level 3: Use a ToolPipeline”Since ToolPipeline implements AgentTool, it works directly with the AgentTool overload:
ToolPipeline pipeline = ToolPipeline.of(httpTool, jsonParserTool);
Task fetchAndParse = Task.builder() .description("https://api.example.com/prices") .expectedOutput("Parsed stock data") .handler(pipeline) .build();Mixed AI and Deterministic Tasks
Section titled “Mixed AI and Deterministic Tasks”// Deterministic: call REST APITask fetchPrices = Task.builder() .description("Fetch current stock prices") .expectedOutput("JSON prices") .handler(ctx -> ToolResult.success(apiClient.getPrices())) .build();
// AI-backed: analyze the dataTask analyze = Task.builder() .description("Analyze the stock prices and identify trends") .expectedOutput("Investment recommendations") .chatLanguageModel(model) .context(List.of(fetchPrices)) .build();
// Deterministic: format the AI outputTask format = Task.builder() .description("Format the analysis as a report") .expectedOutput("Formatted HTML report") .context(List.of(analyze)) .handler(ctx -> { String aiAnalysis = ctx.contextOutputs().get(0).getRaw(); return ToolResult.success(ReportFormatter.toHtml(aiAnalysis)); }) .build();
EnsembleOutput result = Ensemble.builder() .chatLanguageModel(model) .tasks(List.of(fetchPrices, analyze, format)) .build() .run();Execution Path
Section titled “Execution Path”When a task has a handler set, the workflow executors (sequential and parallel)
invoke DeterministicTaskExecutor instead of AgentExecutor:
Ensemble.run() -> resolveAgents() -- skips synthesis for handler tasks -> WorkflowExecutor.execute() -> task.getHandler() != null? YES: DeterministicTaskExecutor.execute() NO: AgentExecutor.execute()DeterministicTaskExecutor lifecycle:
- Run input guardrails (if any)
- Build
TaskHandlerContextwith resolved description, expected output, and context outputs - Call
handler.execute(context)— wrapped in try/catch - On
ToolResult.failure()or exception: throwAgentExecutionException - Run output guardrails (if any)
- Store output in declared memory scopes (if any)
- Return
TaskOutputwithagentRole = "(deterministic)",toolCallCount = 0
Structured Output
Section titled “Structured Output”If the task has outputType declared, the handler can provide a pre-typed Java object
via ToolResult.success(text, typedValue) to skip JSON deserialization:
record PriceReport(String symbol, double price) {}
Task fetchPrices = Task.builder() .description("Fetch AAPL price") .expectedOutput("Price report") .outputType(PriceReport.class) .handler(ctx -> { PriceReport report = apiClient.getPrice("AAPL"); return ToolResult.success(report.toString(), report); // typed value provided }) .build();
EnsembleOutput result = ...;PriceReport report = result.getOutput(fetchPrices).getParsedOutput(PriceReport.class);If structuredOutput is not set in the ToolResult, parsedOutput will be null in
the task output even when outputType is declared — the handler is responsible for
providing the correctly typed value.
Lifecycle Features
Section titled “Lifecycle Features”All lifecycle features work identically for deterministic and AI-backed tasks:
| Feature | Supported |
|---|---|
| Input guardrails | Yes |
| Output guardrails | Yes |
| Before/after review gates | Yes |
| Memory scopes | Yes |
Callbacks (TaskStartEvent, TaskCompleteEvent) | Yes |
| Context (prior task outputs) | Yes |
| Template variable substitution | Yes |
| Parallel workflow dependencies | Yes |
Mutually Exclusive Fields
Section titled “Mutually Exclusive Fields”When handler is set, the following builder fields must not be used (they are
LLM-specific and will be rejected at build time with a ValidationException):
agentchatLanguageModelstreamingChatLanguageModeltoolsmaxIterationsrateLimit
The following fields may be used alongside handler:
context(prior task dependencies)outputType(structured output viaToolResult.success(text, typedValue))inputGuardrails/outputGuardrailsmemoryScopesreview/beforeReview
No LLM Required for Handler-Only Ensembles
Section titled “No LLM Required for Handler-Only Ensembles”An ensemble composed entirely of deterministic tasks does not require a
chatLanguageModel. Use the zero-ceremony Ensemble.run(Task...) factory:
// No ChatModel needed -- all tasks are deterministicEnsembleOutput output = Ensemble.run(fetchTask, parseTask, formatTask);Or use the builder for full control over workflow, callbacks, and guardrails:
EnsembleOutput output = Ensemble.builder() .task(fetchTask) .task(parseTask) .task(formatTask) .workflow(Workflow.SEQUENTIAL) .onTaskComplete(e -> log.info("Done: {}", e.taskDescription())) .build() .run();The Ensemble.run(Task...) factory validates that all supplied tasks have handlers;
if any task lacks a handler and an LLM source, a descriptive IllegalArgumentException
is thrown.
Phase-based deterministic pipelines also require no LLM:
Phase ingest = Phase.of("ingest", ingestTask);Phase process = Phase.builder().name("process").task(processTask).after(ingest).build();Phase publish = Phase.builder().name("publish").task(publishTask).after(process).build();
EnsembleOutput output = Ensemble.builder() .phase(ingest) .phase(process) .phase(publish) .build() .run();For a full treatment of this pattern (including data sharing between tasks, parallel fan-out, and phase-based pipelines), see design doc 20 — Deterministic-Only Orchestration.
Limitations
Section titled “Limitations”-
Hierarchical workflow: Handler tasks are not supported in
Workflow.HIERARCHICAL. The Manager agent delegates to worker agents via the LLM tool-calling loop; deterministic tasks have no agent and cannot be delegated to. AValidationExceptionis thrown at ensemble startup if a handler task is present in a hierarchical ensemble. UseSEQUENTIALorPARALLELworkflow when mixing AI-backed and deterministic tasks. -
Streaming: Deterministic tasks produce no streaming tokens (no LLM is called).
TokenEventcallbacks are not fired for handler tasks.