# How to Send a Cloudflare Workers Build Success to Your Team's Phones

> Use Workers Builds Event Subscriptions, one Queue consumer, and a privacy-minimal PingRoom webhook call for one selected successful build.

- Published: 2026-10-30
- Author: PingRoom
- Category: Developers
- Topics: Cloudflare Workers build notification, Cloudflare Workers Event Subscriptions, Cloudflare Queues webhook, build success push notification, PingRoom webhook
- Canonical: https://pingroom.io/blog/cloudflare-workers-build-success-push-notifications
- Cover image: https://pingroom.io/blog/cloudflare-workers-build-success-push-notifications-cover.webp
- Cover description: Two PingRoom avatars move one finished clay component into an outbound tray while Pingo watches beside a miniature build bench.

**To send one selected Cloudflare Workers build success to a small shared group, subscribe a Cloudflare Queue to `build.succeeded`, filter the event in a Worker, and send fixed copy to a PingRoom Room. Keep Cloudflare build history as the detailed record.**

The important word is **build**. A successful Workers Build means Cloudflare completed the build. It does not, by itself, prove that the version is the active production deployment, that the service is healthy, or that someone acted on the alert.

This is a webhook recipe, not a native Cloudflare integration. It forwards no repository, commit, author, branch, URL, or log data to PingRoom.

## Decide whether build success is the right moment

Cloudflare's [Workers Builds overview](https://developers.cloudflare.com/workers/ci-cd/builds/) separates building a version from deploying it. A successful production-branch build creates a version; that version becomes active only when the project is configured to deploy it.

Use the source that owns the state you actually care about:

| Question                                                      | Better source                                  |
| ------------------------------------------------------------- | ---------------------------------------------- |
| Did Cloudflare finish this build?                             | Workers Builds history and `build.succeeded`   |
| Is this version the active deployment?                        | Cloudflare deployment state                    |
| Is the service healthy after release?                         | Your health checks and incident tooling        |
| Should a small shared group notice one selected build finish? | Event Subscription → Queue consumer → PingRoom |

The last row is this recipe. It is a low-volume attention path, not a deployment audit or pager replacement.

## Create the Room and keep its URL secret

Create or choose a private Room for the people who should receive the signal. The Room owner needs **PingRoom Pro** to add an incoming webhook.

Open **Settings → Connections → From another app**, add an incoming webhook, and copy the full URL. The [incoming-webhook walkthrough](/blog/webhook-to-push-notification) explains the Room setup; the [webhook reference](/documents/webhooks) carries the current request limits, cooldown, idempotency behavior, and response contract.

The URL is a credential. Store it as a Worker secret, never in `wrangler.toml`, source control, logs, or client code:

```bash
npx wrangler secret put PINGROOM_WEBHOOK_URL
```

Add only the non-secret selectors as Worker variables:

```toml
# wrangler.toml
name = "workers-build-to-pingroom"
main = "src/index.js"
compatibility_date = "2026-10-30"

[vars]
CLOUDFLARE_WORKER_NAME = "selected-worker"
CLOUDFLARE_BRANCH = "main"

[[queues.consumers]]
queue = "workers-build-events"
max_batch_size = 1
max_batch_timeout = 5
max_retries = 5
```

Use your real Worker name and exact branch. The one-message batch keeps the consumer easy to reason about; it does not override PingRoom's default five-second Room cooldown. If successful builds can arrive in bursts, aggregate them or choose a quieter boundary instead of multiplying webhook URLs.

## Filter the event before calling PingRoom

Put this in `src/index.js`:

```js
// cloudflare-build-handler-start
const BUILD_SUCCEEDED = "cf.workersBuilds.worker.build.succeeded";
const UUID =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

function destinationFrom(value) {
  try {
    const url = new URL(value);
    if (
      url.protocol !== "https:" ||
      url.hostname !== "api.pingroom.io" ||
      !url.pathname.startsWith("/api/webhooks/")
    ) {
      return null;
    }
    return url;
  } catch {
    return null;
  }
}

function selectedBuild(event, env) {
  return (
    event?.type === BUILD_SUCCEEDED &&
    event?.source?.type === "workersBuilds.worker" &&
    event?.source?.workerName === env.CLOUDFLARE_WORKER_NAME &&
    event?.payload?.status === "success" &&
    event?.payload?.buildOutcome === "success" &&
    event?.payload?.buildTriggerMetadata?.branch === env.CLOUDFLARE_BRANCH &&
    UUID.test(event?.payload?.buildUuid ?? "")
  );
}

export async function handleBuildBatch(batch, env, fetcher = fetch) {
  const destination = destinationFrom(env.PINGROOM_WEBHOOK_URL);

  if (!destination) {
    throw new Error("PINGROOM_WEBHOOK_URL is missing or invalid");
  }

  for (const message of batch.messages) {
    const event = message.body;

    if (!selectedBuild(event, env)) {
      message.ack();
      continue;
    }

    const replayKey = `cf-workers-build:${event.payload.buildUuid}`;
    const body = JSON.stringify({
      action: null,
      title: "Worker build succeeded",
      message: "The selected Cloudflare Workers build completed successfully.",
      correlation_id: replayKey,
    });

    let response;
    try {
      response = await fetcher(destination, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          "Idempotency-Key": replayKey,
        },
        body,
        redirect: "error",
        signal: AbortSignal.timeout(8000),
      });
    } catch {
      message.retry({ delaySeconds: 60 });
      continue;
    }

    if (response.ok) {
      try {
        const accepted = await response.json();
        if (accepted.success === true) {
          message.ack();
          continue;
        }
      } catch {
        // A malformed success response is safe to retry with the same key and body.
      }
      message.retry({ delaySeconds: 60 });
      continue;
    }

    if (response.status === 429 || response.status >= 500) {
      message.retry({ delaySeconds: 60 });
      continue;
    }

    console.error("PingRoom rejected build alert", {
      status: response.status,
      buildUuid: event.payload.buildUuid,
    });
    message.ack();
  }
}

export default {
  async queue(batch, env) {
    await handleBuildBatch(batch, env);
  },
};
// cloudflare-build-handler-end
```

