kestra-io/kestra · error · IllegalArgumentException

Cannot concat {} with {}

Error message

Cannot concat {} with {}

What it means

Thrown by TypedObjectWriter.concatSpecialized when both the current accumulated value and the incoming value are non-scalar objects (not Boolean, Character, String, or Number). The typed writer can only concatenate scalars via string join; attempting to merge two complex objects (e.g., two Maps, or a Map and a List) is not supported.

Source

Thrown at core/src/main/java/io/kestra/core/runners/pebble/TypedObjectWriter.java:67

        concatSpecialized(s);
    }

    private void concatSpecialized(final Object o) {
        if (o == null) {
            return;
        }

        if (current == null) {
            current = o;
            return;
        }

        if (isConcatenableScalar(current) && isConcatenableScalar(o)) {
            current = current.toString() + o;
            return;
        }

        throw new IllegalArgumentException(
            "Cannot concat " + current.getClass().getName() + " with " + o.getClass().getName()
        );
    }

    @SneakyThrows
    @Override
    public void write(Object o) {
        if (o == null) {
            return;
        }

        if (current == null) {
            current = o;
        } else if (isConcatenableScalar(o)) {
            // do call the writeSpecialized method depending on the object type.
            SpecializedWriter.super.write(o);
        } else {
            throw new IllegalArgumentException(

View on GitHub (pinned to 823fada927)

Solutions

  1. Avoid concatenating two complex objects; instead merge them or access individual scalar fields and concatenate those.
  2. Use the json filter or to_json to convert an object to a string before concatenating: '{{ obj1 | json ~ obj2 | json }}'.
  3. Restructure the template to build the desired structure with explicit map/list literals rather than concatenation.

Example fix

{# before #}
{{ outputs.task1.value ~ outputs.task2.value }}

{# after #}
{{ outputs.task1.value | json }}{{ outputs.task2.value | json }}
Defensive patterns

Strategy: type-guard

Type guard

{# In Pebble, check that both values are scalars before concatenating #}
{% set a = outputs.task1.value %}
{% set b = outputs.task2.value %}
{% if a is string and b is string %}
  {{ a ~ b }}
{% else %}
  {{ a | json }}{{ b | json }}
{% endif %}

Prevention

When it happens

Trigger: A Pebble template that uses concatenation ('~' operator or implicit concatenation in a typed/stringified rendering context) to join two complex, non-scalar values — e.g., '{{ obj1 ~ obj2 }}' where both are Maps or Lists. Reached during renderTyped or when the stringify fallback writer processes mixed types. This specific path is through concatSpecialized, which is called by the writeSpecialized overloads.

Common situations: Building a JSON structure in a template where two map/list fragments are concatenated instead of merged. Using the tilde operator on variables that resolve to objects rather than strings in a renderTyped context.

Related errors


AI-assisted analysis of kestra-io/kestra@823fada927 (2026-08-14). Data as JSON: /api/errors/852e5da87c3dcbf6. Report an issue: GitHub.