Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The Duro GraphQL API provides a powerful interface to interact with your product lifecycle management data. This guide will help you understand the core concepts and get started with integration.
Our API is built using GraphQL, offering several advantages:
Request exactly the data you need and ability to fetch multiple resources in parallel
Strong typing and schema validation
Efficient data loading
Interactive documentation and exploration
The Duro API follows these core principles:
RESTful-like resource patterns
Clear name-spaced resources
Consistent error handling
Rate limiting for stability
Before you begin, you'll need:
A Duro account
Basic understanding of GraphQL
Your preferred GraphQL client
We recommend using these tools:
(with GraphQL support)
Learn how to to get started.
Specifications
Test Reports
Quality Documentation
Here's how to fetch documents:
Direct upload URLs
Version control
Access permissions
File metadata
Learn about managing product changes with Change Orders.
query {
documents(first: 10) {
edges {
node {
id
title
fileType
version
createdAt
createdBy {
name
}
}
}
}
}mutation {
createDocument(input: {
title: "Assembly Instructions"
componentId: "comp_123"
fileUrl: "https://example.com/file.pdf"
}) {
document {
id
title
}
}
}Navigate to your account settings
Generate an API token from the Developer section
Your URL to get an API key is: https://durohub.com/org/@<your org slug>/libs/<your library slug>/settings/api-keys
Include your API token in all requests using the x-api-key header:
Never share your API tokens
Rotate tokens regularly
Use different tokens for development and production
Store tokens securely in environment variables
Never commit API tokens to version control or expose them in client-side code.
Learn how to make your first API call in the Hello World guide.
Learn how to manage components and product structures through the API.
Components are the building blocks of your products. Here's how to query components:
query {
components(first: 10) {
edges {
node {
id
name
partNumber
revision
status
}
}
}
}Products represent the complete assembly of components:
Creating components
Updating component details
Managing BOMs
Handling revisions
Learn how to work with associated with components and products.
Welcome to the official documentation for the Duro Platform. This documentation will help you integrate Duro's powerful product lifecycle management capabilities into your applications.
Duro is a modern, highly configurable PLM platform that helps hardware teams manage their product data, collaborate on designs, and streamline their development process.
With the Duro API, you can:
Manage components and sophisticated BOM assembly structures
Manage role based access controls for your organizations and libraries
Search part data with a powerful filtering engine
Access and update technical documentation
The fastest way to get started with the Duro API is to:
Try our example
Our main GraphQL API explorer is available at:
Join our
Check our guide
Contact support at [email protected]
x-api-key: YOUR_API_TOKEN
Automate custom change management workflows
Create custom integrations with your existing tools
Build automated reporting and analytics

