> For the complete documentation index, see [llms.txt](https://docs.durohub.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.durohub.com/core-concepts/importing-components.md).

# Importing Components

Duro supports bulk-importing components from a spreadsheet through the GraphQL API. An import runs in two steps — you **prepare** an import to validate it, then **start** it to write the results — and you poll the resulting job for progress and per-row outcomes.

{% hint style="warning" %}
**Imports are atomic (all-or-nothing).** If any row fails, the entire import is rolled back and nothing is written to your library. A row that passed validation is **not** guaranteed to have been written — see [Atomic behavior](#atomic-behavior-all-or-nothing) below.
{% endhint %}

## Required Headers

All import operations require the library headers:

```bash
curl 'https://api.durohub.com/graphql' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'x-organization: @your-org' \
  -H 'x-library: @your-org/your-library' \
  -H 'Content-Type: application/json' \
  -d '{"query": "..."}'
```

***

## The Import Workflow

Imports live under the `componentImport` namespace on both `Query` and `Mutation`.

### 1. Prepare (validate)

`prepare` parses and validates your rows and returns a `PreparedImportOutput` describing what would happen — how many components would be created, how many rows are valid, and any validation errors. Nothing is written to the library at this stage.

```graphql
mutation PrepareImport {
  componentImport {
    prepare(input: {
      libraryId: "library-uuid"
      fileId: "uploaded-file-uuid"
      keyColumn: CPN
    }) {
      id
      status
      totalComponents
      validCount
      errorCount
      newComponentCount
      linkedToExistingCount
      errors {
        rowNumber
        field
        code
        message
      }
    }
  }
}
```

The `keyColumn` (`NAME`, `EID`, or `CPN`) selects the identifier column used to match import rows to existing components for update mode. A row whose value resolves to exactly one live component is treated as an update of that component; otherwise it is a create.

### Column mappings

`prepare` accepts an optional list of `ColumnMappingInput`s telling Duro which spreadsheet column feeds which field. Each mapping pairs a `columnIndex` (or column header) with a `targetField`. Besides the plain component fields, `targetField` supports prefixed families — `attr:` for custom attributes, `hierarchy:` for category hierarchy values, `rel:` for relationships, and `src:` for sourcing.

#### Sourcing (`src:`) targets

Map a column to a `src:` target to bring a component's source — its manufacturer part and the quote for it — in on the same row as the component itself:

| `targetField`         | Maps to                                               |
| --------------------- | ----------------------------------------------------- |
| `src:manufacturer`    | Manufacturer name for the source                      |
| `src:mpn`             | Manufacturer part number                              |
| `src:mfrDescription`  | Description on the manufacturer part                  |
| `src:datasheet`       | Datasheet URL for the manufacturer part               |
| `src:distributor`     | Distributor name for the quote                        |
| `src:dpn`             | Distributor part number                               |
| `src:distDescription` | Description on the quote                              |
| `src:packageType`     | Package type on the quote (for example `Tape & Reel`) |
| `src:packageQuantity` | Units per package on the quote                        |
| `src:minQuantity`     | Minimum order quantity on the quote                   |
| `src:unitPrice`       | Price per unit                                        |
| `src:leadTime`        | Lead time for the quote                               |

A few things to know about the sourcing columns:

* **Values are carried through as written.** `"$1.00"` and `"1 Day"` are kept as the text you supplied rather than normalized at mapping time, so failure reports can echo back exactly what was in the cell.
* **Manufacturer, distributor, and package values stay as names.** They are resolved to sourcing records later, when the import is applied.
* **A row with every sourcing cell blank yields no source.** It is treated as a component-only row, not as an empty source.
* **One source per component row.** Additional sources on continuation rows are not supported; each row carries at most one source.

`src:` mappings travel through the same prepare/start flow as the rest of the import: `prepare` parses them alongside the component columns and reports any row errors, and `start` records the parsed sourcing rows as part of the job it creates.

#### Descriptions, datasheets, and package types

The four descriptive sourcing targets have behavior worth knowing before you build a sheet around them:

* **Descriptions are length-limited, and over-long values are refused.** `src:mfrDescription` and `src:distDescription` are held to the same length limit as the sourcing API's own `description` fields. A cell that exceeds it fails its row rather than being silently truncated — a half-stored note is a worse outcome than a rejected one.
* **`src:datasheet` must be an absolute URL, and it never replaces an existing datasheet.** The value is validated the same way the API validates a datasheet URL elsewhere, including requiring a protocol: `https://example.com/ds.pdf` is accepted, a bare `example.com/ds.pdf` is not. When the manufacturer part is created, the URL becomes a `DATASHEET` document linked to that part, committed together with it. Against a part that already exists, the datasheet is created only if that part has none; if it already has one, the URL is skipped. The skip is not currently called out anywhere in the row result — the row reports unchanged if the datasheet was its only part-level change, which reads the same as a row that restated existing values. An import never edits, unlinks, or archives a document, and never adds a second datasheet to a part — documents are shared between parts, so a single spreadsheet cell must not be able to change parts that were never in the sheet.
* **`src:packageType` is a lookup, never a create.** Unlike manufacturer and distributor names — which are your own data and are created on demand — package types come from a fixed, shared list. The name is matched case-insensitively; a name Duro does not recognize fails only that row, and the error lists the valid spellings (the usual culprit is punctuation, such as `Tape and Reel` for `Tape & Reel`). A name that matches two package types differing only by case is refused rather than guessed at.
* **Package types resolve before anything is written.** An unrecognized package type fails its row without having created a manufacturer part first.

{% hint style="info" %}
**Re-importing a sheet updates the manufacturer part.** When a row's manufacturer and MPN match a part that already exists, the part is no longer reused as-is: `src:mfrDescription` is written to it and the row reports the part as `updated`. `src:datasheet` is the exception — it is only ever added to a part that has none, as described above. Quote fields, including `src:distDescription` and `src:packageType`, are applied to the matched quote as before.
{% endhint %}

### 2. Start (execute)

Pass the `id` from the prepared import to `start`. This begins an `ImportJob` that stages and promotes the rows in a single transaction.

```graphql
mutation StartImport {
  componentImport {
    start(input: { preparedImportId: "prepared-import-uuid" }) {
      id
      status
    }
  }
}
```

### 3. Poll the job

Poll `componentImport.jobStatus` until the job reaches a terminal state. Per-row results are available on terminal-state jobs.

```graphql
query ImportJobStatus {
  componentImport {
    jobStatus(jobId: "import-job-uuid") {
      id
      status
      rowResultsSummary {
        total
        created
        updated
        unchanged
        skippedDuplicate
        failedValidation
        failedPromotion
        notImported
      }
      rowResults(offset: 0, limit: 500) {
        totalCount
        hasMore
        rows {
          rowNumber
          cpn
          name
          outcome
          errors {
            field
            code
            message
          }
        }
      }
    }
  }
}
```

An `ImportJob` moves through these statuses:

| Status                     | Meaning                                              |
| -------------------------- | ---------------------------------------------------- |
| `PARSING`                  | Reading the source rows                              |
| `STAGING_COMPONENTS`       | Writing components to the transaction                |
| `STAGING_LINKS`            | Writing assembly links to the transaction            |
| `PROMOTING`                | Committing the staged changes                        |
| `COMPLETED`                | Every row was written successfully                   |
| `ROLLING_BACK`             | A failure was hit; the transaction is being reverted |
| `FAILED`                   | The import was rejected; nothing was written         |
| `CANCELLING` / `CANCELLED` | The import was cancelled before completion           |

***

## Sourcing columns (`src:` targets)

A spreadsheet column can be mapped to a component field, to a library attribute (`attr:<attribute-id>`), or to a **sourcing** target using the `src:` prefix. Sourcing targets write the manufacturer part and distributor quote for the row's component instead of a field on the component itself — see [Sourcing](/core-concepts/components.md#sourcing) for the underlying model.

| Target                | Writes to                       | Notes                                                    |
| --------------------- | ------------------------------- | -------------------------------------------------------- |
| `src:manufacturer`    | Manufacturer name               | Identifies the manufacturer part together with `src:mpn` |
| `src:mpn`             | Manufacturer part number        |                                                          |
| `src:mfrDescription`  | Manufacturer part description   |                                                          |
| `src:datasheet`       | Manufacturer part datasheet URL |                                                          |
| `src:distributor`     | Distributor name                | Identifies the quote together with `src:dpn`             |
| `src:dpn`             | Distributor part number         |                                                          |
| `src:distDescription` | Quote description               |                                                          |
| `src:packageType`     | Quote package type              | Resolved by name — see below                             |
| `src:packageQuantity` | Quote package quantity          |                                                          |
| `src:minQuantity`     | Quote minimum order quantity    |                                                          |
| `src:unitPrice`       | Quote unit price                |                                                          |
| `src:leadTime`        | Quote lead time                 |                                                          |

Only the targets in this list are recognized. Mapping a column to an unknown `src:` target is rejected when you prepare the import.

### Package type resolution

`src:packageType` carries a human-readable package name (for example `0402` or `SOIC-8`), which is resolved against the package types Duro knows about. A value that matches nothing, or that matches more than one package type, fails with a dedicated error code — see [Sourcing import errors](/advanced-topics/error-handling.md#sourcing-import-errors).

{% hint style="info" %}
Sourcing values are not validated when you prepare the import; the component rows are. Sourcing is applied after the components themselves are written, so a sourcing failure does not roll back the component import — check the job's row errors to see which sourcing values were rejected.
{% endhint %}

### Sourcing results

A job's sourcing pass reports its own counts, separate from the component row outcomes, on `ImportSourcingSummary`. `partsUpdated` counts the rows whose matched manufacturer part was actually written to:

* It is a real count, not a placeholder. Earlier versions always reported `0`, because a matched part was reused rather than updated.
* It counts changes, not restatements. A row that re-states values the part already holds is `unchanged`, so re-uploading an unedited sheet reports `partsUpdated: 0` — which is what makes an idempotent re-import provable.
* A skipped datasheet is not an update. If the row's only part-level change was `src:datasheet` and the part already has one, nothing was written and the row is reported as `unchanged`.

Introspect `ImportSourcingSummary` in [Apollo Explorer](https://api.durohub.com/graphql) for its full set of counts.

If the matched part changes identity or stops being available while the import is being applied — for example because it was edited or removed concurrently — the row fails with [`SOURCING_IMPORT_PART_IDENTITY_CHANGED`](/advanced-topics/error-handling.md#sourcing-import-errors) rather than writing to a part that is no longer the one the row matched.

***

## Atomic behavior (all-or-nothing)

Every import — create-only, update, or mixed — runs in a **single transaction**. Any error on any row (a create, an update, an assembly link, or a BOM reference) rolls back the whole import. On a rejected import **nothing is written to your library**, and no row is reported as `CREATED` or `UPDATED`.

This matters because import inserts components and links them as separate steps. Without atomicity, a parent row could fail while its children succeed, leaving orphaned children or a structurally broken BOM. All-or-nothing guarantees your library is never left in an incoherent state, and gives one predictable contract across create, update, and mixed sheets.

{% hint style="danger" %}
A row passing validation does **not** mean it was persisted. When the overall import is rejected because a *different* row failed, otherwise-valid rows are reported with the `NOT_IMPORTED` outcome. Always confirm the job reached `COMPLETED` (and check `rowResultsSummary`) before treating any row as written.
{% endhint %}

When an import is rejected, fix the offending row(s) reported in `rowResults`, then re-upload the sheet once — the rows that came back as `NOT_IMPORTED` were not partially applied and are safe to import again.

***

## Row Outcomes

Each row's `outcome` is an `ImportRowOutcome`:

| Outcome             | Meaning                                                                                                                     |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `CREATED`           | A new component was created from this row                                                                                   |
| `UPDATED`           | A matched component was updated because its data changed                                                                    |
| `UNCHANGED`         | The row equalled the existing component; a suppressed no-op                                                                 |
| `SKIPPED_DUPLICATE` | The row duplicated another row in the same import and was skipped                                                           |
| `FAILED_VALIDATION` | The row failed validation (see `errors`)                                                                                    |
| `FAILED_PROMOTION`  | The row was valid but failed while being committed (see `errors`)                                                           |
| `NOT_IMPORTED`      | The row was otherwise valid but the import was rejected because another row failed, so it was rolled back and never written |

`CREATED`, `UPDATED`, and `UNCHANGED` only appear when the whole job reaches `COMPLETED`. On a rejected import, the failing row(s) carry `FAILED_VALIDATION` or `FAILED_PROMOTION` and every other row carries `NOT_IMPORTED`.

## Results Summary

`rowResultsSummary` (`ImportRowResultsSummary`) reports counts by outcome across the whole job. Every field is a non-null `Int`:

| Field              | Meaning                                                                    |
| ------------------ | -------------------------------------------------------------------------- |
| `total`            | Total rows in the import                                                   |
| `created`          | Rows that created a new component                                          |
| `updated`          | Rows that updated an existing component                                    |
| `unchanged`        | Matched rows that were no-ops                                              |
| `skippedDuplicate` | Rows skipped as duplicates within the import                               |
| `failedValidation` | Rows that failed validation                                                |
| `failedPromotion`  | Rows that failed while being committed                                     |
| `notImported`      | Otherwise-valid rows that were rolled back because the import was rejected |

### Example: a rejected import

One bad row rejects the whole import. Here row 2 failed validation, and the remaining valid rows are reported as `notImported` rather than created:

```json
{
  "data": {
    "componentImport": {
      "jobStatus": {
        "status": "FAILED",
        "rowResultsSummary": {
          "total": 3,
          "created": 0,
          "updated": 0,
          "unchanged": 0,
          "skippedDuplicate": 0,
          "failedValidation": 1,
          "failedPromotion": 0,
          "notImported": 2
        },
        "rowResults": {
          "totalCount": 3,
          "hasMore": false,
          "rows": [
            { "rowNumber": 1, "cpn": "100-0001", "outcome": "NOT_IMPORTED", "errors": [] },
            {
              "rowNumber": 2,
              "cpn": "100-0002",
              "outcome": "FAILED_VALIDATION",
              "errors": [
                { "field": "name", "code": "REQUIRED", "message": "Name is required" }
              ]
            },
            { "rowNumber": 3, "cpn": "100-0003", "outcome": "NOT_IMPORTED", "errors": [] }
          ]
        }
      }
    }
  }
}
```

Because `created` is `0` and `status` is `FAILED`, no component was written — including row 1 and row 3, which validated fine. Fix row 2 and re-upload.

***

## Next Steps

* Learn how to [create and update Components](/core-concepts/components.md) individually
* See [Error Handling](/advanced-topics/error-handling.md) for general API error patterns


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.durohub.com/core-concepts/importing-components.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
