> 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/library-configuration/authoring-custom-validations.md).

# Authoring Custom Validations

Custom validations are JavaScript functions that run during change order validation. They receive the full change order context — items, attributes, approval stages, custom fields — and return a pass/fail result with a message. You write the logic; Duro handles execution in a secure sandbox.

## How It Works

When a change order is validated, Duro sends each custom validation rule to an isolated execution environment. Your code receives a `data` object containing the change order and its items, runs your logic, and returns a result. The entire round-trip happens within a 30-second timeout.

The basic contract is simple: export a `validate` function that accepts the data payload and returns `{ valid, message }`.

```javascript
exports.validate = async function(data) {
  // data.change_order — the change order metadata
  // data.items — the components in the change order

  // Your logic here...

  return {
    valid: true,      // or false
    message: 'Human-readable result'
  };
};
```

## The Validation Payload

Your validation function receives a single `data` argument with two top-level properties:

* `data.change_order` — the change order metadata, approval stages, custom fields
* `data.items` — the components in the change order, with full attribute data

### Change Order Fields

| Field         | Type   | Description                                           |
| ------------- | ------ | ----------------------------------------------------- |
| `id`          | string | UUID of the change order                              |
| `name`        | string | Change order name/title                               |
| `description` | string | Description text (can be null)                        |
| `status`      | string | `draft`, `open`, `resolved`, `closed`, or `on_hold`   |
| `resolution`  | string | `pending`, `approved`, `rejected`, or `withdrawn`     |
| `createdBy`   | object | `{ id, name, primaryEmail }` of the creator           |
| `updatedBy`   | object | `{ id, name, primaryEmail }` of the last editor       |
| `contents`    | array  | Custom form fields with values and validation rules   |
| `stages`      | array  | Approval stages with reviewers and notification lists |

### Item Fields

Each entry in `data.items` combines the change order item data with its component information:

| Field              | Type   | Description                                                           |
| ------------------ | ------ | --------------------------------------------------------------------- |
| `id`               | string | Change order item UUID                                                |
| `itemId`           | string | Component UUID                                                        |
| `name`             | string | Component name                                                        |
| `description`      | string | Component description                                                 |
| `category`         | object | `{ id, name, attributes }` — category info with attribute definitions |
| `status`           | object | `{ id, name, mapsTo, color }` — current status                        |
| `version`          | number | Component version number                                              |
| `revisionValue`    | string | Current revision (e.g., `"A"`, `"B"`)                                 |
| `state`            | string | `RELEASED` or `MODIFIED`                                              |
| `attributes`       | object | Human-readable attribute values keyed by name                         |
| `attributeValues`  | object | Raw attribute values keyed by UUID (legacy)                           |
| `proposedRevision` | string | New revision value                                                    |
| `proposedStatus`   | object | `{ id, name }` — new status                                           |

### Component Attributes

Each item carries attribute data in two forms.

**`item.attributes`** — Human-readable, keyed by name. This is what you'll use most of the time.

```javascript
item.attributes['Manufacturer']     // "Acme Co" or null
item.attributes['Mass']             // { value: "25", unit: "g" } or null
item.attributes['Cost']             // { value: 42.50, unit: "USD" } or null
item.attributes['Support Phone']    // "+1-555-0123" or null
```

All attributes assigned to the item's category appear in this map. Unset attributes are `null` — so you can distinguish "not set" from "attribute doesn't exist on this category."

**`item.category.attributes`** — Attribute definitions for the category. Useful when you need to know what attributes exist, their types, or iterate programmatically.

```javascript
item.category.attributes.forEach(attr => {
  console.info(`${attr.name} (${attr.type}): ${item.attributes[attr.name]}`);
});
```

Each definition includes: `id`, `name`, `type`, `source`, `sourceKey`, `unit`.

**`item.attributeValues`** — Raw storage format with UUID keys. Kept for backward compatibility. You shouldn't need this unless you're working with attribute IDs directly.

### Value Shapes

| Attribute Type                           | Example `item.attributes['Name']` |
| ---------------------------------------- | --------------------------------- |
| STRING, URL, EMAIL, PHONE, DATE, BOOLEAN | `"Acme Co"` (unwrapped primitive) |
| NUMBER (simple)                          | `42` (unwrapped number)           |
| NUMBER (measured -- mass, length)        | `{ value: "25", unit: "g" }`      |
| NUMBER (currency)                        | `{ value: 42.50, unit: "USD" }`   |
| LIST                                     | `"Each"` (unwrapped string)       |
| Unset                                    | `null`                            |

## Writing Your First Validation

Create a `.js` file that exports a `validate` function. The function must be `async` (or return a Promise) and return an object with `valid` (boolean) and `message` (string).

### Example: Block Components Missing a Manufacturer

```javascript
exports.validate = async function(data) {
  const missing = data.items.filter(item => !item.attributes['Manufacturer']);

  if (missing.length > 0) {
    const names = missing.map(item => item.name).join(', ');
    return {
      valid: false,
      message: `${missing.length} component(s) missing Manufacturer: ${names}`
    };
  }

  return { valid: true, message: 'All components have a manufacturer' };
};
```

