How to Re-Engage Lapsed NetSuite Customers with a Klaviyo Winback List
Build a scheduled Spojit workflow that finds NetSuite customers with no recent orders, syncs them as Klaviyo profiles, and adds them to a winback list ready for a re-engagement flow.
What This Integration Does
Lapsed customers are people who bought from you once but have gone quiet. Your order history lives in NetSuite, but your email and SMS sending lives in Klaviyo, so there is no easy way to act on "customers who have not ordered in 90 days" without exporting spreadsheets by hand. This Spojit workflow closes that gap: on a schedule it queries NetSuite for customers whose most recent order is older than your chosen window, then makes sure each of those people exists as a Klaviyo profile and lands in a dedicated winback list. A Klaviyo flow attached to that list does the actual sending, so marketing keeps full control of the messaging.
The workflow runs on a Schedule trigger (for example every Monday morning) with no human in the loop. Each run pulls the current lapsed cohort from NetSuite with a single SuiteQL query, loops over the rows, upserts each customer into Klaviyo, and appends the resulting profile IDs to the winback list. Adding a profile to a Klaviyo list is idempotent, so re-runs are safe: a customer who is already on the list is simply re-added with no duplicate, and a customer who has since placed an order drops out of the query and is no longer pushed. The workflow leaves your NetSuite data untouched and only writes to Klaviyo.
Prerequisites
- A NetSuite connection in Spojit with access to run SuiteQL queries against your account (see Connections -> Add connection -> NetSuite).
- A Klaviyo connection in Spojit with permission to read, create, and update profiles and to manage list membership.
- A Klaviyo list created in Klaviyo to act as the winback audience. Copy its
listIdfrom the Klaviyo list settings; you will paste it into the workflow. - A lapsed-window definition you agree on with marketing, for example "no order in the last 90 days".
- The NetSuite transaction and customer table/field names for your account. You can confirm them with the NetSuite Analytics Data Source schema browser before building the query.
Step 1: Add a Schedule trigger
Start a new workflow and set the Trigger node type to Schedule. A Schedule trigger uses a 5-field Unix cron expression plus an IANA timezone, so the run fires in your local business time rather than UTC. For a weekly Monday 9am run in Sydney, set the cron to 0 9 * * 1 and the timezone to Australia/Sydney. A single trigger can hold more than one schedule if you want, for example a weekday morning cadence. The trigger output is simply {{ scheduledAt }}, which you do not need to map anywhere; it just marks when the run began.
Step 2: Query NetSuite for lapsed customers
Add a Connector node in Direct mode, choose the NetSuite connector, and select the run-suiteql tool. This runs a SQL-like query against your NetSuite data and is the right fit because the lapsed cohort is an aggregation that record-by-record reads cannot express. In the query field, find each customer's most recent sales-order date and keep only those whose latest order is older than your window. A starting point that returns id, name, and email for customers with no order in the last 90 days:
SELECT
c.id AS customer_id,
c.companyname AS company_name,
c.email AS email,
MAX(t.trandate) AS last_order_date
FROM customer c
JOIN transaction t ON t.entity = c.id
WHERE t.type = 'SalesOrd'
AND c.email IS NOT NULL
AND c.isinactive = 'F'
GROUP BY c.id, c.companyname, c.email
HAVING MAX(t.trandate) < ADD_MONTHS(SYSDATE, -3)
FETCH FIRST 500 ROWS ONLY
Confirm the exact table and column names against your own NetSuite account, since custom fields and saved-search conventions vary. Use the limit and offset inputs on run-suiteql if your lapsed cohort is large and you want to page through it. Map the result to an output variable such as lapsed so later steps can read {{ lapsed.items }} (the returned rows).
Step 3: Loop over each lapsed customer
Add a Loop node set to ForEach and point it at the rows from the previous step, for example {{ lapsed.items }}. Each iteration exposes one customer row, which you can reference inside the loop body as {{ item.email }}, {{ item.company_name }}, and {{ item.customer_id }} (use the column aliases from your query). Everything in the next two steps lives inside this loop body so it runs once per lapsed customer. If you prefer to keep AI cost and run time down, keep the loop body to plain Direct-mode connector calls as described below.
Step 4: Create or update the customer's Klaviyo profile
Inside the loop, add a Connector node in Direct mode for the Klaviyo connector. To avoid duplicate profiles, first look the customer up by email, then create or update. Add a list-profiles call with a filter of equals(email,"{{ item.email }}") and store it as existing. Follow it with a Condition node that checks whether a profile was found.
On the branch where no profile exists, add a Klaviyo Direct-mode node using create-profile. The tool takes a single attributes object; populate it from the loop row:
{
"email": "{{ item.email }}",
"first_name": "{{ item.company_name }}",
"properties": {
"last_order_date": "{{ item.last_order_date }}",
"winback_source": "netsuite-lapsed"
}
}
On the branch where a profile already exists, use update-profile instead, passing the existing id (from {{ existing.items.[0].id }}) and the same attributes shape so the last_order_date custom property stays current. Capture the profile id from whichever branch ran into a variable such as profileId for the final step.
Step 5: Add the profile to the winback list
Still inside the loop, add a final Klaviyo Connector node in Direct mode using add-profiles-to-list. Set listId to the winback list ID you copied earlier, and set profileIds to an array containing the current profile, for example ["{{ profileId }}"]. This call is idempotent at the list level, so a profile that is already a member is not duplicated. Once a customer is on this list, your Klaviyo winback flow (configured in Klaviyo, triggered by list membership) takes over the actual outreach.
Step 6: Notify your team and finish
After the loop, add a Send Email node so the marketing team gets a short summary each run. Send Email uses Spojit's built-in mail service and needs no connection. Set Recipients to your marketing address, a Subject like Winback sync: {{ lapsed.count }} lapsed customers added, and a plain-text Body referencing upstream variables. Remember that external recipients must be on your org allowlist under Settings -> General -> Email recipients. Save the workflow, then enable it so the schedule starts firing.
Tips
- Keep the SuiteQL window in one place: parameterize the lapsed period by editing only the
HAVING MAX(t.trandate) < ADD_MONTHS(SYSDATE, -3)clause so marketing can ask for 60, 90, or 180 days without touching the rest of the query. - Cap the cohort per run with
FETCH FIRST n ROWS ONLYin SuiteQL (andlimiton the tool) so a one-time backlog does not push thousands of profiles in a single run; let later runs catch up. - Store a
winback_sourceproperty on each Klaviyo profile (as shown above) so marketing can report on how many sends originated from this NetSuite-driven list. - If you want to reuse the "upsert a Klaviyo profile" logic in other workflows, extract Steps 4 and 5 into a Subworkflow and call it from the loop.
Common Pitfalls
- Timezone drift: the Schedule trigger fires in the IANA timezone you set, but
SYSDATEin SuiteQL is evaluated in NetSuite's account timezone. If the boundary matters, align them or compare on whole dates rather than timestamps. - Missing or malformed emails: NetSuite rows without a valid email will fail the Klaviyo create call. The query above filters
c.email IS NOT NULL, but you may also want a Condition or theemailvalidation tool to skip bad addresses inside the loop. - Duplicate profiles: skipping the
list-profileslookup and always callingcreate-profilecan create duplicate Klaviyo profiles. Always look up by email first and branch toupdate-profilewhen one exists. - List membership is not segmentation:
add-profiles-to-listonly adds members; it does not send anything. The actual winback email is sent by a Klaviyo flow you build in Klaviyo against that list.
Testing
Before enabling the schedule, validate on a tiny scope. Temporarily tighten the SuiteQL query with a short window and a small FETCH FIRST 5 ROWS ONLY, or filter to a single known test customer's email, then run the workflow once from the designer to confirm the rows come back as expected. Watch the execution in Spojit to verify the loop runs once per row, that the create/update branch resolves correctly, and that profiles land on the winback list in Klaviyo. Once the small run looks right in both Spojit and Klaviyo, restore the full window and enable the schedule. If a run fails, ask Miraxa, the intelligent layer across your automation, "Why did my last run fail?" and it will investigate the execution in context.