User Import Guide#
The User Import tool loads user records into FOLIO from JSON Lines files. It provides an alternative to FOLIO’s built-in /user-import API with additional features for field protection and flexible user matching.
Overview#
User Import:
Reads individual user objects from JSON Lines files
Supports user objects compatible with both mod-user-import and mod-users record schemas
Resolves reference data (patron groups, address types, departments, service points) by name or code
Handles service point assignments via the
/service-points-usersAPICreates request preferences and permission user records for new users
Provides job-level and per-record field protection during updates
Supports partial updates (only update present fields) or full record replacement (default)
Offers flexible user matching (username, barcode, externalSystemId) with forced matching on
idBulk deletion of matched users via
--delete-all(staff and system users are protected)Batch processing with real-time progress tracking
Basic Usage#
Command Line#
folio-data-import users \
--gateway-url https://folio-snapshot-okapi.dev.folio.org \
--tenant-id diku \
--username diku_admin \
--password admin \
--library-name "My Library" \
--user-file-path users.jsonl
Using a Configuration File#
folio-data-import users config.json
Example config.json:
{
"library_name": "My Library",
"user_file_paths": ["users1.jsonl", "users2.jsonl"],
"batch_size": 250,
"user_match_key": "externalSystemId",
"fields_to_protect": ["username", "barcode"],
"default_preferred_contact_type": "email",
"only_update_present_fields": false,
"limit_simultaneous_requests": 10
}
Command-Line Parameters#
Connection Parameters#
Parameter |
Environment Variable |
Description |
|---|---|---|
|
|
FOLIO gateway URL (required) |
|
|
FOLIO tenant identifier (required) |
|
|
Username for authentication (required) |
|
|
User password (required) |
|
|
ECS member tenant ID (for multi-tenant environments) |
Job Configuration#
Parameter |
Environment Variable |
Default |
Description |
|---|---|---|---|
|
|
Required |
Library name for the import job |
|
- |
Required |
Path(s) to JSON Lines file(s). Supports multiple paths and glob patterns. |
|
|
250 |
Users per batch (1-1000) |
|
- |
|
Match key: |
|
- |
|
Default contact type (see table below) |
|
|
(none) |
Comma-separated list of field paths to protect |
|
- |
|
Only update fields present in input |
|
|
|
Delete users in file(s) instead of creating/updating |
|
|
10 |
Max concurrent HTTP requests (1-100) |
|
- |
Current directory |
Base path for report files |
|
- |
(none) |
Path to JSON config file (overrides CLI parameters) |
|
- |
|
Skip confirmation prompt for destructive operations (e.g. |
|
- |
|
Disable progress display |
Preferred Contact Types#
The --default-preferred-contact-type parameter accepts either the ID or name:
ID |
Name |
|---|---|
|
|
|
|
|
|
|
|
|
|
User Data Format#
JSON Lines Format#
Input must be in JSON Lines format (.jsonl) - one user object per line. Each user object should be compatible with FOLIO’s mod-users API user record format.
Note
This tool does not support the full mod-user-import payload format (which wraps users in a users array). It reads individual user objects directly, one per line.
Note
This tool does not support creating custom field definitions or department definitions. These must already exist in FOLIO.
Example users.jsonl:
{"username": "jdoe", "externalSystemId": "12345", "barcode": "1001", "active": true, "patronGroup": "undergraduate", "personal": {"lastName": "Doe", "firstName": "John", "email": "jdoe@example.edu"}}
{"username": "asmith", "externalSystemId": "12346", "barcode": "1002", "active": true, "patronGroup": "faculty", "personal": {"lastName": "Smith", "firstName": "Alice", "email": "asmith@example.edu"}}
User Object Fields#
Common fields include:
Identification:
username: Unique login identifierexternalSystemId: External system identifierbarcode: User barcodeid: FOLIO UUID (if present, forces matching on this field)active: Boolean status
Personal Information (personal object):
lastName: Last name (required)firstName: First nameemail: Email addressphone: Phone numberpreferredContactTypeId: Preferred contact type (ID or name)addresses: Array of address objects
Group and Department:
patronGroup: Patron group name or UUID (required)departments: Array of department names or UUIDs
Service Points:
servicePointsUser: Service point assignment object (see below)
Reference Data Resolution#
The importer automatically resolves human-friendly names to UUIDs for:
Field |
API Endpoint |
Key Field |
|---|---|---|
|
|
|
|
|
|
|
|
|
Service point codes |
|
|
You can use either the human-friendly name or the UUID directly:
{"patronGroup": "undergraduate", ...}
{"patronGroup": "54e17c4c-e315-4c99-9bb6-6c2f31e3a9e5", ...}
User Matching#
Match Keys#
The importer matches users in FOLIO using the configured match key:
# Match by externalSystemId (default)
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--user-match-key externalSystemId
# Match by username
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--user-match-key username
# Match by barcode
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--user-match-key barcode
Forced Matching on id#
If a user record includes an id field, the importer always matches on id regardless of the configured match key. This ensures that records with explicit UUIDs update the correct user:
{"id": "550e8400-e29b-41d4-a716-446655440000", "username": "jdoe", "externalSystemId": "12345", "active": true, "patronGroup": "undergraduate", "personal": {"lastName": "Doe", "firstName": "John"}}
Field Protection#
Job-Level Protection#
Protect specific fields from being overwritten during updates:
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--fields-to-protect username,barcode,personal.email
Nested fields use dot notation: personal.email, personal.addresses
Per-Record Protection#
Individual user records can specify their own protected fields via customFields.protectedFields (comma-separated string):
{"username": "jdoe", "externalSystemId": "12345", "active": true, "patronGroup": "undergraduate", "personal": {"lastName": "Doe", "firstName": "John"}, "customFields": {"protectedFields": "barcode,personal.phone"}}
Job-level and per-record protections are combined.
Update Only Present Fields#
The --update-only-present-fields option enables partial updates where only fields present in the input are modified:
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--update-only-present-fields
When enabled, missing fields in the input are preserved from the existing record rather than being cleared. This is useful for targeted updates.
When disabled (the default), the incoming record completely replaces the existing record. Only the user’s id is preserved from the original; all other fields come from the incoming record. Any fields present in the existing record but absent from the incoming record are removed. Protected fields (via --fields-to-protect or per-record customFields.protectedFields) are always re-applied after the replacement.
Warning
Breaking change in v0.6.0: Prior to v0.6.0, the default update behavior incorrectly merged the incoming record into the existing record, preserving any fields absent from the incoming data. This was an implementation defect — it did not match the tool’s stated full-replacement semantics. Starting in v0.6.0, the default behavior correctly performs a full replacement: only the user’s id is carried over from the existing record, and all other fields are taken from the incoming record. Fields present in the existing record but missing from the incoming record are removed.
If your workflow relied on the previous merge behavior, add --update-only-present-fields to preserve the old behavior.
Note
The preferred contact type is also preserved from the existing record when the incoming record does not include a preferredContactTypeId. The configured default is only applied when neither the incoming nor the existing record has a valid value.
User Deletion#
Warning
Use this feature with caution. No dependency checks are performed before a user is deleted, so this could result in orphaned circulation transactions and other unexpected system behavior.
The --delete-all flag changes the tool from import mode to deletion mode. Instead of creating or updating users, it deletes users found in the input file(s) from FOLIO:
folio-data-import users \
--library-name "My Library" \
--user-file-path users_to_delete.jsonl \
--delete-all
Or via a config file:
{
"library_name": "My Library",
"user_file_paths": ["users_to_delete.jsonl"],
"user_match_key": "username",
"delete_all": true
}
Note
The --delete-all CLI flag overrides the config file value when both are specified.
How Deletion Works#
Each user in the input file is matched against FOLIO using the configured match key
If a matching user is found, the tool checks the user’s
typefieldStaff, dcb, shadow, and system users are automatically skipped to prevent accidental removal
For eligible users (e.g.,
patrontype), the following records are deleted:The user record itself (
/users/{id})Associated request preferences (
/request-preference-storage/request-preference/{id})Associated permission user record (
/perms/users/{id})Associated service points user record (
/service-points-users/{id})
An interactive confirmation prompt is displayed before deletions begin. Press Enter to proceed or
Ctrl+Cto abort. Use--yes/-yto skip the prompt (required in non-interactive environments such as CI/CD pipelines or cron jobs).
Deletion Statistics#
Deletion progress is tracked alongside other statistics:
Deleted: Users successfully removed
Failed: Users that could not be deleted (e.g., due to API errors)
Staff/system users that are skipped are not counted in either category
Service Point Assignment#
Assign service points using codes (resolved automatically) or UUIDs:
{"username": "jdoe", "externalSystemId": "12345", "active": true, "patronGroup": "staff", "personal": {"lastName": "Doe", "firstName": "John"}, "servicePointsUser": {"servicePointsIds": ["MAIN-CIRC", "LAW-LIB"], "defaultServicePointId": "MAIN-CIRC"}}
The servicePointsUser object:
servicePointsIds: Array of service point codes or UUIDsdefaultServicePointId: Default service point code or UUID
The importer handles service point assignments separately via the /service-points-users API.
Addresses#
Include multiple addresses per user:
{"username": "jdoe", "externalSystemId": "12345", "active": true, "patronGroup": "faculty", "personal": {"lastName": "Doe", "firstName": "John", "addresses": [{"countryId": "US", "addressLine1": "123 Main St", "city": "Springfield", "region": "IL", "postalCode": "62701", "addressTypeId": "Home", "primaryAddress": true}, {"addressLine1": "456 Oak Ave", "city": "Springfield", "region": "IL", "postalCode": "62702", "addressTypeId": "Work"}]}}
Address type names (like “Home”, “Work”) are resolved to UUIDs automatically.
Multiple Input Files#
Process multiple files using glob patterns or multiple paths:
# Multiple explicit paths
folio-data-import users \
--library-name "My Library" \
--user-file-path users1.jsonl \
--user-file-path users2.jsonl
# Glob pattern
folio-data-import users \
--library-name "My Library" \
--user-file-path "data/*.jsonl"
Error Handling#
Failed Records#
Failed imports are automatically logged to failed_user_import_TIMESTAMP.txt in the current directory (or the path specified by --report-file-base-path). The file contains the user objects (one per line in JSON Lines format) that failed to import.
Example failed record:
{"username": "jdoe", "externalSystemId": "12345", "barcode": "1001", "active": true, "patronGroup": "undergraduate", "personal": {"lastName": "Doe", "firstName": "John", "email": "jdoe@example.edu"}}
Error details are logged to the console/log output, not written to the failed records file.
Custom Report Path#
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--report-file-base-path /path/to/reports/
Common Validation Errors#
Missing patron group: Patron group doesn’t exist in FOLIO
Duplicate unique field: Username/barcode/
externalSystemIdalready existsInvalid service point code: Service point code not found
Missing required field:
library-nameoruser-file-pathnot provided
Progress Tracking#
Real-time progress bars show:
Total users processed
Successful creates/updates
Deleted users (when using
--delete-all)Failed imports
Processing speed and time
Disable for automation:
folio-data-import users \
--library-name "My Library" \
--user-file-path users.jsonl \
--no-progress
Environment Variables#
Connection parameters can be set via environment variables:
export FOLIO_GATEWAY_URL="https://folio-snapshot-okapi.dev.folio.org"
export FOLIO_TENANT_ID="diku"
export FOLIO_USERNAME="diku_admin"
export FOLIO_PASSWORD="admin"
export FOLIO_LIBRARY_NAME="My Library"
export FOLIO_USER_IMPORT_BATCH_SIZE="250"
export FOLIO_FIELDS_TO_PROTECT="username,barcode"
export FOLIO_LIMIT_ASYNC_REQUESTS="10"
Workflow Example#
# 1. Prepare user data in JSON Lines format
# Each line is a complete JSON object
# 2. Set environment variables (optional)
export FOLIO_GATEWAY_URL="https://folio-snapshot-okapi.dev.folio.org"
export FOLIO_TENANT_ID="diku"
export FOLIO_USERNAME="diku_admin"
export FOLIO_PASSWORD="admin"
# 3. Import users with field protection
folio-data-import users \
--library-name "Main Library" \
--user-file-path new_students.jsonl \
--user-match-key externalSystemId \
--fields-to-protect username,barcode \
--default-preferred-contact-type email \
--batch-size 250
# 4. Check results
# - New users are created
# - Existing users updated (protected fields preserved)
# - Errors logged to failed_user_import_TIMESTAMP.txt
Complete Example#
Full-featured user import with all options:
folio-data-import users \
--gateway-url https://folio-snapshot-okapi.dev.folio.org \
--tenant-id diku \
--username diku_admin \
--password admin \
--library-name "Main Library" \
--user-file-path new_faculty.jsonl \
--user-match-key externalSystemId \
--fields-to-protect username,barcode,personal.email \
--default-preferred-contact-type email \
--update-only-present-fields \
--batch-size 250 \
--limit-async-requests 20 \
--report-file-base-path /var/log/folio/
Comparison with mod-user-import#
Feature |
mod-user-import |
folio-data-import users |
|---|---|---|
Input format |
Wrapped JSON with |
JSON Lines (one user object per line) |
API approach |
Single POST to |
Individual POST/PUT to |
Service points assignment |
N/A |
User assignments via |
Field protection |
|
Job-level and per-record for any field |
Contact type |
|
Same values plus IDs ( |
Match key |
|
Configurable with forced matching on |
Custom fields |
Can define and manage via |
Values only (definitions must exist in FOLIO) |
Departments |
Can create via |
Values only (must already exist in FOLIO) |
Request preferences |
Per-user with delivery/fulfillment settings |
Auto-created for new users |
Batch processing |
Single request |
Configurable batch size (default 250) |
Progress tracking |
None |
Real-time progress bars |
Concurrent requests |
N/A |
Configurable (default 10, max 100) |
Bulk deletion |
Deactivation only ( |
|
Update behavior |
Full replacement (default) or partial update (top-level fields + address deep merge) |
Full replacement (default) or partial update (all fields) |