This guide explains how to set up an account for a catalog-change flow load test with a custom catalog integration. Use the scripts in this repository to create profiles, products, and events. You can also change product prices or inventory to trigger a load test.
feed.json is the catalog source for the test account. Scripts that add or
update products modify this file.
Before running create_profiles.py or create_events.py, obtain a private API
key from the target test account. Create a virtual environment, install the
dependencies, and save the key in the current terminal session:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
export KLAVIYO_PRIVATE_API_KEY="$(python -c \
'import getpass; print(getpass.getpass("Paste the private API key: "))')"The prompt hides the key and keeps it out of shell history. The exported value
exists only in the current terminal session, so repeat the source and
export commands after opening a new terminal. Feed generation, feed updates,
and local feed validation do not require an API key.
generate_products.py creates the feed when it does not exist or appends to
the existing feed. Each product uses a UUID-based FEED- ID, so the script
does not need to load or scan existing IDs. It streams the existing JSON into
an atomic replacement file, keeping memory use independent of feed size.
python3 generate_products.py --count 10000
python3 generate_products.py --count 5000
python3 generate_products.py --count 100 --feed another-feed.jsonUse --seed to select a repeatable sequence of product attributes. IDs and
SKUs remain unique UUID-based values on every run.
Choose either price or inventory, specify the maximum number of products,
and provide exactly one of --drop-amount or --target-amount.
Without --categories, updates start at the top of the feed:
python3 update_products.py price --count 100 --drop-amount 5.00
python3 update_products.py price --count 100 --drop-amount 20%
python3 update_products.py inventory --count 100 --target-amount 0Use --categories to update the first products containing any listed category.
Matching is case-insensitive and checks every level of each product's category
path:
python3 update_products.py price \
--count 50 \
--categories Electronics "Art Supplies" \
--drop-amount 10%
python3 update_products.py inventory \
--count 50 \
--categories Shoes \
--drop-amount 5Raw inventory values must be whole numbers. Percentage inventory changes are rounded to the nearest whole unit. Price values are rounded to cents. Drops never reduce either field below zero.
Important: generate_products.py and update_products.py only modify
feed.json. Merge a PR that contains the updated feed. Then refresh the
catalog source sync to apply the changes to the test account.
This repository hosts a public JSON product feed for Flows load-test accounts.
The initial feed.json contains 10,000 fake products and follows the shape of
Varun's feed-large.json.
Use this raw feed URL when configuring a custom catalog source:
https://raw.githubusercontent.com/klaviyo/flows-test-account-custom-catalog-sync/master/feed.json
Each product contains:
id,title,description,link, and HTTPSimage_linkpriceandcurrencyinventory_quantityandinventory_policycategories,brand,sku, andradius
The catalog source configures the field mappings automatically. Update the field mappings if necessary.
A custom catalog URL feed creates exactly one variant for each product row.
The generated variant uses the product id as its external ID and reads
sku, price, inventory_quantity, and inventory_policy from that row.
The URL feed format cannot define multiple variants under one catalog item. Use the Catalog API if you want to create products with multiple variants.
Usually, you do not need this step. Use it to validate the catalog feed format.
Install the checker dependency:
python3 -m pip install -r requirements.txtRun the interactive checker:
python3 custom_catalog_checker.pyEnter the public raw feed URL:
https://raw.githubusercontent.com/klaviyo/flows-test-account-custom-catalog-sync/refs/heads/master/feed.json
The checker downloads the JSON and reports good, bad, and total row counts. A
good product must contain id, title, link, image_link, and
description; these field names do not use a $ prefix.
The full terminal instructions from the internal runbook are also included as
comments at the top of custom_catalog_checker.py.
Custom catalog load tests require additional configuration before Athena can build the audience:
- Configure the catalog-change metric override on Staffside for the test account. Its product ID, price, and product-list paths must match the event schema submitted by the integration.
- Ensure those field paths are supported by the
flows_catalog_change_audience_building_eventsmaterialized-views job mapping. The current event generator uses the qualified Jomashop-compatible paths already supported by that mapping. If the generated event schema changes, update and deploy the materialized-views mapping before running the load test. - Add the load-test company ID to the
flows_ccp_custom_catalog_athena_enabledStatsig gate.
Do not start the Price Drop load test until all three requirements are confirmed. Otherwise, Athena can omit the custom events or the account can use the wrong audience-building path.
Complete Set up API access with a key that has the
profiles:write scope.
Run the script with the number of new profiles to create as its only argument:
python3 create_profiles.py 500Each profile receives a Faker-generated readable name, an email such as
steve_smith_<uuid>@example.com, a UUID-based external ID, and custom
properties identifying it as FP-4236 load-test data. Requests are paced at 370
per minute—below the 750/minute steady limit—and retry when the API responds
with 429 or times out. The script prints confirmed progress every 25 profiles.
If the batch stops, it reports the confirmed count; the final timed-out request
may still have reached the API.
create_events.py uses existing profiles and catalog products to submit
item-level events through the Bulk Create Events API. Use a private API key for
the target test account with these scopes:
profiles:readcatalogs:readevents:write
Set up the virtual environment and KLAVIYO_PRIVATE_API_KEY as shown in
Set up API access, then provide one or more metric counts:
python3 create_events.py \
--metric viewed-product=500 \
--metric started-checkout=500Supported metric arguments are:
viewed-productstarted-checkoutadded-to-cartordered-product
By default, the payload omits time, so Klaviyo assigns ingestion time. Use
--days to distribute explicit UTC timestamps uniformly across a historical
window. For example, 30,000 events over 30 days produces approximately 1,000
events in each consecutive 24-hour period:
python3 create_events.py --metric viewed-product=30000 --days 30Use --profiles and --products to limit the resource pools. The script
fetches only the first requested number of eligible profiles and priced
products, then cycles within those subsets until it creates every requested
event:
python3 create_events.py \
--metric ordered-product=600000 \
--profiles 500 \
--products 10000In this example, all 600,000 events use only the first 500 load-test profiles and first 10,000 priced products. Other profiles and products are not used. If a requested pool size exceeds what the account contains, the script uses all available resources in that pool.
Catalog API order is not feed order. When a test must create events for the
same leading products that update_products.py --count will change, select
products directly from the feed:
python3 create_events.py \
--metric viewed-product=150 \
--profiles 3 \
--products 50 \
--product-feed feed.jsonThis produces 50 events per profile across the same first 50 feed products
that update_products.py --count 50 updates. --product-feed requires an
explicit --products value.
Repeated arguments for the same metric are added together. The script queries
only profiles where load_test is true. It uses distinct profiles and
products first, then cycles through the available pool when an event count is
larger than that pool. Every event receives a unique UUID, so Events ingestion
keeps repeated profile/metric combinations distinct even when they have the
same second-level timestamp.
For example, when creating 10,000 events with 500 eligible profiles and 10,000 products, profiles and products cycle independently:
Event 1 -> Profile 1, Product 1
Event 500 -> Profile 500, Product 500
Event 501 -> Profile 1, Product 501
Event 1000 -> Profile 500, Product 1000
Event 10000 -> Profile 500, Product 10000
Each product is used once, while each profile receives 20 events. With multiple metrics, the assignment index continues across metric groups.
Use --overlap-count to make part of the requested Ordered Product count
exclude profiles from a catalog-change audience. Each overlapping order uses
the same profile and product as one requested inclusion event:
python3 create_events.py \
--metric viewed-product=500 \
--metric ordered-product=100 \
--overlap-count 100The overlap count cannot exceed either the combined inclusion-event count or
the Ordered Product count. It does not create extra events; every metric's
requested count remains exact.
Requested event counts are the script's highest priority. --overlap-count
controls how many Ordered Product events intentionally reuse an inclusion
profile/product pair, but it is guidance rather than a maximum. If there are
not enough distinct profiles or products, the script cycles through them and
continues creating all requested events. This can produce more overlap than
the supplied value.
The current script does not create any variant-level events. All generated events contain item-level product IDs but no variant identifier. This is sufficient for load testing Price Drop flows because Price Drop supports only item-level products. Load testing variant-level Low Inventory flows will require additional implementation.
The event property paths mirror Jomashop's Price Drop overrides:
- Viewed Product: top-level
ProductIDandPrice - Ordered Product: top-level
ProductIDandPrice; Price Drop exclusion matching only needsProductID, but retainingPricekeeps the event complete - Started Checkout: an
Itemslist containingProductIDandItemPrice - Added to Cart: top-level
ProductIDandPriceuntil a Jomashop-specific schema is available
The script reads external_id and current price from the Catalog Variants
API. It assumes each item and its single generated variant share the same
external ID, as they do for this repository's URL feed. Verify this before
using another company's catalog: a feed with independently identified
variants requires an explicit variant-to-parent-item mapping.
The API accepts up to 1,000 events or 5 MB per job. The script splits larger runs and streams bounded batches, with at most 24 jobs in flight, so memory use does not grow with the total event count. It limits all initial and retry attempts together to 100 jobs per minute. Sustained runs at 140 jobs per minute repeatedly encountered connection failures despite the API's 150-per-minute limit. Each worker has an isolated HTTP session; transport failures replace that session and retry up to 30 times with exponential backoff capped at 30 seconds. Rate-limit and server responses retry up to 12 times. Submission cannot advance more than 24 jobs beyond the last contiguous accepted batch, which bounds possible duplicate work after a resume. Keep the terminal running until every job is accepted. An accepted job is queued for asynchronous processing; it does not mean every event is immediately visible in Klaviyo.
If an event run is interrupted, resume after the last accepted event count printed by the original run:
python3 create_events.py \
--metric viewed-product=40500000 \
--days 90 \
--resume-at 9640000Use exactly the same metrics, days, profile cap, product cap, and overlap count
as the original command. --resume-at preserves the original profile/product
cycle and timestamp positions; changing another generation argument would
produce a different event sequence. The resume count must be a confirmed
accepted count because the script does not query previous job status.
Use this sequence as the standard Price Drop load-test procedure. Existing test data can be reused, so first decide the target scale and skip any preparation step whose requirement is already satisfied.
-
Ensure the feed contains the target number of products. For example, add 50,000 products to an existing 10,000-product feed:
python3 generate_products.py --count 50000
-
Merge the updated feed and refresh the custom catalog source. This baseline sync must finish before prices change, especially for newly added products. Confirm that the account contains the expected total number of products.
-
Ensure the account has enough load-test profiles. For example, add 2,000:
python3 create_profiles.py 2000
-
Ensure every product has an inclusion event within the flow's lookback window. To cover 60,000 discovered products exactly once, create 60,000 Viewed Product events:
python3 create_events.py --metric viewed-product=60000 --days 30
Confirm that the script discovers 60,000 products before approving the submission.
-
Wait until the events are visible on Athena and have aged beyond the EventLog fallback window. Do not continue until both conditions are true.
-
Shortly before changing prices, create a small recent-event population:
python3 create_events.py --metric viewed-product=100
Wait until the accepted jobs finish processing and confirm that the recent events are visible in the account before changing prices.
These events exercise the EventLog fallback while the older events exercise Athena. Some recent events may reach Athena before their parent executes; asynchronous ingestion means the script cannot guarantee that they remain exclusive to EventLog.
-
Drop every product's price and merge the updated feed. For a 60,000-product feed and a $1 drop:
python3 update_products.py price --count 60000 --drop-amount 1
-
Refresh the custom catalog source again. Record the sync time and monitor side-effect verification, parent creation, audience building, internal events, and end-to-end completion.
Products, profiles, and historical events do not need to be recreated for every run. For example, after a recent test whose events remain inside the flow's lookback window, it may be sufficient to create only the 100 recent events, change prices, and sync again. Always perform the baseline catalog sync when adding products, and verify the current product count before relying on one event per product.
Run the standard-library test suite:
python3 -m unittest discover -s tests