conductor-skills

Template / ${...} Resolution

Conductor resolves ${...} expressions when it builds each task’s input. The resolver is forgiving — sometimes too forgiving — and a few of its behaviors produce silent data corruption rather than loud failures. This page documents the cases that have caused production bugs.

For the basic expression syntax (${workflow.input.x}, ${task.output.y}, ${workflow.variables.z}), see workflow-definition.md. For the JS scope rules ($.varName etc.), see graaljs-gotchas.md.

Pitfall 1: a missing intermediate field silently returns the parent

If you write ${task.output.a.b.c} and task.output.a.b exists but c does not, the resolver does not evaluate to null. It returns the closest resolvable ancestor — typically the parent object — without raising.

Concrete example: an LLM emits

{ "tool_call": { "name": "get_weather", "args": {} } }

and a downstream task tries to read ${chat.output.result.text}. There is no .text field. The downstream field gets set to the whole result object (the parent), Java-toString’d into a string field — and silently flows on as the agent’s “final answer.”

Mitigation:

Pitfall 2: interpolating an object into a string field invokes Java toString

Conductor’s string-typed input fields (e.g. messages[].message, HTTP request bodies as strings, an LLM prompt) accept ${...} interpolation. If the resolved value is a structured object rather than a string, Conductor converts it via Java’s Map.toString():

{key1=value1, key2=value2}

Note the = separators — that is not JSON. Sending this into a message field results in {k=v} garbage entering the LLM’s chat history, the provider almost always returns nonsense, and the downstream output.result route fails.

Fix: stringify upstream with JSON_JQ_TRANSFORM and tojson:

{
  "name": "stringify_tool_result",
  "taskReferenceName": "stringify_tool_result",
  "type": "JSON_JQ_TRANSFORM",
  "inputParameters": {
    "data": "${call_tool.output.content}",
    "queryExpression": ".data | tojson"
  }
}

JQ input shape. Conductor’s JSON_JQ_TRANSFORM passes the entire resolved inputParameters map (minus queryExpression) to JQ as the input value. To reference a single field, use .data, .foo, etc. — not bare .. Writing ". | tojson" stringifies the whole wrapper ({"data": ...}), which is rarely what you want.

Then interpolate ${stringify_tool_result.output.result} into the string field.

You cannot do this in INLINE/graaljs — JSON.stringify on a Java-Map-backed proxy returns "{}". See graaljs-gotchas.md Rule 3.

Pitfall 3: DO_WHILE iteration counter — ${loop.output.iteration}, not ${loop.iteration}

The iteration counter lives inside the loop task’s outputData, not as a top-level field on the task. The correct access paths are:

Where Expression
workflow.outputParameters ${loop.output.iteration}
Another task’s inputParameters ${loop.output.iteration}
Inside the loop’s own loopCondition (after wiring loop: ${loop.output}) $.loop['iteration']

${loop.iteration} resolves to nothing useful — the resolver returns the loop task object, not the iteration number. This is a frequent silent bug in outputParameters.

Pitfall 4: per-iteration outputs are keyed by iteration number

Each iteration of a DO_WHILE writes its body tasks’ outputs under a numeric key on the loop task:

${loop.output.1.<bodyTaskRef>.output...}
${loop.output.2.<bodyTaskRef>.output...}
...

To read the latest body output inside the loop, combine with $.loop['iteration']:

var i = $.loop['iteration'];
return $.loop[i].think.output.result.answer;

($.loop must be wired in via inputParameters — see Rule 6 in graaljs-gotchas.md.)

Pitfall 5: ${workflow.variables.X} only resolves inside ${...} interpolation, not inside JS

${workflow.variables.X} is a template-resolver feature, not a JavaScript binding. It works in inputParameters values, HTTP URIs, and other string fields. It does not work inside an INLINE / DO_WHILE / SWITCH-javascript script — there is no $.workflow object in scope.

// works
"inputParameters": { "endpoint": "${workflow.variables.api_base}/users" }

// fails  throws "Cannot read property 'variables' from undefined"
"expression": "(function(){ return $.workflow.variables.api_base; })();"

If you need a workflow variable inside a script, plumb it through inputParameters:

"inputParameters": {
  "evaluatorType": "graaljs",
  "expression": "(function(){ return $.api_base + '/users'; })();",
  "api_base": "${workflow.variables.api_base}"
}

Quick-reference table

Pattern Result
${task.output.field} (field exists) the field’s value
${task.output.missing} the parent object (silently — no error)
${task.output.path.to.missing} closest resolvable ancestor
Object → string field Java Map.toString(){k=v}
Object → string field via JQ tojson proper JSON string
${loop_ref.iteration} the loop task object (wrong)
${loop_ref.output.iteration} the iteration counter (correct)
$.workflow.input.x inside JS TypeError (not in scope)
${workflow.input.x} inside a string field resolves to the input value