How a US Marketing Agency Moved Close to 129,000 Tasks from ClickUp to Notion, With the Client Re-Deriving Every Number
A US digital marketing agency moved close to 129,000 ClickUp tasks, over 46,000 comments and more than 4,700 document pages into Notion with ClonePartner. Their reviewer independently re-derived every figure we published, read-only, and found something real in every round.
Anonymous on customer request
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
TL;DR
Customer: A US digital marketing agency, running client campaign delivery across paid media, creative and analytics teams.
The Move: ClickUp to Notion. Tasks, comments, custom fields, attachments, documents and the relations between them, across a selected set of workspaces. Not a helpdesk migration — this was the system the agency ran its client work in.
The Scope: Roughly 227,000 tasks pulled, close to 129,000 migrated in the agreed scope, over 46,000 comments posted, more than 4,700 document pages, and tens of thousands of attachments. Four years of delivery history.
The Roadblock: Notion's API returns 404 for a page that exists but is not shared with you — identical to the response for a page that has been deleted. On a workspace the client was actively reorganising, that made "gone" and "invisible to us" indistinguishable, and it produced a false report of mass data loss.
The Outcome: A Notion workspace holding the agency's delivery history with relations, comments, attachments and statuses intact — every figure independently reproduced by the client before sign-off.
By the Numbers
- Tasks pulled from source: roughly 227,000
- Tasks migrated: close to 129,000
- Comments posted: over 46,000
- Status corrections applied in the repair pass: around 8,400
- Peak throughput: around 7,900 tasks an hour
- Custom fields mapped: over 5,400
- Document pages migrated: more than 4,700
- Verification findings on the largest wave: close to 500 of roughly 44,000 items, all classified
- Data span: four years of delivery history
The Challenge: Moving the System the Business Runs In
Most migrations move a record of work that has already happened. This one moved the place the work happens. The agency ran client campaign delivery in ClickUp — tasks, briefs, approvals, creative assets, the relations between a campaign and its deliverables — and every one of those objects had someone depending on it that week.
That changed what "done" meant. A support ticket that migrates with a slightly wrong status is a historical inaccuracy. A live task that migrates with the wrong status is a piece of work somebody either does twice or does not do at all.
Three things made it genuinely hard:
- The client audited everything. Their reviewer re-derived every number we shipped, independently and read-only, from outside our pipeline — and found one real issue per round, every round, for a week. That is not a complaint. It is the reason the final numbers are trustworthy, and it set the standard for how every figure had to be produced: from its own artefact, reproducible, with any disagreement shown as a diff rather than smoothed over.
- Notion's rate limit is the whole performance story. The destination allows a fixed number of requests per second, and early waves ran at a fraction of it — not because the limit was being hit, but because workers spent most of their time waiting on downloads and database reads instead of having a call ready.
- The workspace was moving underneath us. The client was actively reorganising content into new teamspaces while the migration ran, which repeatedly changed what our integration could see.
When a 404 Does Not Mean Deleted
The worst moment of the project was a report that the client had deleted close to 5,800 records.
An existence check across a Notion space returned 404 on every sampled page — 24 out of 24, across six lists — against a control that returned 8 out of 8 alive on the identical code path. On that evidence it was reported as a mass deletion.
It was not. The client had moved the content into new teamspaces, and our integration had no access to the new locations. A re-share brought all of it back.
The flaw was in the control. It ran in a different space that we still had access to, so it could only prove that the probe worked — it could never distinguish "gone" from "invisible to us". A valid liveness control has to sit inside the same access scope as the thing being tested.
Worse, this had already happened three weeks earlier on the same project: nearly 190 databases returned 404, and most of them came back after a re-share. The lesson had been written down and was not applied the second time.
What came out of it are rules now applied everywhere. A 404 never means "deleted" in a report, a log line, or a variable name — it means not visible. Mass 404s with a clean boundary are the signature of an access change, not a deletion. And where a client actively reorganises, landing locations get re-derived from the current parent chain rather than from a path recorded weeks ago.
The ClonePartner Solution: Waves, Then Verification, Then Repair
Migration ran in waves, each one a scoped set of spaces, so throughput and quality could be measured and corrected before the next wave committed more data.
Not everything pulled was in scope. Roughly 227,000 tasks were pulled from ClickUp; close to 129,000 sat inside the spaces the agency chose to move. The gap is scope, not loss — the pull was deliberately wider than the migration so that scoping could be decided against the real contents of each space rather than against an estimate of them. What stayed behind stayed behind by the client's decision, space by space, and the pull is what let them make that decision on the actual data.
Throughput improved by a factor of thirty across those waves, and almost none of it came from raising the rate limit. The ceiling never moved. What moved was utilisation: doubling the number of workers so the queue always had a call ready, skipping a block-listing read that was unnecessary on fresh pages and saved roughly 94,000 reads, bounding attachment uploads in parallel inside each task instead of serialising them, backing off the poll interval while waiting for large uploads to settle, and reusing one warm queue across the whole multi-step chain rather than starting cold at every step. Early waves averaged around 250 tasks an hour. The largest wave peaked near 7,900, and moved close to 43,000 tasks in about six hours — against an original estimate of roughly 43 hours. Most of what makes a Notion import slow is on the caller's side of the connection, not the platform's.
Verification was built to be independent of the push. It re-derives what each record should look like from the raw source rather than trusting the mapping the push used, which is why it caught a property-mapping bug the push's own checks were blind to. It stamps what it has verified, so a later pass only re-checks what changed — a ten-row fix went from two hours of rate-limit budget to seconds. On the largest wave it flagged close to 500 items out of roughly 44,000, a little over 1%, and every one was classified before closeout. None were left unexplained.
Idempotency was assumed to fail, and designed around. Every writer marks what it has finished, so a re-run is a no-op scan rather than rework. The dangerous window — a page created, then the process dies before the marker is written — is handled by a reclaim sweep that adopts the existing destination page using its source ID rather than creating a second one. That design was tested by accident: a verification step threw on 34 deliberately unmigrated rows, and the process supervisor restart-looped the whole chain eleven times in minutes. It produced zero duplicates.
The Status Repair, and the Guard That Made It Safe
A defect in earlier waves had collapsed a set of task statuses to a single default. Fixing it meant writing to around 8,400 live tasks in a workspace the client's own people were editing.
Four conditions were set by the client before any write, and all four were met: an identifiable run stamp on every change, human-edited rows surfaced rather than overwritten, a per-database pre-flight check, and the completion date re-asserted and verified on every repaired row.
The human-edit guard needed care. Notion's page-level last-editor field degrades once any automated pass writes to a page, so a scan taken after our own passes cannot recover who edited what before them. The answer was neither to keep the old baseline nor to replace it, but to take a fresh snapshot of the target databases and union it with the earlier one — close to 4,800 pages in total. That scan found around forty rows last edited by a real person rather than by the migration. A handful were in the repair plan, all of them already reading the correct status and therefore skipped, and zero were in the write set. The guard was genuinely armed rather than nominally present, and the plan went to the client unchanged.
The dry run reconciled to the row: every write decomposing exactly into the client's own ruled scope plus a small number of deliberate reversions, with zero writes to any row whose status was not the collapsed default.
The Results: Closed Out by the Client, Not by Us
The waves completed in sequence, with the status repair running to plan after the client gave an explicit go — having re-derived it themselves first.
What the agency has in Notion is close to 129,000 tasks with their comments, attachments, custom fields and cross-database relations intact, alongside more than 4,700 migrated document pages. The relations matter more than the count: a campaign that pointed at its deliverables in ClickUp points at them in Notion, which is the difference between a migrated archive and a system people can work in.
The client closed both workstreams themselves, each one after re-deriving the result rather than accepting it.
What Made This Migration Different
For teams planning a ClickUp to Notion move, three details from this project are worth noting:
-
A 404 from Notion is not evidence of deletion. It is the same response for a page that was never shared with your integration. If a whole space goes dark at once with a clean boundary, that is an access change, not data loss — and your liveness control has to run inside the same access scope as the thing you are testing, or it proves nothing at all. Getting this wrong means telling a client they lost thousands of records that were sitting there the whole time.
-
Rate limits are rarely the bottleneck; utilisation is. Throughput here improved roughly thirty-fold against a ceiling that never changed once. The wins came from making sure a worker always had a call ready — more workers, fewer unnecessary reads, parallel uploads within a task, and one warm queue instead of a cold one per step. Before asking a vendor to raise a limit, measure whether you are using the one you have.
-
Verification has to be independent of the thing it verifies. Checking a migration using the same mapping the migration used will confirm whatever the migration believed. Re-deriving expectations from the raw source — ideally from a full Notion export or the untouched source records — caught a property bug that every internal check had passed. And when a client re-derives your numbers themselves, the right response is to show them the diff: a reviewer who finds something real in every round is producing a better outcome than one who signs off on sight.
Frequently Asked Questions
- Does a 404 from the Notion API mean a page has been deleted?
- No. Notion returns 404 for a page that exists but is not shared with your integration, which is identical to the response for a page that has been deleted. On a workspace being actively reorganised that makes "gone" and "invisible to us" indistinguishable, and on this project it produced a false report of mass data loss that a re-share reversed completely.
- How do you build a liveness check that can tell a deletion from an access change?
- The control has to run inside the same access scope as the thing being tested. A control that runs in a different space you still have access to can only prove that the probe works; it can never distinguish a deleted page from an invisible one. Mass 404s with a clean boundary are the signature of an access change, not a deletion.
- What limits throughput on a ClickUp to Notion migration?
- Rarely the rate limit, and usually utilisation. On this project throughput improved roughly thirty-fold against a ceiling that never moved once, by doubling the number of workers so the queue always had a call ready, skipping a block-listing read that was unnecessary on fresh pages, bounding attachment uploads in parallel inside each task, and reusing one warm queue across the whole chain rather than starting cold at every step.
- Can a migration run while people are still working in the destination workspace?
- Yes, with guards. Landing locations have to be re-derived from the current parent chain rather than from a path recorded weeks earlier, and any bulk correction needs an identifiable run stamp, a per-database pre-flight check, and a human-edit scan that surfaces rows a real person has touched rather than overwriting them.
- Do ClickUp relations between tasks survive a move to Notion?
- Yes, and they matter more than the task count. A campaign that pointed at its deliverables in ClickUp points at them in Notion, along with comments, attachments, custom fields and statuses. That is the difference between a migrated archive and a system people can actually work in.