Bulk Import (CSV / JSON)
Bulk Import lets you load data into Joryio from a file: user profiles, historical events, or rows for your custom entities. Open Data → Bulk Import (/users/import) in the dashboard, drop a CSV or JSON file, map its columns, and Joryio processes it in the background while you watch the progress.
This page covers file uploads. The separate Data Import connector under Data → Data Import pulls user data from data warehouses (Snowflake, BigQuery) on a connection you configure once. Use Bulk Import for one-off files - a CRM export, a legacy list, an event backfill; use Data Import when the source of truth lives in a warehouse.
File requirements
| Formats | .csv (first row = column headers) or .json (an array of objects - anything else is rejected). |
| Upload size | Up to 100 MB per file from the dashboard. |
| Rows | Up to 1,000,000 rows per import (server-side; rows beyond the cap are not processed). |
| Concurrency | One import at a time per workspace - starting a second while one is processing is rejected. |
Files upload directly to storage via a presigned URL, then a background worker processes the job. Uploaded files are deleted after processing and expire after 24 hours regardless.
Import types
Pick the type before uploading - it changes the available mapping targets:
- Users - create or update contact profiles.
- Events - backfill historical events into the analytics store.
- Entities - load rows into one of your custom entities (you pick which).
Column mapping
After selecting a file you can add field mappings: each row maps a source column (typed exactly as it appears in your file) to a target field (picked from a dropdown).
If you add no mappings, user imports auto-detect: externalId / external_id / userId / user_id become the external ID, email and phone map to themselves, columns that look like subscription-status columns (a channel prefix plus a status-ish suffix, e.g. email_subscription_status, sms_opt_in) set channel consent, and every other column becomes a custom attribute under its own name.
With explicit mappings, only mapped columns are imported. Target fields for user imports:
| Target | Meaning |
|---|---|
externalId | Your stable user ID - the preferred identifier. |
email / phone | Contact identifiers. Email is lowercased and trimmed; phone is normalized to E.164. |
attributes.firstName, attributes.lastName, attributes.<anything> | Profile attributes (pick Custom Attribute... and type the key). |
subscriptions.email / sms / whatsapp / push | Channel-level consent status. |
Group: <name> (<channel>) | Per-group consent - sets the contact's status in a specific subscription group / consent category (stored as subscriptions.list.<listId>.<channel>). See Subscription management. |
How users are matched (dedup / identify)
Every user row must carry an externalId or an email - rows with neither are skipped and listed in the error report.
- If the row has an
externalId, Joryio matches on it (within the workspace). - Otherwise it matches on the normalized
email. - A match updates the existing profile (only the fields present in the row are written - attributes not in your file are left untouched); no match creates a new contact.
The job's summary reports both outcomes separately as Created and Updated. Re-importing the same file is safe: rows just resolve to updates.
Subscription statuses and consent safety
Status cells are parsed liberally: subscribed / true / yes / 1 mean subscribed; unsubscribed / false / no / 0 / opt_out mean unsubscribed; opted_in / double_opt_in / confirmed mean a confirmed opt-in. Blank or unrecognized values leave the channel untouched.
By default (checkbox off), consent-protective rules apply per channel and per group:
- Unsubscribes in the file always apply.
- A prior opt-out is never silently re-subscribed by an import.
- A confirmed opt-in is never downgraded to plain subscribed.
Ticking "Apply subscription status to all rows..." treats the file as the source of truth - it can re-subscribe prior opt-outs and downgrade opt-ins. You are confirming you have consent to contact those users. Either way, every change an import makes is written to the contact's subscription history with source import.
Event imports
Target fields: userId (matched against externalId), email, eventName, timestamp, and properties.<key> for event properties. Rules the worker enforces:
- Every row needs an
eventNameand auserIdoremail- and the user must already exist in the workspace, or the row is skipped (import users first). timestampis optional; an unparseable value fails the row, and implausible values (more than ~2 years past or 1 hour future) are clamped to now - the same rule as live event tracking.- Properties get the same guardrails as the live
/trackpath: size/key-count/depth limits and PII masking. - Imports are retry-safe: a re-run of the same job cannot double-insert events.
Imported events land in the same event stream as live ones, so they are visible to segments and analytics.
Entity imports
Choose the target entity first; its fields become the mapping targets. Without mappings, columns are matched to entity field names directly. Rows missing a required entity field are skipped with an error. If a row carries an _id / id column the row upserts (updates the existing record or creates it); otherwise it inserts a new record. Versioning and soft-delete follow the entity's own settings.
Monitoring an import
The Recent Imports table shows every job with its type, format, progress bar, status, and who started it, refreshing automatically while anything is running. Statuses:
| Status | Meaning |
|---|---|
| Pending / Processing | Queued, then working through the file in batches. |
| Completed | Finished - check the per-row counters. |
| Failed | The job itself failed (e.g. the uploaded file wasn't found); the error message is shown. Failed jobs are retried once automatically before landing here. |
| Expired | An old completed job whose file and error report have been cleaned up (24 hours). |
Click View on a job for the detail modal: total rows, Created / Updated / Skipped / Failed counters, and - when any rows had problems - a Download errors link with each failing row number and the reason (identifier values are redacted). The error report link is available for 24 hours after completion.
A row is Skipped when it fails validation (missing identifier, unknown user on an event import, missing required entity field); Failed counts real write errors.
Related pages
- Data Import (warehouses) - scheduled sync from Snowflake / BigQuery.
- Segments - imported attributes are immediately available to segment on.
- Subscription management - groups and consent categories that per-group import targets write to.