How to use splits and A/B testing in automations
Intro
Branch your automation workflows based on conditions or random allocation using split and abTesting blocks. Splits route contacts down a true/false path based on event properties, contact attributes, or message engagement. A/B tests randomly distribute contacts between two variant paths by percentage, letting you compare which branch drives better results.
See API reference: Automations
Prerequisites
- An API key with
automations.writescope (addemail-templates.writeif creating email templates), or an OAuth token with the same scopes - At least one email template ID if the workflow includes send-email actions — see Email Templates API
Step 1: Create an automation with a conditional split
Create a workflow that triggers on marketing subscription, waits 1 hour, then splits contacts by country. US contacts get a free-shipping welcome email; others get an international shipping email.
A split block evaluates a filterGroup of one or more filters. Contacts matching the conditions go to trueBlocks; the rest go to falseBlocks. At least one branch must contain blocks.
See endpoint: Create automation workflow
curl -X POST 'https://api.omnisend.com/api/automations' \
-H 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
-H 'Omnisend-Version: 2026-03-15' \
-H 'Content-Type: application/json' \
-d '{
"name": "Welcome by Country",
"trigger": {
"condition": {
"event": "subscribed to marketing"
}
},
"blocks": [
{
"temporaryID": "wait-1h",
"type": "delay",
"delay": {
"mode": "duration",
"duration": { "amount": 1, "units": "h" }
}
},
{
"temporaryID": "split-by-country",
"type": "split",
"split": {
"filterGroup": {
"logicalOperator": "and",
"filters": [
{
"type": "contact",
"field": "country",
"operator": "eq",
"value": "US"
}
]
},
"trueBlocks": [
{
"temporaryID": "us-email",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "Welcome! Free shipping in the US",
"senderName": "My Store",
"preheader": "Enjoy free delivery on your first order",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
}
],
"falseBlocks": [
{
"temporaryID": "intl-email",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "Welcome! International shipping options",
"senderName": "My Store",
"preheader": "We ship worldwide",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
}
]
}
}
]
}'The response returns the automation in disabled state with server-assigned block id values. Save the automation id for subsequent steps.
Note: A split must be the last block at its level — do not place sibling blocks after it. Move any downstream blocks into
trueBlocksorfalseBlocks(or both). Note: Supported contact filter fields:tag,country,state,city,postalCode,segmentID,dateAdded,firstName,lastName,gender. See Automations overview — Split filters for allowed operators per field.
Step 2: Create an automation with an A/B test
Create a workflow that splits contacts randomly between two email variants to test which subject line performs better. The abTesting block routes a percentage of contacts to branch A and the remainder to branch B.
See endpoint: Create automation workflow
curl -X POST 'https://api.omnisend.com/api/automations' \
-H 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
-H 'Omnisend-Version: 2026-03-15' \
-H 'Content-Type: application/json' \
-d '{
"name": "Welcome Email A/B Test",
"trigger": {
"condition": {
"event": "subscribed to marketing"
}
},
"blocks": [
{
"temporaryID": "wait-30m",
"type": "delay",
"delay": {
"mode": "duration",
"duration": { "amount": 30, "units": "m" }
}
},
{
"temporaryID": "ab-test-subject",
"type": "abTesting",
"abTesting": {
"aBlocksPercentage": 50,
"aBlocks": [
{
"temporaryID": "variant-a",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "Welcome! Here is 10% off",
"senderName": "My Store",
"preheader": "Your discount code inside",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
}
],
"bBlocks": [
{
"temporaryID": "variant-b",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "Welcome to My Store!",
"senderName": "My Store",
"preheader": "See what is new this week",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
}
]
}
}
]
}'Note:
aBlocksPercentagemust be between 0 and 100. Branch B automatically receives the remainder (100 − aBlocksPercentage). At least one branch must contain blocks. Note: To pick a winner after reviewing results, updateaBlocksPercentageto100(all contacts go to A) or0(all contacts go to B) using the Replace blocks endpoint or a PATCH.
Step 3: Replace blocks to add a split to an existing automation
Use PUT /automations/{id}/blocks to replace the entire block tree. This is the only way to restructure branches — PATCH cannot modify trueBlocks/falseBlocks or aBlocks/bBlocks.
This example replaces the blocks of the automation created in Step 1. It adds a message engagement split after the email: contacts who opened the email get a follow-up; those who did not get tagged for re-engagement.
See endpoint: Replace automation workflow blocks
curl -X PUT 'https://api.omnisend.com/api/automations/AUTOMATION_ID/blocks' \
-H 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
-H 'Omnisend-Version: 2026-03-15' \
-H 'Content-Type: application/json' \
-d '{
"blocks": [
{
"temporaryID": "wait-1h",
"type": "delay",
"delay": {
"mode": "duration",
"duration": { "amount": 1, "units": "h" }
}
},
{
"temporaryID": "send-email",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "Thanks for your order!",
"senderName": "My Store",
"preheader": "We appreciate your purchase",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
},
{
"temporaryID": "wait-1d",
"type": "delay",
"delay": {
"mode": "duration",
"duration": { "amount": 1, "units": "d" }
}
},
{
"temporaryID": "split-by-engagement",
"type": "split",
"split": {
"filterGroup": {
"logicalOperator": "and",
"filters": [
{
"type": "message",
"field": "blockID",
"operator": "openedEmail",
"value": "send-email"
}
]
},
"trueBlocks": [
{
"temporaryID": "follow-up-email",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "A special offer just for you",
"senderName": "My Store",
"preheader": "Because you showed interest",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
}
],
"falseBlocks": [
{
"temporaryID": "tag-re-engage",
"type": "action",
"action": {
"type": "addTag",
"addTag": { "value": "needs-re-engagement" }
}
}
]
}
}
]
}'Note: For message filters (
type: "message"), thevaluemust be the ID (ortemporaryIDwhen creating) of a send-action block that appears before this split in the workflow. Thefieldmust be"blockID". Note: When using message engagement filters (such asopenedEmailorclickedEmail), place a delay block before the split to give contacts time to interact with the message. Note: The automation must be disabled before replacing blocks. If it is currently enabled, disable it first viaPOST /automations/{id}/disable, make changes, then re-enable.
Step 4: Split by event properties
You can also split on trigger event properties using "type": "event" filters. The available fields depend on the trigger event and your integration — use the event metadata endpoint to discover property paths.
This example splits "placed order" contacts by order value. The property paths (e.g. raw._total_price) are specific to the integration that sends the event.
curl -X POST 'https://api.omnisend.com/api/automations' \
-H 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
-H 'Omnisend-Version: 2026-03-15' \
-H 'Content-Type: application/json' \
-d '{
"name": "Post-Purchase Split by Order Value",
"trigger": {
"condition": {
"event": "placed order"
}
},
"blocks": [
{
"temporaryID": "wait-1h",
"type": "delay",
"delay": {
"mode": "duration",
"duration": { "amount": 1, "units": "h" }
}
},
{
"temporaryID": "split-by-value",
"type": "split",
"split": {
"filterGroup": {
"logicalOperator": "and",
"filters": [
{
"type": "event",
"field": "raw._total_price",
"operator": "gt",
"value": 100
}
]
},
"trueBlocks": [
{
"temporaryID": "tag-vip",
"type": "action",
"action": {
"type": "addTag",
"addTag": { "value": "vip-customer" }
}
}
],
"falseBlocks": [
{
"temporaryID": "standard-email",
"type": "action",
"action": {
"type": "sendEmail",
"sendEmail": {
"subject": "Thanks for your order!",
"senderName": "My Store",
"preheader": "We appreciate your purchase",
"language": "en_US",
"templateID": "000000000000000000000001"
}
}
}
]
}
}
]
}'Note: The
valuetype must match the property type. Numeric properties (likeraw._total_price) require a number value, not a string —"value": 100is correct,"value": "100"is rejected. Note: Event filter operators includeeq,neq,gt,gte,lt,lte,contains,notContains,startsWith,endsWith,exists,notExists,in,notIn. Use numeric operators (gt,gte,lt,lte) only with numeric properties.
Verify results
Retrieve the automation to confirm the block tree was created correctly.
See endpoint: Get automation workflow
curl -X GET 'https://api.omnisend.com/api/automations/AUTOMATION_ID' \
-H 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
-H 'Omnisend-Version: 2026-03-15'Check that:
- The
blocksarray contains your split or A/B test block with the expected branches - Each
splitblock has afilterGroupwith the correct filters - Each
abTestingblock has the expectedaBlocksPercentage - The automation
isEnabledisfalse(automations are always created disabled)
Troubleshooting
- 409 Conflict on PUT blocks or PATCH: The automation is currently enabled. Disable it first via
POST /automations/{id}/disable, make changes, then re-enable. - 400 "Split must have at least one branch with blocks": Both
trueBlocksandfalseBlocksare empty (or bothaBlocksandbBlocksfor A/B tests). At least one branch must contain blocks. - 400 "A split must be the last block": There are sibling blocks after the split at the same level. Move them into the split's branches.
- 400 on message filter value: The
valuemust reference a send-action block ID that appears before the split. When creating new blocks, use thetemporaryIDof the earlier block as the value. - PATCH cannot modify branches: Use
PUT /automations/{id}/blocksto restructuretrueBlocks/falseBlocksoraBlocks/bBlocks. PATCH only supports updating the split'sfilterGroupor the A/B test'saBlocksPercentage.