1. identity
Everything downstream references people, so they land first, as Identity rows. Imported identities carry details only: no credential, no login, no invite. How imported people eventually log in is a separate workstream; the boundary doesn't participate in it.
Naming note from review (Jaime): strictly these are user-type identities, so the schema may be better named userImport (or userIdentityImport) if other identity kinds ever reach the boundary; the shape is unaffected either way.
export const identityImport = z.object({
externalRef: pk,
// lawyer / staff / client vocabulary from identity-service
identityType: IdentityTypeSchema,
firstName: z.string().min(1),
lastName: z.string().min(1),
contactEmail: z.string().email().nullable(),
contactPhone: z.string().nullable(),
createdAt: z.coerce.date(),
});
// canonical: identity
{
"external_ref": "person_anna",
"identity_type": "LAWYER",
"first_name": "Anna",
"last_name": "Reid",
"contact_email": "anna.reid@smithlaw.co.uk",
"contact_phone": "+447700900123",
"created_at": "2019-04-01T08:00:00Z"
}
// inserted: Identity (identity-db)
{
// freshly minted
"id": "idn_anna",
"type": "LAWYER",
"firstName": "Anna",
"lastName": "Reid",
"contactEmail": "anna.reid@smithlaw.co.uk",
"contactPhone": "+447700900123",
"createdAt": "2019-04-01T08:00:00Z"
}
// inserted: ImportedRecord (identity-db, same transaction)
{
"entityType": "Identity",
"entityId": "idn_anna",
"externalRef": "person_anna",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
2. firm_member
Membership of the run's target firm, as a FirmMember. The firm exists before any run, created by hand, so no firm reference appears in staged data anywhere.
export const firmMemberImport = z.object({
externalRef: pk,
identityExternalRef: parent(identityImport),
// ADMIN / LAWYER / SUPPORT vocabulary from identity-service
role: FirmMemberRoleSchema,
jobTitle: z.string().nullable(),
createdAt: z.coerce.date(),
});
// canonical: firm_member
{
"external_ref": "person_anna:member",
"identity_external_ref": "person_anna",
"role": "LAWYER",
"job_title": "Senior Associate",
"created_at": "2019-04-01T08:00:00Z"
}
// inserted: FirmMember (identity-db)
{
"id": "fmem_a1",
// registry: person_anna
"identityId": "idn_anna",
// run config
"firmId": "firm_9y2",
"role": "LAWYER",
"jobTitle": "Senior Associate",
"createdAt": "2019-04-01T08:00:00Z"
}
// inserted: ImportedRecord (identity-db, same transaction)
{
"entityType": "FirmMember",
"entityId": "fmem_a1",
"externalRef": "person_anna:member",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
3. team and team_member
Practice groups and their membership: Team and TeamMember.
export const teamImport = z.object({
externalRef: pk,
name: z.string().min(1),
createdAt: z.coerce.date(),
});
export const teamMemberImport = z.object({
externalRef: pk,
teamExternalRef: externalRef,
identityExternalRef: parent(identityImport),
// OWNER | MEMBER
role: TeamMemberRoleSchema,
createdAt: z.coerce.date(),
});
// canonical: team
{
"external_ref": "dept_prop",
"name": "Property",
"created_at": "2019-04-01T08:00:00Z"
}
// canonical: team_member
{
"external_ref": "dept_prop:person_anna",
"team_external_ref": "dept_prop",
"identity_external_ref": "person_anna",
"role": "OWNER",
"created_at": "2019-04-01T08:00:00Z"
}
// inserted: Team (identity-db)
{
"id": "team_5rw",
// run config
"firmId": "firm_9y2",
"name": "Property",
"createdAt": "2019-04-01T08:00:00Z"
}
// inserted: TeamMember (identity-db)
{
"id": "tmem_p7",
// registry: dept_prop
"teamId": "team_5rw",
// registry: person_anna
"identityId": "idn_anna",
"role": "OWNER",
"createdAt": "2019-04-01T08:00:00Z"
}
// inserted: ImportedRecord × 2 (identity-db, same transactions)
[
{
"entityType": "Team",
"entityId": "team_5rw",
"externalRef": "dept_prop",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
},
{
"entityType": "TeamMember",
"entityId": "tmem_p7",
"externalRef": "dept_prop:person_anna",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
]
4. matter and matter_participant
The initial slice, defined and fully worked (v2 → canonical → v3) in the matter boundary schema doc; restated here for completeness of the catalogue. Targets Matter and MatterParticipant, with MatterTeamAccess derived.
export const matterImport = z.object({
externalRef: pk,
// the firm's own matter reference (e.g. "25/16095");
// lands directly on Matter.humanReadableId (per-firm unique)
humanReadableId: z.string().min(1),
title: z.string().min(1),
// OPENING | ACTIVE | COMPLETED | CANCELLED;
// every source's status vocabulary collapses to these
matterStatus: MatterStatusSchema,
// the source's own origin/type classification, kept as data
externalSource: z.string().nullable(),
// US | GB; passed in per run
market: MarketSchema,
// static reference slug, not a run-minted v3 id;
// GB sources: "gb-england-wales", set in the transform
primaryJurisdictionId: JurisdictionIdSchema,
createdAt: z.coerce.date(),
lastActivityAt: z.coerce.date(),
completedAt: z.coerce.date().nullable(),
});
export const matterParticipantImport = z.object({
// synthetic refs allowed where the source has no row,
// namespaced to stay stable across re-runs (case:<id>:owner)
externalRef: pk,
matterExternalRef: parent(matterImport),
personExternalRef: externalRef,
// CLIENT | FIRM; must agree with participantType
side: MatterSideSchema,
// LAWYER | TEAM_MEMBER | CLIENT
participantType: MatterParticipantTypeSchema,
isLead: z.boolean(),
createdAt: z.coerce.date(),
});
Unit invariant: exactly one lead per side; completedAt present exactly when status is COMPLETED.
// canonical: matter
{
"external_ref": "case_71",
"human_readable_id": "16095",
"title": "Purchase of 12 Acacia Avenue",
"matter_status": "ACTIVE",
"external_source": "byoc",
"market": "GB",
"primary_jurisdiction_id": "gb-england-wales",
"created_at": "2024-03-02T09:14:00Z",
"last_activity_at": "2026-07-29T16:02:00Z",
"completed_at": null
}
// canonical: matter_participant
[
{
"external_ref": "cp_301",
"matter_external_ref": "case_71",
"person_external_ref": "person_anna",
"side": "FIRM",
"participant_type": "LAWYER",
"is_lead": true,
"created_at": "2024-03-02T09:14:00Z"
},
{
"external_ref": "cp_302",
"matter_external_ref": "case_71",
"person_external_ref": "person_john",
"side": "CLIENT",
"participant_type": "CLIENT",
"is_lead": true,
"created_at": "2024-03-02T09:14:00Z"
}
]
// inserted: Matter (matter-db)
{
"id": "mat_k4p",
"humanReadableId": "16095",
"title": "Purchase of 12 Acacia Avenue",
"status": "ACTIVE",
"source": "IMPORT",
// run config
"firmId": "firm_9y2",
// derived: the importing firm
"firmOfRecordId": "firm_9y2",
"market": "GB",
"primaryJurisdictionId": "gb-england-wales",
"createdAt": "2024-03-02T09:14:00Z",
"lastActivityAt": "2026-07-29T16:02:00Z",
"completedAt": null
}
// inserted: MatterParticipant × 2 (matter-db)
[
{
"id": "matpar_c2",
"matterId": "mat_k4p",
// registry: person_anna
"identityId": "idn_anna",
"side": "FIRM",
"participantType": "LAWYER",
"isLead": true,
// stamped on firm-side rows
"firmId": "firm_9y2"
},
{
"id": "matpar_c3",
"matterId": "mat_k4p",
// registry: person_john
"identityId": "idn_john",
"side": "CLIENT",
"participantType": "CLIENT",
"isLead": true,
"firmId": null
}
]
// inserted: MatterTeamAccess (matter-db)
// derived from the lead lawyer's team, as the organic create path does
{
"id": "mta_88",
"matterId": "mat_k4p",
"teamId": "team_5rw"
}
// inserted: ImportedRecord × 3 (matter-db, same transaction)
[
{
"entityType": "Matter",
"entityId": "mat_k4p",
"externalRef": "case_71",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
},
{
"entityType": "MatterParticipant",
"entityId": "matpar_c2",
"externalRef": "cp_301",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
},
{
"entityType": "MatterParticipant",
"entityId": "matpar_c3",
"externalRef": "cp_302",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
]
The firm's address book: Contact with its Individual/Company satellite, plus ContactMethod and ContactAddress. The satellite folds into one flat row (rule 7); the importer re-normalises by contactType.
export const contactImport = z.object({
externalRef: pk,
// INDIVIDUAL | COMPANY
contactType: ContactTypeSchema,
// individual fields (null unless INDIVIDUAL)
firstName: z.string().nullable(),
lastName: z.string().nullable(),
title: z.string().nullable(),
// company fields (null unless COMPANY)
legalName: z.string().nullable(),
tradingName: z.string().nullable(),
registrationNumber: z.string().nullable(),
// set when this contact is also a platform user;
// resolves to an identity imported by schema 1
personExternalRef: externalRef.nullable(),
notes: z.string().nullable(),
createdAt: z.coerce.date(),
});
export const contactMethodImport = z.object({
externalRef: pk,
contactExternalRef: parent(contactImport),
// EMAIL | PHONE | ...
methodType: ContactInfoTypeSchema,
value: z.string().min(1),
label: z.string().nullable(),
isPrimary: z.boolean(),
});
export const contactAddressImport = z.object({
externalRef: pk,
contactExternalRef: parent(contactImport),
lines: z.string().min(1),
city: z.string().min(1),
province: z.string().nullable(),
postalCode: z.string().nullable(),
country: z.string().min(1),
label: z.string().nullable(),
isPrimary: z.boolean(),
});
// canonical: contact (the seller on case_71)
{
"external_ref": "party_kate",
"contact_type": "INDIVIDUAL",
"first_name": "Kate",
"last_name": "Morris",
"title": "Ms",
"legal_name": null,
"trading_name": null,
"registration_number": null,
"person_external_ref": null,
"notes": null,
"created_at": "2024-03-02T10:00:00Z"
}
// canonical: contact_method
{
"external_ref": "party_kate:email",
"contact_external_ref": "party_kate",
"method_type": "EMAIL",
"value": "kate.morris@example.com",
"label": null,
"is_primary": true
}
// inserted: Contact (identity-db)
{
"id": "ctc_m3",
// run config
"firmId": "firm_9y2",
"type": "INDIVIDUAL",
"identityId": null,
"notes": null,
"createdAt": "2024-03-02T10:00:00Z"
}
// inserted: Individual (identity-db)
// re-normalised from the flat contact row
{
"id": "ind_m3",
"contactId": "ctc_m3",
"firstName": "Kate",
"lastName": "Morris",
"title": "Ms"
}
// inserted: ContactMethod (identity-db)
{
"id": "cmeth_e1",
"contactId": "ctc_m3",
"type": "EMAIL",
"value": "kate.morris@example.com",
"label": null,
"isPrimary": true
}
// inserted: ImportedRecord × 2 (identity-db, same transaction)
// the folded Individual has no externalRef of its own
[
{
"entityType": "Contact",
"entityId": "ctc_m3",
"externalRef": "party_kate",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
},
{
"entityType": "ContactMethod",
"entityId": "cmeth_e1",
"externalRef": "party_kate:email",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
]
Who the matter is about, as MatterContact. Its own table because the link carries data (rule 8). MatterContactSlot (unfilled template roles) is v3 workflow, never imported.
export const matterContactImport = z.object({
externalRef: pk,
matterExternalRef: parent(matterImport),
contactExternalRef: externalRef,
// CLIENT | OTHER
role: MatterContactRoleSchema,
// the source's free-text role, e.g. "Seller"
roleLabel: z.string().nullable(),
notes: z.string().nullable(),
createdAt: z.coerce.date(),
});
// canonical: matter_contact
{
"external_ref": "case_71:party_kate",
"matter_external_ref": "case_71",
"contact_external_ref": "party_kate",
"role": "OTHER",
"role_label": "Seller",
"notes": null,
"created_at": "2024-03-02T10:00:00Z"
}
// inserted: MatterContact (matter-db)
{
"id": "matcon_s9",
// registry: case_71
"matterId": "mat_k4p",
// registry: party_kate
"contactId": "ctc_m3",
"role": "OTHER",
"roleLabel": "Seller",
"notes": null,
"createdAt": "2024-03-02T10:00:00Z"
}
// inserted: ImportedRecord (matter-db, same transaction)
{
"entityType": "MatterContact",
"entityId": "matcon_s9",
"externalRef": "case_71:party_kate",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
7. matter_file_folder and matter_file
The paper trail: MatterFileFolder, matter-service's own File blob record, and MatterFile. The row is metadata; the bytes ride sidecar (rule 5). Editable source documents flatten to rendered files; recreating editable MatterDocuments is a later capability for sources with portable bodies.
export const matterFileFolderImport = z.object({
externalRef: pk,
matterExternalRef: parent(matterImport),
title: z.string().min(1),
order: z.number().int(),
});
export const matterFileImport = z.object({
externalRef: pk,
matterExternalRef: parent(matterImport),
folderExternalRef: externalRef.nullable(),
// staged object store, owned by the run snapshot;
// scanned on arrival, checksum verified on copy
payloadUri: z.string().min(1),
payloadSha256: z.string().length(64),
filename: z.string().min(1),
fileSize: z.number().int().positive(),
// ALL | FIRM_SIDE_ONLY
visibility: MatterFileVisibilitySchema,
uploaderPersonExternalRef: externalRef.nullable(),
createdAt: z.coerce.date(),
});
// canonical: matter_file_folder
{
"external_ref": "fold_corr",
"matter_external_ref": "case_71",
"title": "Correspondence",
"order": 1
}
// canonical: matter_file
{
"external_ref": "doc_882",
"matter_external_ref": "case_71",
"folder_external_ref": "fold_corr",
"payload_uri": "s3://firm-migrations/firm_9y2/snap_0815/blobs/doc_882.pdf",
"payload_sha256": "9f2c…64 hex chars…a11b",
"filename": "draft-contract.pdf",
"file_size": 481332,
"visibility": "ALL",
"uploader_person_external_ref": "person_anna",
"created_at": "2024-03-14T11:20:00Z"
}
// inserted: MatterFileFolder (matter-db)
{
"id": "mff_c1",
"matterId": "mat_k4p",
"title": "Correspondence",
"order": 1
}
// inserted: File (matter-db)
// blob streamed from the sidecar into matter-service's store, born READY:
// no upload lifecycle, no scan choreography, no notifications
{
"id": "file_7q",
"status": "READY",
"filename": "draft-contract.pdf",
"size": 481332,
"sha256": "9f2c…a11b"
}
// inserted: MatterFile (matter-db)
{
"id": "matfile_d4",
"matterId": "mat_k4p",
// registry: fold_corr
"folderId": "mff_c1",
"fileId": "file_7q",
"source": "IMPORT",
"visibility": "ALL",
// registry: person_anna
"uploaderId": "idn_anna",
"createdAt": "2024-03-14T11:20:00Z"
}
// inserted: ImportedRecord × 2 (matter-db, same transactions)
// one entry per staged row; the File blob record hangs off the MatterFile
[
{
"entityType": "MatterFileFolder",
"entityId": "mff_c1",
"externalRef": "fold_corr",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
},
{
"entityType": "MatterFile",
"entityId": "matfile_d4",
"externalRef": "doc_882",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
]
8. matter_note
A note on the matter: MatterNote, backed by a derived Content. Body is portable markup (rule 6); the importer converts it and creates the Content machinery itself. That conversion is versioned importer code, not a per-source transform problem.
export const matterNoteImport = z.object({
externalRef: pk,
matterExternalRef: parent(matterImport),
name: z.string().nullable(),
bodyMarkdown: z.string().min(1),
authorPersonExternalRef: externalRef,
createdAt: z.coerce.date(),
});
// canonical: matter_note
{
"external_ref": "note_43",
"matter_external_ref": "case_71",
"name": "Call with seller's solicitor",
"body_markdown": "Agreed completion **28 June**. Awaiting searches.",
"author_person_external_ref": "person_anna",
"created_at": "2024-05-10T15:45:00Z"
}
// inserted: Content (content-db)
// derived: the importer converts the markdown into an editable body
{
"id": "cont_x2",
"scope": "firm_9y2"
}
// inserted: MatterNote (matter-db)
{
"id": "matnote_n8",
"matterId": "mat_k4p",
"name": "Call with seller's solicitor",
"contentId": "cont_x2",
// registry: person_anna
"authorId": "idn_anna",
"createdAt": "2024-05-10T15:45:00Z"
}
// inserted: ImportedRecord (matter-db, same transaction)
{
"entityType": "MatterNote",
"entityId": "matnote_n8",
"externalRef": "note_43",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
9. task and key_date
What needs doing: Task with its TaskAssignment folded into the task row (rule 7), and KeyDate. Both are matter-nullable, as in v3, because firm-level work exists. Key dates are a proposed schema: few external systems have a concept that maps to them, so expect this table to be empty for most sources; tasks reference them nullably either way.
export const keyDateImport = z.object({
externalRef: pk,
// nullable parent: matter-scoped rows dump as a child file, firm-level
// rows as their own root file (rule 10), so neither carries a null key
matterExternalRef: parent(matterImport).nullable(),
name: z.string().min(1),
dueAt: z.coerce.date(),
createdAt: z.coerce.date(),
});
export const taskImport = z.object({
externalRef: pk,
// nullable parent: matter-scoped rows dump as a child file, firm-level
// rows as their own root file (rule 10), so neither carries a null key
matterExternalRef: parent(matterImport).nullable(),
keyDateExternalRef: externalRef.nullable(),
name: z.string().min(1),
description: z.string().nullable(),
assigneePersonExternalRef: externalRef.nullable(),
dueAt: z.coerce.date().nullable(),
completedAt: z.coerce.date().nullable(),
createdAt: z.coerce.date(),
});
// canonical: key_date
{
"external_ref": "kd_compl",
"matter_external_ref": "case_71",
"name": "Completion",
"due_at": "2024-06-28T00:00:00Z",
"created_at": "2024-05-10T15:50:00Z"
}
// canonical: task
{
"external_ref": "todo_507",
"matter_external_ref": "case_71",
"key_date_external_ref": "kd_compl",
"name": "Order searches",
"description": null,
"assignee_person_external_ref": "person_anna",
"due_at": "2024-05-17T00:00:00Z",
"completed_at": "2024-05-16T09:30:00Z",
"created_at": "2024-05-10T15:50:00Z"
}
// inserted: KeyDate (matter-db)
{
"id": "kdate_f2",
"matterId": "mat_k4p",
"name": "Completion",
"dueAt": "2024-06-28T00:00:00Z",
"createdAt": "2024-05-10T15:50:00Z"
}
// inserted: Task (matter-db)
{
"id": "task_9m",
"matterId": "mat_k4p",
// registry: kd_compl
"keyDateId": "kdate_f2",
"name": "Order searches",
"description": null,
"dueAt": "2024-05-17T00:00:00Z",
"completedAt": "2024-05-16T09:30:00Z",
"createdAt": "2024-05-10T15:50:00Z"
}
// inserted: TaskAssignment (matter-db)
// re-normalised from the assignee ref
{
"id": "taskass_a3",
"taskId": "task_9m",
"identityId": "idn_anna"
}
// inserted: ImportedRecord × 2 (matter-db, same transactions)
[
{
"entityType": "KeyDate",
"entityId": "kdate_f2",
"externalRef": "kd_compl",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
},
{
"entityType": "Task",
"entityId": "task_9m",
"externalRef": "todo_507",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
]
10. chat_message
Chat history lands as real messaging-service Message rows so the Messages tab shows continuity. The Channel is never staged: v3 creates one per matter at matter creation (channel id = matter id); the importer writes into it and derives ChannelParticipants from the distinct authors.
export const chatMessageImport = z.object({
externalRef: pk,
matterExternalRef: parent(matterImport),
authorPersonExternalRef: externalRef,
body: z.string().min(1),
sentAt: z.coerce.date(),
});
// canonical: chat_message
{
"external_ref": "msg_2210",
"matter_external_ref": "case_71",
"author_person_external_ref": "person_john",
"body": "Any news on the searches?",
"sent_at": "2024-05-20T08:12:00Z"
}
// inserted: Message (messaging-db)
{
"id": "msg_h6",
// channel id IS the matter id
"channelId": "mat_k4p",
// registry: person_john
"authorId": "idn_john",
"body": "Any news on the searches?",
"sentAt": "2024-05-20T08:12:00Z"
}
// inserted: ChannelParticipant (messaging-db)
// derived: one per distinct author not yet in the channel
{
"id": "chpar_j1",
"channelId": "mat_k4p",
"identityId": "idn_john",
"status": "ACTIVE"
}
// inserted: ImportedRecord (messaging-db, same transaction)
{
"entityType": "Message",
"entityId": "msg_h6",
"externalRef": "msg_2210",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
11. call
The call record lands as matter-service's Call, the matter-facing record behind attendance notes. Telephony-service is live phone infrastructure, not history, so it is not a target. Recordings ride sidecar.
Expectation from review (Jaime): this table will be empty for most sources — practice management systems rarely export call history. It exists because v2 does hold platform calls (67 in dev data alone); low priority for anything else.
export const callImport = z.object({
externalRef: pk,
matterExternalRef: parent(matterImport),
// INBOUND | OUTBOUND
direction: CallDirectionSchema,
// the platform-side participant, when known
personExternalRef: externalRef.nullable(),
// the other party as the source recorded them
otherPartyLabel: z.string().nullable(),
startedAt: z.coerce.date(),
endedAt: z.coerce.date().nullable(),
// recording, if any (rule 5)
recordingPayloadUri: z.string().nullable(),
recordingPayloadSha256: z.string().length(64).nullable(),
// transcript or attendance note, plain text
noteText: z.string().nullable(),
});
// canonical: call
{
"external_ref": "call_118",
"matter_external_ref": "case_71",
"direction": "OUTBOUND",
"person_external_ref": "person_anna",
"other_party_label": "Seller's solicitor (Hartley & Co)",
"started_at": "2024-05-10T15:00:00Z",
"ended_at": "2024-05-10T15:22:00Z",
"recording_payload_uri": null,
"recording_payload_sha256": null,
"note_text": "Discussed completion date; agreed 28 June."
}
// inserted: Call (matter-db)
{
"id": "call_v5",
"matterId": "mat_k4p",
"direction": "OUTBOUND",
// registry: person_anna
"personId": "idn_anna",
"otherPartyLabel": "Seller's solicitor (Hartley & Co)",
"startedAt": "2024-05-10T15:00:00Z",
"endedAt": "2024-05-10T15:22:00Z",
"noteText": "Discussed completion date; agreed 28 June."
}
// inserted: ImportedRecord (matter-db, same transaction)
{
"entityType": "Call",
"entityId": "call_v5",
"externalRef": "call_118",
"importRunId": "imprun_platform-v2_firm_9y2_1_20260815"
}
Deliberately not in this catalogue
Flagged hard in review (Jaime, agreed): these omissions are deliberate but not comfortable — the catalogue expands ASAP, and the known-needed list is custom fields, emails, billed time / WIP, invoices / costs, and trust balances. The money items below are one slice with the money team; the rest are their own passes.
- Email. No full spec yet. Emails are identity-owned in v3 (a mailbox
Grant); a matter only associates with them, so filed correspondence may reduce to ordinary matter files, real Communication rows, or Nylas backfill on reconnection. Needs its own design pass first.
- Money. No full spec yet. Spans four services and the intent/settlement split; any schema needs the money team in the room.
- Comments. Unclear whether importing them makes sense at all; parked until a source proves the need.
- Custom fields. Deferred. They barely matter for a v2 migration; they get designed when the first Clio-like source (where they carry half the data) is on the horizon.