Cross-Channel Orchestration
Build cascading notification flows that try email first and fall back to SMS using the cascade() helper.
Build cascading notification flows that try channels in order and stop when engagement is detected. One function call, zero new infrastructure.
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.
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.
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.
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.
// 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' })| Option | Type | Description |
|---|---|---|
type | 'email' | 'sms' | Channel type for this step |
template | string | Template slug to send |
waitFor | DurationConfig | How long to wait for engagement before trying next channel |
engagement | 'opened' | 'clicked' | What counts as engagement (email only, default: 'opened') |
from, senderId | string | Channel-specific sender overrides |
For critical notifications, cascade through multiple channels. Each non-final channel waits for engagement before falling back.
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', }, ], }), ],});Cascades compose naturally with other workflow steps. Use them inside condition branches, after delays, or anywhere you need multi-channel delivery.
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', }, ], }), ], }, }), ],});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.
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.
For transactional notifications (payment failures, account security), use engagement: 'clicked' to ensure the user actually took action, not just opened the email.
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.
Learn the full workflow DSL including triggers, conditions, and all step helpers.
Workflow GuideFull reference for the Wraps client SDK and workflow API.
SDK Reference