Back-in-stock alerts
Collect confirmed signups on sold-out variants, email them through your own provider when stock returns, and see every signup and send in your admin.
When a variant sells out, the product page offers “Email me when it’s back”. Shoppers confirm their address from a confirmation email, so nobody can sign someone else up. When the variant can be bought again, Littleworks emails everyone waiting through your Postmark or Resend account, in signup order, and never emails anyone twice for the same signup. Every alert has an unsubscribe link. Your staff see who is waiting, what was sent and anything that failed on a page in Shopify admin.
Use Littleworks to build back-in-stock alerts for my Shopify store. When the selected variant is sold out, let shoppers enter their email on the product page. Confirm each signup with a double opt-in email, and include an unsubscribe link in every email. When the variant can be bought again, email everyone waiting through my Postmark account (I’ll add the server token in Littleworks secrets), in signup order, without ever emailing anyone twice for the same signup. Add a Littleworks admin page where staff can see signups, what was sent and anything that failed. Read https://littleworks.app/docs/recipe-back-in-stock.md and adapt it to my store, theme and email provider.What you are building
- On the product page: a theme block that appears only while the selected variant is sold out, with an email field and a button. It follows variant changes.
- A confirmation page: one storefront page, /pages/back-in-stock, that email links open to confirm alerts or stop them. Nothing happens until the shopper clicks.
- Sending: a scheduled work notices restocks and sends in batches, oldest signup first. An optional Shopify Flow workflow sends the first batch within seconds of a restock.
- An admin page, “Back-in-stock alerts”: tabs for Waiting, Awaiting confirmation, Notified, Not delivered, Unsubscribed and All; counts; an emails-sent-per-day chart; and Send again, Cancel alert and Delete actions.
An App Store app does this too. Building it keeps the parts that are yours: your own email provider, sender domain and templates; your own rules, such as signup order, sending only as many as you have stock for, or VIP customers first; and the signups in your own collections, next to everything else you build on the same backend.
| Littleworks provides | Your build provides |
|---|---|
| The Shopify app, its permissions and the Storefront access the works use | The signup rules and the unsubscribe experience |
| Schedules, the Shopify Flow action, and rate limiting and input validation on the public signup endpoint | The email templates and what they say |
| Encrypted secrets for the provider key, versions and rollback, run logs, and the admin page | The theme block and the confirmation page |
Decide how a restock is noticed
A restock here is what the shopper sees: the variant’s availableForSale turns true in the Storefront API. That already accounts for every location, the inventory policy and Online Store publication.
| Signal | How | Runs it costs | How fast |
|---|---|---|---|
| Scheduled check (every build) | back_in_stock.dispatch runs on a schedule. One Storefront query checks up to 100 watched variants. | Fixed by the schedule: 720 a month hourly, 1,440 every 30 minutes, 2,880 every 15 minutes. Sales volume doesn’t change it. | Within one interval |
| Shopify Flow (optional fast path) | The merchant adds a workflow: Shopify’s inventory trigger, a condition that continues only when the variant goes from none available to some, and Run a Littleworks work with back_in_stock.restocked. | About one per restock | Seconds |
| `inventory_levels/update` event (not for busy stores) | An event work with a when filter. | One run per inventory change while the variant is in stock, about one per order line plus adjustments: 2,000 orders of two lines is 4,000 or more a month. Needs read_inventory. | Seconds |
Every build gets the scheduled check, because it is also the sender. Add the Flow path when the merchant wants alerts within seconds and agrees to set up one workflow. The event path costs a run per sale because when can’t compare with the previous quantity or with the variants you are watching, so mention it only with its cost.
All paths meet at one watches document per variant, changed only with its revision: waiting, then sending, then deleted once everyone is emailed (or back to waiting if it sells out again). A restock seen by both Flow and the schedule is sent once.
Where the data lives
Use feature back_in_stock.
| Collection | Document | Kept |
|---|---|---|
signups | One per signup: email (trimmed, lowercased), variantId, productId, title snapshots, status (pending, confirmed, notified, failed, unsubscribed), reason, openKey, confirmHash, test, and timestamps | Pending 48 hours; confirmed 180 days (the merchant’s choice); ended signups 30 days, for the page |
watches | One per watched variant, with a stable ID: the numeric variant ID, prefixed test- for test signups. status, restockedAt, inflight (the batch a run has claimed), lastError, failures | Deleted by the dispatcher once nobody is waiting |
sends | One per provider batch: count, accepted, rejected, status (sending, sent, partial, failed, unknown), runId, times | 90 days; feeds the metrics and chart |
contacts | One per address, keyed by the SHA-256 of the address: confirmation emails sent | 24 hours, a sliding cap |
- One open signup per address and variant. A setup work runs
ensureUnique(["email", "openKey"])once.openKeyholds the variant ID only while the signup is pending or confirmed, and a rule ignores records missing a field, so removing it when a signup ends lets the same address sign up for the next sell-out. - Always write `test: context.test`. Real runs filter
test: falseand test runstest: true, so a test never emails a real shopper and a real run never emails a test signup. - Link secrets. A confirmation link carries the signup ID and 32 random bytes; only their SHA-256 is stored. Unsubscribe links are an HMAC-SHA-256 of the signup ID with a
BACK_IN_STOCK_LINK_KEYsecret, so the dispatcher can build them without storing anything and they never expire. Action tokens don’t fit: they last at most seven days. - Documents. Each open signup is one document, and each ended one stays 30 days. Starter’s 2,000 documents hold roughly 1,500 open signups at once after the sends and watches; above that, use Growth or a shorter retention.
Before you build
- Secrets, added by the merchant in Littleworks → Works → Secrets, never in chat:
POSTMARK_SERVER_TOKEN;POSTMARK_SERVER_TOKEN_TESTfor test runs; andBACK_IN_STOCK_LINK_KEY, a long random value for unsubscribe links. With Resend, useRESEND_API_KEYinstead. - At the provider: a verified sender domain, and templates
bis-confirmandbis-restockwith the fields the works send. - Shopify: the included Storefront catalog access reads availability and product details. Add
read_productsso the admin page can show variant and product titles and images. No order or customer access, and no protected customer data: shoppers type their own address. - The endpoints are public, because guests must be able to sign up; Littleworks can’t read a signed-in customer’s email without protected customer data approval. Allowed origins aren’t proof of identity. The double opt-in email is what proves the shopper owns the address.
The works to build
| Work | Entry point | Behavior |
|---|---|---|
| back_in_stock.setup | Callable, run once for real | Create the one-open-signup rule |
| back_in_stock.subscribe | Public HTTP POST | Check the variant is sold out, create a pending signup, send one confirmation email |
| back_in_stock.confirm | Public HTTP POST | Pending to confirmed from the email link, and make sure the variant is watched |
| back_in_stock.unsubscribe | Public HTTP POST | Verify the signed link and end one signup, or every open signup for the address |
| back_in_stock.dispatch | Schedule, such as */15 * * * * | Check watched variants, then claim and send batches, oldest signup first |
| back_in_stock.restocked | Shopify Flow (optional) | Check the variant is available online and send the first batch |
| back_in_stock.signups, back_in_stock.stats | Page data | The list (viewPage with search on email and product title), the metrics and the chart |
| back_in_stock.manage | Page action | Send again, Cancel alert and Delete, with revisions |
A validated example of every work, the page and the theme files is in the Littleworks repository under examples/recipe-back-in-stock. Works can’t call each other: dispatch and restocked share one send module, which CLI users bundle into both and MCP-only agents copy unchanged into each.
Implementation outline for the agent
- Accept signups safely. The input schema bounds the variant ID and email and includes an empty
websitehoneypot field, so bots fail validation before a run starts. Check the variant with one Storefront query: missing returns 404, and available returns 409AVAILABLEso the theme shows Add to cart. Cap confirmation emails at five per address per 24 hours and open signups at 20 per address. Return the same{status: "check_email"}for new, repeated and capped addresses, so the response never reveals who is subscribed. - Claim the confirmation before sending it. Create the pending signup with
confirmationsSent: 1andlastConfirmationAtalready set. A concurrent or repeated request hits the uniqueness rule, finds a confirmation sent within ten minutes, and sends nothing. A later resend rotates the link and claims the send in one revision-checked patch. If the provider fails, release the claim and return 502 so the shopper can try again. - Confirm with a click. The confirm work compares the link secret’s hash, creates the variant’s watch if it doesn’t exist (a conflict means it does), then marks the signup confirmed with its revision. A repeated click returns the same result.
- Check availability in one query. The dispatcher lists watches that are sending and up to 50 that are waiting, continuing from a saved cursor so more than 50 watched variants are all checked over consecutive runs. One Storefront
nodes(ids:)query returns their availability with the titles, image and product URL for the email. Waiting and now available becomes sending; sending and sold out again goes back to waiting. - Claim each batch, then send, then record. Load up to 100 confirmed signups oldest first and fill the request up to 60 KiB, under the 64 KiB request limit; anyone left over goes in the next batch. In one atomic batch, write the claim on the watch (this run, the signup IDs, a send ID) with its revision and create the
sendsrecord. A conflict means another run owns the variant: skip it. Then call Postmark’sbatchWithTemplatesonce, and record each signup’s result in batches of 50 with revisions: ErrorCode 0 is notified, 406 an inactive recipient (Not delivered, suppressed), anything else rejected. Clear the claim in the same write as the final send record. - Handle failures without resending. A refused request (4xx or 5xx) sent nothing: everyone stays waiting, and the watch records
lastError; a 401 means the key is wrong, and sending stops for the run. A timeout or network error may have sent: those signups become Not delivered with reasonunknown, and only staff choose Send again. A claim older than a minute belongs to a run that ended mid-send, and is settled the same way. - Use the run well. Stop starting batches after 6 of the run’s 10 seconds, and send at most five batches per run, about 500 emails. Each batch is one external request, and a run can make 50 external requests. A larger list continues on the next scheduled run, in order.
- Build links for the right theme. Email links use the primary domain’s /pages/back-in-stock. In test runs, the works link to the unpublished theme (
?preview_theme_id=) so the confirmation page there calls the works’ preview URLs. - Offer the Flow path honestly.
back_in_stock.restockedtakesvariant_idand checksavailableForSaleagain, because stock can arrive at a location that doesn’t sell online, then sends one batch with the same claim. Agents can’t create Flow workflows and shouldn’t guess variable names: tell the merchant the trigger to start from, the condition, the Work name and the Input linevariant_id:followed by that trigger’s variant ID variable, checked in the Flow editor. If the trigger offers no previous quantity, skip this path.
See signups and sends in your admin
Build a merchant page named back-in-stock: a table from back_in_stock.signups with email, variantId (variant), productId (product), a status field with the five states, reason, notifiedAt and $createdAt; the six tabs; search on email; and a test filter. Metrics and a bar chart of emails sent per day come from back_in_stock.stats, which leaves test records out. Actions: Send again for Not delivered (back to waiting; a newer signup from the same shopper is reported as already waiting), Cancel alert for pending and waiting, and Delete for privacy requests. Each refreshes the stats.
The page uses one published page slot (Starter has 2). Each view costs two runs, plus one for each tab, page or search change.
Add it to the theme
- Product-page block. A theme block shown only when the selected variant is sold out. It carries a JSON map of variant availability, follows the theme’s variant picker, sends one
Idempotency-Keyper submission so a double click is a replay rather than a second run, and maps responses to messages: success or a replayed key is “Check your inbox to confirm”,AVAILABLEis “It’s back”, 400 or 422 is an invalid address, 429 is “try again in a minute” (read the status first; it may not be JSON), and anything else asks to try again. Settings: the subscribe URL, defaulting to the work’s realhttps://littleworks.app/f/{store}/back_in_stock.subscribe, plus every heading, label, message and the consent text. - Confirmation page. A section on template
page.back-in-stockreads#confirm=…or#unsubscribe=…from the address, removes it straight away, and shows Confirm alerts, or Stop this alert and Stop all back-in-stock emails, as buttons. Buttons rather than automatic requests keep email link scanners from acting. Settings: the confirm and unsubscribe URLs. The merchant creates a page with handleback-in-stockusing the template. - Add the primary domain and the .myshopify.com domain to each public work’s
origins.
How it uses Littleworks
An example store: 400 signup attempts a month, 300 confirmed, 10 unsubscribes, 20 restocks and about 300 alert emails, with staff opening the page about 100 times.
| Source | Runs a month |
|---|---|
| subscribe, including repeats | about 440 |
| confirm | about 300 |
| unsubscribe | about 10 |
| dispatch every 15 minutes | 2,880 (hourly 720, every 30 minutes 1,440, every 10 minutes from 7:00 to 21:59 with */10 7-21 * * * 2,700) |
| restocked from Flow (optional) | about 20 |
| admin page | about 250 |
| Total | about 3,900, within Starter’s 10,000 |
- Emails go out inside the dispatch runs, so a restock with 1,000 people waiting adds no runs: two scheduled runs of about 500 each.
- External requests have no monthly allowance; the provider bills its own emails. Rejected input, rate-limited calls and replayed idempotency keys cost no run.
- The event path would add a run per in-stock inventory change, often thousands a month.
Test before publishing
- Deploy every work as a draft and ask the merchant to add the secrets. Test runs use
POSTMARK_SERVER_TOKEN_TEST: Postmark’s documented test token accepts sends without delivering them, which suits volume tests. Use one real token and a mailbox the merchant controls for an end-to-end check. With Resend, use its documented test addresses. - Use a published test product with tracked inventory at 0 at every location. Point an unpublished theme at the works’ preview URLs and sign up there. Check
read_collectionfor a pending test record, that one confirmation arrived, that a repeat sends nothing within ten minutes, that the honeypot and an invalid address are rejected without a run, and that an in-stock variant answersAVAILABLE. - Confirm and unsubscribe through the preview theme’s page, and check the signup states and the
test-…watch. - Run the dispatcher with
invoke_functionandtrigger: {schedule: {}}(CLI:littleworks invoke back_in_stock.dispatch --schedule). Sold out, it sends nothing. Restock the test product and run it again: the watch becomes sending, emails go out, signups become notified and a sends record appears. A third run sends nothing. - For volume, seed a few hundred confirmed test signups with a temporary callable work and the test token, run the dispatcher twice and check it drains about 500 then the rest, in order.
get_runshows each run’s time. Delete the seeding work afterwards. - Test failures: a wrong test key leaves everyone waiting with the error on the watch, and selling out mid-drain returns the watch to waiting.
- Run
back_in_stock.restockedwith{variant_id, flow: {actionRunId: "test-1"}}. Save the page, check itschecks, and try each action on test records at the draft page’smanageUrl. - Clean up: restore the product’s inventory, report the test record count and ask before
delete_test_records. Leftover open test signups hold the one-open-signup rule against the same address. - Publish when the merchant asks: the public works, then the dispatcher (say “runs every 15 minutes, about 2,880 runs a month”), then the page. Run setup for real, switch the theme settings to the real URLs, and let the merchant publish the theme and add the optional Flow workflow.
Acceptance checks
- Concurrent signups for the same address and variant create one record and at most one confirmation email. Available or unpublished variants are refused.
- The signup response is the same for new, repeated and capped addresses, and no address gets more than five confirmations in 24 hours.
- Unconfirmed signups are never sent a restock email and disappear after 48 hours.
- A restock seen by Flow and the schedule, or by overlapping runs, emails each confirmed signup once. An interrupted send is never repeated automatically; it shows as Not delivered with reason unknown.
- A list of 1,000 drains in signup order across runs within every per-run limit. Selling out again stops sending and the rest keep waiting.
- Every alert’s unsubscribe link works, “Stop all” ends every open signup for the address, and the address can sign up again later. A tampered link is refused.
- Test runs never email real signups and real runs never email test signups. The page’s counts and chart match the collections.
- With the dispatcher paused, signups still work. With an invalid key, nothing is lost and the page shows the error.
Building around today’s limits
whenfilters can’t compare with a previous value or with stored data, so restocks are found by the schedule or Flow rather than a filtered inventory event.- Public works receive the JSON body only, without request headers. Provider webhooks can’t be signature-checked and mailbox one-click unsubscribe (a form POST) can’t reach a work, so the recipe uses each batch’s per-message results and a page link. For bulk senders, use the provider’s broadcast stream, which handles one-click unsubscribe.
- Shopify’s privacy requests reach Littleworks, not your collections. Addresses are stored normalized, searchable on the page and deletable with Delete, and ended signups expire.
Make it yours
Cap each wave to the stock that arrived (add unauthenticated_read_product_inventory for quantityAvailable), email VIP customers first, add SMS through another provider with the same claim-and-record steps, or use the transactional email module for other messages. Discount codes, reserving stock and per-market sends are separate builds.