> 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/components.md).

# Components

Components are the fundamental building blocks in Duro. They represent parts, assemblies, documents, and other items that make up your products. This guide covers how to query, create, and update components through the API.

## Overview

Every component in Duro has:

* A unique identifier (`id`)
* A name and optional description
* A revision value (e.g., "1.A", "2.B") tracking design iterations
* A category defining its type (Part, Assembly, Document, etc.)
* A status indicating its lifecycle state
* Attributes specific to its category

## Required Headers

All component operations require these 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": "..."}'
```

{% hint style="info" %}
The `x-library` header is required for all component operations since components belong to a specific library.
{% endhint %}

***

## Querying Components

### Get a Single Component

Retrieve a component by its ID:

```graphql
query GetComponent {
  component {
    findOne(id: "550e8400-e29b-41d4-a716-446655440000") {
      id
      name
      description
      revisionValue
      version
      state
      status {
        name
        color
      }
      category {
        name
        type
      }
      createdAt
      updatedAt
    }
  }
}
```

### List Components

Query multiple components with optional filtering and pagination:

```graphql
query ListComponents {
  component {
    findAll(
      filter: {
        categoryType: PART
        isArchived: false
      }
      pagination: { first: 20 }
    ) {
      edges {
        node {
          id
          name
          revisionValue
          version
          status {
            name
          }
          category {
            name
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
```

### Filter Options

The `ComponentFilterInput` supports these fields:

| Field          | Type           | Description                                        |
| -------------- | -------------- | -------------------------------------------------- |
| `ids`          | `[ID!]`        | Filter by specific component IDs                   |
| `name`         | `String`       | Filter by name (partial match)                     |
| `names`        | `[String!]`    | Filter by multiple names                           |
| `exactNames`   | `[String!]`    | Filter by exact name matches                       |
| `cpns`         | `[String!]`    | Filter by Component Part Numbers                   |
| `statusId`     | `String`       | Filter by status ID                                |
| `categoryId`   | `String`       | Filter by category ID                              |
| `categoryType` | `CategoryType` | Filter by category type (ASSEMBLY, PART, DOCUMENT) |
| `isArchived`   | `Boolean`      | Include/exclude archived components                |
| `eid`          | `String`       | Filter by external identifier                      |

### Advanced Filtering

For complex queries with AND/OR logic, use the `filter` method:

```graphql
query AdvancedFilter {
  component {
    filter(input: {
      and: [
        { status: { name: "Released" } }
        { category: { type: PART } }
        { createdAt: { gte: "2024-01-01" } }
      ]
      pagination: { first: 50 }
    }) {
      edges {
        node {
          id
          name
          revisionValue
        }
      }
    }
  }
}
```

See [Searching and Filtering](/advanced-topics/searching-and-filtering.md) for comprehensive filtering options.

### Organizing Components into Groups

Group components by category, lifecycle status, type, modified-by user, or label — server-side, in a single call:

```graphql
query GroupByCategory {
  component {
    componentGroups(groupBy: CATEGORY, pagination: { first: 10 }) {
      edges {
        node {
          key
          label
          totalCount
          components {
            edges { node { id name } }
            pageInfo { hasNextPage }
          }
        }
      }
      totalCount
    }
  }
}
```

Each group returns a preview of up to 50 components. Groups with more items signal overflow via `components.pageInfo.hasNextPage`, and the remaining items can be loaded incrementally with the `componentsInGroup` query.

Grouping works with both simple and advanced filters — pass `filter` or `advancedFilter` alongside `groupBy` to scope results before grouping.

See [Component Grouping](/advanced-topics/component-grouping.md) for the full guide, including sorting within groups, sentinel keys for uncategorized items, and the complete API reference.

***

## Validating Attributes

Before creating or updating components, you can check attribute values against the rules defined by their category. Validation is read-only — nothing is written, so it is safe to call as often as your UI needs.

### Validating One Component

```graphql
query ValidateAttributes {
  component {
    validateAttributes(
      categoryId: "category-uuid"
      attributeValues: [
        { attributeId: "attr-uuid-1", value: "12V" }
        { attributeId: "attr-uuid-2", value: "50W" }
      ]
    ) {
      success
      message
      errors {
        attributeId
        message
      }
    }
  }
}
```

`success` is `true` when every value passes. When it is `false`, `errors` names the offending attribute and explains why, and `message` carries an optional summary.

### Validating Many Components in One Call

`validateAttributesBatch` validates a set of components in a single round trip. Use it instead of looping over `validateAttributes` — a grid of rows, a CAD assembly's variants, or an import preview should cost one request, not one per item.

```graphql
query ValidateAttributesBatch {
  component {
    validateAttributesBatch(requests: [
      {
        key: "row-1"
        categoryId: "category-uuid"
        attributeValues: [{ attributeId: "attr-uuid-1", value: "12V" }]
      }
      {
        key: "row-2"
        categoryId: "category-uuid"
        attributeValues: [{ attributeId: "attr-uuid-1", value: "not-a-voltage" }]
      }
    ]) {
      success
      results {
        key
        success
        message
        errors {
          attributeId
          message
        }
      }
    }
  }
}
```

#### AttributeValidationRequestInput Fields

| Field             | Type                      | Required | Description                                                                                                                             |
| ----------------- | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `key`             | `ID!`                     | Yes      | Caller-supplied identifier echoed back on the matching result — a component id, file path, or row key, anything stable within the batch |
| `categoryId`      | `ID!`                     | Yes      | Category whose attribute rules to validate against                                                                                      |
| `attributeValues` | `[AttributeValueInput!]!` | Yes      | The values to validate                                                                                                                  |

#### Result

`validateAttributesBatch` returns a `BatchAttributeValidationResultDto` with two fields:

| Field     | Type                                    | Description                                                                                                                                                                              |
| --------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success` | `Boolean!`                              | `true` only when **every** request passed. Read this when the question is "may I proceed?" and skip the per-request scan.                                                                |
| `results` | `[KeyedAttributeValidationResultDto!]!` | One verdict per request, in submission order. Each carries the same `success`, `message`, and `errors` fields as `validateAttributes`, plus the `key` from the request that produced it. |

Results are returned in submission order, so you can zip them against your requests positionally. Prefer matching on `key` anyway — it keeps your code correct if a request is reordered or dropped upstream, and it is the only reliable join when the same category appears many times in one batch.

A batch accepts up to **10,000** requests. A larger one is rejected whole with a `400` (`Cannot validate more than 10000 components in one request`) — split it before sending.

{% hint style="info" %}
Each request is validated independently. One failing request does not fail the batch — expect a mix of `success: true` and `success: false` entries in a single response.
{% endhint %}

{% hint style="warning" %}
**`success: false` with an empty `errors` list means the check could not run, not that the values are bad.** When validation itself fails for a request — a malformed rule on the category, for example — its result is `success: false`, `errors: []`, and a `message` starting `Validation could not be completed`. Treat that entry as unknown rather than valid; a client that only inspects `errors` would wave it through.
{% endhint %}

{% hint style="warning" %}
Validation reflects the category rules at the time of the call. It is a pre-flight check, not a guarantee: the create or update mutation validates again server-side, and that result is authoritative.
{% endhint %}

***

## Creating Components

Create one or more components using the `create` mutation:

```graphql
mutation CreateComponent {
  component {
    create(inputs: [{
      name: "Motor Assembly"
      description: "Primary drive motor assembly"
      categoryId: "category-uuid"
      attributeValues: [
        { attributeId: "attr-uuid-1", value: "12V" }
        { attributeId: "attr-uuid-2", value: "50W" }
      ]
    }]) {
      id
      name
      revisionValue
      version
      createdAt
    }
  }
}
```

### CreateComponentInput Fields

| Field             | Type                     | Required | Description                                                                                               |
| ----------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------- |
| `name`            | `String`                 | Yes      | Component name                                                                                            |
| `description`     | `String`                 | No       | Detailed description                                                                                      |
| `categoryId`      | `String`                 | No       | Category/type for this component                                                                          |
| `statusId`        | `ID`                     | No       | Initial status                                                                                            |
| `revisionValue`   | `String`                 | No       | Initial revision (defaults based on library config)                                                       |
| `eid`             | `String`                 | No       | External identifier for integrations                                                                      |
| `attributeValues` | `[AttributeValueInput!]` | No       | Category-specific attribute values                                                                        |
| `fileId`          | `ID`                     | No       | Attached file ID                                                                                          |
| `imageFileId`     | `ID`                     | No       | Component image file ID                                                                                   |
| `documentFormats` | `[String!]`              | No       | Document formats this component generates — see [Generated document formats](#generated-document-formats) |

### Example Response

```json
{
  "data": {
    "component": {
      "create": [
        {
          "id": "123e4567-e89b-12d3-a456-426614174000",
          "name": "Motor Assembly",
          "revisionValue": "1.A",
          "version": 1,
          "createdAt": "2024-01-15T10:30:00Z"
        }
      ]
    }
  }
}
```

***

## Validating CPN Overrides

When a caller supplies its own values for the elements of a [CPN scheme](https://github.com/duronext/developer/tree/main/advanced/cpn-scheme.md) — a category, a prefix, a variant — the `identifier` queries validate those values before a component is created. The question these queries answer is **"can a CPN be generated from what I have supplied?"**, not "here is the CPN this component will get."

Duro builds the CPN from the scheme, filling in whatever the caller supplied. Anything not supplied — in practice an **auto-counter**, whose value is only allocated at creation time — is rendered in `displayValue` as a run of `X` characters:

```
433-XXXXX
```

The number of `X`s is the width of the counter, which is the digit/character count of its `format.max_value` (see [Numeric Counter](/library-configuration/cpn-schema-reference.md#numeric-counter)). Treat `X`s as "unresolved counter", the same way the Duro web app renders them.

{% hint style="warning" %}
Earlier responses filled unresolved counters with the counter's `min_value`, so a preview read `433-00001` — a plausible-looking sequence number that the created component would generally *not* receive. That value is now `433-XXXXX`. `displayValue` is still a `String`; only the placeholder changed. Do not parse a validation `displayValue` as the component's final CPN.
{% endhint %}

### Determinism and uniqueness

`ValidateIdentifierOverrideResponse` exposes a determinism signal alongside the preview:

| Field             | Type       | Description                                                                                                                                                 |
| ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `displayValue`    | `String`   | The CPN preview, with unresolved counters rendered as `X`s.                                                                                                 |
| `isDeterministic` | `Boolean!` | `true` only when **every** element resolved from the supplied values — i.e. the preview contains no `X` placeholders and is the CPN the component will get. |
| `isUnique`        | `Boolean!` | Whether the previewed CPN is unique. **Only meaningful when `isDeterministic` is `true`.**                                                                  |

When `isDeterministic` is `false`, uniqueness is not applicable rather than false: the pending counter guarantees uniqueness by construction. `isUnique` is non-nullable on this response and will still carry a value in that case — **disregard it** and branch on `isDeterministic` instead.

```graphql
query ValidateCpnOverride($overrides: [IdElementOverrideInput!]!) {
  identifier {
    validateCompleteOverride(overrides: $overrides) {
      displayValue
      isDeterministic
      isUnique
    }
  }
}
```

A non-deterministic result is the normal, healthy case for a scheme that ends in an auto-counter — it is not an error, and it does not mean the component cannot be created.

{% hint style="info" %}
Because an unresolved counter always renders as a fixed-width run of `X`s, a non-deterministic preview is derivable from the scheme and the element values you already hold. Clients validating many rows at once — a CAD plugin pulling an assembly, for example — can render the preview locally and skip the round trip, calling the API only when they need a uniqueness answer for a fully specified CPN.
{% endhint %}

`isDeterministic` is additive; existing callers that read only `displayValue` and `isUnique` keep working. For the exact response shape and the `IdElementOverrideInput` fields, introspect the schema in [Apollo Explorer](https://api.durohub.com/graphql).

***

## Updating Components

Update existing components using the `update` mutation:

```graphql
mutation UpdateComponent {
  component {
    update(inputs: [{
      id: "123e4567-e89b-12d3-a456-426614174000"
      name: "Motor Assembly v2"
      description: "Updated drive motor with improved efficiency"
      expectedVersion: 1
    }]) {
      id
      name
      description
      version
      updatedAt
    }
  }
}
```

{% hint style="warning" %}
**Optimistic Concurrency**: Use `expectedVersion` to prevent overwriting concurrent changes. If the current version doesn't match, the update will fail with a conflict error.
{% endhint %}

### UpdateComponentInput Fields

| Field             | Type                     | Description                                                                                                                                              |
| ----------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | `ID!`                    | Component ID to update (required)                                                                                                                        |
| `name`            | `String`                 | New name                                                                                                                                                 |
| `description`     | `String`                 | New description                                                                                                                                          |
| `statusId`        | `ID`                     | New status                                                                                                                                               |
| `revisionValue`   | `String`                 | New revision value                                                                                                                                       |
| `categoryId`      | `ID`                     | Change category                                                                                                                                          |
| `attributeValues` | `[AttributeValueInput!]` | Updated attribute values                                                                                                                                 |
| `isArchived`      | `Boolean`                | Archive/unarchive the component                                                                                                                          |
| `documentFormats` | `[String!]`              | Document formats this component generates. Omit to leave unchanged; send `null` to clear — see [Generated document formats](#generated-document-formats) |
| `expectedVersion` | `Int`                    | For optimistic concurrency control                                                                                                                       |

***

## Item Mutations: BOM and Documents in One Call

Saving a component from a CAD tool usually means several writes — the component's own fields, its BOM, and a document link for each drawing or exported file. Done as separate `component.create` / `component.update` calls, each is its own transaction and each writes its own history row, so one user action produces a scattered history.

The **`item` mutation namespace** collapses that into a single transaction. `item.create` and `item.update` take everything `component.create` / `component.update` take, plus the relation fields below, and write **one history row per component**.

{% hint style="info" %}
`component.create` and `component.update` are unchanged and remain the right call when you are only writing a component's own fields. They **reject** the relation fields — send `children`, `documents`, `documentComponentIds`, or `removeDocumentLinkIds` to `item.create` / `item.update` instead.
{% endhint %}

### Additional inputs

| Field                   | Type                        | On             | Description                                                                 |
| ----------------------- | --------------------------- | -------------- | --------------------------------------------------------------------------- |
| `children`              | `[ComponentChildInput!]`    | create, update | The component's direct BOM children.                                        |
| `documents`             | `[ComponentDocumentInput!]` | create, update | Documents to create from already-uploaded files and link to this component. |
| `documentComponentIds`  | `[ID!]`                     | create, update | Existing DOCUMENT-category components to link to this component.            |
| `removeDocumentLinkIds` | `[ID!]`                     | update only    | Document links to remove.                                                   |

```graphql
input ComponentChildInput {
  childId: ID!
  quantity: Float
  refDes: String
  itemNumber: String
  notes: String
  childHistoryId: ID
}

input ComponentDocumentInput {
  fileId: ID!
  name: String!
  categoryId: ID
}
```

### Creating a component with its BOM and documents

```graphql
mutation CreateItem {
  item {
    create(inputs: [{
      name: "Motor Assembly"
      categoryId: "assembly-category-uuid"
      children: [
        { childId: "child-uuid-1", quantity: 2, refDes: "M1,M2", itemNumber: "10" }
        { childId: "child-uuid-2", quantity: 1, childHistoryId: "history-uuid" }
      ]
      documents: [
        { fileId: "uploaded-file-uuid", name: "Motor Assembly Drawing" }
      ]
      documentComponentIds: ["existing-document-component-uuid"]
    }]) {
      id
      name
      revisionValue
      version
    }
  }
}
```

### BOM children semantics

* `children` is a **desired-state list of direct children only** — the same semantics as `assemblies.updateBOM`. Anything not in the list is unlinked from this component.
* There is no nesting. To publish an assembly tree, walk it level by level, one call per component.
* Children must already exist. Create them first, then reference them by `childId`.
* `quantity` is a `Float` — assembly links support fractional quantities.
* `childHistoryId` pins the link to a specific revision of the child. Omit it for a link that follows the child's latest revision.

On **update**, the three cases for `children` are distinct:

| You send                | Result                                                                |
| ----------------------- | --------------------------------------------------------------------- |
| A list of children      | The BOM becomes exactly that list; children not present are unlinked. |
| `[]` (empty list)       | All children are unlinked.                                            |
| Nothing (field omitted) | The BOM is left unchanged.                                            |

{% hint style="warning" %}
Because `children` is desired-state, a client that omits a child it did not touch will **unlink** it. Send the component's complete direct-child list, or omit the field entirely when you are not changing the BOM.
{% endhint %}

### Documents

`documents` and `documentComponentIds` are covered on the [Documents](/core-concepts/documents.md#creating-and-linking-documents-with-item-mutations) page, including why a document cannot be created before its parent.

### History consolidation

An `item.create` or `item.update` call writes **one `component_history` row for the component**, no matter how many of the relation fields it carries, and one row for each document component it creates. A batch stays all-or-nothing: if any input in the array fails, the whole call rolls back.

{% hint style="info" %}
A single call is capped at 10 components, 20 `children` and 20 `documents` per component, and 200 `children` across the call. Anything larger is refused with `ITEM_WRITE_TOO_LARGE` before any work starts, rather than being partially applied. See [Error Handling](/advanced-topics/error-handling.md#item-write-errors) for the full table and how to split a publish.
{% endhint %}

***

## Deleting Components

Delete components by their IDs:

```graphql
mutation DeleteComponents {
  component {
    delete(ids: ["component-uuid-1", "component-uuid-2"])
  }
}
```

{% hint style="warning" %}
Deleting components is permanent. Consider archiving instead by setting `isArchived: true` in an update mutation.
{% endhint %}

***

## Component States and Revisions

### State

Components have a `state` field indicating their modification status:

| State      | Description                      |
| ---------- | -------------------------------- |
| `RELEASED` | Component is released and locked |
| `MODIFIED` | Component has unreleased changes |

### Revisions

The `revisionValue` field tracks design iterations (e.g., "1.A", "1.B", "2.A"). Revision schemes are configured per-library. See [Revision Scheme](/library-configuration/revisions.md) for configuration options.

***

## Not Revision Managed (NRM)

A component can be flagged as **Not Revision Managed** (NRM) when you want to track it in Duro without revising it — for example, generic hardware or reference parts. An NRM component keeps a fixed revision and is held at Production status; its revision is not incremented on release.

NRM is gated per-library and applies only to parts and documents (not assemblies). It is available only when the library has the `notRevisionManaged` entitlement enabled and the caller has the corresponding permission.

### NRM fields on component reads

Two `Boolean` fields expose NRM state when querying a component:

| Field                            | Type      | Description                                                                                                                                                                                                                                                           |
| -------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `notRevisionManaged`             | `Boolean` | Whether the component is currently Not Revision Managed.                                                                                                                                                                                                              |
| `lastReleasedNotRevisionManaged` | `Boolean` | The NRM flag as of the component's most recent release. Returned only for components that have been released at least once (`null` otherwise). Use it to detect a Revision Managed ↔ NRM transition — for example, to establish the baseline for a change order item. |

```graphql
query GetComponentNrm {
  component {
    findOne(id: "550e8400-e29b-41d4-a716-446655440000") {
      id
      name
      revisionValue
      status {
        name
      }
      notRevisionManaged
      lastReleasedNotRevisionManaged
    }
  }
}
```

### NRM in search results

Search result nodes (`ComponentSearchResultDto`) also expose `notRevisionManaged`, so lightweight listing and search contexts can surface an NRM marker without loading the full component:

```graphql
query SearchNrm {
  component {
    filterWithAI(input: { query: "not revision managed parts" }) {
      components {
        id
        name
        cpn
        notRevisionManaged
      }
      totalCount
    }
  }
}
```

{% hint style="info" %}
Marking an Obsolete component as NRM is not allowed. Attempting it in an update returns the `NRM_NOT_ALLOWED_ON_OBSOLETE` validation error — see [Error Handling](/advanced-topics/error-handling.md).
{% endhint %}

***

## Generated document formats

A component can record which document formats it generates — the derived files a CAD integration produces for it, such as a PDF drawing or a STEP model. The field is `documentFormats: [String!]` on `Component`, `CreateComponentInput`, and `UpdateComponentInput`.

Storing the choice on the component means every client sees the same selection: a format list set from a CAD plugin is the list the web app and any other integration read back. Values are plain strings rather than an enum, so the API does not constrain the vocabulary — use the format identifiers your integration recognizes.

```graphql
query GetComponentDocumentFormats {
  component {
    findOne(id: "550e8400-e29b-41d4-a716-446655440000") {
      id
      name
      documentFormats
    }
  }
}
```

### Setting and clearing the list

`documentFormats` is nullable, and on update the difference between omitting it and sending `null` is meaningful:

| Input             | Effect                                                                                                                   |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Omitted           | The stored list is left unchanged.                                                                                       |
| A list of strings | Replaces the stored list wholesale — it is not merged with the existing values.                                          |
| `null`            | Clears the per-component choice. The component has no selection of its own, and clients fall back to their own defaults. |

```graphql
mutation SetDocumentFormats {
  component {
    update(inputs: [{
      id: "123e4567-e89b-12d3-a456-426614174000"
      documentFormats: ["PDF", "STEP"]
      expectedVersion: 1
    }]) {
      id
      documentFormats
      version
    }
  }
}
```

{% hint style="warning" %}
A `null` clear is an explicit operation. Clients that strip `null` fields before serializing will turn it into a no-op and leave the existing list in place — the same hazard as [attribute deletes](#attribute-merge-and-delete-semantics).
{% endhint %}

{% hint style="info" %}
`documentFormats` records *what to generate*, not what has been generated. The documents a component actually holds are queried separately — see [Documents](/core-concepts/documents.md).
{% endhint %}

***

## Working with Assemblies

Assemblies are components that contain other components. Query assembly structure using:

```graphql
query GetAssemblyStructure {
  component {
    findOne(id: "assembly-uuid") {
      id
      name
      category {
        type  # Will be ASSEMBLY
      }
    }
  }
}
```

{% hint style="info" %}
For detailed BOM (Bill of Materials) operations, use the `assemblies` namespace. See assembly documentation for more details. To tell which principal authored a given BOM line, see [BOM line authorship](#bom-line-authorship).
{% endhint %}

***

## Complete cURL Example

Here's a complete example creating a component:

```bash
curl 'https://api.durohub.com/graphql' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'x-organization: @acme-corp' \
  -H 'x-library: @acme-corp/main-library' \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "mutation CreateComponent($input: [CreateComponentInput!]!) { component { create(inputs: $input) { id name revisionValue } } }",
    "variables": {
      "input": [{
        "name": "Capacitor 100uF",
        "description": "Electrolytic capacitor, 100uF 25V",
        "categoryId": "part-category-uuid"
      }]
    }
  }'
```

***

## Sourcing

Components carry **sourcing** data: the manufacturer parts (MPNs) that source a component, the distributor quotes (DPNs) that price each part, and which source is currently primary. The whole sourcing surface is namespaced under `vendorSourcing` on `Query` and `Mutation` and has its own page — see [Sourcing](/core-concepts/sourcing.md) for the model, task-oriented examples, and the full operation reference.

{% hint style="warning" %}
`Quote.unitPrice` (and `CreateQuoteInput.unitPrice`) is now **nullable** — details and other quote-field notes are on the [Sourcing](/core-concepts/sourcing.md) page.
{% endhint %}

Components whose price predates sourcing carry placeholder sourcing rows created by the legacy cost migration. `Part.isLegacyImport` and `Quote.isLegacyImport` flag them so you can tell a migrated price from a real vendor offer — see [Legacy-import parts and quotes](/core-concepts/sourcing.md#legacy-import-parts-and-quotes).

## Integration Access and Provenance

A component can be written by more than one party: people working in Duro, and integrations such as a CAD plugin or an ERP connector. Duro reconciles those writers with a **principal model** — every writer, including Duro itself, is a principal with explicit component links and per-attribute access.

{% hint style="info" %}
This surface is **additive**. An integration that only writes open attributes and never opts into linking keeps working unchanged. See [Principal model (integration access)](/getting-started/api-v2-migration.md#principal-model-integration-access) for what does change.
{% endhint %}

### Principals and links

* **Duro is a principal.** Duro itself is a reserved native integration in every organization, with its own entries in the access matrix and its own component links. There is no special "edited by a human" carve-out.
* **Links are additive.** A component can be linked to several principals at once, and linking one integration never unlinks another.
* **Creation links the creator.** A component created in the Duro web app is linked to Duro; one created by a CAD plugin's *Pull Part* is linked to that integration.
* **Editing never links.** A later write does not silently claim a component — linking is an explicit action (`linkComponent`, or the equivalent toggle in the app).

### Attribute access

For a given principal, each attribute is either **open** or **managed**:

|             | Who can write it                                                                                                                                                  |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Open**    | Any caller with `components.update`. **No component link is required** — including for an integration pushing open attributes to a component it is not linked to. |
| **Managed** | Only principals that are both **linked to the component** and **managers of that attribute**.                                                                     |

A managed attribute with **no linked manager is frozen**, not silently reopened: nothing writes it until a manager is linked. This is deliberate — it is one explicit toggle away from editable, rather than quietly falling back to open.

The write axis is set per principal per attribute, through the `write` field of an `AttributeAccessEntryInput` passed to `setAttributeAccess`. It takes four values:

| Write value       | Meaning for that principal                                                                                                                                                        |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPEN`            | The principal is not designated (the default). It writes the attribute wherever no designated manager is linked to the component; where a manager is linked, the value is theirs. |
| `WRITE`           | The principal is a manager: it writes the attribute on components it is linked to. Other principals can be managers of the same attribute.                                        |
| `NO_WRITE`        | The principal never writes the attribute, whether or not it is linked.                                                                                                            |
| `EXCLUSIVE_WRITE` | The principal is the **sole** manager: it writes the attribute on components it is linked to, and no other principal may hold a write designation on it.                          |

Exclusivity is the designation behind an attribute shown as *exclusively managed* in the app — the setup a library uses when one CAD integration owns a computed value such as `Mass` and nothing else, Duro included, should push it.

{% hint style="warning" %}
**Exclusivity is never granted by demotion.** If another principal already holds `WRITE` or `EXCLUSIVE_WRITE` on the attribute, a `setAttributeAccess` call requesting `EXCLUSIVE_WRITE` is **rejected** — the existing managers are left in place, not silently downgraded. Clear the other principals' designations first, then grant exclusivity. The same guard runs the other way: granting a second principal write on an attribute another principal manages exclusively is rejected too. Both cases surface as [integration access errors](/advanced-topics/error-handling.md#integration-access-errors).
{% endhint %}

`accessPolicy` reads the stored designation back unchanged, so an attribute set to `EXCLUSIVE_WRITE` round-trips as `EXCLUSIVE_WRITE` — clients that re-submit a policy they just read should send the value through as-is rather than mapping it down to `WRITE`, which would silently drop the exclusivity.

The read axis is configured per library. An integration can be configured receive-only or no-access for an attribute, in which case reads made on that integration's behalf **withhold** the value rather than failing the query — so treat a missing attribute as "not accessible to you", not "not set". The Duro principal always reads and cannot be configured no-read, since the PLM has to render its own data; attempting it returns `STORE_PRINCIPAL_ALWAYS_READS`.

### Reading access state from a component

| Field                                       | Description                                                                                                                                                                                                        |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `effectiveAttributeAccess`                  | Component-scoped effective access per attribute for the calling principal. This is the field to branch on when deciding whether to offer an edit — it already accounts for links, management, and the frozen rule. |
| `viewerAccess`                              | Access as resolved for the calling viewer.                                                                                                                                                                         |
| `integrationLock`                           | Whether an integration currently holds the component's managed attributes, and which one.                                                                                                                          |
| `actingIntegrationId`                       | The integration the current request is acting on behalf of, if any.                                                                                                                                                |
| `originIntegrationId` / `originIntegration` | Provenance — the id of the integration the component originated from, and the resolved integration itself.                                                                                                         |

```graphql
query GetComponentAccess {
  component {
    findOne(id: "550e8400-e29b-41d4-a716-446655440000") {
      id
      name
      actingIntegrationId
      originIntegrationId
      originIntegration {
        id
        name
      }
      integrationLock {
        locked
      }
      effectiveAttributeAccess {
        attributeId
        canRead
        canWrite
      }
    }
  }
}
```

### Configuring what an integration manages

| Operation               | Description                                                                                                                                    |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `recommendedAttributes` | The attributes Duro suggests this integration manage — a starting point when setting one up, rather than an enforced list.                     |
| `accessPolicy`          | The integration's configured access policy for the current library: which attributes it reads and which it manages.                            |
| `setAttributeAccess`    | Sets an integration's access for specific attributes. Requires the `integrations.configure` permission — see [RBAC](/advanced-topics/rbac.md). |

### Linking and reconcile

| Mutation          | Description                                                                                                                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `linkComponent`   | Links a component to an integration. Additive — existing links are preserved. Required before that integration can write managed attributes.                                                       |
| `unlinkComponent` | Removes the link. Any managed attribute left with no linked manager becomes frozen.                                                                                                                |
| `reconcile`       | The sync entry point for integrations: submit the integration's view of a component and Duro reconciles it against current state, applying the writes the acting integration is permitted to make. |

`reconcile` enforces exactly the same access rules as a direct update — it is a batching and convergence mechanism, not a way around management. A write it is not allowed to make surfaces as one of the [integration access errors](/advanced-topics/error-handling.md#integration-access-errors).

### Attribute merge and delete semantics

`attributeValues` is a **merge**, not a replacement. Across create, update, and reconcile:

* An attribute **absent** from the input is left unchanged.
* An attribute **present with a value** is set to that value.
* An attribute **present with a wrapped `null`** — the value key supplied, explicitly `null` — **deletes** the stored value.

{% hint style="warning" %}
Omitting an attribute and sending `null` for it are different operations. Clients that strip `null` fields before serializing will silently turn a delete into a no-op — make sure your GraphQL client sends the explicit `null` through.
{% endhint %}

### BOM line authorship

The same provenance idea applies one level down, to the individual lines of an assembly's BOM. Two read-only fields record which principal authored a line, so a component/BOM integration can tell its own contribution apart from everyone else's:

| Field                                     | Description                                                               |
| ----------------------------------------- | ------------------------------------------------------------------------- |
| `AssemblyLink.createdByIntegrationId`     | `ID` — the integration that created this BOM line.                        |
| `AssemblyLinkHistory.actingIntegrationId` | `ID` — the integration acting when that BOM snapshot version was created. |

Both are `null` when the action was **native to Duro** — a line added in the web app, or a snapshot produced by a Duro-side action. Lines created before these fields shipped also read `null`. So read `null` as *"not attributable to an integration"* rather than *"unowned"*.

The practical use is diffing on the integration's side: when pushing a BOM, compare the incoming lines against the lines whose `createdByIntegrationId` is your own integration to work out what you added, changed, or no longer declare — and leave lines authored by another principal, or by Duro, alone. `AssemblyLinkHistory.actingIntegrationId` gives the same attribution per snapshot version, which is what makes it possible to tell "the line I pushed last time" from "the line someone edited since".

{% hint style="warning" %}
These two fields are **provenance, not authority**. They record who created a line; they do not grant or restrict write access, and Duro does not currently refuse a write based on them. Keep branching on `effectiveAttributeAccess` and the [integration access errors](/advanced-topics/error-handling.md#integration-access-errors) for what a caller is allowed to do.
{% endhint %}

{% hint style="info" %}
For the exact field-level shape of the access types, the null wrapper, and the `reconcile` / `setAttributeAccess` inputs, introspect the schema in [Apollo Explorer](https://api.durohub.com/graphql). This page documents the behavior; Explorer always reflects the current contract.
{% endhint %}

***

## Next Steps

* Bulk-create and update components with [Importing Components](/core-concepts/importing-components.md)
* Learn about [Documents](/core-concepts/documents.md) associated with components
* Explore [Change Orders](/core-concepts/change-orders.md) for managing component modifications
* See [Searching and Filtering](/advanced-topics/searching-and-filtering.md) for advanced queries


---

# 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/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.
