cannectors

Templating

The Jinja template engine — syntax, contexts, contextual escaping, and the SQL parameters model.

Cannectors templates request fields with a Jinja engine (Jinja2 syntax). Every dynamic field — HTTP endpoints, headers, request bodies, SOAP envelopes, SQL queries, cache keys — goes through the same engine, with escaping matched to the field's format applied automatically.

output:
  type: httpRequest
  endpoint: https://api.example.com/customers/{{ record.customerId }}
  method: POST
  body: |
    {
      "email": "{{ record.email | lower }}",
      "tier": "{{ record.tier | default('standard') }}"
      {% if record.discount %},"discount": "{{ record.discount }}"{% endif %}
    }

Syntax

ConstructExample
Output tag{{ record.customer.id }}
Array indexing{{ record.items[0].sku }}
Filter{{ record.email | lower }}
Default value{{ record.tier | default('standard') }}
Condition{% if record.priority == "high" %}URGENT{% endif %}
Loop{% for tag in record.tags %}<t>{{ tag }}</t>{% endfor %}
Comment{# ignored #}

Common filters: default(value), lower, upper, capitalize, trim, join(sep), length, replace(old, new). The full set is the standard Jinja builtin catalog.

Unsupported tags

Two families of Jinja tags are rejected at compile time — cannectors validate reports them with the field path:

TagWhy
{% include %}, {% extends %}, {% import %}, {% from %}A template pulled from disk would skip contextual escaping, and its path can be a record expression. Keep each template self-contained; external files go through bodyTemplateFile / queryFile, which are escaped normally.
{% filter %} (block form)It transforms the value after escaping, which corrupts the output (&lt; becomes &LT;). Use an inline filter instead: {{ value | upper }}.

Template context

Top-level variables available inside templates:

VariableContentsAvailable in
recordThe current record.All templated fields.
metaThe record's _metadata map.All templated fields.
statePersisted incremental state: state.lastRunTimestamp (RFC3339, epoch on first run), state.lastRunId.database input parameters.
paginationCurrent page values: pagination.offset, pagination.cursor, pagination.limit.database input parameters (re-evaluated per page).

How values are rendered

Substituted values keep the record's own representation, not Jinja's Python flavour: 42 renders as 42 (not 42.0), and true renders as true (not True). This holds on every target, so {{ record.id }} in a URL and the {id} key placeholder always produce the same text, and a number or boolean substituted into a JSON body stays valid JSON.

Contextual escaping

Substituted values are escaped for the field they land in — the literal text you write around them is never touched:

FieldEscaping
HTTP / SOAP endpointURL query escaping (a b&ca+b%26c).
JSON request bodyJSON string escaping — a value containing " or a newline cannot break the payload.
SOAP body and headersXML escaping (<, >, &, " become entities).
HTTP headers, cache keysNone (raw text).
SQL queryNot applicable — dynamic values go through bound parameters, see below.

SQL: query + parameters

SQL queries never interpolate values into the text. The query is a Jinja template that renders to SQL shape — with $1, $2, … placeholders — and the parameters list supplies the values, bound by the driver:

filters:
  - type: sql_call
    connectionStringRef: ${REFERENCE_DATABASE_URL}
    query: |
      SELECT tier, owner FROM customers
      WHERE id = $1
      {% if record.region %}AND region = $2{% endif %}
    parameters:
      - record.customer.id       # → $1
      - record.region            # → $2

Each parameters entry is an expr expression — the same language as the condition filter. Values bind natively typed: an integer stays an integer, a missing field binds as NULL. Use ?? for explicit fallbacks (pagination.cursor ?? 0) and ?. for optional navigation.

Write $1, $2, … regardless of the database: Cannectors translates them per driver (kept for PostgreSQL, ? for MySQL/SQLite). A placeholder inside a {% if %} branch that isn't taken is neither evaluated nor bound.

cannectors validate checks the pairing statically: a $N with no matching entry, or a parameter never referenced by the query, is a validation error. Queries loaded from queryFile are checked too — the path is resolved from the working directory, as at run time.

An output tag ({{ … }}) inside a SQL query is rejected by cannectors validate — it would splice a raw, unescaped value into the SQL. Dynamic values always go through parameters; only control blocks ({% if %}, {% for %}) may shape the query text.

Missing variables

  • HTTP fields (endpoint, headers, body) render a missing variable as an empty string. Use default('…') to make fallbacks explicit.
  • SOAP bodies and SQL query text are strict: a variable that is missing — or present but null — fails the record instead of silently sending an incomplete request. default('…') covers both cases.
  • SQL parameters are permissive: a missing field binds as NULL.

Not templating: {paramName} keys

Some modules (httpRequest, http_call, SOAP modules) also support a keys list that injects record fields into the URL path, query string, or headers via single-brace placeholders:

endpoint: https://api.example.com/projects/{projectId}/tasks
keys:
  - field: project.id
    paramType: path
    paramName: projectId

This is a separate, structured mechanism — {projectId} is not a Jinja template. Both can coexist in the same endpoint.

Cross-references