Send scheduled SMS with Cloudflare Workers

Use a Cloudflare Cron Trigger to call your TNZ SMS Worker. Configure a daily schedule, test without sending, and enable live SMS.

Updated

Use a Cloudflare Cron Trigger to submit an SMS through your existing TNZ SMS Worker on a daily schedule.

This demonstration sends a fixed message to one configured mobile number. It starts in test mode, which checks the request without sending an SMS.

Prerequisite: Complete Cloudflare Workers and Sending SMS, including its curl test.

Keep that SMS Worker deployed. Have its service name and matching WORKER_API_KEY ready, and use the same Cloudflare account. This guide adds a second Worker.

  1. Scheduled Worker Daily Cron Trigger
  2. SMS Worker Check and Submit
  3. TNZ REST API Send SMS
  4. Mobile Phone Receives the SMS

1. Create and configure the scheduled Worker

1.1 Create the project

Create a second project alongside tnz-worker:

Terminal
cd <your-project-folder>
npm create cloudflare@latest -- scheduled-sms

At the prompts, select:

  1. Hello World example
  2. Worker only
  3. JavaScript
  4. No when asked whether to deploy

1.2 Open the project folder

Run:

Terminal
cd scheduled-sms

1.3 Configure the service binding

Replace scheduled-sms/wrangler.jsonc with this configuration. If you chose another deployed SMS Worker name, replace tnz-worker in service.

wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "scheduled-sms",
  "main": "src/index.js",
  "compatibility_date": "2026-11-01",
  "workers_dev": false,
  "preview_urls": false,
  "vars": { "SMS_TEST_MODE": "true" },
  "services": [
    { "binding": "TNZ_SMS", "service": "tnz-worker", "remote": true }
  ],
  "triggers": { "crons": [] }
}

The empty crons array keeps the schedule disabled while you test. The service binding connects env.TNZ_SMS to your deployed SMS Worker, including during local development.

This Worker has no public send form or HTTP send handler. See Cloudflare's HTTP service binding guide for the Worker-to-Worker connection.

1.4 Configure local secrets

Create scheduled-sms/.dev.vars with these values:

.dev.vars
WORKER_API_KEY="YOUR_SMS_WORKER_KEY"
DEMO_MOBILE="YOUR_MOBILE_NUMBER"
  1. Replace YOUR_SMS_WORKER_KEY with the matching WORKER_API_KEY from your SMS Worker.
  2. Replace YOUR_MOBILE_NUMBER with your own NZ or AU mobile number.
  3. Check that .dev.vars* is in .gitignore.

Keep TNZ_AUTH_TOKEN in the SMS Worker. The scheduled Worker only needs the Worker key, recipient and service binding.

2. Add the scheduled handler

Replace scheduled-sms/src/index.js with this code:

