Import Records

Bulk-import records into a workspace from a CSV file and track progress through a real-time subscription.


Use the importRecords mutation to bulk-import records into a workspace from a CSV file that has already been uploaded to storage. The file is parsed in streaming chunks and records are created in the background. Records are Record objects and a workspace is a Workspace in the API.

The import runs asynchronously: the mutation returns true once the job is accepted, then you follow its progress with the subscribeToImportExportProgress subscription. Use cancelRecordImport to stop an import that is still running.

Before calling importRecords, upload the CSV and obtain its storage key. See Uploading Files for both upload paths.

Request

The smallest call needs the storage key of the uploaded file, an ordered array of column headers, and the workspace id.

mutation ImportRecords {
  importRecords(
    input: {
      s3Key: "uploads/org_123/project_123/import.csv"
      headers: ["Title", "List", "Done", "Due Date"]
      projectId: "project_123"
    }
  )
}

projectId is the workspace CUID (the id field of Workspace). The headers map each CSV column, in order, to a record field; see Recognized headers below.

Parameters

ImportRecordsInput

ParameterTypeRequiredDescription
s3KeyString!YesStorage key of the uploaded CSV file. Validated server-side: keys containing .., a leading /, or characters outside ^[a-zA-Z0-9][a-zA-Z0-9_./-]*$ are rejected.
headersJSON!YesOrdered JSON array of column header names. Each entry maps the CSV column at that position to a record field, matched by name.
projectIdString!YesThe workspace id (CUID) to import records into.
useRustImportBooleanNoDeprecated — no longer used and has no effect. Do not set it.

Recognized headers

The headers array tells the importer how to interpret each CSV column. These built-in names are recognized; any other header is matched to a custom field of the same name in the workspace.

HeaderMaps to
TitleRecord title
ListName of the list to place the record in (created if it does not exist)
DoneCompletion status (true / false)
Start DateRecord start date
Due DateRecord due date
DescriptionRecord description
AssigneesComma-separated assignee names or emails
Created AtOriginal creation timestamp
Updated AtOriginal last-updated timestamp
Created ByOriginal creator name or email
ColorRecord color
TagsComma-separated tag names (created if they do not exist)

To produce a CSV that already lines up with a workspace’s columns, download a CSV template first.

Response

importRecords returns Boolean!true once the import has been accepted and queued.

{ "data": { "importRecords": true } }

A true response means the job is enqueued, not finished. Subscribe to import progress to know when it completes.

Full example

Custom-field columns are imported by adding their exact field name as a header alongside the built-in ones. Here the workspace has Budget and Priority custom fields:

mutation ImportRecordsWithCustomFields {
  importRecords(
    input: {
      s3Key: "uploads/org_123/project_123/import.csv"
      headers: ["Title", "List", "Done", "Due Date", "Assignees", "Tags", "Budget", "Priority"]
      projectId: "project_123"
    }
  )
}

Track import progress

Subscribe to subscribeToImportExportProgress to receive real-time progress while an import (or export) runs. The field returns JSON (nullable); each event is an untyped JSON object with the shape below.

subscription TrackImportProgress {
  subscribeToImportExportProgress(projectId: "project_123", userId: "user_123")
}

The stream is filtered by userId only: you receive every import and export event for that user. projectId is required by the schema but does not currently scope the events.

Event payload

FieldTypeDescription
typeString"IMPORT" or "EXPORT"
userIdStringid of the user who initiated the operation
usernameStringFirst name of that user
projectIdStringWorkspace slug (not the CUID you pass to the mutation)
projectNameStringWorkspace name
statusStringIN_PROGRESS, COMPLETE, CANCELLED, or ERROR
progressFloatPercentage complete, 0–100
errorStringError message, present only when status is ERROR

A sample event:

{
  "data": {
    "subscribeToImportExportProgress": {
      "type": "IMPORT",
      "userId": "clm4n8qwx000008l0g4oxdqn7",
      "username": "Ada",
      "projectId": "marketing-ops",
      "projectName": "Marketing Ops",
      "status": "IN_PROGRESS",
      "progress": 42
    }
  }
}

Note the identifier spaces differ: you pass the workspace CUID as projectId to importRecords, but the payload’s projectId is the workspace slug.

Cancel an import

Use cancelRecordImport to stop an import that is still running. The worker checks for the cancel flag on each chunk and aborts.

mutation CancelImport {
  cancelRecordImport(projectId: "project_123")
}

projectId accepts either the workspace id (CUID) or its slug. The mutation returns Boolean!true once the cancellation has been recorded.

{ "data": { "cancelRecordImport": true } }

Errors

CodeWhen
BAD_USER_INPUTs3Key is missing or fails validation (contains .., starts with /, or has disallowed characters). Message: Invalid s3Key.
PROJECT_NOT_FOUNDThe projectId does not resolve to a workspace, or the caller cannot access it. Message: Project was not found.
PLAN_LIMIT_REACHEDImporting would push the workspace over its plan’s per-workspace record cap. Checked before the import starts (pre-flight), against the exact same recordsPerWorkspace limit enforced everywhere else records are counted. See error codes for the structured error shape.
IMPORT_ALREADY_IN_PROGRESSAn import is already running for this workspace. Message: An import is already running for this workspace. Please wait for it to finish before starting another.
FORBIDDENThe caller lacks permission for the operation (see Permissions).

Permissions

OperationRequirement
importRecordsProject-level OWNER, ADMIN, or MEMBER (view-only and comment-only roles are rejected), the workspace must be active, and any custom role must have record creation enabled.
cancelRecordImportProject-level OWNER or ADMIN on an active workspace. Enforced in the resolver; projectId may be an id or a slug.
One import at a time

Only one import can run per workspace. A second importRecords call while one is active throws IMPORT_ALREADY_IN_PROGRESS. This guard is unconditional — it is enforced by a Redis lock plus a per-workspace BullMQ job ID for every import, with no path that silently succeeds.

Import limit scales with your plan

An import can’t push a workspace’s record count past your plan’s recordsPerWorkspace cap — the same limit enforced for every other way of creating records, not a separate import-only number. Upgrading your plan raises the cap for imports along with everything else.

Behavior notes

  • Lists and tags auto-create. Lists and tags referenced in the CSV that don’t already exist in the workspace are created during the import.
  • Custom fields match by name. Any header that isn’t a built-in name is matched to a custom field of the same name. Add custom-field columns to the headers array to import their values.
  • Upload first. The CSV must be uploaded to storage before you call importRecords. See Uploading Files to get the s3Key.