Liquid — 18 Operations for AI Agents
Liquid powers the storefronts and static sites people actually ship — Shopify themes, Jekyll blogs, email templates. act101 reads those templates as structure, so agents navigate sections, filters, and includes instead of scanning markup.
This page is the canonical reference an AI coding agent uses to refactor, query, and analyze Liquid code through the act MCP server. 18 operations available: 0 refactor, 18 query, 0 analysis. Each operation is callable from Claude Code, Cursor, Codex, OpenCode, or any MCP-compatible agent host. Click any operation for a stable anchor link suitable for citation.
Worked Liquid examples
act101 reads a Liquid template's {% assign %}, {% capture %}, {% increment %}, and {% decrement %} tags as variable declarations, its {% render %} tag as an import declaration keeping the snippet's quoted name, and its {% for %}, {% if %}, and {% case %} tags as block declarations named for the loop item, the leading condition, or the receiver expression. symbols narrows that down to only the four variable-producing tags — assign, capture, increment, decrement — reported under the same variable kind the skeleton gives them; the render, for, if, and case tags that appeared in the skeleton are absent. The unit of structure in this grammar is the tag itself: an {% if %} with {% elsif %}/{% else %} branches reports one block declaration for the whole construct, not one per branch. Each example below is the verbatim output of the command shown, run against the file shown. Query outputs are pretty-printed with the timing block omitted.
Read the digest's variables, render call, and control blocks
fishing-report.liquid is a charter fishing report digest: it assigns tide_state, captures a formatted report_date, renders a header snippet, loops fishing_spots with a spot/elsif/else chain, and switches on tide_state with two {% when %} branches.
$ act query skeleton fishing-report.liquid
Before
{% assign tide_state = 'outgoing' %}
{% capture report_date %}{{ 'now' | date: "%B %d" }}{% endcapture %}
{% render 'header', title: 'Charter Fishing Report' %}
{% for spot in fishing_spots %}
<section class="spot">
<h2>{{ spot.name }}</h2>
{% if spot.limit_reached %}
<p>Limit reached — {{ spot.species }}</p>
{% elsif spot.bite_active %}
<p>Bite is active on {{ spot.species }}</p>
{% else %}
<p>Slow morning</p>
{% endif %}
</section>
{% endfor %}
{% case tide_state %}
{% when 'outgoing' %}
<p>Fishing the outgoing tide</p>
{% when 'incoming' %}
<p>Fishing the incoming tide</p>
{% endcase %}
{% increment trips_logged %}
Output
{
"type": "Skeleton",
"declarations": [
{
"kind": "variable",
"name": "tide_state",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 1,
"column": 4,
"byte_offset": 3
},
"end": {
"file": "fishing-report.liquid",
"line": 1,
"column": 34,
"byte_offset": 33
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 1,
"column": 11,
"byte_offset": 10
},
"end": {
"file": "fishing-report.liquid",
"line": 1,
"column": 21,
"byte_offset": 20
}
}
},
{
"kind": "variable",
"name": "report_date",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 1,
"column": 37,
"byte_offset": 36
},
"end": {
"file": "fishing-report.liquid",
"line": 2,
"column": 69,
"byte_offset": 105
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 2,
"column": 12,
"byte_offset": 48
},
"end": {
"file": "fishing-report.liquid",
"line": 2,
"column": 23,
"byte_offset": 59
}
}
},
{
"kind": "import",
"name": "'header'",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 4,
"column": 4,
"byte_offset": 110
},
"end": {
"file": "fishing-report.liquid",
"line": 4,
"column": 52,
"byte_offset": 158
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 4,
"column": 11,
"byte_offset": 117
},
"end": {
"file": "fishing-report.liquid",
"line": 4,
"column": 19,
"byte_offset": 125
}
}
},
{
"kind": "block",
"name": "spot",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 4,
"column": 55,
"byte_offset": 161
},
"end": {
"file": "fishing-report.liquid",
"line": 17,
"column": 13,
"byte_offset": 496
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 6,
"column": 8,
"byte_offset": 170
},
"end": {
"file": "fishing-report.liquid",
"line": 6,
"column": 12,
"byte_offset": 174
}
}
},
{
"kind": "block",
"name": "spot.limit_reached",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 9,
"column": 5,
"byte_offset": 253
},
"end": {
"file": "fishing-report.liquid",
"line": 15,
"column": 16,
"byte_offset": 470
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 9,
"column": 11,
"byte_offset": 259
},
"end": {
"file": "fishing-report.liquid",
"line": 9,
"column": 29,
"byte_offset": 277
}
}
},
{
"kind": "block",
"name": "tide_state",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 17,
"column": 13,
"byte_offset": 496
},
"end": {
"file": "fishing-report.liquid",
"line": 24,
"column": 14,
"byte_offset": 655
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 19,
"column": 9,
"byte_offset": 506
},
"end": {
"file": "fishing-report.liquid",
"line": 19,
"column": 19,
"byte_offset": 516
}
}
},
{
"kind": "variable",
"name": "trips_logged",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 26,
"column": 4,
"byte_offset": 660
},
"end": {
"file": "fishing-report.liquid",
"line": 26,
"column": 26,
"byte_offset": 682
}
},
"name_range": {
"start": {
"file": "fishing-report.liquid",
"line": 26,
"column": 14,
"byte_offset": 670
},
"end": {
"file": "fishing-report.liquid",
"line": 26,
"column": 26,
"byte_offset": 682
}
}
}
]
}
The skeleton reports seven declarations: tide_state and report_date as variable, 'header' as import, the {% for %} loop as block named spot, the {% if %} chain as block named spot.limit_reached (its leading condition), the {% case %} as block named tide_state, and trips_logged as variable.
List only the variable-defining tags
tide_state is assigned before the loop, report_date is captured from a filtered date, and trips_logged is incremented at the end of the digest.
$ act query symbols fishing-report.liquid
Before
{% assign tide_state = 'outgoing' %}
{% capture report_date %}{{ 'now' | date: "%B %d" }}{% endcapture %}
{% render 'header', title: 'Charter Fishing Report' %}
{% for spot in fishing_spots %}
<section class="spot">
<h2>{{ spot.name }}</h2>
{% if spot.limit_reached %}
<p>Limit reached — {{ spot.species }}</p>
{% elsif spot.bite_active %}
<p>Bite is active on {{ spot.species }}</p>
{% else %}
<p>Slow morning</p>
{% endif %}
</section>
{% endfor %}
{% case tide_state %}
{% when 'outgoing' %}
<p>Fishing the outgoing tide</p>
{% when 'incoming' %}
<p>Fishing the incoming tide</p>
{% endcase %}
{% increment trips_logged %}
Output
{
"type": "Symbols",
"symbols": [
{
"name": "tide_state",
"kind": "variable",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 1,
"column": 11,
"byte_offset": 10
},
"end": {
"file": "fishing-report.liquid",
"line": 1,
"column": 21,
"byte_offset": 20
}
},
"visibility": "unknown"
},
{
"name": "report_date",
"kind": "variable",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 2,
"column": 12,
"byte_offset": 48
},
"end": {
"file": "fishing-report.liquid",
"line": 2,
"column": 23,
"byte_offset": 59
}
},
"visibility": "unknown"
},
{
"name": "trips_logged",
"kind": "variable",
"range": {
"start": {
"file": "fishing-report.liquid",
"line": 26,
"column": 14,
"byte_offset": 670
},
"end": {
"file": "fishing-report.liquid",
"line": 26,
"column": 26,
"byte_offset": 682
}
},
"visibility": "unknown"
}
]
}
symbols reports exactly these three — tide_state, report_date, and trips_logged — all kind variable; the render, for, if, and case tags that produced declarations in the skeleton have no rule in symbols.scm and don't appear.
Score the digest's branching complexity
The digest's only branching lives in the {% if %}/{% elsif %} chain over spot and the two-branch {% case %} over tide_state.
$ act query complexity fishing-report.liquid
Before
{% assign tide_state = 'outgoing' %}
{% capture report_date %}{{ 'now' | date: "%B %d" }}{% endcapture %}
{% render 'header', title: 'Charter Fishing Report' %}
{% for spot in fishing_spots %}
<section class="spot">
<h2>{{ spot.name }}</h2>
{% if spot.limit_reached %}
<p>Limit reached — {{ spot.species }}</p>
{% elsif spot.bite_active %}
<p>Bite is active on {{ spot.species }}</p>
{% else %}
<p>Slow morning</p>
{% endif %}
</section>
{% endfor %}
{% case tide_state %}
{% when 'outgoing' %}
<p>Fishing the outgoing tide</p>
{% when 'incoming' %}
<p>Fishing the incoming tide</p>
{% endcase %}
{% increment trips_logged %}
Output
{
"type": "Complexity",
"score": 6,
"details": "Base complexity: 1, +1 if_statement, +1 elsif_clause, +1 case_statement, +1 when_clause, +1 when_clause"
}
Complexity starts at a base of 1 and adds one point each for the if_statement, its elsif_clause, the case_statement, and its two when_clause branches, for a total score of 6; the {% for %} loop and {% render %} call add nothing, since neither is a scored construct.
Query
18 query tools, the same on every supported language. Descriptions live in the shared reference: /docs/query-tools.
callers control_flow data_flow definition diagnostics effect_closure effect_summary fix_auto get_type graph import_organize interface mutations references repo_outline skeleton symbols symbols_batch