Portal Community

How SMS HIL Works

Since SMS has no buttons or payloads, the correlation approach is different:

  1. Send a numbered context message listing the fields, followed by keyword instructions (APPROVE / REJECT).
  2. Store the active HIL session in a table keyed by the recipient's phone number. One active session per number at a time.
  3. When an inbound SMS arrives from that number, look up the session, read the keyword, and call ContinueAsync.
One session per phone number This approach assumes a person only has one active HIL at a time. If your workflows can concurrently send multiple HIL messages to the same number, use a short numeric code (e.g., "Reply APPROVE 4821" where 4821 is a session code) to disambiguate.

Session Table Design

// SmsHilSession table
// PhoneNumber   varchar(20)         — recipient phone, E.164 format
// ExecutionResId uniqueidentifier
// AllowedKeywords varchar(200)      — e.g. "APPROVE,REJECT,YES,NO"
// ExpiresAt     datetime
// TenantId      uniqueidentifier
// CreatedAt     datetime

// Index on PhoneNumber + TenantId for fast inbound lookup

Building the SMS

private string BuildSmsText(IReadOnlyList<ResolvedHilField> fields)
{
    var lines = new List<string> { "BizFirst - Action Required:" };
    int i = 1;
    foreach (var field in fields)
    {
        if (field.DisplayMode == HilDisplayMode.Concealed) continue;
        string val = field.DisplayMode == HilDisplayMode.ReadableMasked
            ? "****"
            : field.CurrentValue?.ToString() ?? "—";
        lines.Add($"{i++}. {field.Label}: {val}");
    }
    lines.Add("");
    lines.Add("Reply APPROVE or REJECT.");
    lines.Add("This request expires in 24hrs.");
    return string.Join("\n", lines);
}

Sending the SMS and Creating the Session

// In OnProcess:
string text  = BuildSmsText(hilFields);
string phone = settings.RecipientPhone;

// Store session BEFORE sending — avoids a race where reply arrives before session stored
await _smsSessionStore.CreateAsync(
    phone:     phone,
    resId:     resId,
    keywords:  ["APPROVE", "REJECT", "YES", "NO"],
    expiresAt: DateTime.UtcNow.AddHours(24));

// Send via Twilio REST API
await _twilioClient.Messages.CreateAsync(
    to:   new PhoneNumber(phone),
    from: new PhoneNumber(settings.TwilioNumber),
    body: text);

return NodeExecutionResult.Suspend("waiting");

Webhook Handler

Twilio delivers inbound SMS as a form POST to your registered webhook URL. Always validate the Twilio request signature.

[HttpPost("webhooks/sms")]
public async Task<IActionResult> HandleSms([FromForm] TwilioSmsPayload payload)
{
    // 1. Validate Twilio signature
    var requestUrl  = $"{Request.Scheme}://{Request.Host}{Request.Path}";
    var authToken   = _settings.TwilioAuthToken;
    if (!_twilioValidator.Validate(requestUrl, Request.Form, Request.Headers["X-Twilio-Signature"]))
        return Forbid();

    string fromPhone = payload.From;        // E.164 phone number
    string body      = payload.Body?.Trim().ToUpperInvariant();

    // 2. Look up active HIL session for this phone number
    var session = await _smsSessionStore.FindActiveAsync(fromPhone);
    if (session == null)
    {
        // No active HIL for this number — ignore or send a help reply
        return TwimlResponse("No active approval request found for your number.");
    }

    // 3. Match keyword
    bool approved;
    if (body == "APPROVE" || body == "YES")       approved = true;
    else if (body == "REJECT" || body == "NO")     approved = false;
    else
        return TwimlResponse("Reply APPROVE or REJECT to proceed.");

    // 4. Consume the session (prevent double-processing)
    await _smsSessionStore.ConsumeAsync(fromPhone);

    // 5. Resume the workflow
    var responseData = new Dictionary<string, object>
    {
        ["approved"]    = approved,
        ["respondedBy"] = fromPhone,
        ["respondedAt"] = DateTime.UtcNow
    };
    _ = Task.Run(() => _continuationOrchestrator.ContinueAsync(session.ExecutionResId, responseData));

    // 6. TwiML response — sends a confirmation SMS back
    return TwimlResponse(approved ? "Approved ✓ — workflow will continue." : "Rejected — workflow notified.");
}

private IActionResult TwimlResponse(string message)
{
    var response = new MessagingResponse();
    response.Message(message);
    return Content(response.ToString(), "text/xml");
}

Numeric Code Disambiguation

For scenarios where a person may have multiple concurrent HIL requests, use a 4-digit numeric code included in the SMS text:

// Session text includes the code
"Reply 'APPROVE 4821' or 'REJECT 4821' to act on this request."

// Session table keyed by (PhoneNumber, Code) instead of just PhoneNumber

// Inbound parsing
var parts = body.Split(' ');
var keyword = parts[0];   // "APPROVE" or "REJECT"
var code    = parts.ElementAtOrDefault(1) ?? "";

Required Configuration

Config FieldValue
AccountSid@{secret:TwilioAccountSid}
AuthToken@{secret:TwilioAuthToken}
TwilioNumber@{env:TWILIO_FROM_NUMBER}
RecipientPhone@{input:phoneNumber}
SessionExpiryHours24 (default)
Short URL as an alternative You can combine SMS with the email approach: include a short URL in the SMS that links to a web confirmation page (like the email channel). This avoids keyword parsing entirely and supports editable fields. Use a URL shortener that maps to your signed-token endpoint. This is the best approach when the human needs to fill values.