Skip to content

Response Transformation

A Jinja2 template applied to the upstream API response body before it is returned to the widget — letting you filter, reshape, or reformat responses on the server.

The template applies to the incoming response body only — it has no effect on the outgoing request. If the field is blank, the original upstream response is returned as-is.

Fields

Response Body

A Jinja2 template applied to the upstream API response. The template has access to the full response body, status code, and headers.

If blank or not set, the upstream response is passed through unchanged.

Response Content-Type

Overrides the Content-Type header on the response returned to the widget. If blank, the upstream API's original Content-Type is preserved.

Set this when your template changes the output format — for example, if the upstream API returns XML but your template produces JSON, set this to application/json.

Variables

TIP

For the full list of variables, filters, and functions available in templates, see Template Variables.

Three variables are specific to response templates:

  • response.body — the upstream response body as a decoded UTF-8 string. If the response is not valid UTF-8 (e.g. binary content), this is None.
  • response.status_code — the HTTP status code from the upstream API (integer).
  • response.headers — the response headers from the upstream API as a dictionary. Header names are lowercase (e.g., content-type, not Content-Type).

Also available: user.* fields, tenant_id, now(), and all filters (from_json, json_encode, tojson, base64_encode, base64_decode).

get_secret() is not available here

Response templates do not have access to {{ get_secret() }}. Response output is returned to the browser, so secrets are excluded to prevent accidental leakage. Use headers or query parameters if you need to include secret values in the request to the upstream API. See Secrets for details.

jwt_encode() is not available here

Response templates do not have access to jwt_encode(). JWTs are used for authenticating outgoing requests, not for shaping response data.

body_text and body_raw are not available

{{ body_text }} and {{ body_raw }} are not available in response templates. These variables represent the incoming request body — use response.body to access the upstream API's response.

Examples

Extract specific fields from a large response:

jinja2
{% set data = response.body | from_json %}
{
  "id": {{ data.id }},
  "name": "{{ data.name }}",
  "status": "{{ data.status }}"
}

Handle errors based on status code:

jinja2
{% if response.status_code == 200 %}
{% set data = response.body | from_json %}
{ "result": {{ data | tojson }} }
{% else %}
{ "error": true, "status": {{ response.status_code }} }
{% endif %}

Include user context in the response:

jinja2
{% set data = response.body | from_json %}
{
  "tenant": "{{ tenant_id }}",
  "requested_by": "{{ user.email }}",
  "results": {{ data.results | tojson }}
}

Do not output from_json directly

Never use {{ response.body | from_json }} as the entire response template — it produces HTML-escaped Python notation, not valid JSON. Leave the field blank to forward the response as-is, or use {% set %} to extract specific fields. See Template Variables — Behavioral notes for details.

Response Transform Gotchas

Parsing JSON responses

response.body is a raw UTF-8 string. To parse it, use the from_json filter:

jinja2
{% set data = response.body | from_json %}
{{ data.user.name }}

If parsing fails (invalid JSON), the connector returns HTTP 422 with an error.

Dict access: use subscripts, not attributes

When a dictionary key collides with a built-in dict method name (items, keys, values, get, update, …), you must use bracket notation data['items']. Dot notation (data.items) returns the Python method instead of the key's value. Bracket notation is the safe default for all key access:

jinja2
{# Correct — subscript access #}
{{ data['items'] }}
{{ data['user']['name'] }}

{# Wrong — returns built-in dict method, not the key value #}
{{ data.items }}    {# Returns the dict.items() method itself! #}
{{ data.keys }}     {# Returns the dict.keys() method! #}
{{ data.get }}      {# Returns the dict.get() method! #}

This is especially common when the response has a top-level key named items, keys, values, or get (e.g., Contentful's items array). Dot notation on dicts returns the built-in Python method, causing errors like:

error: object of type 'builtin_function_or_method' has no len()

Always use bracket notation: data['items'] instead of data.items.

Accumulating across loops with namespace()

To collect values across a loop you must use namespace(). A plain {% set %} inside a {% for %} is scoped to the loop body and is silently discarded afterwards:

jinja2
{# Correct — namespace persists across iterations #}
{% set ns = namespace(items=[]) %}
{% for record in data['results'] %}
  {% set ns.items = ns.items + [record['id']] %}
{% endfor %}
{{ ns.items | json_encode }}

{# Correct — accumulate only the rows you want #}
{% set ns = namespace(items=[]) %}
{% for item in data['items'] %}
  {% if item['active'] %}
    {% set ns.items = ns.items + [item] %}
  {% endif %}
{% endfor %}
{{ ns.items | json_encode }}

{# Wrong — `items` is empty after the loop (loop-body scope) #}
{% set items = [] %}
{% for record in data['results'] %}
  {% set items = items + [record['id']] %}
{% endfor %}
{{ items | json_encode }}   {# always renders [] #}

Loop-scoping gotcha

{% set items = items + [...] %} inside a {% for %} block does not update the outer items variable — Jinja2 loop bodies have their own scope. The assignment is silently discarded, and items is empty after the loop. Always use {% set ns = namespace(items=[]) %} and {% set ns.items = ... %} to accumulate across iterations.

The environment is sandboxed — you can't reach Python internals such as data.__class__. It does not block calling mutation methods like .append() or .update(), but those produce no output on their own, so always build and emit results with {% set %} / namespace().

get_secret() is not available

Response templates do not have access to {{ get_secret() }}. Response output is returned to the browser, so secrets are excluded to prevent accidental leakage. If you need secret values in the request to the upstream API, put them in the authentication config, headers, or query parameters instead. See Secrets for details.

Worked Examples

The examples below were verified by running the exact templates through ResponseData.transform against real input. The "Rendered output" block shows what the renderer actually returned.

Example 1: Reshape a list

Upstream Contentful-style response with nested sys.id and fields.title — produce a flat array with only the fields the widget needs.

Input JSON (abbreviated)

json
{
  "items": [
    { "sys": { "id": "entry-001" }, "fields": { "title": "Widget Docs" } },
    { "sys": { "id": "entry-002" }, "fields": { "title": "Getting Started" } }
  ]
}

Template

jinja2
{% set data = response.body | from_json %}
{% set ns = namespace(out=[]) %}
{% for item in data['items'] %}
{% set ns.out = ns.out + [{"id": item['sys']['id'], "title": item['fields']['title']}] %}
{% endfor %}
{{ ns.out | json_encode }}

Rendered output

json
[{"id": "entry-001", "title": "Widget Docs"}, {"id": "entry-002", "title": "Getting Started"}]

TIP

namespace() is required here. A plain {% set out = out + [...] %} inside a {% for %} block is silently discarded — see the loop-scoping gotcha above.


Example 2: Resolve linked assets by id (Contentful)

The response contains an items array where each entry references an asset by id, and a separate includes.Asset array holding the full asset data. For each item, attach its image URL by matching on the asset id.

Input JSON (abbreviated)

json
{
  "items": [
    { "sys": { "id": "entry-001" }, "fields": { "title": "First Post", "imageId": "asset-abc" } },
    { "sys": { "id": "entry-002" }, "fields": { "title": "Second Post", "imageId": "asset-def" } }
  ],
  "includes": {
    "Asset": [
      { "id": "asset-abc", "url": "https://images.ctfassets.net/abc.jpg" },
      { "id": "asset-def", "url": "https://images.ctfassets.net/def.jpg" }
    ]
  }
}

Template

jinja2
{% set data = response.body | from_json %}
{% set ns = namespace(out=[]) %}
{% for item in data['items'] %}
{% set matching = data['includes']['Asset'] | selectattr('id', 'equalto', item['fields']['imageId']) | list %}
{% set img_url = matching[0]['url'] if matching else '' %}
{% set ns.out = ns.out + [{"id": item['sys']['id'], "title": item['fields']['title'], "imageUrl": img_url}] %}
{% endfor %}
{{ ns.out | json_encode }}

Rendered output

json
[{"id": "entry-001", "title": "First Post", "imageUrl": "https://images.ctfassets.net/abc.jpg"}, {"id": "entry-002", "title": "Second Post", "imageUrl": "https://images.ctfassets.net/def.jpg"}]

TIP

selectattr('id', 'equalto', ...) is sandbox-safe and avoids building a dictionary lookup manually. The | list coerces the lazy iterator so you can index into matching[0].

WARNING

Always use data['includes']['Asset'] (subscript), not data.includes.Asset. The key items on the top-level dict and Asset nested inside includes both require subscript access to avoid shadowing built-in dict methods.


Example 3: Filter a list

Return only items where a boolean field is truthy — strip unpublished records before they reach the widget.

Input JSON (abbreviated)

json
{
  "items": [
    { "id": "a", "title": "Alpha", "published": true },
    { "id": "b", "title": "Beta",  "published": false },
    { "id": "c", "title": "Gamma", "published": true }
  ]
}

Template

jinja2
{% set data = response.body | from_json %}
{% set ns = namespace(out=[]) %}
{% for item in data['items'] %}
{% if item['published'] %}
{% set ns.out = ns.out + [{"id": item['id'], "title": item['title']}] %}
{% endif %}
{% endfor %}
{{ ns.out | json_encode }}

Rendered output

json
[{"id": "a", "title": "Alpha"}, {"id": "c", "title": "Gamma"}]

TIP

Like the others, this example needs the namespace() accumulator (a {% set %} directly inside the loop would be discarded). If no items match the {% if %}, the result is an empty array [] — handle that case in your widget.


Error handling

If the response template fails to render (e.g. syntax error, type mismatch), the connector returns HTTP 422 with a JSON error envelope whose errors.scope is "Configuration (response template rendering)". This is separate from request template errors, which use "Configuration (template rendering)".

The response template runs on all upstream status codes — including 4xx and 5xx errors. This means you can normalize error responses from different APIs into a consistent format for your widget.

Next Steps

Gainsight CC Developer Portal