Here's a complete example using curl:
Learn about working with Components and Products.
query GetAllLibraries {
organization {
findAll {
libraries {
id
name
}
}
}
}query {
products(first: 10) {
edges {
node {
id
name
version
components {
totalCount
}
}
}
}
}https://api.durohub.com/graphql{
"data": {
"organization": {
"findAll": [
{
"libraries": [
{
"id": "fc28b204-0cd0-46b3-96eb-b0720c16c423",
"name": "Main Library"
}
]
}
]
}
}
}curl 'https://api.durohub.com/graphql' \
-H 'x-api-key: YOUR_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"query":"query { organization { findAll { libraries { id name } } } }"}'Duro started as an "out of the box" PLM solution that worked well for hardware teams transitioning from spreadsheets. However, as organizations scaled and required deeper customization, we recognized the need for a more flexible approach. Instead of maintaining hard-coded rules, we developed a powerful configuration system that puts control in our users' hands.
Our "YAML all the things" philosophy enables extensive customization while maintaining Duro's core value of simplicity. Using YAML's human-readable format, you can configure:
Category definitions and specifications
Data validation rules
Custom revision and status workflows
Configurable part numbering schemes
We chose YAML for its:
Readability: Clean, intuitive syntax that's easy to understand
Accessibility: Approachable for both technical and non-technical users
Flexibility: Supports complex configurations without overwhelming complexity
Industry adoption: Widely used in tools like GitHub Actions and Kubernetes
Our configuration system draws inspiration from proven approaches like , , and , combining their best aspects into a cohesive configuration definition system for your product data.
If you're new to YAML, we recommend these resources:
The following sections will guide you through configuring various aspects of your Duro environment, starting with the Category Registry.
Learn how to handle errors and edge cases in the Duro API.
The API returns different types of errors:
Validation errors
Authentication errors
Authorization errors
Rate limiting errors
Server errors
Implement proper error handling
Add retry logic for rate limits
Log errors for debugging
Handle network timeouts
Join our for support and discussions.
Event-driven notifications and webhooks
And more...
View Only
Editor
Manager
Administrator
Follow least privilege principle
Regular access reviews
Document permission structures
Monitor access changes
Learn about setting up Webhooks for real-time updates.
# Example: Simple category definition
categories:
- code: "920"
type: ASSEMBLY
name: Cable Assembly
specs:
- name: Length
type: string
required: true
validation:
pattern: "^\\d+(\\.\\d+)?\\s*(mm|m)$"{
"errors": [
{
"message": "Not authorized to access Component",
"path": ["component"],
"extensions": {
"code": "FORBIDDEN",
"classification": "AuthorizationError"
}
}
]
}# Rate Limiting Error
{
"errors": [
{
"message": "Rate limit exceeded",
"extensions": {
"code": "RATE_LIMITED",
"retryAfter": 60
}
}
]
}async function queryWithRetry(query: string, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await executeQuery(query);
} catch (error) {
if (!isRetryableError(error) || attempt === maxRetries) {
throw error;
}
await delay(exponentialBackoff(attempt));
}
}
}query {
datarooms {
edges {
node {
id
name
description
members {
totalCount
}
}
}
}
}query {
roles {
edges {
node {
id
name
permissions
members {
totalCount
}
}
}
}
}mutation {
assignUserRole(input: {
userId: "user_123"
roleId: "role_456"
dataroomId: "dataroom_789"
}) {
success
}
}Change Orders (COs) help manage and track modifications to your products and components.
Draft
In Review
Approved
Rejected
Include clear descriptions
Link all affected items
Maintain approval chains
Document implementation results
Learn about to find specific changes.
Join the Duro developer community to get help, share ideas, and stay updated.
Join us on Slack: Reach out to us at [email protected] and we'll add you to the Slack workspace.
query {
changeOrders(first: 10) {
edges {
node {
id
number
title
status
affectedItems {
components {
id
name
}
documents {
id
title
}
}
}
}
}
}mutation {
createChangeOrder(input: {
title: "Update Component Specifications"
description: "Updating material specifications for better durability"
affectedComponentIds: ["comp_123"]
}) {
changeOrder {
id
number
}
}
}We welcome community contributions:
Bug reports
Feature requests
Documentation improvements
Code examples
Integration tutorials
Newsletter subscription
Technical Support: [email protected]
API Status: status.durohub.com
Security Issues: [email protected]
Be respectful and inclusive
Share knowledge freely
Report bugs responsibly
Follow security best practices
Protect user privacy
The revision scheme configuration system allows you to define how component revisions are managed throughout their lifecycle. It enables you to:
Define revision formats for different lifecycle stages
Configure validation rules for revision transitions
Specify allowed characters and patterns
Set up automatic increment rules
Establish consistent revision naming across your library
This configuration serves as the foundation for maintaining traceable and well-structured revision control in your product development process.
The revision scheme is defined in a YAML file that specifies how revisions should be formatted and validated at each lifecycle stage.
Establish the progression of lifecycle stages:
Define revision formats for each status:
Define rules for revision transitions and validation:
Specify characters to exclude from revision schemes:
Revision Format
Keep formats simple and intuitive
Use consistent delimiters
Consider future scaling needs
Note: Examples should reflect your actual use cases and common scenarios.
Lifecycle Stages
Define clear progression paths
Limit revision format changes between stages
Document transition rules
Validation
Blacklist confusing characters
Set appropriate value ranges
Define clear transition rules
Documentation
Include examples for each scheme
Document special cases
Maintain transition matrices
version: "1.0"
schema_type: "revision_scheme_config"
defaults:
segments:
integer:
min_value: 1
max_value: 999
letter:
min_value: "A"
max_value: "ZZ"
delimiter: "."
empty_value: "-"status_order:
- "Design"
- "Prototype"
- "Production"
- "Obsolete"schemes:
- status: "Design"
description: "Initial design phase revisions"
segments:
major:
type: "letter"
delimiter: ""
required: true
minor:
type: "integer"
min_value: 1
max_value: 99
required: false
examples:
- "A"
- "A.1"major:
type: "letter"
delimiter: ""
required: true
min_value: "A"
max_value: "Z"minor:
type: "integer"
delimiter: "."
required: false
min_value: 1
max_value: 99identifier:
type: "either" # Allows both letter and integer
delimiter: ""
required: truevalidation:
allowed_segment_types:
- "integer"
- "letter"
- "either"
required_fields:
- "status"
- "segments.major"
transitions:
allowed:
- from: "Design"
to: ["Prototype", "Obsolete"]
- from: "Prototype"
to: ["Production", "Obsolete"]blacklist:
- "I" # Avoid confusion with 1
- "O" # Avoid confusion with 0
- "Q" # Avoid confusion with 0
- "S" # Avoid confusion with 5schemes:
- status: "Design"
segments:
major:
type: "letter"
required: trueschemes:
- status: "Production"
segments:
major:
type: "letter"
required: true
minor:
type: "integer"
delimiter: "."
required: trueschemes:
- status: "Obsolete"
segments:
major:
type: "either"
required: trueschemes:
- status: "Design"
segments:
major:
type: "letter"
examples:
- "A"
- "B"
- "C"schemes:
- status: "Production"
segments:
major:
type: "letter"
minor:
type: "integer"
delimiter: "."
examples:
- "A.1"
- "B.2"
- "C.10"Enable enterprise Single Sign-On (SSO) for your Duro organization using SAML 2.0 with popular identity providers like Google Workspace, Microsoft Entra ID, Okta, and others.
SAML (Security Assertion Markup Language) allows your users to authenticate using your company's existing identity provider, providing centralized access control and enhanced security through features like multi-factor authentication.
User enters their organization identifier on the Duro login page
Duro redirects to Auth0 with the organization's SAML connection
Auth0 redirects to your Identity Provider (Google, Entra ID, etc.)
User authenticates with corporate credentials
Before starting, ensure you have:
Administrative access to your Identity Provider (Google Workspace, Microsoft Entra ID, etc.)
Auth0 tenant credentials (contact your Duro technical team)
PostHog access (for Duro internal team to enable feature flag)
Duro organization admin access (Site Admin role required)
Duration: 5-10 minutes
First, create a Single Page Application in your Auth0 tenant:
Navigate to Applications → Applications in Auth0 Dashboard
Click Create Application
Select Single Page Application type
Configure the allowed URLs:
Replace your-duro-domain.com with your actual Duro installation domain.
Duration: 5 minutes
Create a SAML connection in Auth0 before configuring your Identity Provider:
Go to Authentication → Enterprise → SAML
Click Create Connection
Choose a descriptive name (e.g., acmecorp-saml)
Copy the Service Provider details
These values are automatically generated based on your Auth0 tenant and connection name. You'll need them in the next step.
Duration: 10-15 minutes
Access Google Admin Console at
Go to Apps → Web and mobile apps → Add App → Add custom SAML app
Set app name (e.g., "Duro") and click Continue
Download IdP Information
Duration: 10-15 minutes
Access Entra Admin Center
Navigate to
Sign in with your Microsoft admin account
Create Enterprise Application
Duration: 5 minutes
Return to your SAML connection in Auth0:
Navigate back to Authentication → Enterprise → SAML
Click on your connection name
Enter IdP details:
Sign In URL: The SSO URL from your IdP
Duration: 2-3 minutes
Note: This step is typically performed by the Duro internal technical team.
The Duro technical team (or your on-prem administrator) will enable the samlAuthentication feature flag in PostHog for your organization.
Duration: 2 minutes
This is the final step, performed by a Duro organization administrator.
Sign In to Duro
Navigate to your Duro installation
Sign in with an account that has SITE Admin role
You must sign in using traditional email/password or Google SSO (not SAML yet)
Before announcing to users, thoroughly test the SAML flow:
Open incognito/private browser (ensures clean session)
Navigate to Duro and click "Sign in with SSO"
Enter organization slug (e.g., acmecorp)
Verify redirect chain:
IdP sends SAML assertion back to Auth0
Auth0 returns user to Duro, fully authenticated
ACS URL: https://{tenant}.auth0.com/login/callback?connection={name}
Entity ID: urn:auth0:{tenant}:{connection-name}
Google displays your IdP details. You'll use these in Auth0 later.
Download Metadata: Click to download the XML metadata file
OR manually note the following values: (protip: these values look nearly identical but are different)
SSO URL: https://accounts.google.com/o/saml2/idp?idpid=XXXXXXXXX
Entity ID: https://accounts.google.com/o/saml2?idpid=XXXXXXXXX
Certificate: Download the .pem or .crt file
Click Continue
Service Provider Details
Use the values you copied from Auth0 in Phase 2:
ACS URL: Paste the ACS URL from Auth0
Example: https://duro-dev.us.auth0.com/login/callback?connection=google-saml
Entity ID: Paste the Entity ID from Auth0
Example: urn:auth0:duro-dev:google-saml
Name ID format: Select EMAIL
Name ID: Select Basic Information > Primary email
Click Continue
Attribute Mapping
You can skip this step and just click Finish
Enable the App
You'll see the app in your Web and mobile apps list with status "OFF for everyone"
Click on the app name
Click User access
Select ON for everyone (or choose specific organizational units)
Click Save
Verify App Status
The app should now show "ON for everyone" (or your selected OUs)
Changes may take a few minutes to propagate
Go to Identity → Applications → Enterprise applications
Click New application
Click Create your own application
Name: Duro
Select Integrate any other application you don't find in the gallery (Non-gallery)
Click Create
Assign Users
Go to Users and groups in the left sidebar
Click Add user/group
Select users or groups that should have access
Click Assign
Configure SAML
Go to Single sign-on in the left sidebar
Select SAML
Click Edit on Basic SAML Configuration
Enter Service Provider details from Auth0:
Identifier (Entity ID): Paste Entity ID from Auth0
Example: urn:auth0:duro-dev:google-saml
Reply URL (Assertion Consumer Service URL): Paste ACS URL from Auth0
Download Certificate and Copy URLs
Go back to the SAML configuration page
Under SAML Certificates, download Certificate (Base64)
Under Set up Duro, copy:
Login URL (this is your SSO URL)
Microsoft Entra Identifier (Entity ID)
Logout URL (optional)
Save Configuration
Keep these values for the next phase
Upload or paste the X509 Signing Certificate
Protocol Binding: HTTP-POST (default)
Click Save Changes
Go to the Applications tab within your SAML connection
Find your Duro application (created in Phase 1)
Navigate to the Connections tab
Toggle ON to enable this connection for the application
Navigate to Organization Settings
Go to your organization settings page:
Format: https://{your-duro-domain}/org/@{company-org-slug}/settings/authentication
Example: https://duro.example.com/org/@acmecorp/settings/authentication
Enable SAML SSO
You should see a "SAML Configuration" section
Toggle ON the "Enable SAML SSO" switch
Auth0 SAML Connection Name: Enter the exact connection name from Phase 2
Example: google-saml or acmecorp-saml
This MUST match the connection name in Auth0 exactly (case-sensitive)
Enforce SAML (Optional):
Toggle ON if you want to require all users to authenticate via SAML
Toggle OFF to allow both SAML and traditional login methods
Recommended: Leave OFF initially for testing
Save Configuration
Click Save or Update Settings
You should see a success message
Verify Configuration
The page should display:
✅ SAML SSO Enabled
Connection name: {your-connection-name}
Enforce SAML: [Your setting]
Redirects to Auth0
Redirects to your IdP (Google/Entra)
Redirects back to Duro
Authenticate with test user credentials
Verify user profile:
Name and email populated correctly
User is member of correct organization
Session persists on page refresh
Test logout functionality
Allowed Callback URLs:
http://localhost:5173/callback,
https://your-duro-domain.com/callback
Allowed Logout URLs:
http://localhost:5173,
https://your-duro-domain.com
Allowed Web Origins:
http://localhost:5173,
https://your-duro-domain.comExample: https://duro-dev.us.auth0.com/login/callback?connection=google-saml
Sign on URL: Same as Reply URL
Click Save
Create custom validation rules and constraints for your parts data
Establish consistent part numbering schemes across your library
Enforce data quality standards through automated validation
This registry serves as the foundation for maintaining clean, well-structured product data that can scale with your organization.
The category & specs registry is defined in a YAML file that is used to define the relationships between your part specifications and their constraints (ie. validation rules).
As a Library Registry Maintainer, you'll have the ability to define, override, and extend the structure of your library parts, and author custom validations on the specification values directly or in relation to other specifications.
Import pre-defined category sets from Duro to leverage existing definitions.
Selectively exclude specific categories from imported sets that aren't relevant to your library.
Create reusable specifications that can be referenced across multiple categories. Common specs are organized in a hierarchical structure:
Create new categories with specific attributes and specifications.
Apply common specifications across all categories of a specific type. This helps maintain consistency and reduces repetition in your category definitions.
You can:
Reference entire specification groups using wildcards (/*)
Reference individual specifications
Set default required stages and severity levels
Apply specifications across all categories of a specific type
This is particularly useful when:
You want to enforce standard specifications across category types
You need to maintain consistent validation rules
You want to reduce repetition in your category definitions
Define validation rules for specifications using different data types and formats.
Define when specifications are required based on the item's lifecycle stage.
Extend pre-defined categories with additional specifications or modifications.
Reuse common specifications across different categories using references.
Use specifications defined in external libraries.
Import all specifications from a set using the wildcard (*) operator. This is useful when you need to include an entire group of related specifications.
The wildcard import is particularly powerful when:
You need all specifications from a logical grouping
You want to maintain consistency with a standard specification set
You want to reduce repetitive references to individual specifications
Specify requirements that trigger warnings rather than errors when not met.
Define specifications for integration with external systems.
Specify quality-related specifications and their validation rules.
Define the type of category being created. The type affects validation rules and available specifications.
Define specifications using different validation types and formats.
The conditional validation supports two types of validation rules:
Range-based conditions: Define minimum and maximum values based on another field's value
Enum-based conditions: Define specific allowed values based on another field's value
You can also specify a display format using the display property, which determines how the combined values should be presented. For example, "{baseColor} {number}" will show values like "Red 40" or "Yellow 5".
When referencing external categories or specifications, you can use different version formats:
Note: Using shorter versions will automatically use the latest compatible version within that scope.
uses:
- duro/mechanical-categories@v1
- duro/[email protected]excludes:
- "Basic Electrical Component" # Excludes category with name "Basic Electrical Component"
- "Capacitance" # Excludes category with name "Capacitance"commonSpecs:
dimensions:
standard_length:
name: "Length"
type: "string"
validation:
pattern: "^\\d+(\\.\\d+)?\\s*(mm|cm|m|in)$"
description: "Must be a number followed by a valid unit"
standard_width:
name: "Width"
type: "string"
validation:
pattern: "^\\d+(\\.\\d+)?\\s*(mm|cm|m|in)$"
description: "Must be a number followed by a valid unit"categories:
- code: "920"
type: ASSEMBLY
name: Cable Assembly
shortName: CABLE
unitOfMeasure: EACH
specs:
- $ref: "#/commonSpecs/dimensions/standard_length"
required: "*"
- name: Conductors
type: integer
required: "*"
validation:
minimum: 1
maximum: 100
description: "Number of conductors (1-100)"categoryTypeSpecs:
ASSEMBLY:
- $ref: "#/commonSpecs/dimensions/*" # Reference all dimension specs
required: "Prototype"
- $ref: "#/commonSpecs/physical/weight"
required: Production
severity: Warning
ELECTRICAL:
- $ref: "#/commonSpecs/electrical/*" # Reference all electrical specs
- $ref: "#/commonSpecs/quality/iso_certification"
required: Productionspecs:
- name: Voltage
type: string
validation:
pattern: "^\\d+(\\.\\d+)?\\s*V$"
description: "Must be a number followed by V (e.g., 5V, 3.3V)"
- name: PPAPLevel
type: integer
validation:
minimum: 1
maximum: 5
description: "Production Part Approval Process level (1-5)"
- name: Material
type: string
validation:
enum: [Plastic, Metal, Glass, Ceramic, Wood, Composite]
description: "Must be one of the specified materials"specs:
- name: NetsuiteItemId
type: string
required: "Production"
validation:
pattern: "^NS-\\d{8}$"
description: "Must be NS- followed by 8 digits"
- name: IONProcessTemplate
type: string
required: "Design"
severity: "Warning"
validation:
pattern: "^TPL-[A-Z0-9]{8}$"categories:
- extends: duro/mechanical-categories/[email protected]
name: Custom Bearing # change the name for this category
specs:
- name: Surface Treatment
type: string
required: Production
validation:
enum: [Chrome, Nickel, Phosphate, None]specs:
- $ref: "#/commonSpecs/dimensions/standard_length"
required: "*"
- $ref: "#/commonSpecs/electrical/operating_temp_max"
required: "Design"specs:
- $ref: "duro/electrical-categories/specs/voltage/input_voltage"
required: Designspecs:
# Imports all dimension specifications from Duro's mechanical specs
- $ref: "duro/mechanical-categories/specs/dimensions/*"
required: Prototype
# This will include Height, Width, Length, Thickness, etc. as defined in the Duro specs
# Each imported spec will maintain its validation rules while inheriting the required statusspecs:
- name: ERPCostCenter
type: string
required: "In Review"
severity: "Warning"
validation:
pattern: "^CC-[A-Z]{2}-\\d{4}$"specs:
- name: JiraECO
type: string
validation:
pattern: "^ECO-\\d{4}$"
description: "Jira ECO number"
- name: SAPMaterialNumber
type: string
validation:
pattern: "^\\d{9}$"
description: "Must be exactly 9 digits"specs:
- name: QualityInspectionFreq
type: string
required: "Production"
validation:
enum: [PerBatch, PerShift, Daily, Weekly, Monthly]
description: "Frequency of quality inspections"
- name: ISOCertification
type: string
required: "In Review"
severity: "Warning"
validation:
pattern: "^ISO\\d{4,5}:\\d{4}$"
description: "ISO certification number and year"categories:
- code: "920"
# Available types:
type: MECHANICAL # For mechanical parts and assemblies
# OR
type: ELECTRICAL # For electrical components
# OR
type: ASSEMBLY # For combined assemblies
# OR
type: DOCUMENT # For documentation
# OR
type: SOFTWARE # For software componentscommonSpecs:
validation:
# Pattern-based string validation
- name: PartNumber
type: string
validation:
pattern: "^[A-Z]{2}-\\d{6}$"
description: "Must be 2 capital letters followed by 6 digits"
# Enum-based string validation
- name: Material
type: string
validation:
enum: [Plastic, Metal, Glass, Ceramic]
description: "Must be one of the allowed materials"
# Unit-based measurements
- name: Length
type: string
validation:
pattern: "^\\d+(\\.\\d+)?\\s*(mm|cm|m|in)$"
description: "Number with valid unit (e.g., 10mm, 2.5cm)"commonSpecs:
validation:
# Range-based integer validation
- name: Quantity
type: integer
validation:
minimum: 1
maximum: 1000
description: "Must be between 1 and 1000"
# Simple integer validation
- name: PinCount
type: integer
validation:
minimum: 1
description: "Must be at least 1"commonSpecs:
dimensions:
- name: Height
type: paired
components:
value:
name: "Height Value"
type: integer
validation:
minimum: 0
maximum: 999999
description: "Numeric value for height"
unit:
name: "Height Unit"
type: string
validation:
enum: ["mm", "cm", "m", "in"]
description: "Unit for height measurement"
display: "{value}{unit}"commonSpecs:
conditional:
# Example 1: Range-based conditional validation
- name: ColorRange
type: conditional
components:
color:
name: "Color"
type: string
validation:
enum: ["red", "blue", "green"]
description: "Color selection"
range:
name: "Range Value"
type: integer
validation:
description: "Range value"
conditions:
- when: "color == 'red'"
minimum: 1
maximum: 20
- when: "color == 'blue'"
minimum: 21
maximum: 40
- when: "color == 'green'"
minimum: 41
maximum: 50
# Example 2: Enum-based conditional validation
- name: Food Dye Color
type: conditional
display: "{baseColor} {number}"
components:
baseColor:
name: "Base Color"
type: string
validation:
enum: [Red, Yellow, Blue, Green]
description: "Base color of the food dye"
number:
name: "Dye Number"
type: integer
validation:
description: "FDA approved dye number for the selected color"
conditions:
- when: "baseColor == 'Red'"
enum: [3, 40]
- when: "baseColor == 'Yellow'"
enum: [5, 6]
- when: "baseColor == 'Blue'"
enum: [1, 2]
- when: "baseColor == 'Green'"
enum: [3]uses:
# Full semantic version
- duro/[email protected]
# Minor version only (equivalent to latest patch)
- duro/[email protected]
# Major version only (equivalent to latest minor.patch)
- duro/software-categories@v1Change Order Workflow Templates provide a powerful way to customize the approval process for change orders in your PLM library. Using YAML configuration files, you can define multi-stage review processes, specify required approvers, and configure automated actions that align with your organization's change management procedures.
Every organization has unique requirements for managing engineering changes. Workflow templates allow you to:
Standardize Processes: Ensure consistent review procedures across all change orders
Enforce Compliance: Meet regulatory requirements with mandatory approval stages
Automate Actions: Configure automatic notifications and resolution behaviors
Customize Fields: Capture the specific information your team needs for decision-making
Each Duro library comes with two default workflow templates:
Default Template (default.yaml): A simple single-stage approval workflow
Double Template (double.yaml): A two-stage workflow with impact analysis fields
These templates serve as starting points that you can customize for your specific needs.
Every workflow template follows this structure:
The details.info.groups section lets you define custom fields that users fill out when creating a change order. Fields are organized into logical groups for better user experience.
text: Single-line text input
longtext: Multi-line text area
number: Numeric values
date: Date picker
currency: Monetary values with currency formatting
enum: Single selection from predefined options
list: Multiple selections from predefined options
The stages.open section defines your review process. You can create multiple stages that execute sequentially.
Unanimous: All reviewers must approve
Majority: More than 50% of reviewers must approve
Minimum: At least the minimum number of reviewers must approve
Field validations ensure data quality and completeness:
Configure automatic actions when change orders are approved, rejected, or withdrawn:
Begin with a basic workflow and add complexity as needed. It's easier to expand a working workflow than debug a complex one.
Choose clear, meaningful names for stages and fields:
✅ Good: manufacturing_impact_assessment
❌ Avoid: field1, stage_a
Organize fields into logical groups to improve the user experience:
Always include a clear description:
Before deploying a new workflow:
Create test change orders using the workflow
Verify all stages execute correctly
Confirm notifications reach the right people
Test edge cases (rejections, withdrawals)
For organizations where higher-cost changes need additional approval:
When different departments need to review changes:
You can create and manage change order templates programmatically using the GraphQL API through Apollo Studio, Duro's interactive API explorer. This approach is useful for:
Testing and validating your workflow configurations before deployment
Creating templates across multiple libraries
Version controlling your workflow configurations
Before you begin, you'll need:
A Duro API Key: Navigate to https://durohub.com/org/@<your-org>/libs/<your-library>/settings/api-keys to generate one. See Getting Started → Authentication for more information.
Your YAML workflow template: Either use one of the default templates or create your own custom configuration
First, create your workflow template YAML file. Here's a starter template:
Apollo Studio requires the YAML to be provided as a single-line string. To convert your YAML:
Visit the YAML Minifier: Go to https://onlineyamltools.com/minify-yaml
Paste your YAML: Copy your YAML configuration and paste it into the left input box
Copy the minified output: The right side will show your minified YAML as a single line. Copy this entire string.
Navigate to Apollo Studio: Open https://api.durohub.com/graphql in your browser
Set up authentication:
Click on the "Headers" tab at the bottom of the Operation panel
Add your API key as an x-api-key header
The value should be your API key (without quotes)
Paste the mutation: In the Operation panel, paste the following GraphQL mutation:
Set up variables: In the Variables panel, create your input object:
Replace YOUR_MINIFIED_YAML_HERE with the minified YAML string you copied from Step 2.
Execute the mutation: Click the "CreateTemplate" button to run the mutation
A successful response will show:
Your template is now created and ready to use in your change order workflows!
If you receive a YAML parsing error:
Ensure your YAML is valid before minifying (use a YAML validator)
Check that the minified string is properly copied without line breaks
Verify all quotes and special characters are properly escaped
If you get an authentication error:
Verify your API key is correct
Ensure the x-api-key header is properly set in the Headers tab
Confirm your API key has the necessary permissions
If the template configuration is rejected:
Review the error message for specific validation issues
Ensure all required fields are present in your YAML
Verify email addresses and other references are valid
Review the Change Order Workflow Reference for complete specification details
Explore example templates in your library's configuration
Start with a default template and customize it for your needs
Test your workflow with a pilot group before full deployment
version: '1.0'
description: A clear description of this workflow's purpose
schema_type: change_order_scheme
details:
info:
groups:
# Custom fields for change order creation
stages:
open:
# Review stages configuration
resolved:
# Actions when change order is resolved
closed:
# Final state configurationdetails:
info:
groups:
- name: Change Classification
icon: mdi-tag-multiple
fields:
- name: change_category
type: enum
label: Category
description: Primary category of this change
options:
- label: Design Update
value: design
- label: Cost Reduction
value: cost
- label: Quality Improvement
value: quality
validations:
required: truestages:
open:
- name: Engineering Review
types: ['Unanimous', 'Majority', 'Minimum']
default: Majority
minReviewers: 2stages:
open:
- name: Technical Review
types: ['Unanimous']
default: Unanimous
minReviewers: 2
reviewers:
users:
- email: [email protected]
isRequired: true
- name: Management Approval
types: ['Majority', 'Minimum']
default: Majority
minReviewers: 1
reviewers:
users:
- email: [email protected]
isRequired: truefields:
- name: estimated_cost
type: currency
label: Estimated Cost
validations:
required: true
min: 0
max: 1000000
- name: part_number
type: text
label: Affected Part Number
validations:
pattern: "^[A-Z]{3}-\\d{4}$"
required: trueresolved:
resolutions:
onapproval: [AUTO_CLOSE] # Automatically close when approved
onrejection: [MANUAL_CLOSE] # Require manual closure when rejected
onwithdrawal: [MANUAL_CLOSE] # Require manual closure when withdrawngroups:
- name: Impact Analysis
icon: mdi-chart-line
fields:
- name: schedule_impact
- name: cost_impact
- name: quality_impactdescription: |
Engineering change workflow for hardware modifications.
Requires technical review followed by management approval
for changes exceeding $10,000 in impact.stages:
open:
- name: Initial Review
types: ['Majority']
default: Majority
minReviewers: 2
- name: Executive Approval
types: ['Minimum']
default: Minimum
minReviewers: 1
reviewers:
users:
- email: [email protected]
isRequiredToApprove: truestages:
open:
- name: Engineering Review
reviewers:
users:
- email: [email protected]
- email: [email protected]
- name: Manufacturing Review
reviewers:
users:
- email: [email protected]
- email: [email protected]
- name: Quality Review
reviewers:
users:
- email: [email protected]
- email: [email protected]version: '1.0'
description: Engineering Change Review Process
schema_type: change_order_scheme
details:
info:
groups:
- name: Change Information
icon: mdi-information
fields:
- name: change_description
type: longtext
label: Change Description
description: Detailed description of the proposed change
validations:
required: true
- name: impact_assessment
type: enum
label: Impact Level
description: Estimated impact of this change
options:
- label: Low Impact
value: low
- label: Medium Impact
value: medium
- label: High Impact
value: high
validations:
required: true
stages:
open:
- name: Engineering Review
types: ['Unanimous', 'Majority']
default: Majority
minReviewers: 2
reviewers:
users:
- email: [email protected]
isRequired: true
resolved:
resolutions:
onapproval: [AUTO_CLOSE]
onrejection: [MANUAL_CLOSE]
onwithdrawal: [MANUAL_CLOSE]
closed:
notifyList:
users: []
emails: []mutation CreateTemplate($input: CreateTemplateInput!) {
changeOrders {
createTemplate(input: $input) {
id
name
config
library {
id
name
}
createdAt
}
}
}{
"input": {
"name": "My Engineering Review Process",
"configYAML": "YOUR_MINIFIED_YAML_HERE"
}
}{
"data": {
"changeOrders": {
"createTemplate": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Engineering Review Process",
"config": {
"version": "1.0",
"description": "Engineering Change Review Process",
// ... rest of your configuration
},
"library": {
"id": "fc28b204-0cd0-46b3-96eb-b0720c16c423",
"name": "Main Library"
},
"createdAt": "2024-01-15T10:30:00Z"
}
}
}
}


Change Order Validations provide a comprehensive system for ensuring data integrity and enforcing business rules throughout your change management process. This powerful feature allows you to run both built-in system validations and custom validation rules that align with your organization's specific requirements.
In complex engineering environments, a single oversight can lead to costly errors, production delays, or compliance issues. The validation system helps you:
Prevent Errors Early: Catch issues before changes are approved and implemented
Enforce Standards: Ensure all changes meet your organization's requirements
Maintain Consistency: Apply the same rules across all change orders
Provide Transparency: Give reviewers clear visibility into validation results
The Duro platform provides two types of validations:
Built-in validations that run automatically to ensure fundamental data integrity:
Items must not exist in other OPEN change orders (prevents conflicts)
Required fields must be populated
Data types must match expected formats
Cross-references must be valid
JavaScript-based rules you create to enforce your specific business logic:
Cost threshold checks
Part number format validation
Required approver verification
Impact assessment requirements
Validations can be triggered manually at any time during the DRAFT state of the change order lifecycle. Here's how to run validations and interpret the results:
Every validation run is stored, allowing you to track how validation results change over time as issues are resolved.
Track how validation results have changed over time:
Each validation result provides detailed information to help you understand and resolve issues:
PASS: The validation succeeded without issues
FAIL: The validation failed and must be resolved
WARNING: The validation found potential issues that should be reviewed
ERROR: Blocks the change order from proceeding (hard stop)
WARNING: Alerts reviewers but doesn't block progress (soft warning)
Each validation generates detailed logs that help with debugging:
Custom validations allow you to implement organization-specific business rules. When a custom validation runs, you can access both the validation result and the underlying rule definition:
When debugging validation failures, focus on just the problems:
Then filter the results in your application code:
Track validation improvements over multiple runs:
Here's a complete workflow for integrating validations into your change order process:
Don't wait until the approval stage to run validations. Run them:
After initial change order creation
After any significant updates
Before submitting for approval
After resolving validation failures
When creating custom validations, ensure error messages are actionable:
✅ Good: "Part number ABC-123 must have an associated drawing document"
❌ Avoid: "Validation failed"
Reserve warnings for issues that should be reviewed but don't necessarily block progress:
Missing optional documentation
Unusual but acceptable values
Recommendations for best practices
Track validation patterns across change orders to identify:
Common failure points in your process
Training opportunities for users
Potential system improvements
Use the detailed logs to:
Debug custom validation logic
Understand validation execution flow
Provide context to users about failures
Ensure you're requesting the results field with proper pagination:
Verify that:
The validation rule is active in your library
The validation rule code is syntactically correct
The change order meets the criteria for the validation to run
Check for:
Data changes between validation runs
Updates to validation rules
Different validation contexts (some validations may be conditional)
Review your organization's validation requirements
Implement custom validations for your specific needs
Integrate validation checks into your change order workflow
Monitor validation metrics to improve your process
For more information on creating custom validation rules, see the documentation.
Enable Custom Logic: Create organization-specific validation rules
mutation ValidateChangeOrder($changeOrderId: ID!) {
changeOrders {
validate(id: $changeOrderId) {
id
isValid # Overall validation result
validationRun {
id
state # PASS or FAIL
summary {
passCount
failCount
warningCount
}
results(pagination: { first: 50 }) {
edges {
node {
name
state
errorMessage
}
}
}
}
}
}
}{
"data": {
"changeOrders": {
"validate": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"isValid": false,
"validationRun": {
"id": "987fcdeb-51a2-43d1-9876-543210fedcba",
"state": "FAIL",
"summary": {
"passCount": 8,
"failCount": 2,
"warningCount": 1
},
"results": {
"edges": [
{
"node": {
"name": "Items must not exist in other OPEN Change Orders",
"state": "FAIL",
"errorMessage": "Item P123-456 exists in Change Order CO-2024-001"
}
}
]
}
}
}
}
}
}query GetLatestValidation($changeOrderId: ID!) {
changeOrders {
get(filter: { ids: [$changeOrderId] }) {
connection {
edges {
node {
id
name
isValid
latestValidationRun {
id
state
createdAt
createdBy {
name
}
summary {
passCount
failCount
warningCount
}
# Get detailed results with logs
results(pagination: { first: 100 }) {
edges {
node {
name
validationType # SYSTEM or CUSTOM
state # PASS, FAIL, or WARNING
onFailure # ERROR or WARNING
errorMessage
# Access validation logs for debugging
logs {
error {
message
}
info {
message
}
all {
message
type
}
}
}
}
}
}
}
}
}
}
}
}query GetValidationHistory($changeOrderId: ID!) {
changeOrders {
get(filter: { ids: [$changeOrderId] }) {
connection {
edges {
node {
id
# Get all historical validation runs
validationRuns(pagination: { first: 10 }) {
edges {
node {
id
collectionId
state
createdAt
createdBy {
name
}
summary {
passCount
failCount
warningCount
}
# Can drill into specific failed validations
results(pagination: { first: 100 }) {
edges {
node {
name
state
errorMessage
}
}
}
}
}
totalCount
}
}
}
}
}
}
}logs {
# Informational messages about validation execution
info {
message
}
# Warnings about potential issues
warn {
message
}
# Error details when validation fails
error {
message
}
# All logs in chronological order
all {
message
type # "info", "warn", "error", or "log"
}
}query GetCustomValidationDetails($changeOrderId: ID!) {
changeOrders {
get(filter: { ids: [$changeOrderId] }) {
connection {
edges {
node {
latestValidationRun {
results(pagination: { first: 50 }) {
edges {
node {
name
validationType
state
errorMessage
# Only populated for CUSTOM validations
validationRule {
id
name
type
version
code # The JavaScript validation code
library {
id
name
}
}
# Debug custom validation execution
logs {
all {
message
type
}
}
}
}
}
}
}
}
}
}
}
}query GetFailedValidations($changeOrderId: ID!) {
changeOrders {
get(filter: { ids: [$changeOrderId] }) {
connection {
edges {
node {
id
name
isValid
latestValidationRun {
state
summary {
failCount
warningCount
}
# Fetch all results, then filter client-side
results(pagination: { first: 100 }) {
edges {
node {
name
state
errorMessage
logs {
error {
message
}
}
}
}
}
}
}
}
}
}
}
}const failedValidations = data.changeOrders.get.connection.edges[0]
.node.latestValidationRun.results.edges
.filter(edge => edge.node.state === 'FAIL' || edge.node.state === 'WARNING');query CompareValidationRuns($changeOrderId: ID!) {
changeOrders {
get(filter: { ids: [$changeOrderId] }) {
connection {
edges {
node {
# Current state
latestValidationRun {
createdAt
summary {
passCount
failCount
warningCount
}
}
# Historical comparison
validationRuns(pagination: { first: 5 }) {
edges {
node {
createdAt
summary {
passCount
failCount
warningCount
}
}
}
}
}
}
}
}
}
}// 1. Create or update a change order
const changeOrderId = await createChangeOrder(changeOrderData);
// 2. Run validations
const validationResult = await runValidation(changeOrderId);
// 3. Check if valid
if (!validationResult.isValid) {
// 4. Get detailed failure information
const failures = validationResult.validationRun.results.edges
.filter(edge => edge.node.state !== 'PASS')
.map(edge => ({
name: edge.node.name,
error: edge.node.errorMessage,
severity: edge.node.onFailure
}));
// 5. Display failures to user
displayValidationErrors(failures);
// 6. Allow user to fix issues
await promptUserToResolveIssues(failures);
// 7. Re-run validations after fixes
const retryResult = await runValidation(changeOrderId);
if (retryResult.isValid) {
console.log('All validations passed!');
}
}
// 8. Proceed with approval workflow only if valid
if (validationResult.isValid) {
await submitForApproval(changeOrderId);
}validationRun {
results(pagination: { first: 100 }) { # Don't forget pagination
edges {
node {
name
state
}
}
}
}This document provides a complete reference for the Change Order Workflow Template YAML specification. The schema defines the structure and constraints for change order approval workflows in the Duro PLM system.
Schema Version: 1.0
Schema URL:
Type: Object
The JSON Schema provides automated validation for your workflow templates. When editing YAML files, you can use this schema to:
Validate your templates before deployment to catch errors early
Get auto-completion in editors that support YAML Language Server
Ensure compliance with all required fields and constraints
To use the schema in your YAML files, add this comment at the top:
Many editors (VS Code, IntelliJ, etc.) will then provide real-time validation and helpful suggestions as you write your workflow templates.
The details object contains custom field definitions organized in groups.
Each field in a group has the following properties:
Global validation rules (currently reserved for future use).
Defines the workflow stages and their behavior.
Array of sequential review stages. Each stage has:
Each stage must support one or more approval types:
User Reviewer Properties
Note: Each reviewer must have either id (UUID) or email, but not both. Use id when you have the user's UUID, or email for easier configuration.
Note: For users in the notification list, use either id (UUID) or email, similar to reviewers.
The schema enforces these validation rules:
Required Fields: All fields marked as required must have values
Pattern Matching: Field names must match ^[a-zA-Z_][a-zA-Z0-9_]*$
Length Limits:
Use Semantic Names: Choose descriptive names for fields and stages
Provide Descriptions: Help users understand field purposes
Set Appropriate Validations: Use min/max for numeric fields
Configure Notifications: Ensure stakeholders are informed
details
object
Yes
Custom field definitions
validations
array
No
Global validation rules
stages
object
Yes
Workflow stage configuration
fields
array
Yes
Array of field definitions
description
string
No
Help text (max 500 chars)
placeholder
string
No
Placeholder text (max 200 chars)
required
boolean
No
Deprecated - use validations.required
validations
object
No
Field validation rules
default
string
No
Default value (for enum fields)
options
array
Conditional
Required for list/enum types
multiSelect
boolean
No
Enable multi-selection (list type only)
currency
Currency amount
validations.min, validations.max
enum
Single selection dropdown
options, default
list
Multiple selection
options, multiSelect
pattern
string
text
Regex validation pattern
minReviewers
integer
No
Minimum reviewers required
reviewers
object
No
Pre-assigned reviewers
notifyList
object
No
Notification recipients
isRequiredToApprove
boolean
No
Must approve for completion
Labels/Names: 100 characters
Placeholders: 200 characters
Type Constraints:
List/Enum fields must have at least one option
Only list fields can use multiSelect
Stage Requirements:
At least one open stage is required
Each stage must have a name, types array, and default type
Default type must be in the types array
Test Workflows: Validate all paths through your workflow
Version Control: Track changes to workflow templates
Document Decisions: Use the description field to explain workflow design choices
version
string
Yes
Schema version (e.g., "1.0")
description
string
Yes
Human-readable description (max 500 chars)
schema_type
string
Yes
name
string
Yes
Display name (max 100 chars)
icon
string
No
Material Design Icon (format: mdi-icon-name)
description
string
No
type
string
Yes
Field type (see Field Types section)
name
string
No
Programmatic name (pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$)
label
string
Yes
text
Single-line text input
-
longtext
Multi-line text area
-
number
Numeric input
validations.min, validations.max
date
Date picker
required
boolean
All types
Field is mandatory
min
integer
number, currency, text
Minimum value or length
max
integer
number, currency, text
label
string
Yes
Display text (max 100 chars)
value
string
Yes
Stored value (max 100 chars)
description
string
No
id
string
Yes
Validation ID (pattern: ^\d+\.\d+$)
severity
string
Yes
One of: error, warn, info
name
string
Yes
Stage name (max 100 chars)
types
array
Yes
Available approval types
default
string
Yes
Unanimous
All reviewers must approve
Majority
More than 50% must approve
Minimum
At least minReviewers must approve
id
string
One of id or email
User UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
email
string
One of id or email
User email address
isRequired
boolean
No
AUTO_CLOSE
Automatically transition to closed
MANUAL_CLOSE
Require manual closure
Must be "change_order_scheme"
Group description (max 500 chars)
Display label (max 100 chars)
-
Maximum value or length
Option help text (max 500 chars)
Default approval type
Must participate in review
# yaml-language-server: $schema=https://phoenix-production.durohub.com/static/schemes/change-orders/schema.jsonversion: "1.0"
description: "Engineering change order workflow with dual approval"
schema_type: "change_order_scheme"
details: { }
validations: [ ]
stages: { }details:
info:
groups:
- name: string
icon: string (optional)
description: string (optional)
fields: [ ]validations:
required: boolean
min: integer
max: integer
pattern: string (regex)options:
- label: string
value: string
description: string (optional)validations:
- id: string
severity: stringstages:
open: [ ] # Required: Active review stages
resolved: { } # Optional: Resolution configuration
closed: { } # Optional: Closed state configuration
onHold: { } # Optional: On-hold state configurationreviewers:
users:
- id: string (UUID) # Use id with user's UUID
isRequired: boolean
isRequiredToApprove: boolean
# OR
- email: string # Use email with user's email address
isRequired: boolean
isRequiredToApprove: booleanresolved:
actions: [string]
notifyList: { }
resolutions:
onapproval: [string]
onrejection: [string]
onwithdrawal: [string]notifyList:
users:
- id: string (UUID) # Use id with user's UUID
# OR
- email: string # Use email with user's email address
emails:
- string (email format) # Additional email addresses not tied to usersversion: "1.0"
description: "Medical device change control workflow"
schema_type: "change_order_scheme"
details:
info:
groups:
- name: "Change Classification"
icon: "mdi-medical-bag"
fields:
- name: "change_type"
type: "enum"
label: "Change Type"
options:
- label: "Design Change"
value: "design"
description: "Modifications to product design"
- label: "Process Change"
value: "process"
description: "Manufacturing process updates"
default: "design"
validations:
required: true
- name: "risk_level"
type: "list"
label: "Risk Categories"
multiSelect: true
options:
- label: "Patient Safety"
value: "safety"
- label: "Product Performance"
value: "performance"
- label: "Regulatory Compliance"
value: "regulatory"
- name: "Impact Assessment"
icon: "mdi-chart-line"
fields:
- name: "validation_required"
type: "enum"
label: "Validation Required"
options:
- label: "Full Validation"
value: "full"
- label: "Partial Validation"
value: "partial"
- label: "No Validation"
value: "none"
validations:
required: true
stages:
open:
- name: "Engineering Review"
types: ["Unanimous"]
default: "Unanimous"
minReviewers: 2
reviewers:
users:
- email: "[email protected]"
isRequired: true
- name: "Quality Review"
types: ["Unanimous", "Majority"]
default: "Unanimous"
minReviewers: 1
reviewers:
users:
- email: "[email protected]"
isRequiredToApprove: true
resolved:
resolutions:
onapproval: ["AUTO_CLOSE"]
onrejection: ["MANUAL_CLOSE"]
notifyList:
users:
- email: "[email protected]"
closed:
notifyList:
emails:
- "[email protected]"Learn how to effectively search and filter data in the Duro API using our powerful filtering system.
All component searches use this base query structure:
Our GraphQL API supports various filtering options:
Text matching (contains, startsWith, endsWith)
Numeric comparisons (eq, gt, lt, gte, lte)
Date ranges
Enum values
We use cursor-based pagination for optimal performance:
The filtering system supports these operators:
eq: Exact match
in: Match any value in an array
contains: String contains
Find components by manufacturer and status:
Search by CPN and creation date:
Find components by category and name pattern:
The API supports complex logical combinations using and and or operators. Here's an advanced example:
This complex filter demonstrates:
Nested logical operators
Multiple condition groups
Mixed filter types
Exclusion conditions
Start with simple filters and add complexity as needed
Use appropriate operators for better performance
Consider pagination for large result sets
Test complex filters with smaller data sets first
Learn about managing access with to control who can perform these searches.
query FilterBOMComponents($filter: ComponentFilter) {
componentSearch(filter: $filter) {
nodes {
id
name
status
}
}
}gte/lte: Greater/Less than or equalisNull: Check for null values
notIn: Exclude values in array
query {
components(
first: 10
filter: {
status: ACTIVE
partNumber: { contains: "ABC" }
createdAt: { gte: "2024-01-01" }
}
) {
edges {
node {
id
name
partNumber
}
}
}
}query {
components(
first: 10
orderBy: { field: CREATED_AT, direction: DESC }
) {
edges {
node {
id
name
createdAt
}
}
}
}query {
components(
first: 10
after: "cursor_value"
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
}
}
}
}{
"filter": {
"and": [
{
"status": {
"eq": "Prototype"
}
},
{
"manufacturer": {
"name": {
"in": ["Texas Instruments", "Analog Devices"]
}
}
}
]
}
}{
"filter": {
"and": [
{
"cpn": {
"in": ["CPN-001", "CPN-002", "CPN-003"]
}
},
{
"createdAt": {
"gte": "2023-01-01",
"lte": "2023-12-31"
}
}
]
}
}{
"filter": {
"and": [
{
"name": {
"contains": "47uH"
}
},
{
"category": {
"name": {
"eq": "Capacitor"
}
}
},
{
"isModified": {
"eq": true
}
}
]
}
}{
"filter": {
"or": [
{
"and": [
{
"status": {
"eq": "Design"
}
},
{
"procurement": {
"type": {
"in": ["MAKE", "BUY"]
}
}
}
]
},
{
"and": [
{
"isFavorited": {
"eq": true
}
},
{
"revision": {
"gte": "B"
}
}
]
}
],
"and": [
{
"mpn": {
"isNull": false
}
},
{
"category": {
"name": {
"notIn": ["Top Level Assembly"]
}
}
}
]
}
}This document provides a complete reference for the CPN (Customer Part Number) Schema YAML specification. The schema defines the structure and constraints for CPN generation schemes in the Duro PLM system.
Schema Version: 1.0
JSON Schema: https://json-schema.org/draft-07/schema#
Type: Object
The settings object contains global configuration options for CPN generation.
When allow_freeform is true, you can optionally specify custom validation rules:
Basic freeform with default validation:
Custom pattern for company naming convention:
Flexible pattern with length limit:
The override_elements setting provides granular control over which specific elements in your CPN scheme can be overridden by users. This is useful when you want to allow customization of only certain parts of the CPN while keeping others fixed.
Allow override of specific elements only:
Allow all elements to be overridden (default behavior):
The elements array contains definitions for each component of the CPN. Each element must be one of the following types:
list — selects a value from a predefined list or reference
constant — inserts a fixed value such as a delimiter or prefix
numeric_counter — generates sequential numeric integer values within a range
All elements include these base properties:
typeSpecifies what kind of element is being defined. Must be one of: list, constant, numeric_counter, hex_counter, free, or group. This property determines how the element behaves in generation.
nameA unique identifier for the element. This is used internally to reference the element (e.g., in attachedTo) and must be unique within the scheme. Names should be descriptive (e.g., prefix, sequence, variant) and not include spaces.
requiredIndicates whether the element must be present in every generated CPN.
If true, the element is always included.
If false, the element may be omitted (e.g., optional group for free‑form suffixes).
Defaults to false if not specified.
allow_freeform (Element-Level)When an element has allow_freeform: true and is listed in the override_elements array, users can provide custom values that go beyond the element's normal constraints. For example, a list element with values ["A", "B", "C"] could accept "Z" as an override if allow_freeform: true.
If true, the element accepts freeform values when overridden (validated against freeform_validation if provided)
If false or omitted, the element only accepts values that conform to its type-specific rules
Only applies when the element is included in settings.override_elements
freeform_validation (Element-Level)Provides custom validation rules for individual element freeform overrides, following the same structure as global freeform_validation:
If not provided, defaults to the same pattern as global freeform validation (alphanumeric, hyphens, underscores, 50 character limit).
attachedToThe attachedTo property is commonly used with counters and variants to ensure that their values remain unique within the context of one or more parent elements. For example, a sequence counter attached to a prefix will generate independent sequences for each prefix value. This prevents conflicts across different contexts without forcing a single global sequence. Multiple attachments are supported (e.g., ['prefix', 'sequence']), allowing values to be scoped by combinations of elements when needed.
Example
A list element allows selection from a predefined set of values. It’s typically used for intelligent schemes where the CPN prefix or another segment is determined by categories, part families, or other fixed options.
Values can be defined inline as strings, as objects with metadata, or pulled dynamically using template references. Optionally, a regex validation pattern can be applied for additional checks.
Values need to be one of:
Array of strings
Array of objects with id, name, and optional description
Template reference in format ${{namespace.field}}
Constants are fixed values that are injected into every generated CPN. They can be used for delimiters (such as - or .), prefixes, or any string that should always appear in the same position. A constant can be any string length.
This inserts a dash (-) as a separator between other CPN elements.
This ensures every CPN begins with the prefix TMP-, e.g., TMP-10001.
A numeric counter generates sequential numbers within the specified range. It is always fixed length, determined by the number of digits in format.max_value, and the system automatically prepends leading zeros to match that length.
This numeric counter generates a 5‑digit sequence ranging from 00001 to 99999. The length is determined by the number of digits in max_value (here, 5 digits). Leading zeros are automatically prepended so that every value is fixed length.
Because in this example the counter is attached to the prefix element, its sequence is tracked separately for each prefix value. This ensures uniqueness within the context of the prefix, rather than across all CPNs.
For example:
First three CPNs for prefix 100:
100-00001
100-00002
A hex counter generates sequential values in hexadecimal within the specified range. It is always fixed length, determined by the number of digits in format.max_value, and the system automatically prepends leading zeros to match that length.
2‑Digit Hex Sequence
Generates values like 00, 01, … FE, FF. Always fixed at 2 hex digits with zero padding.
Groups allow you to combine multiple elements into a single logical unit. They are especially useful for creating more complex structures and, when used with attachedTo, can make the scoping of values clearer and less error‑prone.
A common pattern is grouping a prefix and a sequence into a base_cpn group, so that a variant can attach to the group instead of attaching separately to both prefix and sequence. This ensures that the variant’s values are tracked in the context of the complete base number, improving clarity and reducing ambiguity in the YAML configuration.
Group elements can contain any other element type, including:
List elements
Constant elements
Free text elements (only available within groups)
Numeric counters
Example 1
This group defines an optional variant section for the CPN. It always inserts a period (.) as a separator, followed by a free‑form variant field. The variant allows any word character (A–Z, a–z, 0–9, _) with a length between 1 and 10.
Example CPNs:
123-4567.A
123-4567.TEST1
Example 2
In this example:
The group base_cpn combines prefix and sequence into a single logical unit.
The variant is attached to base_cpn, so its values are tracked in the context of the complete base number rather than being tied individually to both prefix and sequence.
The free element allows users to provide custom input that isn’t generated automatically. Validations ensure that free‑form values follow defined rules, such as a regex pattern and maximum length. This is commonly used for suffixes, variant labels, or other optional identifiers that can be user‑entered.
Note: allow_override must be true for free to be available for user entry.
The examples array must contain at least one example CPN that follows the defined scheme.
Template references allow dynamic value lists to be pulled from the system:
Template references must match the pattern: ^\$\{\{\s*[\w\.]+\s*\}\}$. Currently, the only supported namespace is duro. and the only supported fields are categories and families.
Version must match pattern ^\d+\.\d+$
Schema type must be exactly "id_generation_scheme"
All referenced element names must be unique within their scope
Counter ranges must be valid (min ≤ max) and min must be ≥ 0
Structure
Place required elements before optional ones
Group related elements together
Use consistent naming conventions
elements
array
Yes
Array of element definitions
examples
array
Yes
Array of example CPNs that follow the scheme
array
No
null
Array of element names that can be overridden individually. Only applies when allow_override is true. See "Element-Level Override Control" section below for details.
freeform_validation
object
No
-
Custom validation rules for freeform overrides. Only applies when allow_freeform is true. If not provided, defaults to alphanumeric characters, hyphens, and underscores with 50 character limit.
true
false
["element1"]
User may override only specified elements. Values must conform to each element's validation rules.
true
true
null (default)
User may override with freeform text OR override individual elements. Freeform validates against global freeform_validation.
true
true
["element1"]
User may override with freeform text OR override specified elements with their element-level rules.
hex_counter — generates sequential hexadecimal values within a rangefree — allows user‑entered free‑form text validated by a regex and max length
group — bundles multiple elements into a single logical unit
allow_freeform
boolean
No
Whether this element accepts freeform values beyond its defined constraints when overridden. Defaults to false. Only applies when the element is listed in override_elements.
freeform_validation
object
No
Custom validation rules for element-level freeform overrides. Only applies when allow_freeform is true for this element.
attachedTo
array
No
Names of one or more elements that this element is scoped to. Ensures values are unique within the context of the attached element(s).
validation.pattern
string
No
Regex pattern for additional validation of list values
100-00003Then the first CPN for prefix 101:
101-00001
Returning to prefix 100, the next generated CPN continues from its own sequence:
100-00004
This approach makes the YAML more readable and unambiguous compared to listing multiple attachedTo references separately.
Hex counter values must be valid hexadecimal strings
Template references must match the specified pattern
At least one example CPN must be provided
Group elements must contain at least one element
All elements must be valid and referenced correctly
Groups can be optional even if they contain required elements
Validation
Always include regex patterns for list values
Set appropriate counter ranges for your volume
Consider case sensitivity implications
Documentation
Include descriptions for all list values
Provide diverse examples
Comment complex regex patterns
Maintenance
Use template references for shared value sets
Keep counter ranges generous for future growth
Consider versioning implications
version
string
Yes
Version of the CPN schema. Must match pattern ^\d+\.\d+$
schema_type
string
Yes
Must be "id_generation_scheme"
settings
object
Yes
allow_override
boolean
No
false
Whether manual CPN override is allowed. If false, the user must always accept the system-generated CPN. If true, the user may enter another CPN (see allow_freeform for rules).
allow_freeform
boolean
No
false
Defines how overrides behave. If false, overrides must still conform to the YAML scheme's format and validation rules. If true, overrides may be any unique valid string.
pattern
string
No
Regex pattern that freeform CPNs must match. If not specified, uses default pattern ^[a-zA-Z0-9\-_]+$
max_length
integer
No
Maximum length for freeform CPNs. If not specified or ≤ 0, defaults to 50 characters
description
string
No
null (default)
All elements are overrideable when allow_override is true
["element1", "element2"]
Only the specified elements can be overridden
false
false
ignored
User must accept the system-generated CPN. No manual input allowed.
false
true
ignored
Same as above — allow_freeform has no effect when allow_override is false.
true
false
null (default)
type
string
Yes
Element type identifier (e.g., list, constant, numeric_counter, etc.).
name
string
Yes
Unique name for the element.
required
boolean
No
values
array|string
Yes
Array of values or template reference
allow_freeform
boolean
No
Whether to accept freeform values beyond the list when overridden
freeform_validation
object
No
value
string
Yes
The constant value
attachedTo
array
No
Names of elements this counter is attached to
format.min_value
integer
Yes
Minimum value (must be ≥ 0)
format.max_value
integer
Yes
attachedTo
array
No
Names of elements this counter is attached to
format.min_value
string
Yes
Minimum value in hex (pattern: ^[0-9A-F]+$)
format.max_value
string
Yes
elements
array
Yes
Array of nested element definitions
validation.pattern
string
Yes
Regex pattern for validation
validation.max_length
integer
Yes
Maximum length of the text
Global settings for CPN generation
override_elements
Human-readable description of the format requirements shown to users
User may override entire CPN or individual elements, but values must conform to YAML scheme rules.
Whether the element must appear in every generated CPN. Defaults to false.
Custom validation for element-level freeform values
Maximum value
Maximum value in hex (pattern: ^[0-9A-F]+$)
settings:
allow_override: boolean # Default: false
allow_freeform: boolean # Default: false
override_elements: [string] # Default: null (all elements)
freeform_validation: # Optional, only used when allow_freeform is true
pattern: string # Regex pattern for validation
max_length: integer # Maximum character length
description: string # Human-readable format descriptionsettings:
allow_override: true
allow_freeform: true
# Uses default pattern ^[a-zA-Z0-9\-_]+$ with 50 character limitsettings:
allow_override: true
allow_freeform: true
freeform_validation:
pattern: "^[A-Z]{2,4}-\\d{4,6}$"
max_length: 20
description: "Format: 2-4 letters, hyphen, 4-6 digits"settings:
allow_override: true
allow_freeform: true
freeform_validation:
pattern: "^[A-Z][A-Z0-9\\-_]*$" # Must start with uppercase letter
max_length: 25
description: "Must start with uppercase letter, followed by letters, numbers, hyphens, or underscores"settings:
allow_override: true
allow_freeform: false
override_elements: ["variant"] # Only variant can be overridden
elements:
- type: "list"
name: "category"
# ... category element (NOT overrideable)
- type: "numeric_counter"
name: "sequence"
# ... sequence element (NOT overrideable)
- type: "list"
name: "variant"
values: ["A", "B", "C"]
# This variant element CAN be overriddensettings:
allow_override: true
# override_elements not specified = all elements overrideablefreeform_validation:
pattern: "^[A-Z]{1,3}$" # Regex pattern
max_length: 10 # Maximum character length
description: "1-3 uppercase letters" # User-friendly description- type: "numeric_counter"
name: "sequence"
required: true
attachedTo: ['prefix']
format:
min_value: 1
max_value: 9999- type: "list"
name: string
required: boolean
allow_freeform: boolean
freeform_validation:
pattern: string
max_length: integer
description: string
use: string
values: (string[] | object[] | template_string)
validation:
pattern: string- type: "list"
name: "category"
required: true
values:
# Simple string array
- "410"
- "591"
- "423"
# OR object array with metadata
use: "id" # Optional: use when values have id/name pairs
values:
- id: "410"
name: "Screws"
description: "Mechanical fasteners"
- id: "591"
name: "Resistors"
description: "Electronic components"
# OR reference to system values
values: ${{ duro.categories }}
validation:
pattern: "^\\d{3}$" # Optional regex pattern
# Element-level freeform override example
- type: "list"
name: "variant"
required: true
values: ["A", "B", "C"]
allow_freeform: true
freeform_validation:
pattern: "^[A-Z]{1,3}$"
max_length: 3
description: "1-3 uppercase letters"
# Users can select A, B, C OR enter custom values like "Z", "XY", "ABC"- type: "constant"
name: string
required: boolean
value: string- type: "constant"
name: "separator"
required: true
value: "-"- type: "constant"
name: "prefix"
required: true
value: "TMP-"- type: "numeric_counter"
name: string
required: boolean
attachedTo: string[]
format:
min_value: integer
max_value: integer- type: "numeric_counter"
name: "sequence"
required: true
attachedTo: [category]
format:
min_value: 1
max_value: 99999type: "hex_counter"
name: string
required: boolean
attachedTo: string[]
format:
min_value: string
max_value: string- type: "hex_counter"
name: "sequence"
required: true
attachedTo: [category]
format:
min_value: "0"
max_value: "FF"type: "group"
name: string
required: boolean
elements: array- type: "group"
name: "variant group"
required: false
elements:
- type: "constant"
name: "separator"
value: "."
- type: "free"
name: "variant"
validation:
pattern: "^\\w{1,10}$"- type: "group"
name: "base_cpn"
required: true
elements:
- type: "list"
name: "prefix"
required: true
values:
- id: "Category100"
value: "100"
validation:
pattern: "^[1-9][0-9]{2}$"
- type: "constant"
name: "delimiter_1"
value: "-"
- type: "numeric_counter"
name: "sequence"
required: true
attachedTo: ['prefix']
format:
min_value: 1
max_value: 99999
validation:
pattern: "^[1-9][0-9]{4}$"
- type: "constant"
name: "delimiter_2"
value: "-"
- type: "list"
name: "variant"
required: true
attachedTo: ['base_cpn']
values:
- id: "A"
value: "A"
- id: "B"
value: "B"
validation:
pattern: "^[A-Z]{1}$"- type: "free"
name: string
validation:
pattern: string
max_length: integerexamples:
- "410-0001"
- "ELEC-591-0042.variant"values: "${{ namespace.field }}"$schema: "https://json-schema.org/draft-07/schema#"
version: "1.0"
schema_type: "id_generation_scheme"
settings:
allow_override: true
allow_freeform: true
override_elements: ["variant"] # Only variant can be overridden individually
freeform_validation:
pattern: "^[A-Z]{2,4}-\\d{4,6}$"
max_length: 20
description: "Format: 2-4 letters, hyphen, 4-6 digits"
elements:
- type: list
name: category
required: true
use: id
values:
- id: "410"
name: Screws
description: Mechanical fasteners
validation:
pattern: "^\\d{3}$"
- type: constant
name: separator
required: true
value: "-"
- type: numeric_counter
name: sequence
required: true
attachedTo: [category]
format:
min_value: 1
max_value: 9999
- type: list
name: variant
required: true
attachedTo: [category, sequence]
values: ["A", "B", "C"]
allow_freeform: true
freeform_validation:
pattern: "^[A-Z]{1,3}$"
max_length: 3
description: "1-3 uppercase letters"
examples:
- "410-0001-A"
- "410-0001-Z" # Custom variant overrideversion: "1.0"
schema_type: "id_generation_scheme"
settings:
allow_override: true
allow_freeform: false
override_elements: ["category", "variant"] # Multiple elements can be overridden
elements:
- type: list
name: category
required: true
values: ["ELEC", "MECH", "SOFT"]
# allow_freeform not specified = false (must pick from list)
- type: constant
name: delimiter1
value: "-"
- type: numeric_counter
name: sequence
required: true
attachedTo: [category]
format:
min_value: 1
max_value: 999
# This element is NOT in override_elements, so cannot be overridden
- type: constant
name: delimiter2
value: "-"
- type: list
name: variant
required: true
values: ["STD", "ALT"]
allow_freeform: true
freeform_validation:
pattern: "^[A-Z]{2,5}$"
max_length: 5
description: "2-5 uppercase letters"
# Users can pick STD/ALT OR enter custom variants like "PROTO"
examples:
- "ELEC-001-STD"
- "ELEC-001-PROTO" # Custom variant
- "CHEM-001-STD" # Custom category overrideStay informed about important events in your Duro library with real-time webhook notifications.
Duro webhooks let your applications receive real-time notifications when important events occur in your library. Instead of constantly polling for changes, webhooks push event notifications directly to your specified endpoints, helping you build responsive integrations that react immediately to data changes.
Real-time updates - Get notified instantly when data changes
Efficient integration - No need for constant API polling
Selective subscriptions - Subscribe only to the events you care about
Reliable delivery - Built-in retry mechanisms with exponential backoff
Event occurs - Something happens in Duro (e.g., a component is updated)
Notification sent - Duro sends a lightweight JSON payload to your webhook URL
Fetch full data - Your application uses the provided metadata to fetch complete details via GraphQL as needed
This pattern keeps webhook payloads small and fast while giving you access to all the data you need.
Webhooks are scoped to a specific library. Each webhook can subscribe to one or more event types.
These events fire when components in your library are created, updated, or deleted:
Change order webhook events are defined but not yet implemented. The event names and payload structures below represent the planned implementation. We'll update this documentation when these events become available.
To create a webhook, you'll need:
A libraryId - the library you want to monitor
A url - your HTTPS endpoint that will receive notifications
A list of events - which event types to subscribe to
Retrieve all webhooks for one or more libraries:
You can update any webhook configuration field:
Add additional event subscriptions without removing existing ones:
Remove specific event subscriptions:
When you no longer need a webhook but want to preserve its configuration history, you can archive it instead of deleting it. Archived webhooks:
Stop receiving events - No new event deliveries will be attempted
Are hidden from queries - Won't appear in findAll or findOne results
Free up the name - You can create a new webhook with the same name
Archiving a webhook is permanent and cannot be undone. If you need to temporarily stop webhook deliveries, consider using the update mutation to set isEnabled: false instead.
All webhook notifications follow a consistent JSON structure:
For component events, the metadata object contains:
Each webhook request includes these headers:
If you configure a signingSecret for your webhook, each request will include an X-Webhook-Signature header containing an HMAC-SHA256 signature of the payload. Always verify this signature to ensure the request came from Duro.
Always use HTTPS - Webhook URLs must use HTTPS (enforced by Duro)
Verify signatures - Always validate the X-Webhook-Signature header
Use timing-safe comparison - Prevent timing attacks when comparing signatures
After receiving a webhook notification, use the provided IDs to fetch complete resource details via GraphQL.
Here's a complete example showing how to receive a webhook and fetch the full component data:
Duro automatically retries failed webhook deliveries using exponential backoff.
When a webhook delivery fails, Duro will retry with the following delays:
The retry schedule follows exponential backoff (2x multiplier) with a maximum delay of 5 minutes.
Success: HTTP status codes 200-299
Failure: All other status codes, timeouts, or connection errors
Respond quickly - Return a 200 status code immediately, then process asynchronously
Handle duplicates - Use eventId for idempotency; you may receive the same event more than once
Log failures - Track and investigate webhook processing failures
Query your webhook logs to monitor delivery status and troubleshoot issues:
Sync component and BOM changes to your ERP system in real-time:
Alert your team about important component changes:
Maintain a detailed audit trail of all changes:
Check if webhook is enabled - Query the webhook and verify isEnabled: true
Verify event subscriptions - Ensure the correct events are in the events array
Check your endpoint - Verify your URL is accessible from the internet
Check the secret - Ensure you're using the exact same signingSecret you configured
Use raw body - Signature is computed on the raw JSON string, not a parsed object
Check encoding - Ensure UTF-8 encoding throughout
Check retry status - Some deliveries may be queued for retry
Verify library scope - Webhooks only fire for events in their configured library
Check component filters - Events fire for all components in the library
Before configuring a production webhook, test your endpoint:
Review for securing your API requests
Explore for advanced component queries
Learn about to prepare for change order webhooks
Check out for robust integration patterns
Secure - HMAC-SHA256 signature verification to ensure authenticity
CHANGE_ORDER_STAGE_REVIEWER_DECISION
change_orders.stage_reviewer_decision
A reviewer approved or rejected their stage
CHANGE_ORDER_RESOLUTION
change_orders.resolution
Change order was fully approved, rejected, or withdrawn
events
Yes
Array of event types to subscribe to
-
description
No
Optional description (max 500 characters)
null
signingSecret
No
Secret for HMAC signature verification (min 16 characters if provided)
null
timeoutSeconds
No
Request timeout in seconds (5-300)
30
maxRetries
No
Maximum retry attempts on failure (0-10)
3
isEnabled
No
Whether the webhook is active
true
isArchived
Read-only
Whether the webhook has been archived (set via archive mutation)
false
metadata
object
Event-specific data (see below)
signingSecretRotate secrets periodically - Update your signing secret regularly
7th retry
64 seconds
8th retry
128 seconds
9th retry
256 seconds
10th retry
300 seconds (5 min max)
getLogs query to see delivery attempts and errorsCOMPONENT_CREATED
components.created
A new component was created in the library
COMPONENT_UPDATED
components.updated
An existing component was modified
COMPONENT_DELETED
components.deleted
A component was removed from the library
CHANGE_ORDER_OPENED
change_orders.opened
A new change order was created
CHANGE_ORDER_UPDATED
change_orders.updated
Change order details were modified
CHANGE_ORDER_DELETED
change_orders.deleted
A change order was removed
CHANGE_ORDER_STAGE_TRANSITION
change_orders.stage_transition
libraryId
Yes
UUID of the library to monitor
-
name
Yes
Unique name for this webhook (1-100 characters)
-
url
Yes
HTTPS endpoint URL (must be valid HTTPS)
Disable (isEnabled: false)
Temporarily pause deliveries; webhook remains queryable and can be re-enabled
Archive
Permanently retire a webhook; frees up the name for reuse
event
string
The event type (e.g., components.created)
eventId
string
Unique identifier for this webhook delivery (UUID)
sourceId
string
Internal event ID for tracking and debugging
timestamp
string
ISO 8601 timestamp when the event occurred
componentId
string
UUID of the affected component
revisionValue
string
Current revision value (e.g., "1.A", "2.B")
version
number
Current version number
Content-Type
application/json
User-Agent
Duro-Webhook-Service/1.0
X-Webhook-Signature
HMAC-SHA256 signature (only if signingSecret is configured)
1st retry
1 second
2nd retry
2 seconds
3rd retry
4 seconds
4th retry
8 seconds
5th retry
16 seconds
6th retry
32 seconds
PENDING
Delivery in progress
DELIVERED
Successfully delivered (2xx response)
RETRYING
Failed, waiting for retry
FAILED
All retry attempts exhausted
Change order moved to a different workflow stage
-
mutation CreateWebhook {
webhooks {
create(input: {
libraryId: "your-library-uuid"
name: "ERP Sync Webhook"
description: "Syncs component updates to our ERP system"
url: "https://api.yourcompany.com/webhooks/duro"
events: [COMPONENT_CREATED, COMPONENT_UPDATED]
signingSecret: "your-secret-key-min-16-chars"
timeoutSeconds: 30
maxRetries: 3
isEnabled: true
}) {
id
name
url
events
isEnabled
createdAt
}
}
}query GetMyWebhooks {
webhooks {
findAll(input: {
libraryIds: ["library-uuid-1", "library-uuid-2"]
}) {
id
name
description
url
events
isEnabled
isArchived
timeoutSeconds
maxRetries
createdAt
updatedAt
}
}
}query GetWebhook {
webhooks {
findOne(id: "webhook-uuid") {
id
name
description
url
events
isEnabled
isArchived
signingSecret
timeoutSeconds
maxRetries
createdAt
}
}
}mutation UpdateWebhook {
webhooks {
update(
id: "webhook-uuid"
input: {
name: "Updated Webhook Name"
events: [COMPONENT_CREATED, COMPONENT_UPDATED, COMPONENT_DELETED]
isEnabled: false
maxRetries: 5
}
) {
id
name
events
isEnabled
maxRetries
}
}
}mutation AddWebhookEvents {
webhooks {
addEvents(
id: "webhook-uuid"
input: {
events: [COMPONENT_DELETED]
}
) {
id
events
}
}
}mutation RemoveWebhookEvents {
webhooks {
removeEvents(
id: "webhook-uuid"
input: {
events: [COMPONENT_DELETED]
}
) {
id
events
}
}
}mutation ArchiveWebhook {
webhooks {
archive(id: "webhook-uuid") {
id
name
isArchived
}
}
}{
"event": "components.updated",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"sourceId": "nats-msg-12345",
"timestamp": "2025-01-15T10:30:00.000Z",
"metadata": {
"componentId": "comp-abc123-uuid",
"revisionValue": "1.A",
"version": 1
}
}{
"event": "components.created",
"eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sourceId": "nats-msg-98765",
"timestamp": "2025-01-15T14:22:33.456Z",
"metadata": {
"componentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"revisionValue": "1.A",
"version": 1
}
}{
"event": "components.updated",
"eventId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"sourceId": "nats-msg-54321",
"timestamp": "2025-01-15T15:45:12.789Z",
"metadata": {
"componentId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"revisionValue": "1.B",
"version": 2
}
}const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// payload should be the raw request body as a string
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware example
app.post('/webhooks/duro', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const rawBody = req.body.toString();
if (!signature || !verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Signature verified - process the webhook
const payload = JSON.parse(rawBody);
// ... handle the event
res.status(200).send('OK');
});import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/duro', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
if not signature or not verify_webhook_signature(
request.data,
signature,
os.environ['WEBHOOK_SECRET']
):
abort(401)
payload = request.get_json()
# ... handle the event
return 'OK', 200query GetComponentDetails($componentId: ID!) {
components {
get(filter: { ids: [$componentId] }) {
connection {
edges {
node {
id
cpn
name
description
revision
status
category {
name
code
}
sources {
manufacturer
mpn
}
specs {
name
value
unit
}
}
}
}
}
}
}const express = require('express');
const crypto = require('crypto');
const { GraphQLClient } = require('graphql-request');
const app = express();
const graphqlClient = new GraphQLClient('https://api.durohub.com/graphql', {
headers: {
'x-api-key': process.env.DURO_API_KEY,
},
});
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(payload, signature) {
if (!signature) return false;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Use raw body for signature verification
app.post('/webhooks/duro', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-webhook-signature'];
const rawBody = req.body.toString();
// Verify signature
if (!verifySignature(rawBody, signature)) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Acknowledge receipt immediately (respond within timeout)
res.status(200).send('OK');
// Process asynchronously
const payload = JSON.parse(rawBody);
await processWebhook(payload);
});
async function processWebhook(payload) {
const { event, eventId, metadata } = payload;
console.log(`Processing ${event} event (${eventId})`);
switch (event) {
case 'components.created':
case 'components.updated':
await handleComponentChange(metadata);
break;
case 'components.deleted':
await handleComponentDeleted(metadata);
break;
default:
console.log(`Unhandled event type: ${event}`);
}
}
async function handleComponentChange(metadata) {
const { componentId, revisionValue, version } = metadata;
// Fetch full component details from Duro
const query = `
query GetComponent($id: ID!) {
components {
get(filter: { ids: [$id] }) {
connection {
edges {
node {
id
cpn
name
description
revision
status
}
}
}
}
}
}
`;
const data = await graphqlClient.request(query, { id: componentId });
const component = data.components.get.connection.edges[0]?.node;
if (component) {
console.log(`Component ${component.cpn} (${component.name}) was updated`);
// Sync to your ERP, database, or other system
await syncToExternalSystem(component);
}
}
async function handleComponentDeleted(metadata) {
const { componentId } = metadata;
console.log(`Component ${componentId} was deleted`);
// Handle deletion in your external systems
await removeFromExternalSystem(componentId);
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});const processedEvents = new Set(); // In production, use Redis or a database
async function processWebhook(payload) {
const { eventId, event, metadata } = payload;
// Check if we've already processed this event
if (processedEvents.has(eventId)) {
console.log(`Event ${eventId} already processed, skipping`);
return;
}
// Process the event
await handleEvent(event, metadata);
// Mark as processed
processedEvents.add(eventId);
}query GetWebhookLogs {
webhooks {
getLogs(webhookId: "webhook-uuid") {
id
event
payload
status
responseCode
responseTimeMs
errorMessage
attemptCount
nextRetryAt
isAcknowledged
createdAt
}
}
}async function checkWebhookHealth(webhookId) {
const query = `
query GetLogs($webhookId: String!) {
webhooks {
getLogs(webhookId: $webhookId) {
id
event
status
responseCode
attemptCount
errorMessage
createdAt
}
}
}
`;
const data = await graphqlClient.request(query, { webhookId });
const logs = data.webhooks.getLogs;
const failed = logs.filter(log => log.status === 'FAILED');
const retrying = logs.filter(log => log.status === 'RETRYING');
if (failed.length > 0) {
console.warn(`${failed.length} webhook deliveries failed!`);
failed.forEach(log => {
console.warn(` - ${log.event}: ${log.errorMessage}`);
});
}
if (retrying.length > 0) {
console.log(`${retrying.length} webhooks pending retry`);
}
}async function syncToERP(component) {
// Map Duro fields to your ERP schema
const erpItem = {
partNumber: component.cpn,
description: component.name,
revision: component.revision,
status: mapStatusToERP(component.status),
};
// Upsert to ERP
await erpClient.upsertItem(erpItem);
console.log(`Synced ${component.cpn} to ERP`);
}
app.post('/webhooks/duro', async (req, res) => {
// ... verification code ...
res.status(200).send('OK');
const { event, metadata } = req.body;
if (event === 'components.created' || event === 'components.updated') {
const component = await fetchComponentFromDuro(metadata.componentId);
await syncToERP(component);
}
});const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_TOKEN);
async function notifySlack(event, component) {
const emoji = event === 'components.created' ? ':heavy_plus_sign:' : ':pencil2:';
const action = event === 'components.created' ? 'created' : 'updated';
await slack.chat.postMessage({
channel: '#engineering-updates',
text: `${emoji} Component *${component.cpn}* was ${action}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${emoji} Component *${component.cpn}* was ${action}`,
},
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Name:*\n${component.name}` },
{ type: 'mrkdwn', text: `*Revision:*\n${component.revision}` },
{ type: 'mrkdwn', text: `*Status:*\n${component.status}` },
],
},
],
});
}async function logToAuditSystem(payload, component) {
const auditEntry = {
timestamp: payload.timestamp,
eventId: payload.eventId,
eventType: payload.event,
resourceType: 'component',
resourceId: payload.metadata.componentId,
resourceCpn: component?.cpn,
revision: payload.metadata.revisionValue,
version: payload.metadata.version,
};
await auditDatabase.insert('webhook_audit_log', auditEntry);
console.log(`Audit log created for ${payload.eventId}`);
}# Send a test payload to your endpoint
curl -X POST https://your-endpoint.com/webhooks/duro \
-H "Content-Type: application/json" \
-H "User-Agent: Duro-Webhook-Service/1.0" \
-d '{
"event": "components.updated",
"eventId": "test-event-id",
"sourceId": "test-source-id",
"timestamp": "2025-01-15T10:30:00.000Z",
"metadata": {
"componentId": "test-component-id",
"revisionValue": "1.A",
"version": 1
}
}'