Open-source project
Shopify/liquid avatar
Shopify/liquid

Shopify Liquid for Ruby: a non-evaling template engine your users can edit

Liquid markup language. Safe, customer facing template language for flexible web apps.

11,901 stars1,546 forksRubyMIT

At a glance

What is it?
Liquid is the Ruby template engine Shopify built so that untrusted authors can edit markup without executing code on your server. This review covers the parse/render split, scoped Environments, strict error modes, and where the design costs you.
Who is it for?
Adopt Liquid when non-developers or end users must edit templates and you cannot let them run Ruby: the parse/render split, Environments and the strict modes are the parts that make that safe in practice. Do not adopt it if your templates are written only by your own engineers and you want arbitrary Ruby in views, because Liquid deliberately refuses that.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem Liquid solves: templates written by people you do not trust

Most Ruby template engines assume the person writing the template is the person deploying the application. ERB and its relatives embed Ruby, which is fine when the author is on your team and unacceptable when the author is a customer editing a storefront, a marketing user editing an email, or a tenant on a hosted platform. Liquid takes the opposite position. The README states the engine "needs to be non evaling and secure", and adds that templates "are made so that users can edit them. You don't want to run code on your server which your users wrote."

That constraint shapes everything else. There is no way to call arbitrary Ruby from a Liquid template; the only operations available are the tags and filters the host application has registered, plus the objects it passes into render. If a template author writes something the engine does not recognise, the default behaviour is to render nothing rather than to execute anything. The audience is therefore narrow and specific: teams that host a template language on behalf of someone else. If nobody but your own engineers writes the templates, Liquid's restrictions buy you nothing and cost you the convenience of Ruby in views.

How Liquid separates parsing from rendering

The README describes the engine as stateless and insists that "compile and render steps have to be separate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects". In the API this is two calls. Liquid::Template.parse takes the source string and returns a compiled template; render takes a hash of variables and produces output. The README's own example is a one-liner: parsing "hi {{name}}" and rendering it with 'name' => 'tobi' yields "hi tobi".

The practical consequence is caching. Because a parsed template is not tied to any particular set of data, you can parse once per template revision and render many times, which is the whole reason the split exists. The cost is that you must manage that cache yourself; the library does not decide when a template has changed.

On top of this sits the Environment concept. An Environment is a scoped container for custom tags, filters and configuration. The README's stated reasons are to encapsulate logic, avoid conflicts between custom tags and filters, and limit which tags and filters are available in which context. Two environments can register different tags and never see each other's, and a global one is available as Liquid::Environment.default for smaller projects. The README explicitly prefers this over global overrides, arguing that environments promote modularity and separation of concerns. That is a design opinion, and it is the right one if you have more than one kind of template in your application.

Installing the liquid gem and rendering your first template

Liquid ships as a Ruby gem. The README gives one installation step: add gem 'liquid' to your Gemfile. After that, the API is the Liquid::Template class. The example below is the README's, unchanged, and it is the smallest useful program: parse a string, render it with a hash, print the result.

ruby
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
@template.render('name' => 'tobi')                # => "hi tobi"

The reader should see "hi tobi" on standard output. Two things are worth noticing. First, the variable is passed as a plain Ruby hash keyed by string, not by symbol, in the README's example. Second, parse and render are separate calls even here, which is the pattern you will keep when you move the parsed template into a cache.

The next step, if you are hosting templates for other people, is to give them their own environment rather than adding tags globally. The README builds one with a block and registers a custom tag on it, then passes the environment to parse:

ruby
user_environment = Liquid::Environment.build do |environment|
  environment.register_tag("renderobj", RenderObjTag)
end

Liquid::Template.parse(<<~LIQUID, environment: user_environment)
  {% renderobj src: "path/to/model.obj" %}
LIQUID

RenderObjTag is a class you supply; the README does not define it. The point of the snippet is the scoping: the tag exists only inside user_environment, so a template parsed without that environment cannot use it. The README shows the same pattern for an email context with an unsubscribe_footer tag, which is a good illustration of why you would want more than one environment in a single application.

Strict modes: turning silent failures into errors you can read

The default behaviour is permissive to the point of being unhelpful. The README says the parser "is very lax and will accept almost anything without error", and that this "can make it very hard to debug and can lead to unexpected behaviour". Missing variables and missing filters do not raise; by default the renderer "doesn't raise or in any other way notify you if some variables or filters are missing".

There are two separate switches for this, and conflating them is a common mistake. The first is error_mode, which concerns syntax and has four settings: :lax (the default), :warn, :strict and :strict2. The README describes :strict2 as raising a SyntaxError for invalid syntax in all tags, :strict as raising in some tags, and :warn as adding strict errors to template.errors while continuing normally. It can be set globally on Liquid::Environment.default or per template by passing error_mode to parse, which the README suggests for enabling strict mode only in a theme editor. The README recommends :strict or :warn for new apps and for the template editors of existing ones.

