Skip to main content

Basic Usage

operations:
  - name: send-otp
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      body: Your verification code is: ${input.code}

Configuration

config:
  provider: string           # twilio, vonage, aws-sns
  from: string              # Sender phone number (E.164 format)
  to: string | string[]     # Recipient(s) (E.164 format)
  body: string              # Message text (max 160 chars for 1 segment)
  mediaUrl: string[]        # MMS media URLs (Twilio only, optional)
  accountSid: string        # Twilio Account SID
  authToken: string         # Twilio Auth Token
  apiKey: string            # Vonage API Key
  apiSecret: string         # Vonage API Secret
  accessKeyId: string       # AWS Access Key ID
  secretAccessKey: string   # AWS Secret Access Key
  region: string            # AWS Region

SMS Providers

Industry-leading SMS provider with 99.95% uptime and global coverage:
operations:
  - name: send
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: +1234567890
      body: Your verification code is 123456
Setup:
  1. Sign up at twilio.com
  2. Get Account SID and Auth Token from console
  3. Purchase a phone number or create Messaging Service
  4. Set secrets:
wrangler secret put TWILIO_ACCOUNT_SID
wrangler secret put TWILIO_AUTH_TOKEN
wrangler secret put TWILIO_PHONE_NUMBER
Pricing:
  • US/Canada: $0.0079/SMS
  • UK: $0.04/SMS
  • India: $0.0057/SMS
  • Full pricing
Rate Limits:
  • Standard: 100 SMS/second
  • Messaging Service: 1,000 SMS/second
  • Free trial: 1 SMS/second

Vonage (formerly Nexmo)

Cost-effective SMS with strong international coverage:
operations:
  - name: send
    operation: sms
    config:
      provider: vonage
      apiKey: ${env.VONAGE_API_KEY}
      apiSecret: ${env.VONAGE_API_SECRET}
      from: "Acme"                     # Alphanumeric sender ID
      to: +442071234567
      body: Your code is 123456
Setup:
  1. Sign up at vonage.com
  2. Get API Key and Secret
  3. Set secrets:
wrangler secret put VONAGE_API_KEY
wrangler secret put VONAGE_API_SECRET
Pricing:
  • US: $0.0057/SMS
  • UK: $0.0331/SMS
  • Lower than Twilio in many regions

AWS SNS

Integrate with AWS infrastructure:
operations:
  - name: send
    operation: sms
    config:
      provider: aws-sns
      accessKeyId: ${env.AWS_ACCESS_KEY_ID}
      secretAccessKey: ${env.AWS_SECRET_ACCESS_KEY}
      region: us-east-1
      from: +1234567890
      to: +1234567891
      body: Alert: Server CPU at 95%
Setup:
  1. Enable SNS in AWS Console
  2. Create IAM user with SNS permissions
  3. Set secrets:
wrangler secret put AWS_ACCESS_KEY_ID
wrangler secret put AWS_SECRET_ACCESS_KEY

Phone Number Format

All phone numbers MUST use E.164 format:
+[country code][number]
Valid Examples:
  • US: +1234567890
  • UK: +442071234567
  • France: +33123456789
  • Japan: +81312345678
  • India: +919876543210
Invalid Examples (will be rejected):
  • 1234567890 - Missing + prefix
  • +1 (234) 567-8900 - Contains formatting
  • +1-234-567-8900 - Contains dashes
  • 001-234-567-8900 - Wrong prefix

Common Use Cases

OTP Verification

operations:
  # Generate OTP
  - name: generate-otp
    operation: code
    config:
      script: scripts/generate-otp-code

  # Send OTP
  - name: send-otp
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      body: Your verification code is ${generate-otp.output.code}. Valid for 5 minutes.

  # Store for verification
  - name: store-otp
    operation: storage
    config:
      type: kv
      action: put
      key: otp:${input.phone}
      value: ${generate-otp.output.code}
      ttl: 300                         # 5 minutes
// scripts/generate-otp-code.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function generateOtpCode(context: AgentExecutionContext) {
  const code = Math.floor(100000 + Math.random() * 900000).toString()
  return {
    code,
    expiresAt: Date.now() + 300000  // 5 minutes
  }
}
### Two-Factor Authentication

