> For the complete documentation index, see [llms.txt](https://docs.hackle.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hackle.io/crm-marketing/message-personalization/liquid-ai-spec.md).

# Hackle Liquid 생성 스펙 (AI용)

너(AI)는 Hackle 메시지 템플릿에 쓰일 Liquid를 생성한다. 렌더 엔진은 **liqp 0.8.5.3 기반의 Braze 호환 방언**이다(순정 Shopify/LiquidJS와 다르다).

**철칙: 아래에 나열된 필터·태그만 사용하라. 목록에 없는 것은 절대 만들어 쓰지 마라.** 미등록 필터나 문법 오류는 커스텀 필터와 달리 **발송 전체를 실패**시킨다. 확신이 없으면 표준 필터 조합으로 우회하라.

***

## 1. 변수 — 반드시 이 네임스페이스로 참조

개인화 데이터는 아래 4개 네임스페이스 아래의 임의 키로 들어온다. 최상위 변수명은 이 넷뿐이다.

| 참조                            | 내용     | 사용 채널                           |
| ----------------------------- | ------ | ------------------------------- |
| `{{ user_properties.<속성> }}`  | 유저 속성  | 전 채널                            |
| `{{ event_properties.<속성> }}` | 이벤트 속성 | 전 채널                            |
| `{{ api_properties.<속성> }}`   | API 속성 | 전 채널                            |
| `{{ identifiers.<종류> }}`      | 식별자    | **Webhook 전용** (그 외 채널은 항상 빈 값) |

* 속성 이름은 **대소문자를 구분하지 않는다**.
* 값이 리스트면 `,` 로 이어 붙여 출력된다.
* **존재하지 않는 변수는 빈 문자열**로 렌더된다(에러 없음). strict 모드가 아니다.
* Webhook 채널은 모든 비-리스트 값을 문자열화한다. 그 외 채널은 숫자를 숫자 타입으로 유지한다.

## 2. 예시 템플릿 (golden — 이 형태를 따르라)

인사 + 포인트:

```liquid
{{ user_properties.name | default: '고객' }}님, 현재 포인트는 {{ event_properties.point | number_with_delimiter }}P 입니다.
```

조건 분기 + 한국 시간 날짜:

```liquid
{% if event_properties.grade == 'gold' %}골드 전용 혜택{% else %}이번 달 혜택{% endif %} · 주문일 {{ event_properties.ordered_at | date: '%m월 %d일', 'Asia/Seoul', locale: 'ko' }}
```

랜덤 A/B 변형 (random은 태그 → capture로 담는다):

```liquid
{% capture dice %}{% random 2 %}{% endcapture %}{% if dice == '0' %}A 시안{% else %}B 시안{% endif %}
```

## 3. 핵심 규칙

* **목록에 없는 필터/태그를 만들지 마라.** (§6 사용 금지 목록 참고)
* 커스텀 필터(§4 표시)는 잘못된 입력에도 원본을 반환하며 발송을 죽이지 않는다. 반대로 미등록 필터·문법 오류·내장 필터의 예외는 **발송 실패**로 이어진다.
* `random`은 **필터가 아니라 태그**다. `{% assign x = random %}`은 동작하지 않는다(빈 값). 값을 재사용하려면 `{% capture %}`로 담고, 숫자로 쓰려면 `| plus: 0`(또는 `plus: 1`)로 변환하라.
* `date`:
  * 입력은 날짜 문자열 또는 **epoch 초**(밀리초 아님).
  * **ISO `Z` 접미사(`2026-07-16T10:30:00Z`)는 파싱 못 한다** → 파싱 실패 시 원본 반환. 공백 구분 존(`2026-07-16 10:30:00 UTC`)이나 존 없는 형식을 써라.
  * timezone은 TZ 이름(`Asia/Seoul`)·ISO 오프셋(`+09:00`)만 지원. **분 단위 숫자(360)는 미지원.**
  * locale은 named 파라미터: `date: '%A', locale: 'ko'`.
* 렌더 한도: 3초 / 출력 10만 자 / 반복 1만 회. 초과 시 발송 실패.

## 4. 지원 필터 (이 목록이 전부다)

`*` = Hackle 커스텀. 그 외는 표준 Liquid와 동일.

**문자열**: `append: s` · `prepend: s` · `capitalize` · `downcase` · `upcase` · `lstrip` · `rstrip` · `strip` · `strip_html` · `strip_newlines` · `newline_to_br` · `remove: s` · `remove_first: s` · `replace: from, to` · `replace_first: from, to` · `split: sep`(→배열) · `slice: start[, len]` · `truncate: n[, ellipsis]` · `truncatewords: n[, ellipsis]` · `escape` · `escape_once`

**숫자**: `abs` · `at_least: n` · `at_most: n` · `ceil` · `floor` · `round[: digits]` · `plus: n` · `minus: n` · `times: n` · `divided_by: n`(정수끼리는 정수 몫; 소수 결과는 `divided_by: n.0`) · `modulo: n`

* `number_with_delimiter` \* — 천 단위 쉼표. 예: `{{ 1234567 | number_with_delimiter }}` → `1,234,567`

**배열**: `compact` · `concat: arr` · `first` · `last` · `join: sep` · `map: 'prop'` · `reverse` · `sort`(대문자 우선) · `sort_natural`(대소문자 무시) · `uniq` · `where: 'prop'[, value]` · `size`(문자열 길이도 됨, UTF-16 기준)

**날짜**:

* `date: format[, timezone][, locale: 'tag']` — 예: `{{ v | date: '%Y-%m-%d %H:%M', 'Asia/Seoul', locale: 'ko' }}`. (§3의 date 규칙 필독)

**URL**:

* `url_encode` · `url_decode`
* `url_escape` \* — URL에 못 쓰는 문자만 이스케이프(공백→`%20`)
* `url_param_escape` \* — 쿼리 파라미터 값용(공백→`+`)

**JSON** (모두 Hackle 커스텀):

* `json_escape` \* — 문자열을 JSON 문자열 값으로 이스케이프
* `json_parse` \* — JSON 문자열 → 객체. 예: `{% assign d = api_properties.payload | json_parse %}{{ d.user.name }}`. **파싱 실패 시 nil**(발송은 계속)
* `as_json_string` \* — 값 → JSON 문자열

**기타**:

* `default: fallback` — 값이 비었거나 falsy면 대체값. 예: `{{ user_properties.name | default: '고객' }}`
* `fallback_over_length: max, fallback` \* — 길이(UTF-16) > max면 통째로 fallback으로 대체. 예: `{{ user_properties.name | fallback_over_length: 10, '고객' }}`

## 5. 지원 태그 (이 목록이 전부다)

* 변수: `{% assign x = ... %}` · `{% capture x %}...{% endcapture %}`
* 조건: `{% if %}` / `{% elsif %}` / `{% else %}` / `{% endif %}` · `{% unless %}...{% endunless %}` · `{% case x %}{% when v %}...{% else %}...{% endcase %}`
* 반복: `{% for i in arr %}...{% endfor %}`(범위 `(1..5)` 가능) · `{% break %}` · `{% continue %}` · `{% cycle 'a', 'b' %}`
* 출력 제어: `{% comment %}...{% endcomment %}` · `{% raw %}...{% endraw %}`
* 카운터: `{% increment c %}`(0부터) · `{% decrement c %}`(-1부터) — `assign` 변수와 별개
* `{% connected_content <url> [:method m] [:body b] [:headers json] [:save 변수] %}` — 외부 API(JSON) 호출해 변수에 저장. 실패해도 빈 값으로 계속 렌더. (상세·규칙·예시는 §6)
* `{% random %}` — 0~~1 float. `{% random N %}` — 0~~N-1 정수. **태그다**(§3 규칙 참고)

## 6. connected\_content (외부 API 실시간 호출)

발송 순간 외부 API(JSON)를 호출해 응답을 변수에 담아 본문에 끼워 넣는 태그다. **모든 실패는 빈 값으로 처리되고 발송을 죽이지 않는다** → 결과값에는 **항상 `default` 또는 `{% if %}` 방어코드를 붙여라.**

**문법**

```liquid
{% connected_content <URL> [:method M] [:body B] [:headers JSON] [:save 변수] %}
```

| 플래그        | 규칙                                                                                                          |
| ---------- | ----------------------------------------------------------------------------------------------------------- |
| `<URL>`    | 필수. **`https://`만** 허용, **공인 IP로 해석되는 호스트만**(사설/사내망 차단). URL 안에 `{{user_properties["id"]}}` 등 개인화 변수 삽입 가능. |
| `:method`  | `GET`(기본)·`POST`만. `PUT`/`DELETE`/`PATCH` → **태그 전체 스킵**.                                                   |
| `:body`    | `POST`일 때만. JSON 객체 문자열. GET에선 무시.                                                                          |
| `:headers` | 값이 **전부 문자열**인 JSON 객체. 숫자 값(`{"Retry":3}`) 넣으면 검증 실패 → 스킵. `Content-Type: application/json` 기본 적용.         |
| `:save`    | 응답 JSON 객체를 담을 변수명. 이후 `{{변수.필드}}`로 접근. 같은 템플릿 뒤쪽 태그·출력에서도 참조 가능.                                           |

**응답 처리 — 아래는 전부 "빈 값(빈 맵)"으로 저장된다 (발송 계속):**

* 200 + JSON 배열/원시값(객체 아님), 200 + 빈 본문
* 200 이외 모든 상태코드(3xx·4xx·5xx) — **리다이렉트 안 따라감**
* 응답 본문 1MB 초과, 네트워크 오류·타임아웃
* 빈 맵의 `{{변수.아무필드}}`는 빈 문자열로 렌더된다.

**한도**: 호출당 타임아웃 2초 / 한 메시지 내 누적 호출 2초(초과 후 호출은 HTTP 없이 스킵) / 응답 1MB.

**golden 예시**

```liquid
{% connected_content https://api.example.com/coupon :save coupon %}
{% if coupon.code %}오늘의 쿠폰: {{ coupon.code }}{% else %}지금 바로 확인해 보세요!{% endif %}
```

```liquid
{% connected_content https://api.example.com/orders/{{ event_properties["order_id"] }} :save order %}
주문 상태: {{ order.status | default: '확인 중' }}
```

```liquid
{% connected_content https://api.example.com/recommend :method POST :body {"user_id":"{{ user_properties["id"] }}","limit":3} :headers {"Authorization":"Bearer {{ api_properties.token }}"} :save rec %}
추천 상품: {{ rec.items[0].name | default: '' }}
```

중첩 응답 접근: `{{ data.product.name }}` · 체이닝: 앞 `:save` 결과(`{{ me.id }}`)를 다음 호출 URL에 사용.

## 7. ⛔ 사용 금지 (미지원 — 렌더 프로브로 실측 확인)

아래는 순정 Shopify/LiquidJS/Braze엔 있어도 **이 엔진엔 없다. 생성하면 발송이 실패한다.**

**없는 필터**: `base64_encode` · `base64_decode` · `base64_url_safe_encode` · `base64_url_safe_decode` · `remove_last` · `replace_last` · `find` · `find_index` · `has` · `reject` · `where_exp` · `sum` · `money` · `money_with_currency` · `md5` · `sha1` · `sha256` · `hmac_sha256` · `pluralize` · `camelize` · `handleize`

**없는 태그**: `{% liquid %}` · `{% echo %}` · `{% render %}` · `{% tablerow %}` · `{% section %}` · `{% paginate %}` · `{% form %}` · `{% layout %}`. `{% include %}`은 에러는 안 나지만 **아무것도 출력하지 않는다**(스니펫 저장소 없음) — 쓰지 마라.

**Shopify와 다른 동작 — ❌ 대신 ✅로**:

```
날짜 존을 숫자 오프셋으로 (미지원)
❌ {{ ts | date: '%H:%M', 360 }}
✅ {{ ts | date: '%H:%M', '+09:00' }}      또는 'Asia/Seoul'

ISO Z 접미사 타임스탬프 (파싱 실패 → 원본 그대로 나옴)
❌ {{ '2026-07-16T10:30:00Z' | date: '%H:%M' }}
✅ {{ '2026-07-16 10:30:00 UTC' | date: '%H:%M' }}

난수를 변수에 담기 (assign 우변에 태그 불가)
❌ {% assign r = random %}
✅ {% capture r %}{% random 6 %}{% endcapture %}{{ r | plus: 1 }}

Base64 인코딩 (필터 없음)
❌ {{ token | base64_encode }}
✅ (대안 없음 — 인코딩된 값을 api_properties로 미리 넣어 전달)
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.hackle.io/crm-marketing/message-personalization/liquid-ai-spec.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
