Help Center
Guides, feature docs, and answers to common questions about PromoOS.
Getting Started
Install guide and your first promo
Features Overview
What's live and what's coming
Promo Types
Discount, BOGO, bundle, and more
Creating & Managing
Build, edit, and delete promos
Text to Promo (AI)
Create promos from plain English
Automated Deployment
How scheduling works
Theme Block Setup
Add banners to your storefront
Custom Theme Integration
Liquid snippets and metafields
AI Channel Attribution
Orders from ChatGPT, Copilot & more
Analytics Dashboard
Understand promo performance and lift
Limitations
What PromoOS doesn't do (yet)
Tips & Recommendations
Best practices for campaigns
FAQ
Common questions answered
Pricing
Plans and features
Support
Get in touch with us
Promo Calendar — Visual timeline of all your campaigns. Color-coded by status. Conflict detection built in.
Promo Builder — Step-by-step guided flow to create promos: set a mechanism, configure discounts, target products or collections, design page placements, upload assets, and schedule — all in one place.
Automated Deployment — PromoOS activates and deactivates your promos automatically at the scheduled times.
Discount Code Creation — PromoOS creates the discount code in Shopify automatically when a promo goes live.
Crossed Price (Sale Price) — Set a crossed-out original price next to a sale price directly on your products — no coupon code required. PromoOS updates product prices in Shopify automatically and restores them when the promo ends.
Product & Collection Targeting — Apply discounts to specific products, collections, or your entire store. For BOGO promos, set separate targeting for the 'buy' items and the 'get' items.
Advanced Discount Controls — Fine-tune every discount with usage limits, once-per-customer restrictions, minimum purchase or quantity requirements, and stacking rules (control whether your promo combines with other product, order, or shipping discounts).
Revenue Goal Tracking — Set a revenue target on any promo. PromoOS tracks order revenue attributed to that promo's discount code and shows a live progress bar on the promo card and detail view.
Per-Page Placement Targeting — Configure different storefront placements for the homepage, product pages, and collection pages. Switch between a shared design for all products/collections or define unique placements per product or per collection.
Placement Validation — Built-in validation that checks whether your PromoOS theme block is actually installed in your active theme — directly from the placement editor. No manual theme inspection needed.
Visual Asset Upload — Upload promotional images (desktop and mobile) that can be deployed to your storefront during an active promo.
Draft Management — Save promos as drafts and come back to finish them later.
Conflict Detection — Promos with overlapping dates and targeting surface a warning in the calendar.
Import Existing Discounts — Import your existing Shopify discounts into PromoOS to manage them alongside your planned campaigns.
Analytics Dashboard — Per-promo performance report: KPI cards (Net Sales, Orders, AOV, New Customers), day-by-day revenue chart vs baseline, lift percentile rank, discount summary, customer breakdown, top products, and AI-generated insights. Available for completed promos.
Text to Promo (AI) — Describe a promo in plain English and PromoOS extracts the type, discount, dates, targeting, and promo name automatically. Review the extracted fields and confirm to open the promo builder pre-filled. Pro plan only.
Choose the type that best describes your campaign. The type determines how the discount is configured.
| Type | Best For | Discount Options |
|---|---|---|
| Discount | General % or $ off campaigns with a discount code | Percentage, Fixed Amount |
| Discount — Crossed Price | Show a crossed-out original price next to a sale price, no coupon needed | Percentage, Fixed Amount |
| BOGO | Buy One Get One offers | BOGO config (buy X get Y) |
| Free Shipping | Shipping promotions | Free shipping regions |
For Discount, Bundle, and Gift with Purchase type promos, you choose:
PromoOS moves your promo through these states automatically.
| Status | What It Means | Can You Edit? |
|---|---|---|
| Draft | Saved but not scheduled. Work in progress. | Yes — full edit |
| Scheduled | Scheduled to go live. Deployment will happen automatically. | Yes — before start time |
| Active | Live on your store. Customers can see the discount. | No — end it and create a new one |
| Ended | Campaign finished. Historical record kept. | No — read only |
| Failed | Something went wrong during deployment. | Yes — fix and reschedule |
The promo builder is a multi-step form covering:
Pro plan feature. Available from the Dashboard and Calendar via the "Create with AI" button.
Text to Promo lets you describe a promotional campaign in plain English. PromoOS reads your description, extracts the key fields — promo type, discount value, dates, targeting, and promo name — and opens the promo builder pre-filled with those values. You review and adjust before anything is saved or scheduled.
How to open Text to Promo
Click "Create with AI" on the Dashboard or Calendar. The button is visible on the Pro plan. If you are on a free trial or a lower plan, clicking it shows an upgrade prompt instead. Text to Promo is not available inside the standard promo builder — it is a separate entry point.
Describing your promo
Type a plain-English description of the promo you want to run. Include whatever details you have — promo type, discount amount, start and end dates, and which products or collections it applies to. You do not need to follow a specific format. Examples: "20% off all footwear from July 4th to July 7th, discount code JULY20" or "BOGO on the Summer Collection, runs the last week of August." The more specific your description, the more fields PromoOS can fill in without asking follow-up questions.
Follow-up questions
If a required field is missing or unclear, PromoOS asks one focused follow-up question at a time — up to four questions per session. Each question comes with an input widget matched to the field: a date picker for dates, a number input for discount value, a buy/get quantity picker for BOGO, and an inline selector for products or collections. You can answer each question or skip it if the field does not apply. PromoOS only asks about fields it could not confidently extract from your original description.
The preview panel
As you type and answer follow-up questions, a preview panel shows the fields extracted so far — promo type, discount, dates, targeting, promo name, and discount code. The panel updates in real time. Use it to spot missing or incorrect values before confirming. Fields that could not be extracted are shown as blank — you can fill them in manually once the builder opens.
What happens after you confirm
When you click Confirm, the standard promo builder opens with all extracted fields pre-filled. From there, you follow the normal builder flow — you can adjust any field, add placements and assets, and then save as a draft or schedule the promo. Nothing is saved or scheduled until you complete the builder and choose a save option. Text to Promo does not create or activate anything on its own.
Starting over
Click "Start over" at any point in the conversation to clear the description, all follow-up answers, and the preview panel. The modal resets to the initial state. Use this if you want to describe a different promo or if the extracted fields are too far off to correct.
Limitations and caveats
Text to Promo is available on the Pro plan only. The AI can process up to 20 descriptions per hour per store — if you hit the limit, a message appears and you can use the standard promo builder instead. Extraction accuracy depends on how clearly the promo is described. Complex targeting (specific product SKUs, exclusions) and unusual discount structures may not extract correctly — always review the preview panel before confirming. If the AI fails entirely, the modal falls back to opening the standard promo builder with your input text pre-filled as the promo name.
When to use Text to Promo vs the standard builder
Use Text to Promo when you have a clear idea of what you want and want to skip the step-by-step form for routine promos. Use the standard builder when your promo has complex targeting, per-product placements, or settings that are hard to express in plain English — for example, specific stacking rules or per-collection placement configurations. Both paths produce the same result: a promo in the builder ready to review and schedule.
At the scheduled start time, PromoOS automatically:
At the scheduled end time, PromoOS automatically:
PromoOS runs a background scheduler that checks every 5 minutes. This means:
The promo will be marked as Failed. Open the promo, review the error message, fix the issue (e.g. duplicate discount code), and reschedule. PromoOS operations are safe to retry.
Storefront theme blocks let PromoOS automatically show banners and announcement bars on your store pages whenever a promo is active — no manual updates needed. This is a one-time setup per page type.
Theme block setup is a one-time step. Once added, PromoOS shows quick-access shortcuts directly in your Dashboard to jump straight into the right page in the Theme Editor.
Works with all Online Store 2.0 themes — Dawn, Craft, Sense, Refresh, and most modern Shopify themes.
Open the Theme Editor for the page you want
In Shopify Admin, go to Online Store → Themes → Customize on your active theme. If you're logged in to PromoOS, your Dashboard has one-click shortcuts to jump directly to the right page — Homepage, Product pages, Collection pages, or the Announcement Bar position.
Add the PromoOS block
In the Theme Editor sidebar, click "Add section" (or "Add block" inside an existing section), search for PromoOS, and select the block you want — Hero Banner or Announcement Bar.
Position the block
Drag the block to the position you want on the page. To reorder it, use the drag handle. To remove it later, click the block and select Remove block from the sidebar.
Save and repeat for other pages
Click "Save" in the theme editor. Repeat steps 1–3 for any other page types where you want promos to appear (Homepage, Product pages, Collection pages).
Hero Banner
Full-width promotional banner displayed on the page. Ideal for homepage campaigns, seasonal sales, and product launches. Add this to your Homepage, Product, and Collection page templates.
Announcement Bar
Slim bar at the very top of the page, visible across your entire store. Best for short promo messages, site-wide offers, and discount code callouts.
Homepage
Your store's main landing page — the highest traffic page for most stores.
Product pages
Individual product detail pages — ideal for product-specific deals.
Collection pages
Category/collection listing pages — great for collection-wide sales.
Announcement bar
Store-wide slim bar shown on every page of your store.
Use the shortcut links below to jump directly into the right page in your Shopify Theme Editor — no manual navigation needed. You must be logged in to your Shopify store for these links to work.
Log in to PromoOS to get one-click links that open the Theme Editor on the right page automatically.
If you prefer to build your own storefront components instead of using PromoOS theme blocks, you can read active promo data directly from a Shopify metafield in any Liquid template. PromoOS writes structured JSON to `shop.metafields.promoos.active_promo` whenever a promo is active, and clears it when the promo ends.
Requires the PromoOS app to be installed. The metafield is created automatically on install. If it's completely missing, you can recreate it without reinstalling — open DevTools on your PromoOS dashboard and run the re-run setup fetch call from the test steps. Note: if the definition already exists but shows "None" for Storefront access (from an older install), you must delete it in Shopify Admin → Settings → Custom data → Shops first, then run re-run setup — the endpoint cannot upgrade an existing definition's access settings.
Access via shop.metafields.promoos.active_promo.value in any Liquid template.
| Field | Type | Description |
|---|---|---|
| promo_id | string | UUID of the active promo |
| name | string | Promo name as entered in PromoOS |
| start_date | string (ISO 8601) | Promo start datetime |
| end_date | string | null | Promo end datetime, null if open-ended |
| announcement_text | string | null | Best-available promo message (headline → announcement_text). Null when neither is set. |
| display_text | string | null | announcement_text truncated to 80 characters — safe to render directly |
| announcement_text_short | string | null | announcement_text field specifically, truncated to 80 characters |
| text_length | number | Character count of display_text |
| compare_at_enabled | boolean | true for Crossed Price promos — product prices are updated directly, no discount code is used |
| discount_code | string | null | Shopify discount code. Always null for Crossed Price promos. |
| discount_value | number | null | Numeric discount value (e.g. 20 for 20%) |
| discount_type | string | null | "percentage" or "fixed_amount" |
| discount_display | string | null | Pre-formatted string: "20% OFF" or "$10 OFF". Always null for Crossed Price promos. |
| has_image | boolean | true if a hero image is attached to this promo |
| has_cta | boolean | true if the promo has a CTA button configured |
| has_discount_code | boolean | true if discount_code is set |
| hero_image_desktop_1920 | string | null | Hero desktop image at 1920px width (Shopify CDN) |
| hero_image_desktop_800 | string | null | Hero desktop image at 800px width |
| hero_image_mobile_600 | string | null | Hero mobile image at 600px width |
| placements | object | Full placement config keyed by page type (home, product, cart, …) |
Announcement Bar
Wire PromoOS data into your theme's existing announcement bar — keeps your bar's position, colours, and styling unchanged. Use the per-theme examples below to find the right line to edit.
Where to add: Option B only — edit your existing announcement bar file. See per-theme instructions below.
{%- assign promo = shop.metafields.promoos.active_promo.value -%}
{%- if promo != blank -%}
{{- promo.display_text | escape -}}
{%- endif -%}Option B — Inject into your existing announcement bar
Online Store → Themes → Edit code → find the file below → locate the text output line → replace it with the PromoOS conditional.
sections/announcement-bar.liquidDawn renders text in two places: once for a single block (line ~43) and once inside the carousel loop (line ~103). Edit both.
Single announcement — find:
{{ section.blocks.first.settings.text | escape }}Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
{{- _p.display_text | default: _p.announcement_text | escape -}}
{%- else -%}
{{ section.blocks.first.settings.text | escape }}
{%- endif -%}Carousel (inside {% for block in section.blocks %}) — find:
{{ block.settings.text | escape }}Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
{{- _p.display_text | default: _p.announcement_text | escape -}}
{%- else -%}
{{ block.settings.text | escape }}
{%- endif -%}blocks/_announcement.liquid — or blocks/_announcement-bar.liquidHorizon uses a separate block file for announcement content. Open the file in Edit code and search for the line that outputs the announcement text (typically block.settings.text or block.settings.announcement_text). Wrap that output line using the same conditional pattern as shown in the 'Other themes' example below.
Search for the text output line in the file, then wrap it:
Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
{{- _p.display_text | default: _p.announcement_text | escape -}}
{%- else -%}
{{- block.settings.text -}} {%- comment -%} replace with whatever your theme uses {%- endcomment -%}
{%- endif -%}sections/announcement-bar.liquid — or search your theme for the announcement bar fileMost OS 2.0 themes follow the same pattern. Open the file in Edit code, search for section.settings.text or block.settings.text, and wrap the text output using the same conditional.
Find the text output line, e.g.:
{{ section.settings.text | escape }}Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
{{- _p.display_text | default: _p.announcement_text | escape -}}
{%- else -%}
{{ section.settings.text | escape }}
{%- endif -%}Hero Banner
Replace your theme's hero image with a PromoOS campaign image when a promo is active, falling back to your original image when no promo is running. PromoOS writes the hero image as a Shopify file_reference metafield so you can use the native image_url / image_tag filters — no plain <img> tags needed.
Where to add: Option B only — edit your existing hero section file. See per-theme instructions below.
{%- assign promo_d = shop.metafields.promoos.hero_desktop.value -%}
{%- assign promo_m = shop.metafields.promoos.hero_mobile.value | default: promo_d -%}
{%- if promo_d != blank -%}
<picture>
{%- if promo_m != blank -%}
<source media="(max-width: 749px)"
srcset="{{ promo_m | image_url: width: 750 }}"
sizes="100vw">
{%- endif -%}
{{- promo_d | image_url: width: 1920 | image_tag: loading: 'lazy', sizes: '100vw' -}}
</picture>
{%- elsif shop.metafields.promoos.active_promo.value != blank and shop.metafields.promoos.active_promo.value.has_image -%}
{%- assign promo = shop.metafields.promoos.active_promo.value -%}
<img src="{{ promo.hero_image_desktop_1920 }}"
srcset="{{ promo.hero_image_desktop_800 }} 800w, {{ promo.hero_image_desktop_1920 }} 1920w"
sizes="100vw" alt="{{ promo.placements.home.hero.altText | default: '' | escape }}"
loading="lazy">
{%- endif -%}Option B — Inject into your existing announcement bar
Online Store → Themes → Edit code → find the file below → locate the text output line → replace it with the PromoOS conditional.
sections/image-banner.liquidDawn renders the desktop image and a separate mobile image (image_2) using the image_url + image_tag filter chain. Wrap each block so PromoOS swaps the image when a promo is active. The desktop block is around line 75; the mobile block is around line 107.
Desktop image — find (~line 75):
{{
section.settings.image
| image_url: width: 3840
| image_tag:
width: section.settings.image.width,
height: image_height,
class: image_class,
sizes: sizes,
widths: widths,
fetchpriority: fetch_priority
}}Replace with:
{%- assign _promo_img = shop.metafields.promoos.hero_desktop.value -%}
{%- if _promo_img != blank -%}
{{-
_promo_img
| image_url: width: 3840
| image_tag:
class: image_class,
sizes: sizes,
widths: widths,
fetchpriority: fetch_priority
-}}
{%- else -%}
{{
section.settings.image
| image_url: width: 3840
| image_tag:
width: section.settings.image.width,
height: image_height,
class: image_class,
sizes: sizes,
widths: widths,
fetchpriority: fetch_priority
}}
{%- endif -%}Mobile image (image_2) — find (~line 107):
{{
section.settings.image_2
| image_url: width: 3840
| image_tag:
width: section.settings.image_2.width,
height: image_height_2,
class: image_class_2,
sizes: sizes,
widths: widths,
fetchpriority: fetch_priority
}}Replace with:
{%- assign _promo_img_m = shop.metafields.promoos.hero_mobile.value | default: shop.metafields.promoos.hero_desktop.value -%}
{%- if _promo_img_m != blank -%}
{{-
_promo_img_m
| image_url: width: 3840
| image_tag:
class: image_class_2,
sizes: sizes,
widths: widths,
fetchpriority: fetch_priority
-}}
{%- else -%}
{{
section.settings.image_2
| image_url: width: 3840
| image_tag:
width: section.settings.image_2.width,
height: image_height_2,
class: image_class_2,
sizes: sizes,
widths: widths,
fetchpriority: fetch_priority
}}
{%- endif -%}sections/hero.liquidHorizon uses a <picture> element for its hero image with a mobile <source>. Open sections/hero.liquid in the theme editor (Code), search for 'image_url' to find the desktop image block, then wrap it as shown below. The exact variable names (image_1, sizes, etc.) may vary by version — match what you see in your file.
Find the desktop image_url | image_tag block and wrap it:
Replace with:
{%- assign _promo_d = shop.metafields.promoos.hero_desktop.value -%}
{%- assign _promo_m = shop.metafields.promoos.hero_mobile.value | default: _promo_d -%}
<picture class="hero__media">
{%- if _promo_m != blank -%}
<source media="(max-width: 749px)"
srcset="{{ _promo_m | image_url: width: 750 }}"
sizes="100vw">
{%- else -%}
{%- comment -%} keep your theme's original mobile source here {%- endcomment -%}
{%- endif -%}
{%- if _promo_d != blank -%}
{{-
_promo_d
| image_url: width: 3840
| image_tag:
class: 'hero__media',
sizes: sizes,
fetchpriority: fetch_priority
-}}
{%- else -%}
{%- comment -%} keep your theme's original desktop image_tag here {%- endcomment -%}
{%- endif -%}
</picture>sections/image-banner.liquid — or sections/slideshow.liquid — or your theme's hero section fileMost themes use a section.settings.image | image_url | image_tag chain for the hero. Wrap it with a PromoOS check — the file_reference value works with the same filter chain your theme already uses.
Find the hero image block, e.g.:
{{ section.settings.image | image_url: width: 3840 | image_tag: class: 'hero__image', sizes: '100vw' }}Replace with:
{%- assign _promo_img = shop.metafields.promoos.hero_desktop.value -%}
{%- if _promo_img != blank -%}
{{- _promo_img | image_url: width: 3840 | image_tag: class: 'hero__image', sizes: '100vw' -}}
{%- else -%}
{{ section.settings.image | image_url: width: 3840 | image_tag: class: 'hero__image', sizes: '100vw' }}
{%- endif -%}Optional — add a mobile image (if your theme has a separate mobile hero image setting):
{{ section.settings.image_mobile | image_url: width: 1100 | image_tag: class: 'hero__image hero__image--mobile', sizes: '100vw' }}Replace with:
{%- assign _promo_img_m = shop.metafields.promoos.hero_mobile.value | default: shop.metafields.promoos.hero_desktop.value -%}
{%- if _promo_img_m != blank -%}
{{- _promo_img_m | image_url: width: 1100 | image_tag: class: 'hero__image hero__image--mobile', sizes: '100vw' -}}
{%- else -%}
{{ section.settings.image_mobile | image_url: width: 1100 | image_tag: class: 'hero__image hero__image--mobile', sizes: '100vw' }}
{%- endif -%}Product Badge
Wire PromoOS data into your theme's existing product card. Insert the conditional inside your product card file where you want the badge to appear — the merchant controls the markup.
Where to add: Option B only — insert inside your existing product card file. See per-theme instructions below.
{%- assign promo = shop.metafields.promoos.active_promo.value -%}
{%- if promo != blank -%}
{{- promo.discount_display | escape -}}
{%- endif -%}Option B — Inject into your existing announcement bar
Online Store → Themes → Edit code → find the file below → locate the text output line → replace it with the PromoOS conditional.
snippets/card-product.liquidDawn has a card__badge div with sold-out and sale badge logic. Add the PromoOS badge after the closing {%- endif -%} of the existing badge block, before the closing </div>.
Search for `card__badge` in the file. Find the closing `{%- endif -%}` and `</div>` that end the badge block, then insert before the `</div>`:
Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank and _p.discount_display != blank -%}
<span class="badge badge--bottom-left" style="background:#e53e3e;color:#fff;">
{{- _p.discount_display | escape -}}
</span>
{%- endif -%}Theme Editor (no code edit needed)Horizon product cards support Custom Liquid blocks directly in the theme editor — no file editing required.
In the Theme Editor:
Online Store → Themes → Customize → any collection or product page → click a product card → Add block → Custom LiquidReplace with:
Paste the snippet above into the Custom Liquid block. Horizon will render it inside every product card automatically.snippets/card-product.liquid — or snippets/product-card.liquid — or your theme's product card snippetFind the badge or media area of your product card and insert the PromoOS badge span there.
Find a suitable anchor, e.g. after an existing sold-out badge:
{{- 'products.product.sold_out' | t -}}Replace with:
{{- 'products.product.sold_out' | t -}}
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank and _p.discount_display != blank -%}
<span style="display:inline-block;background:#e53e3e;color:#fff;font-size:11px;font-weight:700;padding:3px 8px;border-radius:3px;text-transform:uppercase;">
{{- _p.discount_display | escape -}}
</span>
{%- endif -%}Cart Drawer Promo Reminder
Wire PromoOS data into your theme's existing cart drawer. Insert the conditional inside your cart file where you want the promo reminder to appear — the merchant controls the markup.
Where to add: Option B only — insert inside your existing cart drawer file. See per-theme instructions below.
{%- assign promo = shop.metafields.promoos.active_promo.value -%}
{%- if promo != blank -%}
{{- promo.display_text | escape -}}
{%- endif -%}Option B — Inject into your existing announcement bar
Online Store → Themes → Edit code → find the file below → locate the text output line → replace it with the PromoOS conditional.
snippets/cart-drawer.liquidDawn renders cart items inside a div with id CartDrawer-CartItems. Insert the PromoOS reminder immediately before that div so it appears at the top of the open drawer.
Find the CartDrawer-CartItems div (~line 88):
<div id="CartDrawer-CartItems" class="drawer__contents js-contents">Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
{%- assign _bg = _p.placements.cart.banner.backgroundColor | default: '#f0fdf4' -%}
{%- assign _fg = _p.placements.cart.banner.textColor | default: '#166534' -%}
<div style="background:{{ _bg }};color:{{ _fg }};padding:12px 16px;margin:0 0 12px;border-radius:6px;font-size:13px;">
{%- if _p.display_text != blank -%}<strong>{{- _p.display_text | escape -}}</strong>{%- endif -%}
</div>
{%- endif -%}
<div id="CartDrawer-CartItems" class="drawer__contents js-contents">sections/main-cart.liquid — or sections/cart.liquidOpen your Horizon cart section file in Edit code. Search for a div that wraps the list of cart items (typically a class containing 'cart' and 'items'). Insert the PromoOS reminder block immediately before that div.
Search for your cart items wrapper div, then insert before it:
Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
{%- assign _bg = _p.placements.cart.banner.backgroundColor | default: '#f0fdf4' -%}
{%- assign _fg = _p.placements.cart.banner.textColor | default: '#166534' -%}
<div style="background:{{ _bg }};color:{{ _fg }};padding:12px 16px;margin-bottom:16px;border-radius:6px;font-size:13px;">
{%- if _p.display_text != blank -%}<strong>{{- _p.display_text | escape -}}</strong>{%- endif -%}
</div>
{%- endif -%}sections/cart-drawer.liquid — or snippets/cart-drawer.liquid — or your theme's cart drawer fileFind where the cart line items start (usually a <table> or <ul> with class cart-items) and insert the PromoOS reminder immediately before it.
Find your cart items container, e.g.:
<table class="cart-items">Replace with:
{%- assign _p = shop.metafields.promoos.active_promo.value -%}
{%- if _p != blank -%}
<div style="background:#f0fdf4;color:#166534;padding:12px 16px;margin-bottom:12px;border-radius:6px;font-size:13px;">
{%- if _p.display_text != blank -%}<strong>{{- _p.display_text | escape -}}</strong>{%- endif -%}
</div>
{%- endif -%}
<table class="cart-items">Metafield returns blank — existing install
If you installed PromoOS before this feature shipped, your metafield definition may be missing or lack storefront access. Go to your Dashboard, open DevTools console, and run the re-run setup fetch call from the test steps to recreate it.
display_text is 80 chars max
PromoOS truncates display_text to 80 characters. Don't rely on it for full-length copy — use announcement_text for the raw value if you need to truncate differently.
Image sizing and CDN params
hero_image_desktop_1920 and other sized variants use Shopify CDN ?width= params. These only work on images uploaded via Shopify (cdn.shopify.com). External image URLs will not be resized.
color_scheme conflicts
Some themes (Prestige, Impulse) apply a color_scheme setting to sections that overrides inline styles. If your announcement bar colours look wrong, wrap your snippet in a section schema that sets color_scheme, or use CSS custom properties instead of inline styles.
No promo = no DOM output
All four snippets render nothing when no promo is active — there are no empty containers or placeholder elements. Verify this in DevTools to make sure there's no invisible space in your layout.
When no promo is active, shop.metafields.promoos.active_promo.value evaluates to blank in Liquid. All four snippets check for blank before rendering anything, so your theme layout is never affected by an empty state.
Beta feature
AI Channel Attribution is a Beta feature. Data may not be fully accurate — channel signals from AI storefronts are still evolving and some orders may be misattributed or not captured at all. Treat the numbers as directional indicators, not exact figures.
AI Channel Attribution tracks which orders were placed by shoppers arriving via AI-powered storefronts — like ChatGPT shopping, Google AI Mode, Microsoft Copilot, and Gemini. When orders arrive from these channels, PromoOS records the source alongside the revenue and surfaces a breakdown in your promo analytics.
Orders/create webhook
PromoOS listens to the existing orders/create Shopify webhook. No additional subscriptions are needed.
Channel detection
For ChatGPT, PromoOS reads the HTTP referrer on the order. For Google AI Mode, Microsoft Copilot, and Gemini, PromoOS reads the channelInformation.channelDefinition.handle field returned by the Shopify Orders API.
Attribution storage
The resolved channel handle is stored with the order's revenue data. Orders with no recognisable AI signal are attributed to Online Store.
Visibility threshold
The Revenue by Channel breakdown only appears when at least one AI channel order exists for that promo. It stays hidden when there is no AI traffic — no clutter for merchants who haven't seen any yet.
| Channel | Detection method |
|---|---|
| ChatGPT | HTTP referrer (openai.com / chatgpt.com) |
| Google AI Mode | Shopify channel handle |
| Microsoft Copilot | Shopify channel handle |
| Gemini | Shopify channel handle |
Where to find it
Open any active or completed promo and go to the Performance tab. If AI channel orders exist, the Revenue by Channel card appears showing Online Store vs AI Channels, with a drilldown per AI channel (expand to see ChatGPT, Google AI Mode, etc. individually).
Channel data is collected as orders arrive — historical orders placed before PromoOS was installed are not backfilled.
ChatGPT attribution relies on a referrer header that shoppers' browsers may not send in all cases (e.g. private browsing).
Shopify's channel handle values for AI storefronts are not formally documented and may change — handle mappings will be updated as new channels emerge.
Some orders may arrive with no detectable channel signal even when the shopper came via an AI tool.
The analytics dashboard gives you a full picture of how each promo performed — revenue vs goal, day-by-day chart, lift vs baseline, customer breakdown, and top products. It's only available for completed promos.
Find analytics in three places: the Performance tab inside any promo's detail panel, the Analytics link on each promo card in the calendar popover, and the Analytics section in the sidebar. Analytics are only available once a promo has ended — use the revenue goal progress bar to track active promos.
KPI Cards
The four cards at the top show Net Sales (revenue attributed to this promo's discount code), Orders (number of orders using the code), AOV (average order value), and New Customers (first-time buyers). Each card includes a delta badge comparing performance to your baseline period. If it's your first promo, the delta shows 'No baseline yet' — this is normal until you have prior order history for comparison.
Day-by-Day Chart
The chart plots two lines: your promo period (colored) and the equivalent baseline period (gray). If only one line appears, there's no baseline data for that window — this is normal for your first few promos.
How Your Baseline is Calculated
PromoOS looks back 4 weeks before your promo start date and uses that period as the comparison baseline. Any weeks that overlap with other promo periods are excluded to avoid contamination. If overlap is unavoidable, a warning appears on the page — the data is still shown, but flagged as potentially affected.
Lift & Percentile Rank
Lift is the percentage change in net sales vs baseline: (promo revenue − baseline) / baseline × 100. The percentile rank (e.g. '#2 of your 9 completed promos by lift this year') shows where this promo sits relative to your other completed promos. It appears once you have at least two completed promos to compare.
Discount Summary
For discount code promos: shows the code used, discount type, and redemption count. For Crossed Price promos: shows the implied discount percentage calculated from the original vs sale price. The two display differently because Crossed Price promos don't generate a code.
Customer Breakdown
Shows the split between new customers (first order ever at your store) and returning customers. 'Not enough data' appears when there are too few orders to show a meaningful split.
Top Products
A ranked table of the products that generated the most revenue during the promo period. Available for discount code promos where order line items can be attributed. Not available for Crossed Price promos.
AI Insights
After a promo ends, PromoOS generates a plain-English summary of what worked, what didn't, and what to try next. The verdict badge (Exceeded / Met / Missed goal) appears alongside the narrative. AI insights are in Beta — they only appear after the promo ends, never while it's active. Use the thumbs up/down buttons to share feedback.
Results Email
A summary email is sent within a few minutes of the promo ending. It includes the verdict, key metrics, and a link to the full analytics dashboard. Only one email is sent per promo. If you didn't receive it, check your spam folder and confirm your store's notification email is set in Shopify Admin.
Understanding what PromoOS currently doesn't do helps you plan around it.
Active promos can't be edited
Once a promo is live (Active), you can't change its settings. To make changes, end the promo and create a new one with updated settings.
Exclusion targeting not supported
You can include specific products or collections, but you can't exclude them. Workaround: create a Shopify collection with exactly the products you want to target.
Storefront blocks require one-time setup
Automatic injection of promotional banners and badges into your storefront theme requires adding the PromoOS theme block to your theme once via the Shopify Theme Editor.
Starter includes a 14-day free trial — no credit card required.
Starter
$49
per month
Pro
$79
per month
| Feature | Starter | Pro |
|---|---|---|
| Promo calendar & planning | ||
| Dashboard overview & metrics | ||
| Unlimited active promos | ||
| Conflict detection | ||
| Automated scheduling & deployment | ||
| Discount code creation (Shopify) | ||
| Crossed price (compare-at) promos | ||
| Product & collection targeting | ||
| Advanced discount controls (limits, stacking) | ||
| Revenue goal tracking | ||
| Storefront theme blocks | ||
| Full analytics dashboard | ||
| AI channel attribution | ||
| Email support | ||
| Text to Promo — AI promo creation | — |
Email Support
contact@promly.app
Response within 24 hours
When contacting support, include: