Flow Studio
SMS
Configuring the SendSMS node — phone number, message body, Twilio/Vonage credential resolution, and delivery status tracking.
Node Configuration
{
"nodeType": "SendSMS",
"name": "sendApprovalSms",
"config": {
"credentialId": 15,
"provider": "twilio",
"from": "+15551234567",
"to": "$output.fetchEmployee.mobilePhone",
"body": "Action required: Invoice $output.createInvoice.invoiceNumber needs your approval. Log in at $context.appUrl",
"statusCallbackUrl": null,
"retryCount": 2,
"retryDelayMs": 3000
}
}
Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
credentialId | int | Yes | Twilio Account SID + Auth Token pair, stored via ICredentialResolver |
provider | twilio | vonage | Yes | SMS provider. Determines which adapter is resolved from DI. |
from | string | Yes | Sender phone number in E.164 format (+15551234567). Must be a purchased Twilio number. |
to | string / expr | Yes | Recipient phone number in E.164 format. Expressions allowed. |
body | string / expr | Yes | SMS body text. Max 1600 characters (multi-segment). Expression syntax supported. |
statusCallbackUrl | string | No | Twilio delivery status webhook URL for delivery receipts. |
retryCount | int | No | Retry attempts on transient error. Default: 2. |
Twilio Credential Storage
Twilio requires two values: Account SID and Auth Token. These are stored as a JSON object in the credential store and retrieved atomically:
// Stored credential value (JSON string)
// { "accountSid": "ACxxxxx", "authToken": "token-value" }
var rawCredential = await _credentials.GetPasswordAsync(config.CredentialId, ct);
var twilioAuth = JsonSerializer.Deserialize<TwilioCredential>(rawCredential);
TwilioClient.Init(twilioAuth.AccountSid, twilioAuth.AuthToken);
var message = await MessageResource.CreateAsync(
to: new PhoneNumber(config.To),
from: new PhoneNumber(config.From),
body: evaluatedBody
);
SMSChannelAdapter
public class SMSChannelAdapter : IMessagingChannelAdapter
{
public string ChannelType => "SMS";
public async Task<MessagingResult> SendAsync(
MessagingMessage message,
MessagingConfig config,
CancellationToken ct)
{
var rawCredential = await _credentials.GetPasswordAsync(config.CredentialId, ct);
var auth = JsonSerializer.Deserialize<TwilioCredential>(rawCredential)!;
TwilioClient.Init(auth.AccountSid, auth.AuthToken);
var msg = await MessageResource.CreateAsync(
to: new PhoneNumber(message.To),
from: new PhoneNumber(config.From),
body: message.Body,
statusCallback: config.StatusCallbackUrl != null
? new Uri(config.StatusCallbackUrl)
: null
);
return new MessagingResult
{
MessageId = msg.Sid,
Channel = "sms",
Recipient = message.To,
SentAt = DateTimeOffset.UtcNow,
Status = msg.Status.ToString().ToLower()
};
}
}
Node Output
{
"messageId": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"channel": "sms",
"sentAt": "2026-05-25T10:00:00Z",
"status": "queued",
"recipient": "+447700900123"
}
Character limits: Standard GSM SMS messages are 160 characters. Unicode messages (emoji, non-Latin characters) are 70 characters per segment. Twilio automatically splits long messages into multiple segments, each billed separately. Keep body expressions concise or check length before sending.
E.164 format: Always store and pass phone numbers in E.164 format (
+{country code}{number}). Use an expression to normalise if numbers from your data source may vary: "$output.fetchEmployee.mobilePhone.replace(/[^+\d]/g, '')"