Cloudflare's [event schema](https://developers.cloudflare.com/workers/ci-cd/builds/event-subscriptions/) provides the event type, source, Worker name, build UUID, status, outcome, and branch used by the gates above. Everything else stays in Cloudflare.

The consumer validates the secret destination before processing. It acknowledges irrelevant events, retries network errors, rate limits, malformed success responses, and server errors, and stops retrying permanent `4xx` responses. Its log contains only the response status and build UUID—not the secret URL or build payload.

## Subscribe the Queue to successful builds

Deploy the consumer, then open **Cloudflare Dashboard → Queues → workers-build-events → Subscriptions → Subscribe to events**. Choose Workers Builds as the source and subscribe only to **Worker build succeeded**. Cloudflare's [subscription-management guide](https://developers.cloudflare.com/queues/event-subscriptions/manage-event-subscriptions/) documents the current dashboard flow.

The Event Subscription sends matching account events to the Queue. The Worker still checks the event type, Worker name, branch, status, and outcome because routing narrowly at both boundaries makes the behavior reviewable.

![Two PingRoom avatars inspect one green clay status token beside a small queue chute while Pingo closes a wooden parts drawer.](/blog/cloudflare-workers-build-success-push-notifications-1.webp)

[Pingo the Pigeon](/Pingo), PingRoom's fictional Chief Marketing Officer, closes the parts drawer after the one finished component reaches the outbound tray. He is not declaring production live. Pingo cannot access Cloudflare, enter a private Room, hold the webhook credential, or send the Ping; the human team owns the configuration and response.

_These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo. They are not Cloudflare or PingRoom screenshots and do not depict customers._

## Reuse the build UUID when Cloudflare retries

Cloudflare Queues use [at-least-once delivery](https://developers.cloudflare.com/queues/reference/delivery-guarantees/), so a message can arrive more than once. The consumer maps Cloudflare's `buildUuid` to both `Idempotency-Key` and `correlation_id`, then repeats the exact same request body.

That gives PingRoom a stable identity for the same build. Do not change the fixed copy while a build is still retrying; the same key with a different body can conflict. A new build UUID gets a new identity.

This handles duplicate delivery to PingRoom. It does not make the entire Cloudflare-to-device path exactly once, and it does not prove that a phone displayed the notification.

## Test the boundaries before connecting production

Use a test Queue, test Worker, and small test Room with non-sensitive copy.

1. Send the documented `build.succeeded` shape for the selected Worker and branch. One fixed Ping should enter the Room.
2. Repeat the same event with the same build UUID. The request key and body should be identical, so no second Ping should be created.
3. Change the event type, Worker name, branch, status, or outcome. The message should be acknowledged without a PingRoom call.
4. Use a malformed build UUID. The consumer should acknowledge it without a call.
5. Make the test endpoint return `429` or `500`. The message should be marked for retry with the same identity.
6. Make it return a permanent `4xx`. The consumer should log the status and build UUID, then stop retrying that message.
7. Run a real test build. Confirm build success in Cloudflare first, then check the Room. Check deployment state separately if production activation is what matters.

For this article, we extracted the exact handler above and ran it against Cloudflare-shaped fixtures plus a stateful local PingRoom mock. The checks covered acceptance, exact replay, event/source/Worker/branch/status/outcome filters, malformed identities, invalid destination, transient and permanent responses, malformed success data, network failure, required headers, fixed copy, and privacy exclusions. They did not use a live Cloudflare account, Queue, Worker, PingRoom Room, phone, or notification receipt.

An accepted PingRoom response means the webhook request succeeded. Phone connectivity, notification permissions, Focus settings, Room mutes, and operating-system policy still affect presentation.

If one Workers Build genuinely deserves shared attention, [create the destination Room and incoming webhook](/app/rooms), deploy the narrow consumer, and test one non-sensitive build end to end. For a broader way to decide what belongs in a shared alert channel, read [Monitoring Alerts Should Feel Like a Team, Not a Siren](/blog/monitoring-alerts-for-teams).

_Cloudflare is a trademark of Cloudflare, Inc. PingRoom is not affiliated with or endorsed by Cloudflare._
