How to Run a Retail Loyalty Points Sync for Your Pharmacy Storefront

Build a nightly Spojit workflow that reads the day's front-of-store sales from Shopify, awards loyalty points per customer, updates each shopper's Klaviyo profile, and tags shoppers who cross a VIP threshold so your marketing list stays in step with spend.

What This Integration Does

Many pharmacies run a retail storefront alongside their counter: vitamins, skincare, OTC remedies, gifts, and seasonal stock. Rewarding that spend keeps shoppers coming back, but maintaining a loyalty ledger by hand is slow and error-prone. This workflow does it for you every night. It pulls the day's paid retail orders from Shopify, computes points for each customer from the amount they spent, writes the running balance and tier onto their Klaviyo profile, and flags anyone who has just crossed into VIP territory. Your marketing flows in Klaviyo can then segment and reward those tiers automatically. This is pure retail loyalty: no prescription, dispensing, or clinical data is involved, only purchase totals and customer contact records you already hold for marketing.

A Schedule trigger starts the run once per night on a cron expression and timezone you choose. Spojit fetches that day's orders, groups spend by customer in a Transform node, then loops over each shopper to read their current balance and write the new one to Klaviyo. A Condition node checks the new total against your VIP threshold and adds those shoppers to a VIP list. The workflow leaves no partial state you need to clean up: each run recomputes from the orders it reads, and because points are written as an absolute balance rather than a blind increment, a re-run over the same day is idempotent and will not double-count. If your loyalty ledger lives in an external system rather than on the Klaviyo profile, the same Loop reads and writes it through the http connector instead.

Prerequisites

  • A Shopify connection for your storefront, authorized to read orders and customers. See the Shopify connector reference for setup.
  • A Klaviyo connection with permission to read and update profiles and to add profiles to a list. See the Klaviyo connector reference.
  • A Klaviyo audience list to hold VIP shoppers. Note its list ID (run list-lists once to find it).
  • A points rule (for example, 1 point per dollar of retail spend) and your VIP threshold (for example, 1000 points).
  • If your points ledger is external, the REST endpoint and an API key for reading and writing balances. The built-in http and json connectors need no connection.
  • This is a retail loyalty workflow only. Do not route prescription, dispensing, or clinical records through it.

Step 1: Add a Schedule trigger to run nightly

Start a new workflow and set its trigger to Schedule. Add one schedule with a 5-field Unix cron expression and an IANA timezone so the run lands after close. For 11:30 PM Sydney time every night, use cron 30 23 * * * with timezone Australia/Sydney. The trigger output is { scheduledAt }, which you can reference downstream as {{ trigger.scheduledAt }} when you build the date filter in the next step. A single Schedule trigger can hold more than one schedule if you ever want a second nightly pass.

Step 2: List the day's retail orders from Shopify

Add a Connector node on the shopify connector in Direct mode and pick the list-orders tool. Use the query field with Shopify search syntax to scope it to paid orders from the current day, and set first to a high page size (max 250). A starting query:

financial_status:paid AND created_at:>=2026-06-21T00:00:00+10:00

To make the date dynamic, build the day's start timestamp with a Connector node on the date connector (for example start-of on {{ trigger.scheduledAt }}) and template it into the query string. list-orders returns paginated results: if a busy day exceeds your page size, follow the returned cursor with the after field in a Loop until no cursor remains. Each order in the result carries its customer, total, and line items.

Step 3: Compute points per customer with a Transform node

Add a Transform node to reshape the order list into one entry per customer with summed spend and points. Group the orders by customer email or customer ID, total the order amounts, and multiply by your points rule. Aim for an output array like this so the loop has a clean shape to iterate:

[
  {
    "customerId": "gid://shopify/Customer/123",
    "email": "shopper@example.com",
    "spend": 84.50,
    "pointsEarned": 84
  }
]

If you prefer explicit utility steps over a single Transform, the json connector covers the same work: pick to keep only the fields you need, query to extract nested order totals, and flatten or merge to combine entries. Round points with the math connector (floor or round) so balances stay whole numbers.

Step 4: Loop over each customer and update the Klaviyo profile

Add a Loop node in ForEach mode over the array from Step 3, referencing each entry as {{ customer }}. Inside the loop, first read the shopper's current balance. If you keep the balance on the Klaviyo profile, add a Connector node on klaviyo in Direct mode using get-profile with the profile id, then read the existing points property. Add the loop's pointsEarned to it, and write the new absolute balance back with a second Connector node using the update-profile tool. The update-profile tool takes the profile id plus an attributes object; put the running total and tier under custom properties:

