> 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": "..."}'
```

***

## Permissions

Imports split across two permissions, and which one you hold decides how much of an import you can see.

| You hold         | You can                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------- |
| `imports.read`   | Read import history: the job list, a job's status, and its `rowResultsSummary` counts   |
| `imports.manage` | Everything above, plus run imports — `prepare` and `start` — and read row-level results |

`imports.manage` satisfies an `imports.read` check, so a role that runs imports needs only `imports.manage`. The reverse is not true: `imports.read` never grants execution.

{% hint style="info" %}
**`errorDetails` is `null` for read-only callers.** `ImportJobOutput.errorDetails` is returned only to a caller holding `imports.manage`. A caller with just `imports.read` gets `null` for that field — the query itself still succeeds, so check for `null` rather than expecting an error.
{% endhint %}

`imports.manage` is elevated: an import creates and updates components and writes sourcing data, and it is the only permission checked on the import mutations. See [RBAC](/advanced-topics/rbac.md#imports-and-exports) before granting it.

***

## 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 row, several sources per component.** A row carries at most one source. To attach more than one source to the same component, repeat that component on consecutive rows — each continuation row contributes only its source, and reports the [`SOURCE_ONLY`](#row-outcomes) outcome.

`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 created on demand.** Package types are no longer a fixed, shared vocabulary you have to spell exactly: a name Duro does not recognize is created and then used, the same way an unknown manufacturer or distributor name is. The name is matched case-insensitively first, so `tape & reel` resolves to an existing `Tape & Reel` rather than creating a near-duplicate. A value longer than 100 characters is refused with `SOURCING_IMPORT_UNREADABLE_VALUE` — a cell that long is a mis-mapped column, not a package name.
* **Matching includes archived package types, and an archived match fails the row.** A name that matches an archived package type is not created over and is not silently revived; the row fails with `SOURCING_IMPORT_PACKAGE_TYPE_ARCHIVED`. Un-archiving is a deliberate act, not something a spreadsheet does.
* **A name that matches two package types differing only by case is still refused** with `SOURCING_IMPORT_PACKAGE_TYPE_AMBIGUOUS` rather than guessed at. Creating does not tell Duro which of the two the sheet meant.
* **Package types resolve before anything is written.** A package type that cannot be resolved fails its row without having created a manufacturer part first.

{% hint style="warning" %}
**Newly created package types are visible everywhere.** Package types are global rather than per-library, so a typo in one sheet adds a package type that every library in the organization then sees. Normalize the column before importing if you care about keeping the list tidy.
{% endhint %}

{% 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.

`jobStatus` and its `rowResultsSummary` are readable with `imports.read`; the `rowResults` rows and `errorDetails` require `imports.manage`.

```graphql
query ImportJobStatus {
  componentImport {
    jobStatus(jobId: "import-job-uuid") {
      id
      status
      rowResultsSummary {
        total
        created
        updated
        unchanged
        skippedDuplicate
        sourceOnly
        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/sourcing.md) 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, and created if unknown — 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. The lookup is case-insensitive and spans every package type, archived ones included:

| The value                                        | What happens                                                                                                              |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| Matches one live package type                    | Resolved to it                                                                                                            |
| Matches nothing                                  | The package type is **created**, then used. It becomes visible to every library                                           |
| Matches an archived package type                 | The row fails with `SOURCING_IMPORT_PACKAGE_TYPE_ARCHIVED` (409). Restore the package type first, or use a different name |
| Matches two package types differing only by case | The row fails with `SOURCING_IMPORT_PACKAGE_TYPE_AMBIGUOUS`                                                               |
| Is longer than 100 characters                    | The row fails with `SOURCING_IMPORT_UNREADABLE_VALUE`                                                                     |

If the create itself cannot be completed — two rows of the same import racing on the same new name in a way that does not settle, or any other unresolvable failure — the row fails with `SOURCING_IMPORT_PACKAGE_TYPE_UNRESOLVABLE` (409); re-uploading the sheet resolves the now-existing package type. See [Sourcing import errors](/advanced-topics/error-handling.md#sourcing-import-errors) for all of the codes.

{% 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                                                                                         |
| `SOURCE_ONLY`       | The row was a continuation row: it was folded into a component declared on an earlier row, so it contributed a source and created or changed no component |
| `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`.

### `SOURCE_ONLY` and continuation rows

When a sheet lists the same component on several consecutive rows to give it several sources, the rows are folded into one component: the first row of the run declares the component and reports `CREATED`, `UPDATED`, or `UNCHANGED`, and every row after it reports `SOURCE_ONLY`. So a three-row run yields three row results — one component outcome and two `SOURCE_ONLY` — but is counted as a single component in `rowResultsSummary`.

`SOURCE_ONLY` is not `SKIPPED_DUPLICATE`. A skipped duplicate was ignored; a `SOURCE_ONLY` row did real work, it just did not touch the component. It is also not a claim that the source landed: row outcomes are written before the sourcing pass runs, so `SOURCE_ONLY` says what the row was, not whether its source applied.

{% hint style="info" %}
Read the two axes separately. Whether a continuation row's source succeeded is reported on the sourcing side, not in its `ImportRowOutcome` — a failed quote is reported against the row it was written on, so its row number points back at the line in your sheet rather than at the row that declared the component.
{% endhint %}

## 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                               |
| `sourceOnly`       | Continuation rows folded into a component declared on an earlier row       |
| `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,
          "sourceOnly": 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
