Contact Ingestion Overview
Scope
This page summarizes the current contact-ingestion and cleanup paths in cred-api-commercial, cred-web-commercial, and cred-ios-commercial, with emphasis on the local-tested iOS flow as of March 11, 2026.
Executive Summary
There are currently two materially different ways contacts enter CRED in this repo family:
| Path | Canonical purpose | Primary primitives |
|---|---|---|
| Bulk contact import | Ingest external datasets through modeled import artifacts, field mapping, and optional reconcile/comparison | Import, ImportField, ImportRecord |
| Bulk create and cleanup contacts | Accept client-supplied contact payloads, create contacts asynchronously, and optionally clean them up later | startBulkCreateContacts, bulkCreateContactsJob, startBulkDeleteCreatedContacts, bulkDeleteCreatedContactsJob |
The currently validated iOS device-contact path is the second one. It does not use the legacy import pipeline.
When to Use Which Path
flowchart TD
Start{"What contact-ingestion problem are you solving?"}
ImportCriteria{"Need source-field mapping,\npersisted row materialization,\nreconcile workflows,\nor comparison/import provenance?"}
CreateCriteria{"Do you already have client-supplied\ncontact objects and want async create UX,\nfast appearance in lists,\nor operator/test cleanup?"}
ImportPath[Bulk Import]
CreatePath[Bulk Create & Cleanup]
ImportCases[CRM, file, webhook,\nor Universal API ingestion]
CreateCases[Client-supplied contact objects,\njob polling, and cleanup]
ImportDoc[See Bulk Contact Import]
CreateDoc[See Bulk Create & Cleanup]
Start --> ImportCriteria
ImportCriteria -- Yes --> ImportPath
ImportCriteria -- No --> CreateCriteria
CreateCriteria -- Yes --> CreatePath
CreateCriteria -- No --> ImportPath
ImportPath --> ImportCases --> ImportDoc
CreatePath --> CreateCases --> CreateDoc
Use Bulk Contact Import for modeled import workflows and Bulk Create & Cleanup for the async client-driven create/delete path.
Use bulk import when you need:
- source-field mapping
- persisted row materialization
- reconcile workflows
- comparison or import provenance
- CRM/file/webhook/Universal API style ingestion
Use bulk create and cleanup when you need:
- client-supplied contact objects
- async create UX with job polling
- a fast path for "make these contacts appear in a list"
- an operator/test cleanup path after creation
Current Local-Tested iOS Flow
The proven local flow is:
- iOS resolves or creates a "Device Contacts" collection
- iOS calls
startBulkCreateContacts - iOS polls
bulkCreateContactsJob(jobId) - Worker creates contacts and associates them with the collection
- iOS shows the populated
Device Contactslist - Operator runs
startBulkDeleteCreatedContacts(dryRun: true)throughcred-platform query --env local - Operator verifies
candidateContactIds - Operator runs
startBulkDeleteCreatedContacts(dryRun: false) - Worker deletes the tracked contacts
- iOS refresh shows the
Device Contactslist empty again
Call Flow and Job Types
Create
- Start mutation:
startBulkCreateContacts - Status query:
bulkCreateContactsJob - Worker task:
BULK_CREATE_CONTACTS
Delete
- Start mutation:
startBulkDeleteCreatedContacts - Status query:
bulkDeleteCreatedContactsJob - Worker task:
BULK_DELETE_CREATED_CONTACTS
Related follow-up work
Some successful create/delete runs also enqueue follow-up tasks such as SET_PERSON_COMMERCIAL_DATA. Those are downstream side effects, not evidence that the bulk create/delete job itself failed.
Identifiers That Matter
For each test or support run, keep:
collectionId- bulk create
jobId - bulk create
trackingKey - bulk delete dry-run
jobId - bulk delete real
jobId
What each identifier means
| Identifier | Scope | Durability | Why it matters |
|---|---|---|---|
jobId |
One async create/delete job | Cache-backed, 24h TTL | Polling and log correlation |
trackingKey |
One bulk-create run | Persisted onto contacts | Cleanup and recovery |
collectionId |
Target contact collection | Persisted in DB | UI verification and auth behavior |
Durable Contact Marker
Bulk-created contacts are stamped with:
Contact.externalSource = trackingKey
Current tracking keys look like:
bcc:j:<bulkCreateJobId>- optionally
bcc:j:<bulkCreateJobId>:s:<clientSource>:r:<clientReferenceId>
That marker is the durable recovery hook for "which contacts came from this run?"
Observability and Recovery
Normal operator flow
Use the GraphQL job queries for normal create/delete polling:
bulkCreateContactsJob(jobId)bulkDeleteCreatedContactsJob(jobId)
Recent jobs
Recent create/delete jobs live in Redis cache for 24 hours.
Useful Redis key patterns:
bulk-create-contacts:job:<jobId>bulk-create-contacts:idempotency:<userId>:<idempotencyKey>bulk-delete-created-contacts:job:<jobId>
Older create runs
Once cache expires, bulk-create runs are still recoverable from Contact.externalSource like 'bcc:j:%'.
That is the durable query surface for:
- forgotten jobs
- orphaned test data
- support cleanup
Older delete runs
Delete jobs do not currently leave a comparable durable history marker once cache expires. After that, the signal is:
- logs
- whether tracked contacts still exist
Expected States
Create and delete jobs both use:
QUEUEDPROCESSINGCOMPLETEDPARTIALFAILED
Operationally:
COMPLETEDmeans the requested work succeededPARTIALmeans some rows/entities succeeded and some did notFAILEDmeans the job itself failed at a higher level
Expected Errors and Failure Modes
Collection authorization failure
Observed error:
FORBIDDEN: Not authorized to update the collection
Meaning:
- create was pointed at a collection the current caller could not edit
- the mutation-side auth check is functioning correctly
The validated iOS fix was to resolve/create a device-contacts collection editable by the current user instead of reusing a same-title collection owned by someone else.
Job stuck at QUEUED
Historical local causes included:
- no real worker process
- local env accidentally forcing mock-worker behavior
Current expectation is that the top-level local startup wrapper brings up the worker automatically.
Local Tools
Start the stack
cd <your-workspace>
./start-local-federated-development.sh
Tail logs
cd <your-workspace>/api/cred-api-commercial
./scripts/tail-local-bulk-contact-logs.sh \
--pattern 'bulk-create-contacts|bulk-delete-created-contacts|jobId|trackingKey|bcc:j:'
Run authenticated local GraphQL
cd <your-workspace>/mobile/cred-ios-commercial
./cred-platform query --env local 'query { __typename }'
The current local operator path intentionally uses cred-platform query --env local for cleanup and job inspection.
Current Documentation Map
- Bulk Contact Import: canonical import-pipeline behavior, triggers, and limitations
- Bulk Create Contacts and Cleanup: async create/delete API, tracking, cleanup, and operator behavior