{
  "id": "01H8XYZ...",
  "attributes": {
    "properties": {
      "loyalty_points": 612,
      "loyalty_tier": "Gold"
    }
  }
}

Writing the balance as an absolute value (rather than telling Klaviyo to add to whatever is there) is what makes a re-run safe. If a customer has no Klaviyo profile yet, branch to create-profile with their email before updating. To look up a profile by email when you only have the address from the order, use list-profiles with a filter and take the first match.

Step 5: Read and write an external ledger through the http connector (optional)

If your authoritative loyalty ledger lives outside Klaviyo, do the read and write inside the same Loop with the http connector instead of the profile property. Add a Connector node on http in Direct mode with http-get to read the current balance from your ledger's REST API, passing your API key in the Authorization header:

GET https://api.yourloyaltyledger.com/v1/members/{{ customer.email }}/balance
Authorization: Bearer YOUR_API_KEY

Parse the response with the json connector (get or query) to pull out the current points, add the earned points, then write the new balance back with an http-post or http-put node. Use the same absolute-balance approach so the nightly run stays idempotent. For a fuller walkthrough of reaching a system that has no native tile, see the guide on connecting to any REST API.

Step 6: Tag VIP customers with a Condition node

Still inside the Loop, add a Condition node that compares the new balance to your VIP threshold, for example whether {{ newBalance }} is greater than or equal to 1000. On the true branch, add a Connector node on klaviyo in Direct mode using add-profiles-to-list, passing your VIP listId and a profileIds array containing this shopper's profile ID:

{
  "listId": "YbV4Wx",
  "profileIds": ["01H8XYZ..."]
}

Adding a profile that is already on the list is harmless, so you do not need to check membership first. To avoid re-tagging shoppers who were already VIP, make the true branch fire only when the customer crossed the threshold on this run: compare the old balance (below threshold) with the new balance (at or above it) in the Condition. The false branch simply continues the loop.

Step 7: Notify the team and finish the run

After the Loop completes, add a Send Email node to summarize the night's award run for your retail manager. Set Recipients to the team address, a templated Subject such as Loyalty points awarded for {{ trigger.scheduledAt }}, and a Body listing how many customers were updated and how many crossed into VIP. Keep an upstream count handy (the array connector's length tool over your customer array works well) so the totals resolve in the email. Recipients must be on your org allowlist under Settings -> General -> Email recipients. To send from your own pharmacy domain rather than Spojit's built-in mail service, use a Connector node on the resend or smtp connector with the send-email tool instead.

Tips

  • Keep the points rule and VIP threshold in one place. Store them as workflow variables and reference them as {{ pointsPerDollar }} and {{ vipThreshold }} so you change the rule in one spot, not in several nodes.
  • Cap list-orders at 250 per page and follow the cursor for high-volume days. A silent partial page would under-award points.
  • Always write balances as absolute totals, never as blind increments. That single decision makes both retries and a manual re-run of the same night safe.
  • If you want a person to sign off before VIP tiers go live during a big promotion, drop a Human approval node before the tagging step. See the Human approval node guide.

Common Pitfalls

  • Timezone drift. The Schedule cron timezone and the date boundary in your Shopify query must match, or you will read the wrong day's orders. Build the query timestamp from {{ trigger.scheduledAt }} in the same timezone.
  • Guest checkouts with no customer record. Orders placed without an account have no customer to award. Filter those out in the Transform, or branch to create-profile keyed on the order email.
  • Double counting on overlapping windows. If two nightly schedules or a manual re-run cover the same orders, an additive points write would inflate balances. The absolute-balance pattern above prevents this.
  • Stale profile lookups. When you match a Klaviyo profile by email, normalize case and trim whitespace first (the text connector's case and trim tools) so Shopper@Example.com and shopper@example.com resolve to the same profile.

Testing

Before scheduling, validate on a tiny scope. Temporarily narrow the Shopify query to a single known test order (add a tag filter or a specific date), and run the workflow with the Run button rather than waiting for the schedule. Open the execution log and confirm the Transform produced the expected per-customer points, the Loop wrote the correct absolute balance to the test Klaviyo profile, and the Condition added the test shopper to the VIP list only when the balance crossed the threshold. Run it a second time over the same order and confirm the balance does not change, proving the run is idempotent. If anything looks off, ask Miraxa, the intelligent layer across your automation, "Why did my last run fail?" or "Show me what the Transform node output for this run." Once a small scope is correct, widen the query back to the full day and turn the schedule on.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.