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 }.
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 fieldsdata.items— the components in the change order, with full attribute data
Change Order Fields
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:
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.
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.
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
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
Checking Category-Specific Rules
Use item.category.name to apply rules only to certain component types:
Validating Approval Stages
The data.change_order.stages array gives you full visibility into the approval workflow:
Enforcing Naming Conventions
Working with Measured Values
Measured attributes (mass, length, etc.) return { value, unit } rather than a bare number. Parse accordingly:
Logging
All console methods are captured and visible in the validation results:
console.info()— informational messages about executionconsole.log()— general-purpose loggingconsole.warn()— warnings about potential issuesconsole.error()— error details
Use logging liberally. It costs nothing and makes debugging much easier when a validation fails unexpectedly.
Logs are accessible via the GraphQL API on each validation result's logs field — see Change Order Validations 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
Supported Link Types
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:
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 }:
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.
If you manage validations through the library-validations-template, the mode is set in validations.yaml via the onFailure field.
Execution Environment
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.
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
Variables:
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
You can update any combination of code, description, onFailure, isActive, and version. The name and type cannot be changed after creation.
Listing Validation Rules
Deleting a Validation Rule
Deletion is permanent. If you want to temporarily disable a rule, update isActive to false instead.
Source-Controlled Validations
For teams that want to manage validations in version control, Duro provides the library-validations-template — a GitHub template repository you can fork for your library. It gives you:
A
validations.yamlfile defining each rule's name, description, version, active state, and failure modeA
validations/directory for your JavaScript filesA 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."
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.
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.
Last updated
Was this helpful?