That's the whole file. No imports, no boilerplate, no class wiring. Duro loads it, calls `exports.validate`, and records the result.

## Common Patterns

### Checking Attribute Values

```javascript
exports.validate = async function(data) {
  const problems = [];

  for (const item of data.items) {
    const cost = item.attributes['Cost'];
    if (cost && cost.value > 10000) {
      problems.push(`${item.name} has cost $${cost.value} (exceeds $10,000)`);
    }
  }

  if (problems.length > 0) {
    return { valid: false, message: problems.join('; ') };
  }
  return { valid: true, message: 'All costs within threshold' };
};
```

### Checking Category-Specific Rules

Use `item.category.name` to apply rules only to certain component types:

```javascript
exports.validate = async function(data) {
  for (const item of data.items) {
    if (item.category.name === 'Capacitor' && !item.attributes['Capacitance']) {
      return {
        valid: false,
        message: `Capacitor "${item.name}" is missing the Capacitance attribute`
      };
    }
  }
  return { valid: true, message: 'Category-specific checks passed' };
};
```

### Validating Approval Stages

The `data.change_order.stages` array gives you full visibility into the approval workflow:

```javascript
exports.validate = async function(data) {
  for (const stage of data.change_order.stages || []) {
    if (!stage.reviewers || stage.reviewers.length < 2) {
      return {
        valid: false,
        message: `Stage "${stage.name}" needs at least 2 reviewers`
      };
    }
  }
  return { valid: true, message: 'All stages have sufficient reviewers' };
};
```

### Enforcing Naming Conventions

```javascript
exports.validate = async function(data) {
  const pattern = /^ECO-\d{4}-\d{2,3}$/;
  if (!pattern.test(data.change_order.name)) {
    return {
      valid: false,
      message: `Name "${data.change_order.name}" must match pattern ECO-YYYY-NNN`
    };
  }
  return { valid: true, message: 'Name format is valid' };
};
```

### Working with Measured Values

Measured attributes (mass, length, etc.) return `{ value, unit }` rather than a bare number. Parse accordingly:

```javascript
exports.validate = async function(data) {
  for (const item of data.items) {
    const mass = item.attributes['Mass'];
    if (mass && parseFloat(mass.value) > 1000) {
      console.warn(`${item.name} exceeds 1kg: ${mass.value}${mass.unit}`);
      return {
        valid: false,
        message: `${item.name} mass is ${mass.value}${mass.unit} — exceeds 1kg limit`
      };
    }
  }
  return { valid: true, message: 'All components within mass limits' };
};
```

## Logging

All `console` methods are captured and visible in the validation results:

* `console.info()` — informational messages about execution
* `console.log()` — general-purpose logging
* `console.warn()` — warnings about potential issues
* `console.error()` — error details

Use logging liberally. It costs nothing and makes debugging much easier when a validation fails unexpectedly.

```javascript
console.info(`Checking ${data.items.length} components`);
console.info(`Change order: ${data.change_order.name}`);

for (const item of data.items) {
  console.log(`Processing ${item.name} (${item.category.name})`);
}
```

Logs are accessible via the GraphQL API on each validation result's `logs` field — see [Change Order Validations](/library-configuration/change-order-validations.md) for the query structure.

## Linking to Entities

Duro supports a markdown-inspired syntax for creating clickable links in validation messages and logs. When you reference a component or change order, wrap it in a link so users can click straight through to it in the Duro UI.

### Syntax

```
[display text](protocol:type/id)
```

### Supported Link Types

| Entity       | Syntax                      | Example                                 |
| ------------ | --------------------------- | --------------------------------------- |
| Component    | `[text](item:component/id)` | `[CAP-001](item:component/abc-123)`     |
| Assembly     | `[text](item:assembly/id)`  | `[Main Board](item:assembly/xyz-789)`   |
| Change Order | `[text](co:changeorder/id)` | `[ECO-2024-001](co:changeorder/co-123)` |
| User         | `[text](user:profile/id)`   | `[Jane Smith](user:profile/user-456)`   |

### Helper Function

A small helper keeps link construction consistent:

```javascript
function componentLink(item) {
  const display = item.name || item.eid || item.itemId;
  return `[${display}](item:component/${item.itemId})`;
}

// In your validation:
console.error(`${componentLink(item)} is missing a description`);
```

Links degrade gracefully — if the UI can't parse them, users still see the display text.

## Error vs Warning Mode

The `onFailure` setting controls what happens when your validation returns `{ valid: false }`:

| Mode      | Behavior                                            |
| --------- | --------------------------------------------------- |
| `error`   | Blocks the change order from proceeding (hard stop) |
| `warning` | Alerts reviewers but does not block progress        |

This is a configuration setting, not something you control in JavaScript. Your code is identical either way — you return `{ valid: false, message }` and the platform decides whether to block or warn based on the mode.