src/index.js
View complete JavaScript (39 lines)
export default {
  async scheduled(controller, env) {
    const scheduledAt = new Date(controller.scheduledTime).toISOString();
    if (!env.TNZ_SMS || !env.WORKER_API_KEY || !env.DEMO_MOBILE ||
        !["true", "false"].includes(env.SMS_TEST_MODE)) {
      console.error(JSON.stringify({ scheduledAt, error: "Check binding, secrets and SMS_TEST_MODE" }));
      return;
    }
    const test = env.SMS_TEST_MODE === "true";
    try {
      const response = await env.TNZ_SMS.fetch("https://sms.internal/send-sms", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${env.WORKER_API_KEY}`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          to: env.DEMO_MOBILE,
          message: "Demo: your scheduled Cloudflare SMS.",
          test
        })
      });
      if (!response.ok) {
        console.error(JSON.stringify({ scheduledAt, status: response.status,
          error: "SMS not confirmed; check before retrying" }));
        return;
      }
      const result = await response.json();
      if (result.accepted !== true || result.test !== test ||
          typeof result.messageId !== "string" || !result.messageId) {
        throw new Error("Unexpected response");
      }
      console.log(JSON.stringify({ scheduledAt, accepted: true, test, messageId: result.messageId }));
    } catch {
      // Do not retry: TNZ may have accepted the first submission.
      console.error(JSON.stringify({ scheduledAt, error: "SMS outcome unknown; check before retrying" }));
    }
  }
};

Cloudflare invokes scheduled() when the Cron Trigger fires. The handler sends one request to the SMS Worker and records the submission result.

The URL https://sms.internal/send-sms supplies the path for the service binding. You do not need to register that domain.

The code records the scheduled time, test mode and returned message ID. It excludes keys, recipient numbers and message text from logs.

3. Test the handler locally

Local test

Keep SMS_TEST_MODE set to the string "true". Stop any other local Worker using port 8787 before you start.

  1. Run npx wrangler dev --ip 127.0.0.1 --port 8787 from scheduled-sms. Sign in to Cloudflare if prompted.
  2. Open http://127.0.0.1:8787/cdn-cgi/local/scheduled in your browser once to invoke the local scheduled handler.
  3. Check the Wrangler terminal for the SMS submission result. The browser's handler response alone does not confirm SMS acceptance.

This test runs immediately without sending an SMS. It does not wait for the daily schedule.

See Cloudflare's Cron Trigger testing instructions if your local invocation does not run.

4. Deploy and enable the schedule

Deployed test schedule

4.1 Deploy without a schedule

Stop local development with Ctrl+C. Keep the crons array empty and test mode enabled while you deploy:

Terminal
npx wrangler deploy
npx wrangler secret put WORKER_API_KEY
npx wrangler secret put DEMO_MOBILE
  1. Run npx wrangler deploy from scheduled-sms.
  2. Run the WORKER_API_KEY command and enter the matching value from .dev.vars.
  3. Run the DEMO_MOBILE command and enter the mobile number from .dev.vars.

Local secrets are not automatically copied to the deployed Worker.

4.2 Set the daily schedule

Then replace the empty triggers setting in wrangler.jsonc with:

wrangler.jsonc
"triggers": { "crons": ["0 9 * * *"] }

This example runs daily at 09:00 UTC. Cloudflare Cron Triggers use UTC. Convert your intended local time to UTC and account for daylight saving changes.

4.3 Deploy and check the scheduled test

Terminal
npx wrangler deploy
npx wrangler tail

Keep the log session open around the scheduled time. Trigger changes can take up to 15 minutes to propagate. Check the deployment's trigger configuration if no run appears.

The deployed schedule still uses test mode. Confirm its result in the logs before enabling live sends.

5. Enable live SMS and stop the demonstration

Live schedule
This schedule repeats every day. Live messages may incur charges. Use an agreed test recipient and check their opt-out status before enabling live sends.

5.1 Enable live sends

Change the configured variable to "SMS_TEST_MODE": "false" and run npx wrangler deploy again.

5.2 Check the live result

At the next scheduled run, check for accepted: true and test: false in the logs. Check your phone for Demo: your scheduled Cloudflare SMS.

Acceptance confirms submission, not handset delivery. Use the returned messageId to check delivery.

Troubleshooting

ProblemWhat to check
No scheduled runCheck the deployed cron expression, UTC time and propagation delay. Local development requires the manual test invocation.
Configuration errorCheck TNZ_SMS, both secrets and the exact string "true" or "false" for SMS_TEST_MODE.

The handler logs errors and returns without retrying. Inspect the log fields even when the Cron invocation completes. For SMS errors, use the shared troubleshooting guide. For an uncertain result, follow the retry checks before invoking the handler again.

Before using this in production

This example has no database, recipient list or duplicate protection. It demonstrates a recurring trigger, not a complete reminder system.

  • For reminders, read due records from your application and check their status and messaging permission before sending.
  • Store a unique event or schedule-run ID and processing state. Prevent concurrent or repeated invocations from sending the same intended message twice.
  • Store the returned message ID and resolve uncertain results before retrying. A later scheduled run is a new submission, not a retry of the previous one.
  • Monitor failures and message usage. Follow the shared SMS Worker production controls.

Topics:   Developers SMS

Start your free trial today!

Ready to revolutionise your messaging?
Connect with people anywhere - boost engagement, responses and conversations.

25 SMS Credits over 14-days. No obligation. No credit card required.