How two engineers ran Clipboard's notification migration from Braze to Knock

Stefan Chrobot
Stefan Chrobot
September 15, 202614 min read

An urgent shift opens four hours from now, and it pays well. A worker who would have taken it never sees the text message - they miss out on the extra money and the shift goes unfilled. Maybe they replied STOP to something months ago. Maybe the carrier filtered it, or they turned that notification off themselves and forgot. We would have a hard time figuring out which. At 46 million messages a month, that's too much gray area.

Two engineers led the migration off Braze that followed. While making the code changes easy for the product teams helped, scoping the work, finding owners, and finishing the cleanup took most of the time.

Why we left Braze

The Platform Team at Clipboard builds the tools and foundations the rest of engineering ships on, so teams can move fast and with confidence. Notifications are one of those foundations, and one of the largest: mostly push notifications, but also millions of emails and text messages. Most of it is transactional and time-sensitive: a shift got canceled; a credential is about to expire; an urgent shift just opened at a good rate. When one of those messages doesn't arrive, a worker misses a shift or a workplace goes unstaffed. So our engineers and support agents need to answer a narrow question: why didn't this message reach this user? Answering it is what pushed us to look beyond Braze, our notification provider at the time. Braze helped us grow engagement, but tracing a single failed delivery took far longer than it should have. We needed application logs and exported data to investigate, and provider errors were hard to see in Braze's dashboard. The pieces we wanted around transactional sending - preference management, idempotency, batching and digests - were things we ended up building and maintaining ourselves.

After evaluating a few platforms for the engineering team, we moved to Knock. It provided detailed logs with downstream provider responses, notification preferences across channels (email, SMS, push), idempotency, and batching. It also had concepts we were already familiar with, like git-style versioning of changes promoted across environments. Its features for marketing and operations teams raised the possibility of landing on one tool for the whole company.

Inventory

Our Braze account had been filling up for years: thousands of campaigns and canvases, built by engineers, marketers, and the operations team, and triggered in every way Braze allows - manually, via the API, by events, and on a schedule. So the first task was working out whether a complete migration was feasible at all, which meant building our own tooling to separate what genuinely had to move from what could simply be deleted. The good news was that only a few hundred campaigns and canvases had sent a single message in the previous two weeks. The bad news was that Braze wasn't the only place we were sending notifications from: older code in a handful of places called Amazon SES and Twilio directly - the same providers Knock sends through today, but without the retries, idempotency, and delivery tracking we wanted around them.

How we run things

The product teams at Clipboard autonomously ship features in their respective areas. Platform takes ownership of shared technical debt, but that does not mean we fix every line of code ourselves. For that we have engineering-wide initiatives: department-wide efforts that require participation from all teams. Each team's part is usually small, and we cap these initiatives at three at a time. Having seen unfinished migrations elsewhere, we take pride in having finished some major initiatives already.

The foundations

We wanted to make sure we didn't end up where we'd ended up with Braze. The first thing we built was an internal audit tool we could run repeatedly: a CLI command that fetches all of our content from Knock and checks it against our guidance and best practices - naming, template variables, ownership tracking, and more. Any issues found are grouped by owner and posted to Slack, each with a link to the relevant section of the best practices doc, so they can be acted upon.

In our biggest repo we created a generic notification client that wraps the Knock SDK and makes its use consistent across services: structured logging, tracing, metrics, redaction of sensitive fields, stable keys for Knock's idempotency support, notification expiry checks, automatic chunking of large recipient lists, and testing tools. We also used our background jobs for retries and recovery if Knock was unavailable. But the client was just a small part of it - there was a lot of infrastructure that needed to be set up: syncing users to Knock, ensuring the phone numbers adhere to the E.164 format, setting up email domains and phone numbers, configuring the providers for email and SMS, building email templates for different audiences, connecting our in-house notification preferences to Knock, making sure Knock has access to users' push notification tokens, setting up rollout feature flags, setting up a sandbox for non-production emails and more.

With that work in place, the project gained more traction: we started migrating the first couple of notifications ourselves, the notification client was pulled into a shared package and other teams started creating new notifications via Knock.

This gave us the opportunity to review how people were using the client and how they were building new notifications. Both revealed rough edges and common pitfalls. So we reshaped the client's API to do the heavy lifting internally, leaving callers to provide only what's specific to their use case, and we added more checks to the audit tool.

The prep work

The Braze script we'd built for scoping not only identified campaigns and canvases to be migrated, but also automated the manual process of setting up the content on the other end. In Knock, these are called workflows and can include multiple steps with different channels. The script downloaded the Braze content, performed clean-up (naming, HTML to Markdown, replacing deprecated terms, adding descriptions, tagging by owning team) and created the workflows in Knock.

The final step was to create 88 Linear tickets across 11 teams, one per notification, assigned to whichever team owned it. Because the Knock workflow already existed and the client had one correct usage pattern, each ticket was a simple, mechanical change. That aligned with the major improvements in AI coding and growing AI adoption across Clipboard: we wrote a prompt that instructs the agent to replace the Braze call, wire up the template variables and put it behind a feature flag. We tested the prompt against Claude Code and Devin, and included it in every ticket alongside the validation steps and the rollout instructions.

For example, we had a ticket to move our attendance confirmation reminder to the confirm-attendance-nudge workflow. The ticket supplied the old campaign identifier, the new workflow key, whether the workflow included SMS, and the reviewers. The prompt pointed the agent to .claude/commands/migrate-notification.md. From there the agent would wire up the workflow data (the recipient, shift start time, application deep-link for the confirmation action) and add tests for both sides of the rollout flag. Our testing guides would then walk the engineer through testing the notification on a real device before marking the ticket complete.

With that in place, we were ready for the kick-off.

The launch