{% hint style="info" %}
If you manage validations through the [library-validations-template](https://github.com/duronext/library-validations-template), the mode is set in `validations.yaml` via the `onFailure` field.
{% endhint %}

## Execution Environment

{% hint style="info" %}
Validations run in an isolated sandbox with a 30-second timeout. No filesystem access, no network access (unless your admin has enabled the fetch proxy). All console output is captured.
{% endhint %}

Your code cannot:

* Read or write files
* Make HTTP requests (by default)
* Import Node.js built-in modules
* Access environment variables

If your validation needs external data, talk to your Duro admin about enabling the fetch proxy.

## Managing Validations via the API

You can create, update, and list custom validation rules through the GraphQL API. The library is resolved from your API key — you don't need to specify it explicitly.

### Creating a Validation Rule

```graphql
mutation CreateValidation($input: CreateValidationRuleInput!) {
  validations {
    create(input: $input) {
      id
      name
    }
  }
}
```

Variables:

```json
{
  "input": {
    "name": "Block: Missing Manufacturer",
    "description": "Rejects change orders where any component is missing a Manufacturer attribute",
    "code": "exports.validate = async function(data) {\n  const missing = data.items.filter(item => !item.attributes['Manufacturer']);\n  if (missing.length > 0) {\n    return { valid: false, message: missing.map(i => i.name).join(', ') + ' missing Manufacturer' };\n  }\n  return { valid: true, message: 'All components have a manufacturer' };\n};",
    "type": "custom",
    "onFailure": "ERROR",
    "isActive": true,
    "version": "1.0.0"
  }
}
```

| Field         | Type    | Description                                      |
| ------------- | ------- | ------------------------------------------------ |
| `name`        | string  | Display name for the validation rule             |
| `description` | string  | What the rule checks                             |
| `code`        | string  | The JavaScript source code                       |
| `type`        | string  | Always `"custom"`                                |
| `onFailure`   | string  | `"ERROR"` (blocks) or `"WARNING"` (non-blocking) |
| `isActive`    | boolean | Whether the rule runs during validation          |
| `version`     | string  | Semantic version for tracking changes            |

### Updating a Validation Rule

```graphql
mutation UpdateValidation($id: ID!, $input: UpdateValidationRuleInput!) {
  validations {
    update(id: $id, input: $input) {
      id
      name
      version
    }
  }
}
```

You can update any combination of `code`, `description`, `onFailure`, `isActive`, and `version`. The `name` and `type` cannot be changed after creation.

### Listing Validation Rules

```graphql
query ListValidations {
  validations {
    list {
      edges {
        node {
          id
          name
          description
          code
          onFailure
          isActive
          version
        }
      }
    }
  }
}
```

### Deleting a Validation Rule

```graphql
mutation DeleteValidation($id: ID!) {
  validations {
    delete(id: $id)
  }
}
```

{% hint style="warning" %}
Deletion is permanent. If you want to temporarily disable a rule, update `isActive` to `false` instead.
{% endhint %}

## Source-Controlled Validations

For teams that want to manage validations in version control, Duro provides the [library-validations-template](https://github.com/duronext/library-validations-template) — a GitHub template repository you can fork for your library. It gives you:

* A `validations.yaml` file defining each rule's name, description, version, active state, and failure mode
* A `validations/` directory for your JavaScript files
* A sync script that pushes your local definitions to the Duro API via the mutations above
* A GitHub Actions workflow that syncs automatically on push to `main`

Fork the template, add your API key as a GitHub secret (`DURO_LIBRARY_API_KEY`), and every push to `main` will sync your validation rules to your Duro library. The sync is additive — it creates new rules, updates changed ones, and deactivates rules removed from the config (it never deletes).

This approach lets you review validation changes in pull requests, track history in git, and deploy consistently across libraries. The repository also includes a `CLAUDE.md` file that enables AI-assisted validation authoring — describe what you want in natural language and Claude Code generates the JavaScript and YAML config.

## Best Practices

**Keep validations focused.** One validation, one concern. A rule that checks manufacturer data and also enforces naming conventions should be two separate validations. Smaller validations are easier to debug, toggle, and explain to users.

**Provide clear messages.** Your message is the first thing a user sees when something fails. Tell them what's wrong and which components are affected — not just "validation failed."

```javascript
// Helpful
`3 components missing Manufacturer: CAP-001, RES-042, IC-100`

// Not helpful
`Validation failed`
```

**Use component links.** When referencing specific components in messages or logs, use the `[name](item:component/id)` syntax so users can click through to the problem.

**Handle null and undefined gracefully.** Attributes can be `null` (unset), items can have missing fields, and the `stages` array can be empty. Defensive checks prevent runtime errors that surface as cryptic failures.

```javascript
// Defensive
const manufacturer = item.attributes['Manufacturer'];
if (manufacturer && manufacturer.toLowerCase() === 'acme co') { ... }

// Fragile — throws if attribute is null
if (item.attributes['Manufacturer'].toLowerCase() === 'acme co') { ... }
```

**Log diagnostic info.** Drop `console.info` calls at the start of your validation to record how many items you're checking, which categories are present, and any thresholds you're using. When something goes wrong, the logs tell the story.

**Return a message on success too.** A passing message like `"All 12 components have a manufacturer"` gives users confidence that the check actually ran and inspected their data.


---

# 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/library-configuration/authoring-custom-validations.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.
