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.write scope (add email-templates.write if 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-preview' \
  -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 trueBlocks or falseBlocks (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-preview' \
  -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: aBlocksPercentage must 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, update aBlocksPercentage to 100 (all contacts go to A) or 0 (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-preview' \
  -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"), the value must be the ID (or temporaryID when creating) of a send-action block that appears before this split in the workflow. The field must be "blockID". Note: When using message engagement filters (such as openedEmail or clickedEmail), 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 via POST /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-preview' \
  -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 value type must match the property type. Numeric properties (like raw._total_price) require a number value, not a string — "value": 100 is correct, "value": "100" is rejected. Note: Event filter operators include eq, 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-preview'

Check that:

  • The blocks array contains your split or A/B test block with the expected branches
  • Each split block has a filterGroup with the correct filters
  • Each abTesting block has the expected aBlocksPercentage
  • The automation isEnabled is false (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 trueBlocks and falseBlocks are empty (or both aBlocks and bBlocks for 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 value must reference a send-action block ID that appears before the split. When creating new blocks, use the temporaryID of the earlier block as the value.
  • PATCH cannot modify branches: Use PUT /automations/{id}/blocks to restructure trueBlocks/falseBlocks or aBlocks/bBlocks. PATCH only supports updating the split's filterGroup or the A/B test's aBlocksPercentage.

Related resources