> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trytalkvalue.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe: CSV import end-to-end

> Analyze a CSV, create the import job, poll until terminal, and export any rejected rows. All from the CLI.

Run the full CSV bulk-import workflow from the command line: upload and analyze the file, create the job with a column mapping, poll until the job reaches a terminal state, and pull the rejected rows if any landed. The recipe composes every leaf in [`path import`](/cli/commands/path/import/index).

<Note>
  **Prerequisites:** authenticated CLI, a CSV file with a header row, and a channel ID to import the contacts into. Find a channel ID with [`path channel list`](/cli/commands/path/channel/list).
</Note>

## Steps

<Steps>
  <Step title="Analyze the CSV">
    ```bash theme={null}
    talkvalue path import analyze --file ./contacts.csv --json
    ```

    Uploads the file, returns a `fileKey`, the headers, a preview, and the server's suggested column mapping. Note the `fileKey` and the column indices. You need both for the next step.
  </Step>

  <Step title="Capture the fileKey">
    ```bash theme={null}
    FILE_KEY=$(talkvalue path import analyze --file ./contacts.csv --json | jq -r '.data.fileKey')
    echo "$FILE_KEY"
    ```

    Saves the key in a shell variable so the rest of the script can reference it without re-uploading.
  </Step>

  <Step title="Create the import job">
    ```bash theme={null}
    SOURCE_CHANNEL_ID=7

    JOB_ID=$(
      talkvalue path import create \
        --file-key "$FILE_KEY" \
        --source-id "$SOURCE_CHANNEL_ID" \
        --mode UPDATE \
        --mapping 0:EMAIL \
        --mapping 1:FIRST_NAME \
        --mapping 2:LAST_NAME \
        --json \
      | jq -r '.data.importJobId'
    )
    echo "Job: $JOB_ID"
    ```

    Returns immediately with the `importJobId`. The job runs in the background. Your terminal is free for the polling loop below.

    `--mode UPDATE` overwrites fields on existing contacts matched by email. Switch to `--mode SKIP` if you want to leave existing records untouched and treat collisions as duplicates.
  </Step>

  <Step title="Poll until the job reaches a terminal state">
    ```bash theme={null}
    while :; do
      STATUS=$(talkvalue path import get "$JOB_ID" --json | jq -r '.data.status')
      echo "  status: $STATUS"
      case "$STATUS" in
        COMPLETED|PARTIAL_SUCCESS|FAILED) break ;;
      esac
      sleep 5
    done
    ```

    The job moves through `PENDING` → `RUNNING` → one of `COMPLETED`, `PARTIAL_SUCCESS`, or `FAILED`. The loop exits on any terminal state. Tighten or relax `sleep` based on import size.
  </Step>

  <Step title="Export rejected rows when there are failures">
    ```bash theme={null}
    FAILED=$(talkvalue path import get "$JOB_ID" --json | jq -r '.data.failedCount')
    if [ "$FAILED" -gt 0 ]; then
      talkvalue path import failed-export "$JOB_ID" > "failed-$JOB_ID.csv"
      echo "$FAILED rejected row(s) written to failed-$JOB_ID.csv"
    else
      echo "No failures."
    fi
    ```

    Pulls the full rejected-row CSV only when the job had failures. The output header is `rowNum,errorCode,errorMessage,rawValue`. Fix the rows, re-run the import on the corrected file, and the failure count drops.
  </Step>
</Steps>

## Variants

### Treat duplicates as silent skips

```bash theme={null}
talkvalue path import create \
  --file-key "$FILE_KEY" \
  --source-id "$SOURCE_CHANNEL_ID" \
  --mode SKIP \
  --mapping 0:EMAIL --mapping 1:NAME --mapping 2:COMPANY_NAME \
  --json
```

`SKIP` is the right mode for a weekly registrant export when you want every new email but you don't want to overwrite manual edits made in the dashboard.

### Group failures by error code before fixing

```bash theme={null}
talkvalue path import failed-export "$JOB_ID" \
  | tail -n +2 \
  | awk -F',' '{print $2}' \
  | sort | uniq -c | sort -rn
```

Counts failures by `errorCode` so you can prioritize the fix that unlocks the most rows.

### One-shot script

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

FILE="$1"
SOURCE_CHANNEL_ID="$2"

FILE_KEY=$(talkvalue path import analyze --file "$FILE" --json | jq -r '.data.fileKey')
JOB_ID=$(
  talkvalue path import create \
    --file-key "$FILE_KEY" \
    --source-id "$SOURCE_CHANNEL_ID" \
    --mode UPDATE \
    --mapping 0:EMAIL --mapping 1:FIRST_NAME --mapping 2:LAST_NAME \
    --json | jq -r '.data.importJobId'
)

while :; do
  STATUS=$(talkvalue path import get "$JOB_ID" --json | jq -r '.data.status')
  case "$STATUS" in COMPLETED|PARTIAL_SUCCESS|FAILED) break ;; esac
  sleep 5
done

FAILED=$(talkvalue path import get "$JOB_ID" --json | jq -r '.data.failedCount')
[ "$FAILED" -gt 0 ] && talkvalue path import failed-export "$JOB_ID" > "failed-$JOB_ID.csv"
echo "Final status: $STATUS, $FAILED failure(s)"
```

Drop into `import-csv.sh`, then run `./import-csv.sh ./contacts.csv 7`.

## Tips

* Valid mapping target fields: `EMAIL`, `FIRST_NAME`, `LAST_NAME`, `NAME`, `PHONE`, `JOB_TITLE`, `COMPANY_NAME`, `ADDRESS`, `LINKEDIN_URL`, `X_URL`, `JOINED_AT`. The full reference lives at [Column mapping](/path/import/column-mapping).
* Re-importing `failed-$JOB_ID.csv` after corrections clears the failure count. It does not double-import the successful rows from the first run.
* Each `import create` queues a separate job. If you re-run with the same `fileKey` you get a second job ID, not an overwrite of the first.
* Need to inspect a job later? Every import is listed by [`import list`](/cli/commands/path/import/index) and individual jobs by [`import get <id>`](/cli/commands/path/import/status).

## See also

* [`import analyze`](/cli/commands/path/import/analyze). File upload, headers, preview.
* [`import create`](/cli/commands/path/import/create). Mapping and mode reference.
* [`import get`](/cli/commands/path/import/status). Full job-record schema.
* [`import failed-export`](/cli/commands/path/import/export-failures). Rejected-row CSV format.
* [Troubleshooting](/cli/troubleshooting). Mapping errors, file errors, and rate limits.
* [AI agent skill: recipe-csv-import](/cli/agents/skills). Install as a skill.
