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-users API

  • Creates 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 id

  • Bulk 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

--gateway-url

FOLIO_GATEWAY_URL

FOLIO gateway URL (required)

--tenant-id

FOLIO_TENANT_ID

FOLIO tenant identifier (required)

--username

FOLIO_USERNAME

Username for authentication (required)

--password

FOLIO_PASSWORD

User password (required)

--member-tenant-id

FOLIO_MEMBER_TENANT_ID

ECS member tenant ID (for multi-tenant environments)

Job Configuration#

Parameter

Environment Variable

Default

Description

--library-name

FOLIO_LIBRARY_NAME

Required

Library name for the import job

--user-file-path / --user-file-paths

-

Required

Path(s) to JSON Lines file(s). Supports multiple paths and glob patterns.

--batch-size

FOLIO_USER_IMPORT_BATCH_SIZE

250

Users per batch (1-1000)

--user-match-key

-

externalSystemId

Match key: username, barcode, or externalSystemId

--default-preferred-contact-type

-

email

Default contact type (see table below)

--fields-to-protect

FOLIO_FIELDS_TO_PROTECT

(none)

Comma-separated list of field paths to protect

--update-only-present-fields

-

false

Only update fields present in input

--delete-all

FOLIO_DELETE_ALL_USERS

false

Delete users in file(s) instead of creating/updating

--limit-async-requests

FOLIO_LIMIT_ASYNC_REQUESTS

10

Max concurrent HTTP requests (1-100)

--report-file-base-path

-

Current directory

Base path for report files

--config-file

-

(none)

Path to JSON config file (overrides CLI parameters)

--yes / -y

-

false

Skip confirmation prompt for destructive operations (e.g. --delete-all)

--no-progress

-

false

Disable progress display

Preferred Contact Types#

The --default-preferred-contact-type parameter accepts either the ID or name:

ID

Name

001

mail

002

email

003

text

004

phone

005

mobile

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 identifier

  • externalSystemId: External system identifier

  • barcode: User barcode

  • id: FOLIO UUID (if present, forces matching on this field)

  • active: Boolean status

Personal Information (personal object):

  • lastName: Last name (required)

  • firstName: First name

  • email: Email address

  • phone: Phone number

  • preferredContactTypeId: 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

patronGroup

/groups

group

addresses[].addressTypeId

/addresstypes

addressType

departments[]

/departments

name

Service point codes

/service-points

code

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#

  1. Each user in the input file is matched against FOLIO using the configured match key

  2. If a matching user is found, the tool checks the user’s type field

  3. Staff, dcb, shadow, and system users are automatically skipped to prevent accidental removal

  4. For eligible users (e.g., patron type), 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})

  5. An interactive confirmation prompt is displayed before deletions begin. Press Enter to proceed or Ctrl+C to abort. Use --yes/-y to 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 UUIDs

  • defaultServicePointId: 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/externalSystemId already exists

  • Invalid service point code: Service point code not found

  • Missing required field: library-name or user-file-path not 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 users array

JSON Lines (one user object per line)

API approach

Single POST to /user-import

Individual POST/PUT to /users

Service points assignment

N/A

User assignments via /service-points-users

Field protection

updateOnlyPresentFields (top-level fields preserved; addresses deep-merged by type)

Job-level and per-record for any field

Contact type

mail, email, text, phone, mobile

Same values plus IDs (001-005)

Match key

externalSystemId only

Configurable with forced matching on id

Custom fields

Can define and manage via included

Values only (definitions must exist in FOLIO)

Departments

Can create via included

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 (deactivateMissingUsers)

--delete-all flag (skips staff/system users)

Update behavior

Full replacement (default) or partial update (top-level fields + address deep merge)

Full replacement (default) or partial update (all fields)

See Also#