Wraps Logo
Guide

Cross-Channel Orchestration

Build cascading notification flows that try channels in order and stop when engagement is detected. One function call, zero new infrastructure.

What is a Cascade?

A cascade is an ordered sequence of notification channels. Your message is sent on the first channel (usually email), and if the user doesn't engage within a timeout, it falls back to the next channel (usually SMS). The cascade stops as soon as engagement is detected.

Send Email
Opened?
YesStop
No
Send SMS

The cascade() Helper

The cascade() function is a compile-time helper that expands into standard workflow steps. No new infrastructure or step types are needed — it uses the same primitives you already know.

TypeScriptwraps/workflows/cart-recovery.ts
import { defineWorkflow, cascade } from '@wraps.dev/client';export default defineWorkflow({  name: 'Cart Recovery Cascade',  trigger: { type: 'event', eventName: 'cart.abandoned' },  steps: [    ...cascade('recover-cart', {      channels: [        {          type: 'email',          template: 'cart-recovery',          waitFor: { hours: 2 },          engagement: 'opened',        },        {          type: 'sms',          template: 'cart-sms-reminder',        },      ],    }),  ],});

Zero runtime overhead

cascade() runs at definition time, not execution time. It returns an array of standard step definitions that your existing workflow processor already understands.

How It Works

Under the hood, cascade() expands each channel into a send → wait → check → fallback sequence. The last channel in the array is the final fallback and has no wait or check.

TypeScriptexpansion.ts
// What cascade() expands to://// 1. sendEmail('recover-cart-email', { template: 'cart-recovery' })// 2. waitForEmailEngagement('recover-cart-email-wait', {//      emailStepId: 'recover-cart-email',//      engagementType: 'opened',//      timeout: { hours: 2 },//    })// 3. condition('recover-cart-email-check', {//      field: 'steps.recover-cart-email-wait.engaged',//      operator: 'equals',//      value: true,//      branches: {//        yes: [exit('recover-cart-engaged')],//      },//    })// 4. sendSms('recover-cart-sms', { template: 'cart-sms-reminder' })
OptionTypeDescription
type'email' | 'sms'Channel type for this step
templatestringTemplate slug to send
waitForDurationConfigHow long to wait for engagement before trying next channel
engagement'opened' | 'clicked'What counts as engagement (email only, default: 'opened')
from, senderIdstringChannel-specific sender overrides

Examples

3-Channel Cascade

For critical notifications, cascade through multiple channels. Each non-final channel waits for engagement before falling back.

TypeScriptwraps/workflows/payment-alert.ts
import { defineWorkflow, cascade } from '@wraps.dev/client';export default defineWorkflow({  name: 'Urgent Notification',  trigger: { type: 'event', eventName: 'payment.failed' },  steps: [    ...cascade('payment-alert', {      channels: [        {          type: 'email',          template: 'payment-failed',          waitFor: { hours: 4 },          engagement: 'clicked',        },        {          type: 'sms',          template: 'payment-failed-sms',          waitFor: { hours: 2 },        },        {          type: 'email',          template: 'payment-final-notice',        },      ],    }),  ],});

Mixed Workflow with Cascade

Cascades compose naturally with other workflow steps. Use them inside condition branches, after delays, or anywhere you need multi-channel delivery.

TypeScriptwraps/workflows/onboarding.ts
import {  defineWorkflow,  cascade,  sendEmail,  delay,  condition,  exit,} from '@wraps.dev/client';export default defineWorkflow({  name: 'Onboarding with Cascade',  trigger: { type: 'contact_created' },  steps: [    // Standard welcome email    sendEmail('welcome', { template: 'welcome' }),    delay('wait-1-day', { days: 1 }),    // Check if user activated    condition('check-activated', {      field: 'contact.hasActivated',      operator: 'equals',      value: true,      branches: {        yes: [exit('activated')],        no: [          // Cascade: try email, fall back to SMS          ...cascade('activation-nudge', {            channels: [              {                type: 'email',                template: 'activation-reminder',                waitFor: { hours: 6 },                engagement: 'opened',              },              {                type: 'sms',                template: 'activate-now-sms',              },            ],          }),        ],      },    }),  ],});

Best Practices

Start with the least intrusive channel

Email first, SMS second. Email is free (or near-free) and non-intrusive. SMS costs more and should be reserved for users who didn't engage with email.

Set reasonable timeouts

2-4 hours is typical for email engagement checks. Too short and you'll spam users who just haven't checked their inbox yet. Too long and the message loses urgency.

Use clicked for high-intent actions

For transactional notifications (payment failures, account security), use engagement: 'clicked' to ensure the user actually took action, not just opened the email.

Keep cascades short

2-3 channels is ideal. More than that and you risk annoying users. If they didn't engage after 3 attempts across different channels, they probably don't want to hear from you right now.

Next Steps

Building Workflows

Learn the full workflow DSL including triggers, conditions, and all step helpers.

Workflow Guide
Platform SDK

Full reference for the Wraps client SDK and workflow API.

SDK Reference