The second switch concerns names, not syntax, and is passed to render rather than parse. With strict_variables: true, undefined variables are collected in the template's errors array instead of being silently rendered as nil. With strict_filters: true, an undefined filter causes the whole expression to render as nil and an error to be recorded. The README's examples show both: rendering "{{x}} {{y}} {{z.a}} {{z.b}}" with only x and z.a supplied produces "1 2 " and two UndefinedVariable errors, and a chain containing an unknown filter1 produces an empty string plus one UndefinedFilter error. If you would rather stop at the first problem than collect them all, the README mentions a render! method that raises instead.

Where Liquid is the wrong tool

The first limitation is the one the project intends. Liquid cannot express arbitrary computation, and the README frames that as a security property rather than a gap. If your templates need to call into your domain model, branch on complex conditions, or reuse Ruby helpers directly, you will spend your time writing custom tags and filters to bridge the distance, and each one you add widens the surface that template authors can reach.

The second is that the permissive default is genuinely risky for hosts. A template that references a variable your render call forgot to pass does not fail; it renders an empty string. In production that looks like a blank price or a missing name, not like a bug report. The strict options exist, but they are opt-in, and the README notes that strict_variables and strict_filters only populate the errors array. Nothing forces you to inspect that array. If your code renders and discards template.errors, you have the lax behaviour with extra steps.

The third is operational: the README does not document rollback, migration between error modes, or what happens to already-stored templates when you tighten error_mode from :lax to :strict. If you have templates in a database and you flip that switch, the README does not say which of them will start raising. That is a gap worth testing on a copy of your own data before you change the setting in production.

Liquid compared with ERB and with a host-provided template language

The obvious alternative inside Ruby is ERB, and the difference is not stylistic. ERB templates are Ruby programs; the person who writes the template can execute code in your process. Liquid is built so that they cannot, which is exactly why the README lists "you want to allow your users to edit the appearance of your application but don't want them to run insecure code on your server" as the first reason to use it. Choosing ERB means accepting that the template author is trusted. Choosing Liquid means accepting that the template author is not, and paying for that with a smaller language.

A second comparison is more relevant to Shopify's own situation: using the host platform's template language rather than embedding one. If you are building on a platform that already gives merchants a template language, you inherit its tags, its filters and its documentation, and you take on its upgrade cycle and its limits. Embedding Liquid yourself gives you control over which tags exist, which is what the Environment mechanism is for, but it also means you own the parser version, the error modes and the custom filters. The README points at the Shopify documentation and the GitHub wiki for the language itself, which suggests the intended reading path treats the gem as the engine and those pages as the language reference.

Maintenance, releases and the MIT licence

The repository is not archived, and the last push was on 2026-09-17, four days before this review. The most recent release listed is v5.13.0 from 2026-06-29, preceded by v5.12.0 in March 2026 and v5.11.0 in November 2025. That is a steady cadence rather than a burst, and the History.md file at the repository root is where the project keeps its version history, so an upgrade path is documented rather than inferred. The gem is published as liquid and pinned in a Gemfile, which means version bumps are ordinary Bundler work.

The upgrade cost that matters is not the gem version, it is your custom tags and filters. They are Ruby classes registered against an Environment, and any of them that reach into Liquid internals rather than the documented registration API are the ones likely to break on a minor bump. The README's push toward Environments over global overrides helps here, because a named environment is a small, enumerable list of what you have extended. The licence is MIT, stated in the LICENSE file at the repository root; read that file for the actual terms rather than treating this article as a statement of what you may redistribute.

Editorial conclusion

Adopt Liquid when non-developers or end users must edit templates and you cannot let them run Ruby: the parse/render split, Environments and the strict modes are the parts that make that safe in practice. Do not adopt it if your templates are written only by your own engineers and you want arbitrary Ruby in views, because Liquid deliberately refuses that. Before committing, verify two things against your own copy: that the errors array from a strict_variables and strict_filters render covers every undefined name you care about, and that every custom tag you need can be registered on a named Environment rather than added to Liquid::Environment.default. The gem is MIT-licensed, so read LICENSE for the exact terms rather than assuming what redistribution allows.

Frequently asked questions

What is Shopify Liquid and what is it for?

It is a Ruby template engine designed to be non-evaling and secure, so that templates can be edited by users without running their code on your server. The README lists rendering templates straight from a database and letting users edit your application's appearance as intended uses.

How do I install the Liquid gem?

The README gives one step: add gem 'liquid' to your Gemfile. From there the API is the Liquid::Template class, with parse to compile a template and render to produce output from a hash of variables.

How do I get Liquid to report undefined variables instead of rendering them as nil?

Pass strict_variables: true or strict_filters: true to render. The README states that the resulting errors are stored in the errors array of the Liquid::Template instance, and that render! raises on the first exception instead of collecting them.

What is a Liquid Environment and why use one instead of overriding globally?

An Environment is a scoped container for custom tags, filters and configuration. The README encourages environments over global overrides because they encapsulate logic, prevent custom tags and filters from clashing, and limit which tags are available in which context; Liquid::Environment.default is the global one for smaller projects.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. Shopify/liquid on GitHub
For maintainers

Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/shopify-liquid.svg)](https://hysenlabs.com/projects/shopify-liquid)
Community notes

Community notes