Skip to content

Template Variables

Task descriptions and expected outputs support {variable} placeholder substitution. Variables are resolved at run time from inputs configured on the builder or passed to ensemble.run(Map<String, String>).


Use curly braces to define a placeholder in any task description or expected output:

Task task = Task.builder()
.description("Research the latest developments in {topic} for the {audience} audience")
.expectedOutput("A 400-word summary of {topic} suitable for {audience}")
.agent(researcher)
.build();

Supply values on the builder with .input("key", "value"):

EnsembleOutput output = Ensemble.builder()
.agent(researcher)
.task(task)
.input("topic", "quantum computing")
.input("audience", "software engineers")
.build()
.run();

The resolved description becomes:

Research the latest developments in quantum computing for the software engineers audience

Chain .input() calls — they accumulate and are all applied at run time:

Ensemble.builder()
.agent(analyst)
.task(financialTask)
.input("company", "Acme Corp")
.input("quarter", "Q4")
.input("year", "2025")
.build()
.run();

To supply a whole map at once, use .inputs(Map<String, String>):

Ensemble.builder()
.agent(analyst)
.task(financialTask)
.inputs(Map.of("company", "Acme Corp", "quarter", "Q4", "year", "2025"))
.build()
.run();

Template variables are resolved in both description and expectedOutput:

Task task = Task.builder()
.description("Analyse {company} financial results for Q{quarter} {year}")
.expectedOutput("A financial analysis report for {company} Q{quarter} {year} with three key findings")
.agent(analyst)
.build();

When no variables are needed, call run() without any inputs configured:

EnsembleOutput output = ensemble.run();

Dynamic Runs: Overriding Inputs at Invocation Time

Section titled “Dynamic Runs: Overriding Inputs at Invocation Time”

When you need to run the same ensemble multiple times with different variable values, keep the ensemble instance and pass values to run(Map<String, String>). Run-time values are merged with any builder inputs; run-time values win on key conflicts:

// Create tasks and ensemble once
Ensemble ensemble = Ensemble.builder()
.agent(analyst).agent(advisor)
.task(analysisTask).task(recommendationTask)
.build();
// Invoke multiple times with different inputs
ensemble.run(Map.of("week", "2026-01-06"));
ensemble.run(Map.of("week", "2026-01-13"));
ensemble.run(Map.of("week", "2026-01-20"));

Merge example — builder provides a default, run-time call overrides it:

Ensemble ensemble = Ensemble.builder()
.agent(researcher)
.task(task)
.input("audience", "developers") // default
.build();
// audience = "developers" (builder default)
ensemble.run(Map.of("topic", "AI agents"));
// audience = "executives" (run-time overrides builder)
ensemble.run(Map.of("topic", "AI agents", "audience", "executives"));

If a task description contains a placeholder that is not supplied by either the builder inputs or the run-time inputs, a PromptTemplateException is thrown before any LLM calls:

// Task has {topic} and {year} placeholders
// Builder has no inputs configured
try {
ensemble.run(Map.of("topic", "AI")); // missing "year"
} catch (PromptTemplateException e) {
System.err.println("Missing: " + e.getMissingVariables());
// Missing: [year]
}

To include a literal {variable} in the output without substitution, use double braces:

Task task = Task.builder()
.description("Write a Java method that parses {{variable}} from a string. Variable name: {varName}")
.expectedOutput("A Java method with parameter name {varName}")
.agent(coder)
.build();

With input("varName", "userId"), the resolved description is:

Write a Java method that parses {variable} from a string. Variable name: userId

All tasks in the ensemble are resolved with the same inputs. Any variable defined on the builder is available in all task descriptions and expected outputs:

var researchTask = Task.builder()
.description("Research {topic} in depth")
.expectedOutput("A summary of {topic}")
.agent(researcher)
.build();
var writeTask = Task.builder()
.description("Write a blog post about {topic}")
.expectedOutput("A 700-word blog post about {topic}")
.agent(writer)
.context(List.of(researchTask))
.build();
// Both tasks receive the same variable
Ensemble.builder()
.agent(researcher).agent(writer)
.task(researchTask).task(writeTask)
.input("topic", "AI agents")
.build()
.run();

Variable names are case-sensitive and can contain letters, digits, and underscores only (no hyphens or spaces):

{topic} -- valid
{company_name} -- valid
{year2025} -- valid
{TOPIC} -- valid, but different from {topic}
{company-name} -- invalid (hyphen not allowed)
{company name} -- invalid (space not allowed)

Values are always strings. Passing null as a value is not permitted by Map.of(). Empty strings are allowed but will produce empty substitutions:

Ensemble.builder()
.agent(researcher)
.task(task)
.input("topic", "") // substitutes empty string
.build()
.run();