```yaml
ensemble: send-2fa-code

inputs:
  userId: string

operations:
  # Get user phone
  - name: get-user
    operation: storage
    config:
      type: d1
      query: SELECT phone FROM users WHERE id = ?
      params: [${input.userId}]

  # Generate 2FA code
  - name: generate-code
    operation: code
    config:
      script: scripts/generate-2fa-code

  # Send 2FA SMS
  - name: send-2fa
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${get-user.output.results[0].phone}
      body: |
        Your MyApp verification code is: ${generate-code.output.code}

        This code expires in 10 minutes.
        Never share this code with anyone.

  # Log attempt
  - name: log-attempt
    operation: storage
    config:
      type: d1
      query: |
        INSERT INTO auth_attempts (user_id, code, created_at)
        VALUES (?, ?, datetime('now'))
      params:
        - ${input.userId}
        - ${generate-code.output.code}
// scripts/generate-2fa-code.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function generate2faCode(context: AgentExecutionContext) {
  return {
    code: Math.floor(100000 + Math.random() * 900000).toString()
  }
}
outputs:
  success: true
  code: ${generate-code.output.code}

System Alerts

operations:
  - name: alert-admin
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${env.ADMIN_PHONE}
      body: |
        [ALERT] ${input.alert_type}
        ${input.message}
        Time: ${new Date().toISOString()}

Order Notifications

operations:
  - name: notify-customer
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.customer_phone}
      body: |
        Hi ${input.customer_name}! Your order #${input.order_id} is ready for pickup.
        Total: $${input.total}

        Questions? Reply to this message.

Appointment Reminders

operations:
  - name: send-reminder
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.patient_phone}
      body: |
        Reminder: You have an appointment tomorrow at ${input.appointment_time}.

        Reply C to confirm or R to reschedule.

Batch Sending

Send personalized SMS to multiple recipients:
operations:
  # Fetch customers
  - name: get-customers
    operation: storage
    config:
      type: d1
      query: |
        SELECT phone, name, balance
        FROM customers
        WHERE opt_in_sms = true
        LIMIT 100

  # Send batch SMS
  - name: send-batch
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      batch:
        recipients: ${get-customers.output.results}
        phoneField: phone              # Field containing phone number
        template: |
          Hi {{name}}, your account balance is ${{balance}}.
          Reply STOP to unsubscribe.
      rateLimit: 5                     # 5 SMS per second
Output:
{
  sent: 98,
  failed: 2,
  messageIds: ["SM123...", "SM124...", ...],
  errors: [
    { phone: "+1234567890", error: "Invalid phone number" },
    { phone: "+1234567891", error: "Blocked number" }
  ]
}

MMS (Multimedia Messages)

Send images and media with Twilio:
operations:
  - name: send-mms
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      body: Check out this product!
      mediaUrl:
        - https://example.com/product-image.jpg
        - https://example.com/product-specs.pdf
Supported Media:
  • Images: JPG, PNG, GIF
  • Video: MP4, 3GP
  • Audio: MP3, WAV
  • Documents: PDF
  • Max size: 5 MB per file
  • Max files: 10 per message
MMS Pricing:
  • US/Canada: 0.02/MMS(vs0.02/MMS (vs 0.0079/SMS)
  • International: Varies by country

Template Rendering

Use Liquid templates for dynamic content:
operations:
  - name: send-welcome
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      template: liquid
      body: |
        Welcome {{ name | capitalize }}!
        Account balance: {{ balance | money: "USD" }}
        {% if premium %}You have premium access.{% endif %}

Rate Limiting

Control sending rate to respect provider limits:
operations:
  - name: send-batch
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      batch:
        recipients: ${input.recipients}
      rateLimit: 3                     # 3 SMS per second
Recommended Rates:
  • Twilio Free Trial: 1 SMS/sec
  • Twilio Standard: 10 SMS/sec
  • Twilio Messaging Service: 100 SMS/sec
  • Vonage: 10 SMS/sec
  • AWS SNS: 20 SMS/sec

Error Handling

Retry on Failure

operations:
  - name: send
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      body: ${input.message}
    retry:
      maxAttempts: 3
      backoff: exponential           # 1s, 2s, 4s delays

Fallback Provider

operations:
  # Try Twilio first
  - name: send-twilio
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      body: ${input.message}

  # Fallback to Vonage
  - name: send-vonage
    condition: ${send-twilio.failed}
    operation: sms
    config:
      provider: vonage
      apiKey: ${env.VONAGE_API_KEY}
      apiSecret: ${env.VONAGE_API_SECRET}
      from: "Acme"
      to: ${input.phone}
      body: ${input.message}

outputs:
  sent: ${send-twilio.success || send-vonage.success}
  provider: ${send-twilio.success ? 'twilio' : 'vonage'}

Validate Phone Numbers

operations:
  # Validate E.164 format
  - name: validate
    operation: code
    config:
      script: scripts/validate-phone-e164-sms
    input:
      phone: ${input.phone}

  # Send if valid
  - name: send
    condition: ${validate.output.valid}
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      to: ${input.phone}
      body: ${input.message}
// scripts/validate-phone-e164-sms.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function validatePhoneE164Sms(context: AgentExecutionContext) {
  const { phone } = context.input
  const e164Regex = /^\+[1-9]\d{1,14}$/

  if (!e164Regex.test(phone)) {
    throw new Error('Invalid phone number format. Use E.164: +1234567890')
  }

  return { valid: true }
}
### Handle Batch Errors

```yaml
operations:
  - name: send-batch
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      from: ${env.TWILIO_PHONE_NUMBER}
      batch:
        recipients: ${input.recipients}

  # Log failed sends
  - name: log-errors
    condition: ${send-batch.output.failed > 0}
    operation: storage
    config:
      type: kv
      action: put
      key: sms-errors:${Date.now()}
      value: ${send-batch.output.errors}

