Custom Field IDs After Migration: Why Integrations Break
An integration does not hold your data. It holds pointers to your data — a record ID in one place, a field key in another, sometimes a slug sitting in a URL — and a migration reissues every one of them while leaving the values alone. That is the whole of it. The counts match because the values moved. The sync is wrong because the pointers did not move with them.
This is a different failure from the one where the automation platform goes quiet, which is a connection and trigger problem and has its own set of symptoms. This page is about the layer underneath: the identifiers themselves. Why a connector that authenticates perfectly, runs on schedule and reports success writes its update into a row that belongs to somebody else.
The reason it is hard to catch is that every test people run after an import reads values. Row counts, spot checks, a sample of five customers opened side by side — all of them compare content, and the content is fine. Nothing in that battery of checks touches a key.
Three kinds of pointer, and the destination reissues all three
Take any one record and there are usually three separate handles on it.
The record ID. Issued by the system, opaque, not yours. In Airtable it is the string beginning
rec that appears in the URL when you expand a record, and the help centre's ID article gives you
two ways to get at it: the URL bar, or a formula field containing RECORD_ID()
(Airtable support, checked 20 September 2026).
In Asana it is the gid, described in the API reference for a single custom field as the "Globally
unique identifier for the custom field"
(Asana API reference, checked 20 September 2026).
In HubSpot it is hs_object_id, and HubSpot's properties guide is worth reading closely on its
scope: "When a record is created in HubSpot, a unique ID (hs_object_id) is automatically generated
and should be treated as a string. These IDs are unique only within that object type, so there can be
both a contact and company with the same ID"
(HubSpot developer docs, checked 20 September 2026).
Generated by the destination, on creation. Your old number is not an input to that process.
The field key. Separate from the field's name, and the distinction is the one most people have
never had to think about. HubSpot's create-a-property call takes both: name is documented as "the
internal name of the property (e.g., favorite_food)" and label as "the name of the property as it
appears in HubSpot (e.g., Favorite Food)"
(HubSpot developer docs, checked 20 September 2026).
Notion's data source property object carries the same
split — id is "An identifier for the property, usually a short string of random letters and
symbols", with the example value fy:{, while name is "The name of the property as it appears in
Notion"
(Notion API reference, checked 20 September 2026).
Jira's version is the one people have actually seen, because it leaks into every automation rule:
customfield_10034, where the number means nothing outside the instance that issued it.
The slug. Less universal, but where it exists it is load-bearing, because it is usually the piece
that appears in a public URL. Shopify's handle field on the Product object is documented as "A
unique, human-readable string of the product's title. A handle can contain letters, hyphens (-), and
numbers, but no spaces. The handle is used in the online store URL for the product"
(Shopify developer docs, checked 20 September 2026).
Rebuild a catalogue in a new store and the handles are derived again, which is fine for the two
hundred products and not fine for the ad campaign, the affiliate link and the spreadsheet of
bestsellers that were all written against the old ones.
Shopify also illustrates the thing that makes an inventory of pointers harder than it sounds: one
record can carry more than one ID inside a single system. The same Product object exposes
legacyResourceId, documented as "The ID of the corresponding resource in the REST Admin API",
alongside the GraphQL id. Two identifiers, one product, and an integration written last year may
be holding either.
Name-keyed and ID-keyed both break — pick the one you can see coming
The instinct, once you understand the above, is to find the safe option. There is not one. There are two failure modes and you are choosing between them, usually without being told you are choosing.
Airtable makes the choice visible, and the default is the interesting part. The list-records endpoint
takes a parameter called returnFieldsByFieldId, described in the API schema as "An optional boolean
value that lets you return field objects where the key is the field id", followed by the sentence
that matters: "This defaults to false, which returns field objects where the key is the field
name"
(Airtable Web API, checked 20 September 2026).
I pulled that schema directly because the rendered page does not surface the parameter in the visible
text. So the ordinary case, the one that happens when nobody makes a decision, is name-keyed. Every
script and no-code step written against that base resolves fields by their display labels.
Name-keyed means the integration survives a rebuild — recreate the field in the new tool, call it Purchase Order, and the lookup works. It also means the integration dies when someone renames a column, and renaming is a thing people do freely because it feels cosmetic. HubSpot's help documentation puts a hard edge on that asymmetry: when you edit a property, "You cannot edit the property's object type or internal name" (HubSpot knowledge base, checked 20 September 2026). The label is editable, the internal name is not. Which is protective if your integrations are keyed on the internal name and a loaded gun if they are keyed on the label, because the platform will let anyone change the label without warning and nothing in the interface says an integration depends on it.
ID-keyed inverts everything. The rename cannot touch it. The rebuild kills it outright, because the field in the new system is a new field with a new ID, and this is the case a migration guarantees.
And names carry a defect of their own that IDs do not: they can collide. Atlassian's guidance on advanced field editing shows the same field referenced both ways in one JSON payload and notes that using both "causes an error", then states the condition that removes your choice entirely: "If there are custom fields with the same name, or a custom field has the same name as a system field, you will need to use the custom field ID" (Atlassian support, checked 20 September 2026). Two fields called Status is not a hypothetical in a system that has been in use for four years and has had three admins.
The practical reading: work out which of the two your integrations use, per integration, before you import anything. It is the difference between knowing they will all fail on cutover day and finding out one at a time over six weeks.
The four failure shapes, and how long each stays invisible
| What happened to the pointer | What the integration does | What you see |
|---|---|---|
| Resolves to nothing — the ID does not exist in the destination | Errors, retries, eventually alerts | An error. The good case, and the rare one |
| Resolves to a different record — the new system issued that ID to someone else | Writes successfully, to the wrong row | Nothing at all |
| Field key resolves to a different field with a similar label | Writes a correct value into the wrong column | A column that slowly fills with the wrong kind of data |
| Slug resolves to a different page or product | Sends traffic and links somewhere plausible | Rising bounce rate, no error anywhere |
The second row is the expensive one, and it is worth being precise about the mechanism rather than treating it as bad luck — including precise about how much of it is documented. A system that issues sequential or near-sequential numeric IDs starts a fresh account at the bottom of the range, and the first records created in that account are your import. If an old integration is still holding customer 4,182 and the new system has issued its own customer 4,182 out of the import batch, the write succeeds and both systems agree the operation completed. Whether a particular vendor does this is the part you cannot look up: almost none of them publish whether record IDs are reused or recycled, either way. Treat this row as a risk to design against, not as behaviour anyone has confirmed to you. What is documented is the same hazard one scale down — HubSpot's note that its IDs are "unique only within that object type", which means an ID that is meaningful only in context will resolve in the wrong context without complaining.
The third row is the one that produces the strangest support tickets. A connector pushing into a field it believes is Delivery Date and is actually Invoice Date does not corrupt anything visibly. Dates are dates. The report built on that column simply becomes wrong, and stays wrong until somebody senior enough to distrust it goes looking.
The mapping table, and why it has to exist before you cancel
Everything above has one remedy, and it is unglamorous: a table that says which old identifier became which new identifier. It is not a nice-to-have artefact of a tidy migration. It is the only thing that lets you repair a mis-pointed integration afterwards, and it can only be produced while both systems exist.
That timing is the whole trap. The day you need the map is six weeks after the switch, when somebody finds the wrong column. The last day you can produce it is the day the old subscription ends.
For each object type you sync, capture six columns: object type, old record ID, new record ID, the natural key you matched on (email, invoice number, SKU), the source of that match, and the date you extracted it. The fourth and fifth columns are the ones people skip and the ones that make the file usable later, because a bare pair of IDs cannot tell you whether the row was matched confidently or guessed.
Getting the old side out is per-vendor and mostly documented:
- Airtable. Add a formula field — the documented step is to "Enter RECORD_ID() into the provided
“Formula” window" — which puts the
recidentifier into an exportable column. For the field keys, the same article points at Tools then Manage fields, where "You'll now see a list of the the fields and a column for the “Field ID.”" (the doubled word is Airtable's), hidden behind the column visibility toggle if it is not showing (Airtable support, checked 20 September 2026). - Jira. Request an issue through the REST API with
?expand=namesappended. Atlassian's page on finding smart values uses it for exactly this purpose and walks through an example where a field called Cascade List turns out to becustomfield_10034(Atlassian support, checked 20 September 2026). One request gives you the full label-to-key mapping for every field on that issue type. - ClickUp. If your workspace uses readable task IDs rather than the internal ones, the API needs
telling. The Get Tasks endpoint documents
custom_task_idsas: "If you want to reference a task by its custom task id, this value must betrue", and pairs it withteam_id— "When thecustom_task_idsparameter is set totrue, the Workspace ID must be provided using theteam_idparameter" (ClickUp API reference, checked 20 September 2026). Two identifier schemes in one product, and an export that uses the wrong one produces a map that joins to nothing. Readable task IDs are a workspace-level feature rather than something every account has switched on, so check yours before planning an export around them. - Anything with a real API. The same extraction discipline as a full data pull — pagination, rate limits, token expiry — applies, and it is the same job described in the guide to taking a complete dump through the vendor's API. If you are running that anyway, add the ID columns to it rather than doing a second pass later.
One caution on the extraction itself. Airtable's schema notes that its "API only accepts request with
a URL shorter than 16,000 characters" and suggests switching to a POST against
/v0/{baseId}/{tableIdOrName}/listRecords when the query grows past it
(Airtable Web API, checked 20 September 2026). A field list long enough to
include every ID column is exactly the request that trips that limit, and the failure arrives as a
malformed request rather than as anything mentioning length.
Where the destination will let you keep the old ID
A map in a spreadsheet repairs things by hand. A map stored in the destination records repairs them automatically, because the integration can look up by your identifier instead of the platform's. Several vendors have built the slot for this and given it a name.
HubSpot documents its version in the most detail, which is the only reason it is the one worth
walking through here. Create a property with hasUniqueValue set to true and the value becomes a
lookup key: the documentation gives the request shape as
/crm/v3/objects/deals/abc?idProperty=system_a_unique, retrieving the deal whose
system_a_unique value is abc, and says the value can then be used "in the same way you could use
hs_object_id, email (contacts), or domain (companies)"
(HubSpot developer docs, checked 20 September 2026).
Two constraints come with it, and both change how you plan. The ceiling is "up to ten unique ID properties per object", so a company
migrating three systems into one HubSpot portal should be deliberate about which legacy IDs earn a
slot. And there is a sharp edge worth knowing before somebody uses the duplicate button: "When
cloning a record with a unique property value, the value will not be copied to the new record",
because uniqueness is enforced across the object type. Clone a deal as a template — a normal thing to
do — and the clone is the one record in your system with no legacy ID. That page documents the
mechanism without naming a subscription tier, so confirm it is available on your portal's plan before
the cutover depends on it.
Stripe's equivalent is metadata, and the API reference lists this use case first: "Link IDs: Attach your system's unique IDs to a Stripe object to simplify lookups. For example, add your order number to a charge, your user ID to a customer or recipient, or a unique receipt number to a transfer" (Stripe API reference, checked 20 September 2026). The limits are published and worth checking your key format against before you write 4,000 of them: "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long", and square brackets are not allowed in keys. The same page draws a line you should not cross even though the field would accept it — "Don't store any sensitive information (bank account numbers, card details, and so on) as metadata or in the description parameter."
Elsewhere the slot is just a custom field you create yourself, called something like Legacy ID, made read-only if the platform allows it. That is less elegant and works identically. The point is that the old identifier lives on the record rather than in a file on somebody's laptop, so the repair does not depend on a spreadsheet still being findable in 2029.
The order it has to happen in
| When | Step | Why it cannot move later |
|---|---|---|
| Before the first import | Inventory every integration and record, for each one, whether it resolves fields by name or by ID | This determines which break at rename and which break at rebuild. After cutover you are working it out from symptoms |
| Before the first import | Extract old record IDs and the label-to-field-key map from the source system | Both die with the subscription, and neither is in a normal export |
| Before the first import | Create the legacy-ID holding field in the destination, unique-valued where the platform supports it | Populating it during import is one pass. Backfilling it afterwards is a second migration |
| During import | Populate the legacy ID on every record, including ones you create manually that day | The manually created records are the ones that are missing it in six weeks, and they are usually the urgent ones |
| Repointing | Rebuild each integration against the legacy ID where the platform supports lookup by it, not against the new internal ID | The new internal ID is stable until the next move. The legacy ID is the thing that makes the next move cheaper |
| After cutover | Keep the map with the archive, not in the project folder | It is a key to the archive and is worth nothing separated from it. The same argument as everything else in keeping a retired system readable |
The middle rows are where the discipline actually costs something. Populating a legacy ID on every record during the import is tedious, and the reason it belongs there rather than in a follow-up pass is that the import is the last moment the two ID sets are sitting next to each other in one file.
Tests that can fail
The checks that pass on cutover day pass because they were built to compare values. Three tests compare pointers instead, and all three can be run in under an hour.
The round trip on one record. Pick a single record. Change something through the integration rather than in the interface. Then open the record in the destination and confirm that this record changed, and go and look at the two records either side of it in the ID sequence to confirm they did not. Checking that the write succeeded is not the test. Checking that it landed on the row you named is the test.
The deliberate rename. In a sandbox or a copy, rename a custom field's label and run the integration. If it keeps working, you are ID-keyed and your exposure is the next rebuild. If it fails, you are name-keyed and your exposure is the next person who tidies up a column heading. Either answer is useful; not knowing is the only bad outcome, and this is the only test that produces the answer in five minutes.
The orphan count. After import, count records in the destination whose legacy ID field is empty. On day one that number tells you what the import missed. Run weekly for a month and it tells you something more useful: how fast new records are being created outside the process, which is the rate at which your map is going stale.
Under all three sits the same structural fact, which is why fields that look identical in two systems are not the same field: a field is defined by its key, and its key was issued by the system it lives in. Two columns called Priority holding the same five values in two tools are not one field seen twice. They are two fields that agree, which is a thing that has to be maintained rather than a thing that is true. An importer moving those values across — and there are several things a good importer still leaves for you to rebuild by hand — has moved the agreement, not the identity.
Verified against Airtable, Asana, Atlassian, ClickUp, HubSpot, Notion, Shopify and Stripe
documentation on 20 September 2026. Every sentence in quotation marks above is copied from the page
linked beside it. Two of those came out of machine-readable schemas rather than rendered help pages —
Airtable's returnFieldsByFieldId description is in the API definition behind the list-records
reference, and the ClickUp parameter text is in the OpenAPI document served with the Get Tasks page —
so if you are checking them, expect the wording rather than the layout to match. Products are named
here because their documentation states these mechanisms in their own words. There is no ranking on
this page and no recommendation about which tool to move to.
Two limits on what is above. Whether a specific destination system reuses or recycles numeric record IDs is not something most vendors publish either way, so the second row of the failure table is a risk to design around rather than a documented behaviour I can attribute to any named product. The three account-level mechanisms above need separating rather than lumping, because they are not gated the same way. Stripe's metadata limits are stated for the API generally, with no tier attached. HubSpot's unique properties and ClickUp's readable task IDs are documented without naming a subscription level at all, which is not the same as being available everywhere — check your own plan page for those two before a cutover step depends on them.
Identifier schemes get versioned, parameters get renamed, and defaults occasionally flip. If a quotation here no longer matches the page it was taken from, say so — the line is checked against the source again and the date above changes. Who is writing and what that is worth is on the about page.
Frequently asked questions
Why did my integration break when the data migration succeeded?
Because an integration stores pointers, not values. It holds a record ID, a field key and sometimes a URL slug, and the destination system issues its own versions of all three at import time. The values arrive intact, which is what a reconciliation checks, and every pointer is now wrong, which is what a reconciliation cannot see. HubSpot's properties documentation is explicit that a record ID is generated by the destination: when a record is created, a unique ID (hsobjectid) is automatically generated. Nothing about your old ID travels with the row unless you deliberately carry it in a field.
Should my integration reference fields by name or by ID?
Neither survives a migration untouched, and knowing which one you are using tells you what will break. Reference by name and the integration breaks when someone edits a label — Airtable's list-records endpoint documents returnFieldsByFieldId as defaulting to false, which returns field objects where the key is the field name, so most Airtable integrations are name-keyed without anyone choosing it. Reference by ID and it breaks when the field is rebuilt in a new system, because the new field gets a new ID. Atlassian's guidance on editing fields with JSON adds the case that forces the ID anyway: if there are custom fields with the same name, or a custom field has the same name as a system field, you will need to use the custom field ID.
What do I need to record before I cancel the old system?
The identifier map, because the old IDs die with the account. For every object type you sync, capture the old record ID, the new record ID, the field labels with both old and new field keys, and the date you pulled it. Airtable gives you record IDs through a formula field containing RECORDID() and field IDs through the Field ID column in Manage fields. Jira exposes the mapping between labels and customfield IDs through the REST API with expand=names appended to an issue request. Pull it while the subscription is live and store it outside both systems.
Where can I put the old ID in the new system so lookups still work?
Most platforms have a documented place for exactly this. HubSpot lets you create a property with hasUniqueValue set to true and then look a record up by it — a request to /crm/v3/objects/deals/abc?idProperty=systemaunique fetches the deal whose value for that property is abc — with a limit of ten unique ID properties per object. Stripe uses metadata, and its API reference lists linking IDs as a sample use case: attach your system's unique IDs to a Stripe object to simplify lookups. Stripe's limits are 50 keys, key names up to 40 characters and values up to 500 characters, which is enough for an ID and not enough for a composite key you invent on the day.