We announced the engineering-wide initiative and a shared goal for the engineering organization: migrate all worker notifications in Q1 2026 (we would later announce a migration of workplace user notifications in Q2 2026). We also announced that Braze was entering a conceptual read-only mode: all new notifications had to be created in Knock.

As the teams slotted the work into their backlogs, we watched the open ticket count go down. The Platform team provided support along the way, figured out how to handle edge cases and shared regular updates on the progress via Slack. In one case, a team went back to the drawing board and, after review, cut their notification count considerably - the best outcome we could hope for since that directly improves the customer experience. Other teams wanted to keep the momentum and knocked out their workplace user tickets even before the Q2 goal was announced.

In the meantime, we were busy working on deliverability tracking, internal HOW-TO docs and notification preferences. Throughout Q2 the Platform team focused on migrating what was left: emails sent directly via Amazon SES, text messages sent directly via Twilio, and event-triggered notifications. There were a few reasons for the direct sends. Braze was something of a black box when deliverability was the issue. When some of our corporate customers weren't receiving Braze emails, we switched them to Amazon SES. With Twilio, setting up a Braze campaign was enough of a chore that an engineer needing to send one text message would just use Twilio's user-friendly APIs. Until this project, there was no better path available. Event-triggered notifications were mostly set up by non-engineers and so the Platform team owned that migration.

This work coincided with the company-wide branding update, which the migration made significantly easier as everything was now centralized and version-controlled.

Getting to zero

The engineering initiative was complete, but we were still sending via Braze.

We knew from the start that SMS couldn't move notification by notification. Braze owned our short code, and we could transfer it to Twilio for use with Knock, but only by giving it up in Braze - so as long as both platforms were sending, customers would see two message threads for the same conversation. That's why every ticket recorded whether its workflow included SMS. We set up a single global feature flag for all SMS notifications and planned a big-bang rollout.

While Braze still sent the messages, the new code built and queued the Knock requests in dry-run mode: our client validated and logged those requests without sending them to Knock in production. In development the flag was on, so templates and real sends were tested there. Flipping the rollout flag in production disabled the Braze path and enabled sends through Knock. The switch was uneventful.

With SMS migrated, we wanted to start removing the conditional code and archiving the rollout flag. As it turned out, the flag was still evaluating to "use Braze" for a handful of notifications. Some teams had merged their migration but never flipped the flag. Once we pushed those through, every engineering notification was flowing through Knock.

Later, we introduced Rich Communication Services (RCS) for transactional notifications, and our previous work paid off again: our audit tool flags workflows that could fall outside strict policies governing RCS.

Meanwhile, there was still activity in Braze outside engineering. The operations team ran recurring campaigns encouraging workers to pick up more shifts, and product managers ran in-app surveys to gather feedback. Knock supports campaigns too, where they're called broadcasts, so moving operations was mostly a matter of writing good docs, asking people to transition by a given date, and helping with the first few sends. The in-app channel took a small engineering effort: we shipped custom UI components in our mobile apps backed by Knock Guides, so product managers can configure an audience and content themselves and have it appear the next time a user opens the app.

We also kept archiving campaigns and canvases from a shortlist that we asked people to fill in, double-checking whether things were actually needed (a lot of them weren't!). Once we reached the point where the only remaining campaigns did not require mobile app integration (SMS and email only), we started deleting code. AI tools proved very helpful here: they helped remove redundant code multiple levels deep into the codebase and check for dangling references. Our backends were Braze-free first, then mobile apps followed. We ended up dropping over 35,000 lines of code.

The shutdown, and what's next

Our first Knock notification went out in July 2025. By August 2026 we had the green light to shut down our Braze account. Braze had been an important part of the company's growth, but Knock gave us the delivery diagnostics and workflow controls we had been looking for.

The migration was led by two engineers through all four major parts: discovery and prototyping, preparation, running the engineering-wide initiative, and cleanup and decommissioning. Engineers would rather be writing code, and this project had plenty of it - but 88 tickets across 11 teams still sounds like the slow, tedious part. The opposite turned out to be true. We made sure to make the change easy for the teams and then asked them to make the easy change: the workflow was already there, the notification client was straightforward, the rollout flag was in place, and the tickets with the AI prompt were open and assigned. What actually took the time was the non-coding part - scoping, chasing down owners, migrating people rather than systems, and deciding what didn't need to exist at all.

The work paid off in places we didn't plan for. We finally have best practices for notifications, written down and enforced by a tool. Several teams rethought what they were sending instead of migrating it. And when a company-wide rebrand landed mid-migration, updating our notification content was a matter of editing centralized, version-controlled templates instead of hunting through campaigns and hardcoded HTML.

We also gained a great partner: the Knock team has been responsive throughout, shipping fixes and improvements faster than we expected from a vendor. They support us with what the team is looking for next: a notification center, in-app messaging, per-tenant and fine-grained preferences.

The gain we actually went looking for is simpler than any of that. When a notification doesn't arrive now, we can find out why. A failed Android push in our development environment, for example, had a timeline showing when it was created, queued, and attempted. The details showed an Undelivered status and Google's 403 response with a SENDER_ID_MISMATCH error: the sender does not match the device's registration token. We also knew that the attempt would not be retried. That's enough for an engineer to find and fix the root cause quickly.

A push, email, or SMS accepted by a provider still isn't a notification someone saw. But we can now cut the problem in half - we know whether the message left our system correctly and what the provider did with it. What's left is a narrower question about what happened after that.

That visibility isn't only for engineers. Our support team can now look up a message for themselves and see its full lifecycle and where the issue is. Many of the questions that used to require an engineer no longer do, which means a customer asking why they missed a notification gets the answer in the same conversation rather than a callback days later.

Stefan Chrobot
Stefan Chrobot
More from Stefan