# Cross-Channel Orchestration

Source: https://wraps.dev/docs/guides/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.

wraps/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.

expansion.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' })
```

| 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 |

## Examples

### 3-Channel Cascade

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

wraps/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.

wraps/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](https://wraps.dev/docs/guides/workflows)

Platform SDK

Full reference for the Wraps client SDK and workflow API.

[SDK Reference](https://wraps.dev/docs/client-sdk-reference)