outputs:
  sent: ${send-batch.output.sent}
  failed: ${send-batch.output.failed}
  successRate: ${(send-batch.output.sent / (send-batch.output.sent + send-batch.output.failed)) * 100}

Testing

Test with TestConductor

import { describe, it, expect } from 'vitest';
import { TestConductor } from '@ensemble/conductor/testing';

describe('send-otp', () => {
  it('should send OTP via SMS', async () => {
    const conductor = await TestConductor.create({
      projectPath: './conductor',
      mocks: {
        sms: {
          'send-otp': {
            success: true,
            messageId: 'SM123456',
            status: 'sent',
            provider: 'twilio'
          }
        }
      }
    });

    const result = await conductor.executeAgent('send-otp', {
      phone: '+1234567890',
      code: '123456'
    });

    expect(result).toBeSuccessful();
    expect(result.output.messageId).toBe('SM123456');
  });

  it('should handle invalid phone number', async () => {
    const conductor = await TestConductor.create({
      projectPath: './conductor'
    });

    const result = await conductor.executeAgent('send-otp', {
      phone: 'invalid',
      code: '123456'
    });

    expect(result).toBeFailed();
    expect(result.error).toContain('Invalid phone number');
  });
});

Mock SMS Provider

const conductor = await TestConductor.create({
  mocks: {
    sms: {
      '*': {                           // Mock all SMS operations
        success: true,
        messageId: 'mock-sms-id',
        status: 'sent',
        timestamp: new Date().toISOString()
      }
    }
  }
});

Best Practices

1. Keep Messages Short
# Good: 160 characters or less
body: Your code is 123456. Valid for 5 minutes.

# Bad: Too long (3 segments = 3x cost)
body: |
  Hello! Your verification code for accessing your account on
  our platform is 123456. This code will expire in 5 minutes.
  If you did not request this code, please ignore this message.
2. Include Opt-Out Instructions
# Good: Include unsubscribe
body: |
  Hi ${name}! Order #${orderId} is ready.
  Reply STOP to unsubscribe.

# Bad: No opt-out
body: Hi ${name}! Order #${orderId} is ready.
3. Validate Before Sending
# Good: Validate E.164 format
operations:
  - name: validate
    operation: code
    config:
      script: scripts/validate-phone-simple
    input:
      phone: ${input.phone}

  - name: send
    operation: sms

# Bad: No validation
operations:
  - name: send
    operation: sms
    config:
      to: ${input.phone}  # Could be invalid
// scripts/validate-phone-simple.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function validatePhoneSimple(context: AgentExecutionContext) {
  const { phone } = context.input

  if (!/^\+[1-9]\d{1,14}$/.test(phone)) {
    throw new Error('Invalid phone')
  }

  return { valid: true }
}
**4. Rate Limit Batch Sends**

