My role
Product Onboarding, owned the data side of the migration
Team
Me and Claude on the data; Magda Olszewska (manager) on the launch; Arthur de Deus (engineer) on product fixes
Timeline
July to October 2026
Outcome
Vitally off and the new email sequence live on 15 Oct 2026, with 292 tests green
Our CRM was running out, and everything we knew lived inside it
PostHog’s onboarding team ran on Vitally, a customer success CRM. It decided which new accounts we’d onboard and who was next, sent the automated emails, and held years of customer conversations. The contract ended in October, and the plan was to move onto PostHog’s own Customer Analytics and Workflows, which were still being built while we moved.
The emails were the easy part. The hard part was the data, which meant building all of it from scratch, cleaning it and verifying every piece, working with Claude the whole way. It was basically what a team of ten would do, and I did it alone.
And the stakes ran both ways. If the data was wrong one way nobody got onboarded, and if it was wrong the other way an automated email went to every account in the book.
Constraints
The date moved under us. In August we’d agreed Vitally would be cancelled at the end of September, and the firm cutover date only arrived on 22 September: 16 October, 24 days later than everything had been planned against.
The tooling wasn’t ready for our use case. The PM for Workflows told us plainly that it was a marketing automation tool built on people rather than on accounts, and onboarding works on accounts. Warehouse tables fire no events, so nothing could trigger a workflow the way a Vitally playbook did. And the property syncs feeding our queue were switching themselves off every three or four days, with nothing alerting anyone.
On the people side, the data was just me. Magda Olszewska, my manager, owned the email templates and the launch with me. Arthur de Deus was the engineer building Customer Analytics, and he shipped the product gaps we hit. I stepped off customer work for the migration, and I spent a week of October in New York with my MBA cohort right before launch.
What I did
Built the account queue without Vitally
It took four passes, because the first one quietly pulled in a Vitally column, but by late August the queue ran entirely off billing and usage tables and was surfacing accounts Vitally had been missing.
Took the product gaps to Arthur
He built a create-account step when Customer Analytics couldn’t create accounts on its own, and shipped shared-view editing the same day I named it as our biggest gap. When I traced the syncs that were switching themselves off, he merged alerting for them that evening, and it caught the next freeze two days later.
Collapsed three models into one
By September three generations of the onboarding model were live at once, and each gave a different answer to the team’s main question, who we’re onboarding: 614, 745 or 760 accounts. I made one canonical model and deleted 12 views in dependency order, consumers before producers, only after checking each had no synced property, saved insight or other view reading it. That took us from 27 onboarding views to 14, and 15 orphaned warehouse tables went too. Keeping all three live would have felt safer, but every conversation about the queue started with which number to trust.
Gave Claude a permission model for production
Claude did most of the hands-on work, the queries, edits, test runs and write-ups. It could change objects I owned, like my own views and draft workflows, on its own, but only after backing them up and only when nothing could reach a customer or a teammate. Switching on any workflow, writing to accounts in bulk, editing anything live or touching someone else’s work needed my yes. It still missed things. On 22 September it found three broken workflows feeding the queue, but I was the one who spotted the root cause underneath them, reading the SQL myself.
Wrote tests that had to fail first
Every condition in every workflow ran through PostHog’s test-run endpoint with side effects mocked. A clean case had to pass, and each guard flipped on its own had to fail, so a test couldn’t quietly pass for the wrong reason. I also planted a wrong expectation to prove the suite would catch it. The suite grew from 196 cases on 1 October to 268 on 13 October and 292 on launch day.
Saved what customers had told us
The archives we’d frozen kept thread details but not the messages themselves, so on the day Vitally’s data went away I saved every inbound customer message, 1,583 replies over 828 threads. It’s the only record of what customers wrote back.
The calls I made and what I traded off
Customer Analytics as the source of truth, instead of a copy of Vitally
The obvious move was to rebuild Vitally field for field until the numbers matched. I set the opposite rule with the team: cross-check against Vitally, but fix what Customer Analytics shows rather than making it match, because matching would have meant copying Vitally’s bugs. One billing field matched Vitally to the cent on 11,783 accounts and carried the same double count, so 462 organizations read about twice their real last payment.
The company’s forecast over my own
On launch day I built a copy of the forecast off my own model, then reverted it the same morning, because onboarding should read the same number as the rest of PostHog.
Speed now, with a bus factor I accepted
Running account allocation off a SQL view I’d built made Magda uneasy, and she said so: “if you break your leg, then there is nobody on our team to be able to do that for us.” My answer was that the view is about half comments and nobody should need to touch it unless the definition of eligibility changes. That made the risk smaller without making it go away, and I took it to hit the date.
What happened
On 15 October, on a call with Magda, we switched every Vitally playbook off and the new email sequence went live at 15:58, which met my goal of moving off Vitally by 16 October. The first runs the next day stamped all 15 first emails, and the close-out workflow went through all 34 of its accounts with no failures.
The bigger result is what customers never saw:
- Account creation had been dead for 17 days. An unpublished draft left the live workflow requiring a $500 forecast, which a brand new org can never have. I published the fix and creation came back at about 2,000 a day the same afternoon, after checking that volume with Arthur first.
- The first-outreach audience was reading a property whose source I’d just deleted, so it matched all 29,704 accounts in the book, completed and sales-owned ones included. I repointed it to 0 before anything was switched on.
- An editor save on 6 October quietly dropped the close-out workflow’s audience filter. Once enabled, it would have marked all 31,462 accounts as finished. I restored it on 13 October, two days before launch.
Some things didn’t go cleanly. Vitally still sent three graduation emails after every playbook was off, most likely already queued. The failed-payment email reached 62 accounts against the 58 I’d confirmed, and 5 bounced. A snapshot I made on launch day turned out to be an alias and was gone, though the history archive I’d frozen earlier held all 142,047 threads. And no workflow in our project has yet woken from a 28-day wait, so Claude has dated checks on the first ones.
The day after launch Magda wrote to me:
thank you for being my rock support in this migration – i wouldn’t have been able to do it without you
Magda Olszewska, my manager
What I’d do differently
I’d save the customers’ own words on day one instead of the last day, since they were the one thing nobody could rebuild.
I’d test the audiences as well as the conditions. The suite proved every guard worked, but the lost close-out filter was caught by a review pass, not by a test.
And I’d pair a second person on the model from the start. Magda’s bus-factor point was fair, and the real fix is someone else who can run it, which no amount of comments gives you.
