Write Handlebars template expressions for Celigo integrations -- dynamic values in mappings, HTTP bodies, SQL queries, URIs, and filters. Use when building any resource configuration that needs computed, conditional, or formatted field values.
Handlebars is Celigo's template language for embedding dynamic values into resource configurations. Any string field that the platform evaluates at runtime can contain Handlebars expressions.
Concerns when writing Handlebars:
Context -- where the expression runs determines what data is available and how output is treated
Field access -- record. prefix in all contexts (AFE 2.0), @root for job/settings/connection, bracket notation for special characters
Helpers -- 79 custom helpers for math, string manipulation, dates, encoding, regex, and more
Block helpers -- #each, #if, #compare, #with for iteration and conditional logic
Date/time -- moment.js format tokens with timezone support
Used across exports, imports, mappings, output filters, and APIs.
Where Handlebars Are Used
Mapping extracts
In import mappings[].extract fields, Handlebars concatenates, transforms, or conditionally selects values. The context is the current record.
HTTP request templates
Export and import http blocks use Handlebars in relativeURI, body, headers, and postBody. Triple braces are essential to avoid HTML encoding of query parameters and JSON.
RDBMS SQL queries
SQL queries in rdbms.query use Handlebars with the mandatory record. prefix. Triple braces prevent encoding of SQL-significant characters like commas and quotes. For full SQL patterns (MERGE, upsert, bulk operations, dialect differences), see writing-sql.
Output filters
Expression-based filters on exports use Handlebars to evaluate whether a record passes through or gets skipped.
File paths and names
Dynamic file names in FTP/S3 exports and imports use Handlebars for timestamps and record-derived values.
Delta tokens
Platform-injected variables like {{{lastExportDateTime}}} provide the last successful export timestamp for incremental syncs. These are not record fields -- the platform injects them at runtime into the export's HTTP/query context only.
Quick Reference
Context Decision Matrix (AFE 2.0)
All contexts use record. prefix to access the current record's fields (AFE 2.0). Do NOT use bare field names, data.field, or data.0.field -- those are deprecated AFE 1.0 patterns. Exception: Mapper 1.0 (Salesforce/NetSuite) uses bare field names without record. prefix.
Where
Syntax
Data prefix
Example
Mapping extract
{{ }} (double)
record.
{{record.firstName}}
HTTP relative URI
{{{ }}} (triple)
record.
{{{record.orderId}}} in URI
HTTP body / postBody
{{{ }}} (triple)
record.
{{{record.orderId}}} in JSON body
SQL query (RDBMS)
{{{ }}} (triple)
record.
{{{record.email}}} in WHERE clause
Output filter
{{ }} (double)
record.
{{record.status}}
Delta URI parameter
{{{ }}} (triple)
(platform-injected)
{{{lastExportDateTime}}}
Additional context objects available via @root:
Object
Description
record
Current record being processed
job
Current job metadata
settings
Integration/flow settings
connection
Connection object (for auth headers)
When one-to-many grouping is configured, the data shape changes to batch_of_records -- iterate with {{#each batch_of_records}} to access individual records.
Key Syntax
{{{triple-braces}}} -- raw output, no escaping. Use for URIs, SQL, JSON bodies, file paths -- anywhere commas, quotes, or ampersands matter. In RDBMS, triple braces output the raw value (value); double braces wrap in single quotes ('value'). Prefer triple and add literal quotes explicitly where needed.
{{double-braces}} -- context-dependent formatting. In RDBMS adds single quotes around the value. In URLs, URL-encodes. Use triple braces for explicit control.
Always use record. prefix (AFE 2.0) -- {{{record.fieldName}}} in all contexts, never bare {{{fieldName}}} or {{{data.fieldName}}} (AFE 1.0). Nested fields: {{{record.properties.email}}}.
Exception: Mapper 1.0 (Salesforce/NetSuite) -- uses bare field names without record. prefix. This is the only context where bare field references are correct.
Uses moment.js tokens. Always use triple braces for date output.
Common tokens: YYYY (4-digit year), MM (2-digit month), DD (2-digit day), HH (24h hour), mm (minute), ss (second), SSS (millisecond), Z (timezone offset), X (Unix seconds), x (Unix milliseconds).
Timezone: pass as third argument -- {{{dateFormat "YYYY-MM-DD" record.date "US/Eastern"}}}.
Date arithmetic
dateAdd works in milliseconds:
1 hour = 3,600,000
1 day = 86,400,000
7 days = 604,800,000
Runtime Context at Each Stage
What {{record.X}} or {{settings.Y}} actually resolves to depends on which bubble the expression runs in. The shapes below were captured by setting body: "{{{jsonSerialize this}}}" on import/lookup bubbles and echoing through a mirror endpoint — they represent exactly what's available at runtime.
Body templates and Handlebars in mappings[].extract run per-record with this context:
{
"0": { ...the record at batch index 0, with mapped fields... }, // per-record
"data": [ ...array of all records in this page, post-mapping... ],
"lookup": { ...merged results from preceding lookup steps... },
"recordLookupError": null | { ... }, // set when a lookup failed
"settings": { "import": {...}, "connection": {...} },
"connection": { /* FULL connection object: auth, baseURI, etc. */ },
"import": { /* full import config */ },
"job": { "parentJob": { "_id", "type", "startedAt", ... } },
"templateVersion": 1,
"testMode": false
}
Lookup bubble (HTTPExport with isLookup: true)
Lookup request templates run per-record with a different shape:
{
"exportStartTime": "ISO timestamp",
"settings": { "export": {...}, "connection": {...} },
"connection": { /* full connection object */ },
// the record is spread at TOP LEVEL (not indexed by `0` like imports)
"_id": "...",
"name": "...",
"<other record fields>": "...",
"data": {
/* copy of the record */,
"_INITDATA": { /* original record before transforms */ }
}
}
Export bubble (source generator)
Export URI templates and delta tokens have a minimal context — the platform injects {{{lastExportDateTime}}}, {{{currentExportDateTime}}}, plus settings and connection. No record. context exists yet (records haven't been fetched).
Key differences between import and lookup contexts
Context key
Import
Lookup
Record location
0.<field> + data[].<field>
<field> (top-level) + data.<field>
data shape
array of records
single record (with _INITDATA nested)
exportStartTime
No
Yes
lookup
Yes (merged preceding results)
N/A
import / job / recordLookupError / testMode
Yes
No
connection (full)
Yes
Yes
How to rediscover the shape for any bubble
Set the body on an HTTP import or lookup to {{{jsonSerialize this}}} and point it at an echo endpoint (integrator.io's /v1/mirror works). Enable flow execution logging, run the flow, and inspect the apiCall.response.body — it's a copy of what you sent, which is the full runtime context. This works for any bubble whose adaptor sends an HTTP body.
How to Write a Handlebars Expression
1. Identify the context
Where the expression runs determines what data is available. In AFE 2.0, all contexts use record. to access the current record:
Context
Available data
Prefix
Mapping extract
Current record
record.
HTTP body/URI
Current record
record.
RDBMS query
Current record
record.
Output filter
Current record
record.
Delta URI parameter
Platform variables
lastExportDateTime, lastExportDateTimeUTC
Other context objects (job, settings, connection) are accessible via @root -- e.g., {{@root.connection.http.encrypted.apiKey}}.
When one-to-many grouping is active, the shape is batch_of_records and you must iterate: {{#each batch_of_records}}{{record.field}}{{/each}}.
2. Know the data shape
Before writing any expression, inspect what the input data looks like:
# Test-run an export to see actual record shapes
celigo exports invoke <exportId>
# Check mock output for the expected shape
celigo --jq '.mockOutput' exports get <exportId>
3. Choose the right braces
Default to {{{ }}} (triple) for HTTP bodies, SQL, URIs, file paths
Use {{ }} (double) only in mapping extracts and display text where HTML escaping is acceptable
When in doubt, use triple -- raw output never breaks SQL or JSON; HTML-escaped output can
4. Find the right helper
See the helper index for all 79 custom helpers. Key categories:
Build by a preSavePage hook (which can inject fields into the record), rendered with triple braces:
SELECT id FROM orders WHERE status IN ({{{record.statusList}}})
JavaScript-to-Handlebars equivalents
JavaScript
Handlebars
str.split("?id=")[1]
{{split record.field "?id=" 1}}
str.replace("old", "new")
{{replace record.field "old" "new"}}
str.match(/pattern/)
{{{regexMatch record.field "pattern"}}}
Math.abs(n)
{{abs record.field}}
arr.length
{{record.items.length}}
Pre-Submit Checklist
Before finalizing any Handlebars expression, verify each item:
Prefer triple braces {{{ }}}. Double braces apply context-dependent formatting -- in RDBMS they wrap values in single quotes ('value'), in URLs they URL-encode. Use triple braces for explicit control and add literal quotes where needed.
record. prefix everywhere (AFE 2.0). All contexts use record.fieldName -- mappings, HTTP bodies, SQL, filters. Never use bare fieldName, data.fieldName, or data.0.fieldName (AFE 1.0). Exception: Mapper 1.0 (Salesforce/NetSuite) uses bare field names.
lastExportDateTime only in export context. This platform-injected variable is available in the export's HTTP/query context for delta syncs only -- not in mappings or import templates.
dateAdd values in milliseconds. 1 day = 86,400,000. Not seconds, not hours.
#each context shifts. Inside {{#each}}, this is the current item. Use ../ for parent or @root for top-level fields.
Missing fields fail silently. Handlebars outputs empty string for undefined fields. Guard with {{#if field}} when the downstream system rejects empty values.
Bracket notation for special characters. Field names with spaces, dots, or hyphens need record.[Field Name] syntax.
compare is string-based.{{#compare "9" ">" "10"}} is TRUE (lexicographic). Convert values first or use strict operators.
Test with real data. Run celigo exports invoke or celigo imports invoke to verify the expression renders correctly with actual records.
Gotchas
Double braces apply auto-formatting.{{ }} adds context-dependent formatting -- in RDBMS it wraps values in single quotes ('value'), in URLs it URL-encodes. This can corrupt SQL queries and JSON bodies. Prefer {{{ }}} (raw output) and add literal quotes explicitly where needed.
Always use record. prefix (AFE 2.0). Use {{{record.fieldName}}}, not {{{fieldName}}} or {{{data.fieldName}}}. The record. prefix applies in all contexts -- mappings, HTTP, SQL, filters. Bare field names and data. prefix are deprecated AFE 1.0 syntax.
lastExportDateTime is platform-injected. It exists only in the export's HTTP/query context for delta syncs -- not available in mappings or import templates.
compare does string comparison.{{#compare "9" ">" "10"}} is TRUE because "9" > "1" lexicographically. Use the strict equality operators or convert values first.
Nested #each changes context. Inside {{#each record.items}}, this is the current item, not the record. Use ../ to reach the parent or @root for the top-level context.
dateAdd uses milliseconds, not seconds. Adding 1 day is 86400000, not 86400. A common mistake that produces dates seconds in the future instead of days.
regexMatch returns the match string; regexSearch returns the position. Don't confuse them -- regexSearch returns a number (0-indexed position), not the matched text.
Raw blocks {{{{ }}}} output literal Handlebars syntax. They are for escaping {{ }} in output, not for "extra raw" rendering.
Missing fields produce empty string silently. No error on missing fields -- Handlebars outputs nothing. Use {{#if field}} to guard when the downstream system rejects empty values.
jsonEncode wraps a single value, not a whole body. It adds quotes and escapes special characters for embedding one field in a JSON string. Don't wrap the entire template in it.
Common Errors
Symptom
Cause
Fix
&, <, or unexpected 'quotes' in SQL/JSON output
Double braces {{ }} applying auto-formatting (RDBMS adds single quotes, URLs get encoded)
Switch to triple braces {{{ }}} and add literal quotes where needed
Empty output, no error
Missing record. prefix (or using AFE 1.0 data.field)
Change to {{{record.fieldName}}} -- applies in all contexts
Delta export returns all records
lastExportDateTime used outside export context (e.g., in mapping)
Move to the export's relativeURI or query parameter
dateAdd produces date seconds ahead instead of days
Value in seconds instead of milliseconds
Multiply by 1000: use 86400000 not 86400
{{#compare "9" ">" "10"}} is TRUE
String comparison, not numeric
Convert to number first or restructure logic
undefined or empty in nested #each
this scope changed; referencing parent field without ../
Use ../fieldName or @root.fieldName
JSON body has trailing comma
{{#each}} without comma-guard logic
Add {{#if @last}}{{else}},{{/if}} between items
Bracket notation field returns empty
Using record.Field Name instead of record.[Field Name]
Wrap field name in brackets: record.[Field Name]
regexMatch returns a number
Used regexSearch (returns position) instead of regexMatch
Switch to regexMatch for the matched text
Entire body wrapped in quotes
Used jsonEncode on the whole template
Use jsonEncode only on individual field values, not the whole body
1---2name: writing-handlebars3description: Write Handlebars template expressions for Celigo integrations -- dynamic values in mappings, HTTP bodies, SQL queries, URIs, and filters. Use when building any resource configuration that needs computed, conditional, or formatted field values.4---56<!-- TIER:1 -->78# Writing Handlebars Expressions910Handlebars is Celigo's template language for embedding dynamic values into resource configurations. Any string field that the platform evaluates at runtime can contain Handlebars expressions.1112Concerns when writing Handlebars:1314- **Context** -- where the expression runs determines what data is available and how output is treated15- **Braces** -- double `{{ }}` vs triple `{{{ }}}` controls output escaping16- **Field access** -- `record.` prefix in all contexts (AFE 2.0), `@root` for job/settings/connection, bracket notation for special characters17- **Helpers** -- 79 custom helpers for math, string manipulation, dates, encoding, regex, and more18- **Block helpers** -- `#each`, `#if`, `#compare`, `#with` for iteration and conditional logic19- **Date/time** -- moment.js format tokens with timezone support2021Used across exports, imports, mappings, output filters, and APIs.2223## Where Handlebars Are Used2425### Mapping extracts2627In import `mappings[].extract` fields, Handlebars concatenates, transforms, or conditionally selects values. The context is the current record.2829### HTTP request templates3031Export and import `http` blocks use Handlebars in `relativeURI`, `body`, `headers`, and `postBody`. Triple braces are essential to avoid HTML encoding of query parameters and JSON.3233### RDBMS SQL queries3435SQL queries in `rdbms.query` use Handlebars with the mandatory `record.` prefix. Triple braces prevent encoding of SQL-significant characters like commas and quotes. For full SQL patterns (MERGE, upsert, bulk operations, dialect differences), see [writing-sql](../writing-sql/SKILL.md).3637### Output filters3839Expression-based filters on exports use Handlebars to evaluate whether a record passes through or gets skipped.4041### File paths and names4243Dynamic file names in FTP/S3 exports and imports use Handlebars for timestamps and record-derived values.4445### Delta tokens4647Platform-injected variables like `{{{lastExportDateTime}}}` provide the last successful export timestamp for incremental syncs. These are not record fields -- the platform injects them at runtime into the export's HTTP/query context only.4849## Quick Reference5051### Context Decision Matrix (AFE 2.0)5253All contexts use `record.` prefix to access the current record's fields (AFE 2.0). Do NOT use bare field names, `data.field`, or `data.0.field` -- those are deprecated AFE 1.0 patterns. **Exception:** Mapper 1.0 (Salesforce/NetSuite) uses bare field names without `record.` prefix.5455| Where | Syntax | Data prefix | Example |56|---|---|---|---|57| Mapping extract | `{{ }}` (double) | `record.` | `{{record.firstName}}` |58| HTTP relative URI | `{{{ }}}` (triple) | `record.` | `{{{record.orderId}}}` in URI |59| HTTP body / postBody | `{{{ }}}` (triple) | `record.` | `{{{record.orderId}}}` in JSON body |60| SQL query (RDBMS) | `{{{ }}}` (triple) | `record.` | `{{{record.email}}}` in WHERE clause |61| Output filter | `{{ }}` (double) | `record.` | `{{record.status}}` |62| Delta URI parameter | `{{{ }}}` (triple) | (platform-injected) | `{{{lastExportDateTime}}}` |6364Additional context objects available via `@root`:6566| Object | Description |67|--------|-------------|68| `record` | Current record being processed |69| `job` | Current job metadata |70| `settings` | Integration/flow settings |71| `connection` | Connection object (for auth headers) |7273When **one-to-many grouping** is configured, the data shape changes to `batch_of_records` -- iterate with `{{#each batch_of_records}}` to access individual records.7475### Key Syntax7677- **`{{{triple-braces}}}`** -- raw output, no escaping. Use for URIs, SQL, JSON bodies, file paths -- anywhere commas, quotes, or ampersands matter. **In RDBMS**, triple braces output the raw value (`value`); double braces wrap in single quotes (`'value'`). Prefer triple and add literal quotes explicitly where needed.78- **`{{double-braces}}`** -- context-dependent formatting. In RDBMS adds single quotes around the value. In URLs, URL-encodes. Use triple braces for explicit control.79- **Always use `record.` prefix (AFE 2.0)** -- `{{{record.fieldName}}}` in all contexts, never bare `{{{fieldName}}}` or `{{{data.fieldName}}}` (AFE 1.0). Nested fields: `{{{record.properties.email}}}`.80- **Exception: Mapper 1.0 (Salesforce/NetSuite)** -- uses bare field names without `record.` prefix. This is the only context where bare field references are correct.8182## Related Skills8384- [configuring-exports > Quick Reference](../configuring-exports/SKILL.md#quick-reference) -- export adaptor types, delta sync setup, output filters85- [configuring-imports > Quick Reference](../configuring-imports/SKILL.md#quick-reference) -- import adaptor types, operation modes, mapping systems86- [writing-mappings > Quick Reference](../writing-mappings/SKILL.md#quick-reference) -- Mapper 2.0 fields, lookups, conditional mappings8788<!-- TIER:2 -->8990## Syntax Fundamentals9192### Braces9394| Syntax | Behavior | When to use |95|--------|----------|-------------|96| `{{ }}` | Context-dependent formatting -- RDBMS wraps value in single quotes (`'value'`), URLs get URL-encoded | Use only when auto-formatting is desired |97| `{{{ }}}` | Raw output, no escaping or wrapping | Prefer everywhere -- SQL, JSON bodies, URIs, file paths. Add literal quotes yourself where needed |98| `{{{{ }}}}` | Raw block -- contents treated as literal string | Escaping Handlebars syntax itself |99100### Field access101102| Pattern | Meaning |103|---------|---------|104| `record.fieldName` | Standard field reference -- all contexts (AFE 2.0) |105| `record.nested.field` | Dot-notation for nested objects |106| `record.[Field With Spaces]` | Bracket notation for special characters in field names |107| `record.items.[0].name` | Array index access |108| `@root.fieldName` | Root context -- escape nested `#each` scope |109| `../fieldName` | Parent context -- one level up from current `#each` |110| `this` | Current iteration element |111| `@index` / `@key` | Current array index / object key in `#each` |112| `@first` / `@last` | Boolean -- first/last element in `#each` iteration |113114### Subexpressions (nesting helpers)115116Use `()` to nest one helper's output as input to another. The inner helper evaluates first:117118```119{{uppercase (split record.fullName " " 0)}} -- split then uppercase the first word120{{{base64Encode (join ":" record.user record.pass)}}} -- join then encode121{{#compare (add record.qty 1) ">" "100"}}...{{/compare}} -- add then compare122{{#each (after record.tags 3)}}...{{/each}} -- slice then iterate123```124125Subexpressions can be nested multiple levels deep. Each `()` resolves inside-out.126127### Block helpers128129- `{{#each record.items}}...{{/each}}` -- iterate array or object130- `{{#if record.active}}...{{else}}...{{/if}}` -- conditional131- `{{#compare val1 "==" val2}}...{{/compare}}` -- comparison (`==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=`)132- `{{#with record.address}}...{{/with}}` -- change context scope133134### Date/time formatting135136Uses moment.js tokens. Always use triple braces for date output.137138Common tokens: `YYYY` (4-digit year), `MM` (2-digit month), `DD` (2-digit day), `HH` (24h hour), `mm` (minute), `ss` (second), `SSS` (millisecond), `Z` (timezone offset), `X` (Unix seconds), `x` (Unix milliseconds).139140Timezone: pass as third argument -- `{{{dateFormat "YYYY-MM-DD" record.date "US/Eastern"}}}`.141142### Date arithmetic143144`dateAdd` works in **milliseconds**:145- 1 hour = 3,600,000146- 1 day = 86,400,000147- 7 days = 604,800,000148149## Runtime Context at Each Stage150151What `{{record.X}}` or `{{settings.Y}}` actually resolves to depends on **which bubble** the expression runs in. The shapes below were captured by setting `body: "{{{jsonSerialize this}}}"` on import/lookup bubbles and echoing through a mirror endpoint — they represent exactly what's available at runtime.152153### Import bubble (HTTPImport, NetSuiteDistributedImport, etc.)154155Body templates and Handlebars in `mappings[].extract` run per-record with this context:156157```158{159 "0": { ...the record at batch index 0, with mapped fields... }, // per-record160 "data": [ ...array of all records in this page, post-mapping... ],161 "lookup": { ...merged results from preceding lookup steps... },162 "recordLookupError": null | { ... }, // set when a lookup failed163 "settings": { "import": {...}, "connection": {...} },164 "connection": { /* FULL connection object: auth, baseURI, etc. */ },165 "import": { /* full import config */ },166 "job": { "parentJob": { "_id", "type", "startedAt", ... } },167 "templateVersion": 1,168 "testMode": false169}170```171172### Lookup bubble (HTTPExport with `isLookup: true`)173174Lookup request templates run per-record with a **different** shape:175176```177{178 "exportStartTime": "ISO timestamp",179 "settings": { "export": {...}, "connection": {...} },180 "connection": { /* full connection object */ },181182 // the record is spread at TOP LEVEL (not indexed by `0` like imports)183 "_id": "...",184 "name": "...",185 "<other record fields>": "...",186187 "data": {188 /* copy of the record */,189 "_INITDATA": { /* original record before transforms */ }190 }191}192```193194### Export bubble (source generator)195196Export URI templates and delta tokens have a minimal context — the platform injects `{{{lastExportDateTime}}}`, `{{{currentExportDateTime}}}`, plus `settings` and `connection`. No `record.` context exists yet (records haven't been fetched).197198### Key differences between import and lookup contexts199200| Context key | Import | Lookup |201|---|---|---|202| Record location | `0.<field>` + `data[].<field>` | `<field>` (top-level) + `data.<field>` |203| `data` shape | array of records | single record (with `_INITDATA` nested) |204| `exportStartTime` | No | Yes |205| `lookup` | Yes (merged preceding results) | N/A |206| `import` / `job` / `recordLookupError` / `testMode` | Yes | No |207| `connection` (full) | Yes | Yes |208209### How to rediscover the shape for any bubble210211Set the body on an HTTP import or lookup to `{{{jsonSerialize this}}}` and point it at an echo endpoint (integrator.io's `/v1/mirror` works). Enable flow execution logging, run the flow, and inspect the `apiCall.response.body` — it's a copy of what you sent, which is the full runtime context. This works for any bubble whose adaptor sends an HTTP body.212213## How to Write a Handlebars Expression214215### 1. Identify the context216217Where the expression runs determines what data is available. In AFE 2.0, all contexts use `record.` to access the current record:218219| Context | Available data | Prefix |220|---------|---------------|--------|221| Mapping extract | Current record | `record.` |222| HTTP body/URI | Current record | `record.` |223| RDBMS query | Current record | `record.` |224| Output filter | Current record | `record.` |225| Delta URI parameter | Platform variables | `lastExportDateTime`, `lastExportDateTimeUTC` |226227Other context objects (`job`, `settings`, `connection`) are accessible via `@root` -- e.g., `{{@root.connection.http.encrypted.apiKey}}`.228229When one-to-many grouping is active, the shape is `batch_of_records` and you must iterate: `{{#each batch_of_records}}{{record.field}}{{/each}}`.230231### 2. Know the data shape232233Before writing any expression, inspect what the input data looks like:234235```bash236# Test-run an export to see actual record shapes237celigo exports invoke <exportId>238239# Check mock output for the expected shape240celigo --jq '.mockOutput' exports get <exportId>241```242243### 3. Choose the right braces244245- Default to `{{{ }}}` (triple) for HTTP bodies, SQL, URIs, file paths246- Use `{{ }}` (double) only in mapping extracts and display text where HTML escaping is acceptable247- When in doubt, use triple -- raw output never breaks SQL or JSON; HTML-escaped output can248249### 4. Find the right helper250251See the [helper index](references/helpers/helper-index.md) for all 79 custom helpers. Key categories:252253- **[Math](references/helpers/math.md)** -- `abs`, `add`, `subtract`, `multiply`, `divide`, `modulo`, `ceil`, `floor`, `round`, `sum`, `avg`, `random`, `toFixed`, `toExponential`, `toPrecision`254- **[String](references/helpers/string.md)** -- `uppercase`, `lowercase`, `capitalize`, `capitalizeAll`, `camelcase`, `pascalcase`, `snakecase`, `dashcase`, `dotcase`, `pathcase`, `sentence`, `trim`, `trimLeft`, `trimRight`, `padLeft`, `padRight`, `replace`, `replacefirst`, `removefirst`, `chop`, `truncateWords`, `sanitize`, `split`, `join`, `reverse`, `occurrences`, `substring`255- **[Array](references/helpers/array.md)** -- `after`, `before`, `first`, `last`, `reverse`, `sort`, `unique`, `pluck`, `arrayify`, `lookup`, `getValue`, `sum`256- **[Date/time](references/helpers/date.md)** -- `dateFormat`, `dateAdd`, `timestamp`257- **[Encoding](references/helpers/encoding.md)** -- `base64Encode`, `base64Decode`, `htmlEncode`, `htmlDecode`, `jsonEncode`, `jsonParse`, `jsonSerialize`, `encodeURI`, `decodeURI`, `stripProtocol`, `stripQuerystring`258- **[Regex](references/helpers/regex.md)** -- `regexMatch`, `regexReplace`, `regexSearch`259- **[Auth/crypto](references/helpers/auth.md)** -- `hash`, `hmac`, `aws4`260- **[Type/logic](references/helpers/type-logic.md)** -- `typeOf`, `eq`, `isTruthy`, `isFalsey`, `hasOwn`, `hasNoItems`, `compare`261- **[Format](references/helpers/format.md)** -- `addCommas`, `bytes`, `ordinalize`262- **[Block helpers](references/helpers/block-helpers.md)** -- `#each`, `#if`, `#compare`, `#contains`, `#filter`, `#and`, `#or`, `#not`, `#unless`, `#with`, `#some`, `#startsWith`, `#inArray`, `#isEmpty`263264### 5. Test the expression265266```bash267# Invoke export to see if dynamic URI/query produces results268celigo exports invoke <exportId>269270# Invoke import to validate body template renders correctly271celigo imports invoke <importId>272```273274## Common Patterns275276### JSON comma separation in HTTP body templates277278Avoid trailing commas when building JSON arrays:279280```281{{#each record.items}}{...}{{#if @last}}{{else}},{{/if}}{{/each}}282```283284### Grouped data access (one-to-many / batch_of_records)285286When one-to-many grouping is configured, the data shape becomes `batch_of_records`. Iterate to access individual records:287288```289{{#each batch_of_records}}290 {{record.orderId}}291 {{record.[Shipping City]}}292{{/each}}293```294295### Conditional field with fallback296297```298{{#if record.nickname}}{{{record.nickname}}}{{else}}{{{record.firstName}}}{{/if}}299```300301### Nested iteration with parent context302303```304{{#each record.orders}}305 Order: {{{this.id}}} Customer: {{{../customerName}}}306 {{#each this.items}}307 Item: {{{this.sku}}}308 {{/each}}309{{/each}}310```311312### SQL IN clause from list variable313314Build by a preSavePage hook (which can inject fields into the record), rendered with triple braces:315316```317SELECT id FROM orders WHERE status IN ({{{record.statusList}}})318```319320### JavaScript-to-Handlebars equivalents321322| JavaScript | Handlebars |323|-----------|------------|324| `str.split("?id=")[1]` | `{{split record.field "?id=" 1}}` |325| `str.replace("old", "new")` | `{{replace record.field "old" "new"}}` |326| `str.match(/pattern/)` | `{{{regexMatch record.field "pattern"}}}` |327| `Math.abs(n)` | `{{abs record.field}}` |328| `arr.length` | `{{record.items.length}}` |329330<!-- TIER:3 -->331332## Pre-Submit Checklist333334Before finalizing any Handlebars expression, verify each item:335336- [ ] **Prefer triple braces `{{{ }}}`.** Double braces apply context-dependent formatting -- in RDBMS they wrap values in single quotes (`'value'`), in URLs they URL-encode. Use triple braces for explicit control and add literal quotes where needed.337- [ ] **`record.` prefix everywhere (AFE 2.0).** All contexts use `record.fieldName` -- mappings, HTTP bodies, SQL, filters. Never use bare `fieldName`, `data.fieldName`, or `data.0.fieldName` (AFE 1.0). Exception: Mapper 1.0 (Salesforce/NetSuite) uses bare field names.338- [ ] **`lastExportDateTime` only in export context.** This platform-injected variable is available in the export's HTTP/query context for delta syncs only -- not in mappings or import templates.339- [ ] **`dateAdd` values in milliseconds.** 1 day = 86,400,000. Not seconds, not hours.340- [ ] **`#each` context shifts.** Inside `{{#each}}`, `this` is the current item. Use `../` for parent or `@root` for top-level fields.341- [ ] **Missing fields fail silently.** Handlebars outputs empty string for undefined fields. Guard with `{{#if field}}` when the downstream system rejects empty values.342- [ ] **Bracket notation for special characters.** Field names with spaces, dots, or hyphens need `record.[Field Name]` syntax.343- [ ] **`compare` is string-based.** `{{#compare "9" ">" "10"}}` is TRUE (lexicographic). Convert values first or use strict operators.344- [ ] **Test with real data.** Run `celigo exports invoke` or `celigo imports invoke` to verify the expression renders correctly with actual records.345346## Gotchas3473481. **Double braces apply auto-formatting.** `{{ }}` adds context-dependent formatting -- in RDBMS it wraps values in single quotes (`'value'`), in URLs it URL-encodes. This can corrupt SQL queries and JSON bodies. Prefer `{{{ }}}` (raw output) and add literal quotes explicitly where needed.3492. **Always use `record.` prefix (AFE 2.0).** Use `{{{record.fieldName}}}`, not `{{{fieldName}}}` or `{{{data.fieldName}}}`. The `record.` prefix applies in all contexts -- mappings, HTTP, SQL, filters. Bare field names and `data.` prefix are deprecated AFE 1.0 syntax.3503. **`lastExportDateTime` is platform-injected.** It exists only in the export's HTTP/query context for delta syncs -- not available in mappings or import templates.3514. **`compare` does string comparison.** `{{#compare "9" ">" "10"}}` is TRUE because `"9" > "1"` lexicographically. Use the strict equality operators or convert values first.3525. **Nested `#each` changes context.** Inside `{{#each record.items}}`, `this` is the current item, not the record. Use `../` to reach the parent or `@root` for the top-level context.3536. **`dateAdd` uses milliseconds, not seconds.** Adding 1 day is `86400000`, not `86400`. A common mistake that produces dates seconds in the future instead of days.3547. **`regexMatch` returns the match string; `regexSearch` returns the position.** Don't confuse them -- `regexSearch` returns a number (0-indexed position), not the matched text.3558. **Raw blocks `{{{{ }}}}` output literal Handlebars syntax.** They are for escaping `{{ }}` in output, not for "extra raw" rendering.3569. **Missing fields produce empty string silently.** No error on missing fields -- Handlebars outputs nothing. Use `{{#if field}}` to guard when the downstream system rejects empty values.35710. **`jsonEncode` wraps a single value, not a whole body.** It adds quotes and escapes special characters for embedding one field in a JSON string. Don't wrap the entire template in it.358359## Common Errors360361| Symptom | Cause | Fix |362|---|---|---|363| `&`, `<`, or unexpected `'quotes'` in SQL/JSON output | Double braces `{{ }}` applying auto-formatting (RDBMS adds single quotes, URLs get encoded) | Switch to triple braces `{{{ }}}` and add literal quotes where needed |364| Empty output, no error | Missing `record.` prefix (or using AFE 1.0 `data.field`) | Change to `{{{record.fieldName}}}` -- applies in all contexts |365| Delta export returns all records | `lastExportDateTime` used outside export context (e.g., in mapping) | Move to the export's `relativeURI` or query parameter |366| `dateAdd` produces date seconds ahead instead of days | Value in seconds instead of milliseconds | Multiply by 1000: use `86400000` not `86400` |367| `{{#compare "9" ">" "10"}}` is TRUE | String comparison, not numeric | Convert to number first or restructure logic |368| `undefined` or empty in nested `#each` | `this` scope changed; referencing parent field without `../` | Use `../fieldName` or `@root.fieldName` |369| JSON body has trailing comma | `{{#each}}` without comma-guard logic | Add `{{#if @last}}{{else}},{{/if}}` between items |370| Bracket notation field returns empty | Using `record.Field Name` instead of `record.[Field Name]` | Wrap field name in brackets: `record.[Field Name]` |371| `regexMatch` returns a number | Used `regexSearch` (returns position) instead of `regexMatch` | Switch to `regexMatch` for the matched text |372| Entire body wrapped in quotes | Used `jsonEncode` on the whole template | Use `jsonEncode` only on individual field values, not the whole body |
Run npx skillmds@latest add celigo/writing-handlebars in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Write Handlebars template expressions for Celigo integrations -- dynamic values in mappings, HTTP bodies, SQL queries, URIs, and filters. Use when building any resource configuration that needs computed, conditional, or formatted field values. It is listed under Data & Analytics on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Independent scanners report: SkillSpector: PASS, Skill Scanner: PASS. Capability flags: reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
celigo (@celigo) published this skill. Their other Agent Skills are listed on their SkillMD profile.