```yaml
# Good: Set rate limit
operations:
  - name: send-batch
    operation: sms
    config:
      batch:
        recipients: ${input.recipients}
      rateLimit: 5

# Bad: No rate limiting
operations:
  - name: send-batch
    operation: sms
    config:
      batch:
        recipients: ${input.recipients}
5. Store Credentials Securely
# Good: Use secrets
operations:
  - name: send
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}

# Bad: Hardcoded credentials
operations:
  - name: send
    operation: sms
    config:
      provider: twilio
      accountSid: AC1234567890  # Never do this!
      authToken: secret123
6. Use Messaging Services for Scale
# Good: Use Messaging Service for high volume
operations:
  - name: send-campaign
    operation: sms
    config:
      provider: twilio
      accountSid: ${env.TWILIO_ACCOUNT_SID}
      authToken: ${env.TWILIO_AUTH_TOKEN}
      messagingServiceSid: ${env.TWILIO_MESSAGING_SERVICE_SID}

# Bad: Single phone number (limited throughput)
operations:
  - name: send-campaign
    operation: sms
    config:
      provider: twilio
      from: ${env.TWILIO_PHONE_NUMBER}
7. Monitor Delivery Status
# Good: Log and monitor
operations:
  - name: send
    operation: sms

  - name: log-status
    operation: storage
    config:
      type: d1
      query: |
        INSERT INTO sms_log (phone, status, message_id, timestamp)
        VALUES (?, ?, ?, datetime('now'))
      params:
        - ${input.phone}
        - ${send.output.status}
        - ${send.output.messageId}

# Bad: No logging or monitoring
operations:
  - name: send
    operation: sms
8. Respect Time Zones
# Good: Check time before sending
operations:
  - name: check-time
    operation: code
    config:
      script: scripts/check-business-hours

  - name: send
    condition: ${check-time.output.canSend}
    operation: sms

# Bad: Send anytime
operations:
  - name: send
    operation: sms
// scripts/check-business-hours.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function checkBusinessHours(context: AgentExecutionContext) {
  const hour = new Date().getHours()

  // Don't send between 9 PM and 8 AM
  if (hour >= 21 || hour < 8) {
    throw new Error('Outside business hours')
  }

  return { canSend: true }
}
## Common Pitfalls

### Pitfall: Wrong Phone Format

```yaml
# Bad: Missing + or wrong format
- name: send
  operation: sms
  config:
    to: 1234567890

# Good: E.164 format
- name: send
  operation: sms
  config:
    to: +1234567890

Pitfall: Too Long Messages

# Bad: 300 characters (2 segments = 2x cost)
body: |
  Your verification code is 123456. This code will expire
  in 5 minutes. If you did not request this code, please
  ignore this message. For support, contact us at...

# Good: 60 characters (1 segment)
body: Your code is 123456. Valid for 5 minutes.

Pitfall: No Error Handling

# Bad: No retry or fallback
- name: send
  operation: sms
  config:
    provider: twilio

# Good: Retry and fallback
- name: send-twilio
  operation: sms
  config:
    provider: twilio
  retry:
    maxAttempts: 3

- name: send-vonage
  condition: ${send-twilio.failed}
  operation: sms
  config:
    provider: vonage

Pitfall: Missing Opt-Out

# Bad: No unsubscribe info (violates regulations)
body: Special offer! Buy now!

# Good: Include opt-out
body: Special offer! Buy now! Reply STOP to unsubscribe.

SMS vs Email Comparison

FeatureSMSEmail
Open Rate98%20%
Delivery SpeedInstantMinutes to hours
Cost0.00750.0075-0.04/msg0.00010.0001-0.001/msg
Length160 chars (1 segment)Unlimited
MediaMMS only (5 MB)Full HTML, attachments
Best ForAlerts, OTP, urgentNewsletters, receipts, long-form
ComplianceTCPA, GDPRCAN-SPAM, GDPR

Troubleshooting

SMS Not Delivering

  1. Verify phone format - Must be E.164 (+1234567890)
  2. Check provider credentials - Ensure Account SID/Auth Token correct
  3. Review rate limits - You may be hitting throttles
  4. Check provider console - View delivery reports
  5. Test with own number - Confirm basic functionality

Rate Limit Errors

# Reduce sending rate
config:
  rateLimit: 1                        # 1 SMS per second

Invalid Phone Number Errors

// Validate E.164 format
function validateE164(phone: string): boolean {
  return /^\+[1-9]\d{1,14}$/.test(phone);
}

// Remove formatting
function formatToE164(phone: string, countryCode: string = '+1'): string {
  const digits = phone.replace(/\D/g, '');
  return `${countryCode}${digits}`;
}

Message Too Long

  • Single SMS: 160 chars (GSM-7) or 70 chars (Unicode)
  • Concatenated SMS: Up to 1600 chars (10 segments)
  • Cost: Each segment charged separately
  • Solution: Shorten message or use link shorteners

Next Steps