How to Build a Daily Klaviyo Suppression List from Bounced Emails
Build a scheduled Spojit workflow that pulls recent Klaviyo bounce and unsubscribe events each morning, flags the affected profiles, and posts a deliverability summary to Slack.
What This Integration Does
Email deliverability quietly erodes when bounced and unsubscribed addresses keep receiving sends. Mailbox providers treat repeated hard bounces and stale contacts as a spam signal, which drags down inbox placement for your whole list. This workflow runs on a schedule, reads the last day of bounce and unsubscribe activity from Klaviyo, marks each affected profile so your campaigns and flows stop targeting it, and drops a short deliverability digest into a Slack channel so the marketing team sees the trend without logging into Klaviyo.
The run model is time-based. A Schedule trigger fires once a day at a fixed time in your timezone. Each run computes a 24-hour lookback window, calls the Klaviyo connector to list the relevant events, loops over the affected profiles and updates a custom attribute on each one, then sends a single Slack message with the counts. The workflow holds no state between runs other than the attributes it writes onto Klaviyo profiles, so a re-run for the same window is safe: setting the same suppression attribute twice is idempotent and produces the same result.
Prerequisites
- A Klaviyo connection added in Spojit under Connections -> Add connection, with an API key that can read events and update profiles. See the Klaviyo connector reference for setup.
- A Slack connection authorized for the workspace, with the bot able to post to your target channel. See the Slack connector reference.
- The Slack channel ID or name where the digest should land (for example
#deliverability). - An agreed suppression attribute on Klaviyo profiles. This tutorial writes a custom property named
spojit_suppressedso your campaign and flow audience filters can exclude it. - The IANA timezone your team works in (for example
Australia/Sydney) so the schedule and the lookback window line up.
Step 1: Start with a Schedule trigger
Create a new workflow and add a Trigger node, then set its type to Schedule. Schedules use a 5-field Unix cron expression plus an IANA timezone. To run every weekday at 07:00 in Sydney, use:
0 7 * * 1-5
Australia/Sydney
A single trigger can hold multiple schedules if you want extra runs, but one daily fire is enough here. The trigger output is { scheduledAt }, which you can reference downstream as {{ scheduledAt }} for logging.
Step 2: Compute the lookback window
Add a Connector node in Direct mode using the date utility connector and the subtract tool to get the timestamp 24 hours before the run. Set the base time to {{ scheduledAt }}, the unit to hours, and the amount to 24. Bind the result so you can reference it as {{ since.result }}. You will pass this ISO timestamp into the Klaviyo event filter in the next step.
Klaviyo's event filters use JSON:API syntax against the event datetime, so an ISO-8601 string such as 2026-06-19T07:00:00Z is exactly what you want here.
Step 3: Pull bounce and unsubscribe events from Klaviyo
Add a Connector node in Direct mode, choose the Klaviyo connector, and pick the list-events tool. This returns tracked events newest-first when you sort by datetime. Set the fields as follows:
filter: restrict to recent bounce and unsubscribe activity, for example a JSON:API expression that combines a metric name with the lookback timestamp from Step 2.sort:-datetimeso the newest events come first.pageSize:100(the maximum) to reduce the number of pages you need.
filter: greater-than(datetime,{{ since.result }})
sort: -datetime
pageSize: 100
Bind the output to {{ events }}. If your account separates hard bounces, soft bounces, and unsubscribes into distinct metrics, run this step once per metric inside a Parallel node, or call list-events with a broader filter and narrow it in the next step. Each event carries the profile reference you need to suppress.
Step 4: Reduce events to a unique list of affected profiles
Add a Transform node to pull the profile id out of each event and remove duplicates, since one address can bounce several times in a day. Map each event to its profile id, then keep only unique values. The output should be a flat array of profile ids, bound to {{ targets }}.
If you prefer not to hand-write the mapping, scaffold this step with Miraxa, the intelligent layer across your automation: open the chat on the canvas and ask it to "add a Transform node that maps {{ events }} to the unique list of profile ids and connect it after the Klaviyo step." Miraxa knows the workflow you are editing and will wire the node in for you; you can then fine-tune the expression in the properties panel.
Step 5: Suppress each affected profile
Add a Loop node in ForEach mode over {{ targets }}. Inside the loop body, add a Connector node in Direct mode using the Klaviyo connector and the update-profile tool. Map the fields:
id: the current loop item, for example{{ item }}.attributes: a JSON object that sets your suppression property and records when it happened.
{
"properties": {
"spojit_suppressed": true,
"spojit_suppressed_at": "{{ scheduledAt }}"
}
}
Because update-profile writes the same value every time, re-running the workflow for an overlapping window will not create duplicates or side effects. To stop sending to these contacts, add spojit_suppressed as an exclusion in your Klaviyo campaign and flow audience filters. If you maintain a dedicated suppression list, you can instead collect the ids and call add-profiles-to-list with your list id once after the loop.
Step 6: Post a deliverability summary to Slack
After the loop, add a Connector node in Direct mode using the Slack connector and the send-message tool. Set the channel to your deliverability channel and build the message text from the counts you gathered. Reference the number of events from Step 3 and the number of unique profiles from Step 4.
channel: #deliverability
text: Daily deliverability check ({{ scheduledAt }}):
{{ events.data.length }} bounce/unsubscribe events found,
{{ targets.length }} profiles suppressed in Klaviyo.
If you would rather email the digest instead of (or in addition to) Slack, add a Send Email node, which sends from Spojit's built-in mail service with no extra connection. Just remember that external recipients must be on your organization allowlist under Settings -> General -> Email recipients.
Tips
- Keep
pageSizeat100and sort by-datetimeso a busy day still fits in as few pages as possible. If your list is large, page through results until the returned window predates your lookback timestamp. - Run the schedule slightly after your daily send window so the bounce data has settled. A 07:00 run that reports on yesterday's sends is more accurate than one that fires mid-campaign.
- Use a clear, namespaced property like
spojit_suppressedso it never collides with attributes your other tools write, and so it is obvious in Klaviyo where the flag came from. - Watch the Slack counts over time: a sudden spike in bounces often points to a bad import or a list you bought rather than grew.
Common Pitfalls
- Timezone drift. The cron timezone and your lookback math must agree. If the schedule runs in
Australia/Sydneybut your window is computed in UTC without converting, you can miss an hour of events around the boundary. Anchor both to{{ scheduledAt }}. - Duplicate events. A single address can generate many bounce events per day. Skipping the dedupe Transform in Step 4 means you call
update-profilefar more often than needed and may hit Klaviyo rate limits. - Suppressing without filtering campaigns. Writing
spojit_suppresseddoes nothing on its own. You must add it as an exclusion in each campaign and flow audience, or the suppressed contacts keep receiving mail. - Missing API scope. If the Klaviyo connection cannot update profiles, the loop fails partway through. Confirm the key can both read events and patch profiles before turning the schedule on.
Testing
Before enabling the daily schedule, run the workflow once with the Run button while pointing the Klaviyo filter at a wide window (say the last 7 days) so you are sure events come back. Inspect the execution log to confirm {{ events }} is populated, {{ targets }} is a deduplicated list, and the Slack message posts with sensible counts. Pick one of the updated profile ids and check in Klaviyo that spojit_suppressed is set. Once that looks right, narrow the filter back to the 24-hour window, verify a single profile is flagged correctly, then enable the schedule. The schedule trigger guide covers how to confirm the next fire time.