| name | wraps-workflows |
| description | Build email and SMS automation workflows using the @wraps.dev/client TypeScript DSL. Covers triggers, steps, branching, cascades, and the full defineWorkflow API. |
Wraps Workflow DSL
Build email and SMS automation workflows as TypeScript files using @wraps.dev/client. Workflows live in wraps/workflows/*.ts and are pushed to the Wraps dashboard with wraps email workflows push.
Installation
npm install @wraps.dev/client
pnpm add @wraps.dev/client
Quick Start
import { defineWorkflow, sendEmail, delay, condition, exit } from '@wraps.dev/client';
export default defineWorkflow({
name: 'Welcome Sequence',
trigger: { type: 'contact_created' },
steps: [
sendEmail('send-welcome', { template: 'welcome-email' }),
delay('wait-1-day', { days: 1 }),
condition('check-activated', {
field: 'contact.hasActivated',
operator: 'equals',
value: true,
branches: {
yes: [exit('already-active')],
no: [sendEmail('send-tips', { template: 'getting-started-tips' })],
},
}),
],
});
Workflow Structure
Every workflow file must:
- Use
export default defineWorkflow({ ... })
- Import only what it uses from
@wraps.dev/client
- Have exactly one
trigger
- Have a
steps array of step helper calls
import { defineWorkflow, sendEmail, delay } from '@wraps.dev/client';
export default defineWorkflow({
name: 'Workflow Name',
description: 'Optional description',
trigger: { type: 'contact_created' },
steps: [
sendEmail('welcome', { template: 'welcome' }),
delay('wait', { days: 1 }),
],
settings: {
allowReentry: false,
reentryDelaySeconds: 86400,
maxConcurrentExecutions: 1000,
contactCooldownSeconds: 3600,
},
defaults: {
from: 'hello@example.com',
fromName: 'My App',
replyTo: 'support@example.com',
},
});
Trigger Types
trigger: { type: 'contact_created' }
trigger: { type: 'contact_updated' }
trigger: { type: 'event', eventName: 'cart.abandoned' }
trigger: { type: 'segment_entry', segmentId: 'segment-id' }
trigger: { type: 'segment_exit', segmentId: 'segment-id' }
trigger: { type: 'topic_subscribed', topicId: 'topic-id' }
trigger: { type: 'topic_unsubscribed', topicId: 'topic-id' }
trigger: { type: 'schedule', schedule: '0 9 * * 1', timezone: 'America/New_York' }
trigger: { type: 'api' }
Step Helpers
sendEmail
Send an email using a template slug.
sendEmail('step-id', { template: 'welcome-email' })
sendEmail('promo', {
template: 'promo-email',
from: 'marketing@example.com',
fromName: 'Marketing Team',
replyTo: 'replies@example.com',
})
sendSms
Send an SMS using a template slug or inline message.
sendSms('reminder', { template: 'appointment-reminder' })
sendSms('otp', { message: 'Your code is {{otp}}' })
sendSms('alert', { template: 'alert-sms', senderId: '+15551234567' })
delay
Wait for a duration before continuing.
delay('wait-1-day', { days: 1 })
delay('wait-2-hours', { hours: 2 })
delay('wait-30-min', { minutes: 30 })
condition
Branch based on a contact field or event data.
condition('check-plan', {
field: 'contact.plan',
operator: 'equals',
value: 'premium',
branches: {
yes: [sendEmail('premium-offer', { template: 'premium-perks' })],
no: [sendEmail('upgrade-offer', { template: 'upgrade-cta' })],
},
})
Operators: equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than, greater_than_or_equals, less_than_or_equals, is_set, is_not_set, is_true, is_false
waitForEvent
Pause until a specific event occurs, with an optional timeout.
waitForEvent('wait-for-purchase', {
eventName: 'purchase_completed',
timeout: { days: 7 },
})
waitForEmailEngagement
Wait for engagement with a previous email step.
waitForEmailEngagement('wait-for-open', {
emailStepId: 'welcome',
engagementType: 'opened',
timeout: { days: 3 },
})
exit
End the workflow.
exit('completed')
exit('cancelled', { reason: 'User unsubscribed', markAs: 'cancelled' })
markAs options: completed (default), cancelled, failed
updateContact
Modify contact fields.
updateContact('mark-welcomed', {
updates: [
{ field: 'welcomeEmailSent', operation: 'set', value: true },
{ field: 'emailCount', operation: 'increment', value: 1 },
],
})
Operations: set, increment, decrement, append, remove
subscribeTopic / unsubscribeTopic
Manage topic subscriptions.
subscribeTopic('subscribe-newsletter', {
topicId: 'newsletter',
channel: 'email',
})
unsubscribeTopic('unsubscribe-promo', {
topicId: 'promotions',
channel: 'email',
})
webhook
Call an external URL.
webhook('notify-slack', {
url: 'https://hooks.slack.com/services/...',
method: 'POST',
headers: { 'X-Custom': 'value' },
body: { text: 'New user signed up!' },
})
cascade
Cross-channel cascade: try channels in order, stop when engagement is detected. Returns an array — use spread syntax.
...cascade('notify-user', {
channels: [
{
type: 'email',
template: 'welcome-email',
waitFor: { hours: 2 },
engagement: 'opened',
},
{
type: 'sms',
template: 'welcome-sms',
},
],
})
Expands to: sendEmail -> waitForEmailEngagement -> condition (engaged?) -> yes: exit -> no: sendSms
Common Patterns
Welcome Sequence
import { defineWorkflow, sendEmail, delay, condition, exit } from '@wraps.dev/client';
export default defineWorkflow({
name: 'Welcome Sequence',
trigger: { type: 'contact_created' },
steps: [
sendEmail('send-welcome', { template: 'welcome-email' }),
delay('wait-1-day', { days: 1 }),
condition('check-activated', {
field: 'contact.hasActivated',
operator: 'equals',
value: true,
branches: {
yes: [exit('already-active')],
no: [
sendEmail('send-tips', { template: 'getting-started-tips' }),
],
},
}),
],
});
Cart Recovery with Cascade
import { defineWorkflow, delay, cascade, exit } from '@wraps.dev/client';
export default defineWorkflow({
name: 'Cart Recovery',
trigger: { type: 'event', eventName: 'cart.abandoned' },
steps: [
delay('initial-wait', { minutes: 30 }),
...cascade('recover-cart', {
channels: [
{ type: 'email', template: 'cart-recovery', waitFor: { hours: 2 }, engagement: 'opened' },
{ type: 'sms', template: 'cart-sms-reminder' },
],
}),
exit('cascade-complete'),
],
});
Re-engagement with Event Wait
import {
defineWorkflow, sendEmail, waitForEvent, condition, delay, exit,
} from '@wraps.dev/client';
export default defineWorkflow({
name: 'Re-engagement',
trigger: { type: 'event', eventName: 'contact.inactive' },
steps: [
sendEmail('send-win-back', { template: 'we-miss-you' }),
waitForEvent('wait-for-activity', {
eventName: 'contact.active',
timeout: { days: 7 },
}),
condition('check-returned', {
field: 'contact.lastActiveAt',
operator: 'is_set',
value: true,
branches: {
yes: [exit('re-engaged')],
no: [
sendEmail('final-offer', { template: 'last-chance-offer' }),
delay('wait-before-cleanup', { days: 3 }),
exit('not-engaged'),
],
},
}),
],
});
Rules
- Always use
export default defineWorkflow({ ... })
- Import only what you use from
@wraps.dev/client
- Step IDs must be kebab-case and unique (e.g.,
'send-welcome', 'wait-1-day')
- Template slugs should be descriptive kebab-case (e.g.,
'welcome-email', 'cart-recovery')
- Use the
...cascade() spread pattern when using cascade
- Add a JSDoc comment at the top describing the workflow purpose
- Keep workflows focused — typically 3-10 steps, one clear goal per workflow
- Use meaningful, human-readable names for the workflow and steps
- Include appropriate delays between messages (don't spam — wait at least hours or days)
- Add conditions to personalize flow based on user behavior
- End branches with exit nodes when appropriate
Common Event Names
These are conventions — users can define any custom event name:
user.signup — New user registration
order.completed — Purchase completed
order.created — Order placed
cart.abandoned — Cart abandoned
subscription.started — Subscription began
subscription.cancelled — Subscription cancelled
trial.ending — Trial expiring soon
contact.inactive — Contact went inactive
form.submitted — Form submission
CLI Commands
wraps email workflows generate --template welcome
wraps email workflows validate --workflow welcome
wraps email workflows push
File Structure
Workflow files live in wraps/workflows/ at the project root:
my-project/
├── wraps/
│ └── workflows/
│ ├── welcome.ts
│ ├── cart-recovery.ts
│ └── trial-conversion.ts
├── src/
└── package.json