Two helpers let a template ask what the site’s plan includes. hasTier compares against a rung on the plan ladder; hasFeature asks about a named capability. Both return a boolean, so both are for subexpressions rather than for printing.
Where entitlements comes from
Both helpers read one value: entitlements.tier on the page context. Nothing else. Knowing where that value comes from is what explains why a local build and a deployed build can disagree, and how to reproduce a plan locally.
| Build | Source of entitlements | If it is missing |
|---|---|---|
| CI (your site’s real builds) | the LEED_ENTITLEMENTS environment variable on the build trigger, set by Leed | the build fails with an explicit error — repository contents can never supply entitlements in CI |
Local (leed site build on your machine) | falls back to entitlements in src/src.11tydata.json | nothing is applied and the helpers see no tier, which fails closed |
The env var carries a two-field JSON payload, { "tier": "…", "mcp": true|false }, and it is deliberately narrow — no quota numbers ever reach a customer build. When the env var and the repository file disagree in CI, the env value wins, the discrepancy is logged, and the file is rewritten for that build so the data cascade sees the authoritative value. That is what makes tampering with the repository file pointless in CI and harmless locally.
To try a template on a different plan, change entitlements.tier in src/src.11tydata.json and rebuild locally:
{
"entitlements": { "tier": "free", "mcp": true },
"siteTitle": "Example"
}Because the value reaches templates through the ordinary data cascade, it is available anywhere a page’s data is — no special import, no plugin call. Global Site Data and the Data Cascade covers the rest of what that file carries.
hasTier — a plain threshold
One required parameter: a minimum, which must be exactly one of free, starter, growth or enterprise. It answers true when the site’s plan sits at or above that rung.
{{#if (hasTier "starter")}}
© {{ year }} {{ siteTitle }}. All rights reserved.
{{else}}
<a href="https://leed.ai">Powered by Leed</a>
{{/if}}Anything else in that slot — a typo, a capitalized "Starter", a plan name Leed does not use — logs a warning and returns false. It does not throw, so a misspelled tier produces a template that silently renders its {{else}} branch on every plan, forever.
hasTier called with unknown minimum "Starter" — refusing access. Usage: {{ hasTier "starter" }}How the comparison works
The ladder is a rank lookup and the comparison is >=. There is no partial access, no per-capability purchase and no ordering other than this one.
| Tier | Rank |
|---|---|
free | 0 |
starter | 1 |
growth | 2 |
enterprise | 3 |
A site’s tier is looked up in the same table before the comparison runs. A value that is not one of the four keys has no rank, so it satisfies no minimum — which is the whole of the fail-closed behavior described below.
hasFeature — a named capability
One required parameter: a key from Leed’s shared feature catalog. It answers true when the site’s plan meets that feature’s own minimum.
{{#if (hasFeature "docsLiveSearch")}}
<script src="{{ this.leedSiteEnv.env.javascript.liveSearchJS }}"></script>
{{else}}
<script src="{{ this.leedSiteEnv.env.javascript.searchJS }}"></script>
{{/if}}An unknown key logs a warning and returns false, exactly as hasTier does with an unknown tier:
hasFeature called with unknown feature "autolinking" — refusing access. Usage: {{ hasFeature "autoLinking" }}Every accepted feature name
These 26 keys are the complete set hasFeature will accept. This page prints no plan minimums — the minimum for every one of them, what it looks like below that plan, and where you meet it in Leed are owned by Feature Availability by Plan, which is the one page in these docs that answers tier questions. What a template author needs here is which strings are legal and what each one gates.
Starter — 9 keys. What each one needs and what it does below that plan.
| Feature key | What it gates |
|---|---|
marketingEmail | Marketing email — sending a campaign to a contact group |
readerIdentity | Reader-level identity — the per-reader history on a contact record |
releaseNotesEmail | Release notes email — arming a release-notes channel on a page workflow |
removeBadge | Badge removal — see the note below; nothing in the product reads this key |
autoLinking | Auto-linking — phrase rewriting inside process when your site builds |
contentExtraction | Auto-transcription & content extraction — retrieving transcripts and extracted document text |
contentRbac | Content-level RBAC — creating page-type and label access overrides |
customThemeNames | Custom theme & font names — naming your own documentation color theme, code theme or font |
docsLiveSearch | AI-powered docs search — live, server-side search on your published documentation |
Growth — 12 keys. What each one needs and what it does below that plan.
| Feature key | What it gates |
|---|---|
customThemes | Custom themes & layouts — the Email Templates and Site Layouts editors |
customHighlightThemes | Custom highlight themes — no code reads this key |
leadWorkflows | Lead workflows — creating and enabling a lead workflow on a page |
campaignPlanner | Campaign planner — the Campaigns section of Engage |
autoSocialPosting | Auto social posting — arming a social channel on a page workflow |
dynamicCta | Dynamic CTAs — configuring them, and serving them to your site |
shortUrlTracking | Short-URL tracking — branded /s/ links and their metrics |
agentAnalytics | Agent analytics — cataloged; no surface in the product yet |
recommendationEngine | Recommendation engine — serving recommendation cards to your site |
mediaEngagementAnalytics | Video & audio engagement analytics — the media metrics on the Know dashboard |
menuClickAnalytics | Menu click analytics — element and internal-link click metrics |
journeyAttribution | Full-journey attribution — the inbound attribution metric |
Enterprise — 5 keys. What each one needs and what it does below that plan.
| Feature key | What it gates |
|---|---|
customSyntaxLanguages | Custom syntax languages — highlighting languages beyond the built-in set |
accountQualification | Account-based qualification — cataloged; no surface in the product yet |
clusterAnalytics | Cluster analytics — cataloged; no surface in the product yet |
crmIntegration | CRM integration — cataloged; no surface in the product yet |
sso | SSO — cataloged; no surface in the product yet |
Three things about that list are worth stating rather than leaving you to discover:
- Any name not in it returns
false, including the names of genuinely free capabilities. There is no third answer. - Five keys have no surface in the product today. They are legal arguments and they answer correctly, but nothing behind them exists to turn on, so gating your own template on one gates it on nothing. Feature Availability by Plan says which and why.
removeBadgeis accepted and unread. The “Powered by Leed” badge in the documentation footer is gated by a literalhasTier "starter", not by this key, so the customer-facing outcome is Starter and above either way — but changing the catalog entry would not move the badge, and neither would askinghasFeature "removeBadge"in a template you wrote. The badge is not something an override can remove; that is covered at Customizing the Documentation Header and Footer.
Two keys — customThemes and customHighlightThemes — sit where Leed’s pricing page and Leed’s product do not currently line up, and docsLiveSearch is a paid capability the pricing page does not advertise at all. All three differences are recorded openly on Feature Availability by Plan. They are also the strongest practical argument for the recommendation below: a template that names a feature stays correct when one of those is reconciled, and a template that hard-codes a tier does not.
Both fail closed
A missing, empty or unrecognized tier meets no paid minimum. Neither helper throws, neither logs an error, and neither treats “I cannot read this” as “allow it”.
Operationally that means three things:
- A stale or malformed entitlements record keeps free-tier behavior. It never accidentally unlocks paid behavior, which is the right direction for the failure to point.
- A typo in your own feature name hides your own feature.
{{#if (hasFeature "dynamicCTA")}}— wrong capitalization — isfalseon Enterprise, and the block never renders. Nothing in the build turns red. - A local build with no
entitlementsblock behaves like Free. That is useful for checking your degraded path and misleading if you forget it.
Both mistakes surface the same way: a warning in the build console, and a branch that never runs. Keep leed site build in view while you write a gate, and read the warnings rather than trusting a build that finished.
Prefer hasFeature
The source says it and it is worth repeating: reach for hasFeature unless you cannot.
A feature’s minimum tier can move — that is a business decision, made in Leed’s catalog, with no template edit anywhere. A template asking hasFeature "recommendationEngine" follows that move for free. A template asking hasTier "growth" because recommendations were Growth when it was written goes stale silently on the day they are not, and the only symptom is a block that renders for the wrong customers.
Leed’s own templates are not entirely consistent about this, and it is better to say so than to present a pattern that the shipped code contradicts. The documentation footer badge and the two documentation-chrome override slots are gated with a literal hasTier "starter" rather than with a feature key, which is exactly why removeBadge sits in the catalog unread. Copy the shape of those templates for their structure; do not copy the choice of helper.
Writing a template that degrades well
The rule underneath all three patterns below: never let a gated branch be the only branch. A reader on a Free site should get a working page, not a missing one.
Substitute, do not omit. Leed’s own head partial does not drop search on a site without live search — it loads a different client. The reader still gets a search box; what changes is what is behind it.
{{#if (hasFeature "docsLiveSearch")}}
<script src="{{ this.leedSiteEnv.env.javascript.liveSearchJS }}"></script>
{{else}}
<script src="{{ this.leedSiteEnv.env.javascript.searchJS }}"></script>
{{/if}}Guard the expensive block. When the entitled path makes a whole block pointless rather than different, {{#unless}} reads better than an {{#if}} with an empty branch. This is the shipped guard around the prebuilt search index’s hashes:
{{#unless (hasFeature "docsLiveSearch")}}
{{#if documentationConfiguration.searchIndexId}}
const searchIndexHash = "{{ searchIndexHash }}";
const searchDocumentsHash = "{{ searchDocumentHash }}";
{{/if}}
{{/unless}}Note the inner guard: on a documentation site the hash helpers throw without a documentation configuration, so the tier check is not the only thing standing between you and a failed build. Utility Helpers covers that pair.
Compose with the logical helpers. Both helpers return real booleans, so they combine with the and, or and not helpers that ride along with Leed’s own. The shipped footer slot needs two conditions — you supplied an override file, and your plan lets Leed use it:
{{#if (and (useCustomTemplate "docsFooter") (hasTier "starter"))}}
{{> docs-footer }}
{{else}}
{{> leed/docs/docs-footer }}
{{/if}}Which override slots carry a plan requirement, and what each one is allowed to replace, is at Overriding Leed Templates.
Build-time gates you do not control
Two behaviors read the same tier data without going through either helper, so they change what your site does without appearing in any template you wrote.
| Behavior | Reads | What changes |
|---|---|---|
Autolinking inside process | autoLinking | Below the minimum, phrase rewriting is skipped. The page still renders; one line goes into the build log for the whole build. |
| The prebuilt search index | docsLiveSearch | Inverted — the Lunr index plugin is skipped entirely when the site does have live search, because the server-side index replaces it. |
The second is the one that surprises people: having the feature is what removes the build artifact. A site that gains live search stops emitting an index file, and searchIndexHash and searchDocumentHash return empty strings because there is nothing to hash. Both sides of that are described at Live Documentation Search; the autolinking pipeline is at Date and Content Helpers.
Reference
| Helper | Form | Parameter | Accepted values | Returns | On an unrecognized value |
|---|---|---|---|---|---|
hasTier | subexpression → boolean | 1. minimum, required | exactly free, starter, growth or enterprise | true when the site’s plan rank is at least the minimum’s | log.warn and false; never throws |
hasFeature | subexpression → boolean | 1. name, required | any of the 26 catalog keys above | true when the site’s plan meets that feature’s minimum | log.warn and false; never throws |
Both are listed with the rest of the surface in the Helper Index (A–Z), and the silent-false behavior they share with several other helpers is cataloged at Helper Gotchas and Failures. What a gate looks like to a person rather than to a template — which produce a visible upgrade prompt and which quietly do less — is at When a Feature Is Gated.