Only this pageAll pages
Powered by GitBook
1 of 25

Duro API

Loading...

Getting Started

Loading...

Loading...

Loading...

Core Concepts

Loading...

Loading...

Loading...

Advanced Topics

Loading...

Loading...

Loading...

Loading...

Loading...

Library Configuration

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Community

Loading...

Introduction

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.

Why GraphQL?

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

API Architecture

The Duro API follows these core principles:

  • RESTful-like resource patterns

  • Clear name-spaced resources

  • Consistent error handling

  • Rate limiting for stability

Prerequisites

Before you begin, you'll need:

  • A Duro account

  • Basic understanding of GraphQL

  • Your preferred GraphQL client

Tools and SDKs

We recommend using these tools:

  • (with GraphQL support)

Next Steps

Learn how to to get started.

Authentication

The Duro GraphQL API uses Bearer token and API key (via x-api-key header) authentication. All API requests must include an authorization token in the header.

Obtaining an API Authentication Token

To get started with the Duro API, you'll need to:

  1. Create a Duro account at

Documents

Learn how to manage technical documentation and files through the Duro API.

Document Types

Duro supports various document types:

  • CAD Files

Versioned schema updates
GraphQL Playground
Apollo Client
Postman
authenticate your requests
Technical Drawings
  • Specifications

  • Test Reports

  • Quality Documentation

  • Querying Documents

    Here's how to fetch documents:

    Document Operations

    File Management

    • Direct upload URLs

    • Version control

    • Access permissions

    • File metadata

    Next Steps

    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

  • API Key Created Window

    Using the Token

    Include your API token in all requests using the x-api-key header:

    Token Security

    • 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.

    Next Steps

    Learn how to make your first API call in the Hello World guide.

    durohub.com

    Components & Products

    Learn how to manage components and product structures through the API.

    Components

    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

    Products represent the complete assembly of components:

    Common Operations

    • Creating components

    • Updating component details

    • Managing BOMs

    • Handling revisions

    Next Steps

    Learn how to work with associated with components and products.

    Welcome to Duro Dev Center

    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.

    What is Duro?

    Duro is a modern, highly configurable PLM platform that helps hardware teams manage their product data, collaborate on designs, and streamline their development process.

    What can you build?

    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

    Getting Started

    The fastest way to get started with the Duro API is to:

    1. Try our example

    API Endpoints

    Our main GraphQL API explorer is available at:

    Need Help?

    • Join our

    • Check our guide

    • Contact support at [email protected]

    This documentation is continuously updated. Make sure to check back regularly for the latest information.

    Hello World

    Let's make your first API call to Duro. This guide will walk you through a simple query to verify your setup.

    Basic Query

    Here's a simple query to get your libraries:

    Example Response

    x-api-key: YOUR_API_TOKEN
    Documents

    Automate custom change management workflows

  • Create custom integrations with your existing tools

  • Build automated reporting and analytics

  • Create a Duro account
    Get authenticated
    Hello World
    Developer Community
    Error Handling

    Complete Example

    Here's a complete example using curl:

    Next Steps

    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 } } } }"}'

    Introduction to Duro's Config System

    Evolution of Duro's Configuration

    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.

    The YAML-Based Configuration

    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

    Why YAML?

    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.

    Getting Started

    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.

    Error Handling

    Learn how to handle errors and edge cases in the Duro API.

    Error Types

    The API returns different types of errors:

    • Validation errors

    • Authentication errors

    • Authorization errors

    • Rate limiting errors

    • Server errors

    Error Format

    Common Error Scenarios

    Best Practices

    • Implement proper error handling

    • Add retry logic for rate limits

    • Log errors for debugging

    • Handle network timeouts

    Error Recovery

    Next Steps

    Join our for support and discussions.

    Datarooms & RBAC

    Understand how to manage access control and data segregation in Duro.

    Datarooms

    Datarooms provide isolated environments for different projects or teams:

    Role-Based Access Control

    Change order validation and approval flows
  • Event-driven notifications and webhooks

  • And more...

  • GitHub Actions
    Render.com Blueprints
    JSON Schema
    YAML Syntax Overview
    Learn YAML in 10 minutes
    YAML Tutorial by CloudBees
    Provide user-friendly error messages
    Developer Community

    Managing Access

    Permission Levels

    • View Only

    • Editor

    • Manager

    • Administrator

    Best Practices

    • Follow least privilege principle

    • Regular access reviews

    • Document permission structures

    • Monitor access changes

    Next Steps

    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

    Change Orders (COs) help manage and track modifications to your products and components.

    Change Order Structure

    Creating Change Orders

    Workflow States

    • Draft

    • In Review

    • Approved

    • Rejected

    Best Practices

    • Include clear descriptions

    • Link all affected items

    • Maintain approval chains

    • Document implementation results

    Next Steps

    Learn about to find specific changes.

    Developer Community

    Join the Duro developer community to get help, share ideas, and stay updated.

    Community Resources

    • 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
        }
      }
    }
    Stack Overflow Tag
  • Check out our roadmap

  • Contributing

    We welcome community contributions:

    • Bug reports

    • Feature requests

    • Documentation improvements

    • Code examples

    • Integration tutorials

    Stay Updated

    • API Changelog

    • Blog

    • LinkedIn

    • Newsletter subscription

    Support Channels

    • Technical Support: [email protected]

    • API Status: status.durohub.com

    • Security Issues: [email protected]

    Community Guidelines

    1. Be respectful and inclusive

    2. Share knowledge freely

    3. Report bugs responsibly

    4. Follow security best practices

    5. Protect user privacy

    Remember to check the documentation and existing discussions before posting questions.

    Searching and Filtering

    Revision Scheme

    Overview

    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.

    Schema

    The revision scheme is defined in a YAML file that specifies how revisions should be formatted and validated at each lifecycle stage.

    Basic Structure

    Define Status Order

    Establish the progression of lifecycle stages:

    Configure Revision Schemes

    Define revision formats for each status:

    Segment Types

    Letter Segment

    Integer Segment

    Either Type Segment

    Validation Rules

    Define rules for revision transitions and validation:

    Character Blacklist

    Specify characters to exclude from revision schemes:

    Common Patterns

    Simple Letter-Based Revisions

    Letter with Numeric Suffix

    Mixed Format for Obsolescence

    Best Practices

    1. Revision Format

      • Keep formats simple and intuitive

      • Use consistent delimiters

      • Consider future scaling needs

    Examples

    Basic Development Scheme

    Production Scheme

    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: 99
    identifier:
      type: "either"  # Allows both letter and integer
      delimiter: ""
      required: true
    validation:
      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 5
    schemes:
      - status: "Design"
        segments:
          major:
            type: "letter"
            required: true
    schemes:
      - status: "Production"
        segments:
          major:
            type: "letter"
            required: true
          minor:
            type: "integer"
            delimiter: "."
            required: true
    schemes:
      - status: "Obsolete"
        segments:
          major:
            type: "either"
            required: true
    schemes:
      - 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"

    SAML SSO Setup

    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.

    This guide covers the complete SAML integration workflow including Auth0 configuration, Identity Provider setup, and Duro organization settings.

    Overview

    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.

    Authentication Flow

    1. User enters their organization identifier on the Duro login page

    2. Duro redirects to Auth0 with the organization's SAML connection

    3. Auth0 redirects to your Identity Provider (Google, Entra ID, etc.)

    4. User authenticates with corporate credentials

    Prerequisites

    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)

    Quick Start

    Phase 1: Create Auth0 Application

    Duration: 5-10 minutes

    First, create a Single Page Application in your Auth0 tenant:

    1. Navigate to Applications → Applications in Auth0 Dashboard

    2. Click Create Application

    3. Select Single Page Application type

    4. Configure the allowed URLs:

    Replace your-duro-domain.com with your actual Duro installation domain.

    Phase 2: Create SAML Connection

    Duration: 5 minutes

    Create a SAML connection in Auth0 before configuring your Identity Provider:

    1. Go to Authentication → Enterprise → SAML

    2. Click Create Connection

    3. Choose a descriptive name (e.g., acmecorp-saml)

    4. 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.

    Phase 3: Configure Your Identity Provider

    Google Workspace

    Duration: 10-15 minutes

    1. Access Google Admin Console at

    2. Go to Apps → Web and mobile apps → Add App → Add custom SAML app

    3. Set app name (e.g., "Duro") and click Continue

    4. Download IdP Information

    The SSO URL and Entity ID from Google look similar but are different - one has /idp and one has just /saml2. Make sure to copy the correct values.

    Microsoft Entra ID (Azure AD)

    Duration: 10-15 minutes

    1. Access Entra Admin Center

      • Navigate to

      • Sign in with your Microsoft admin account

    2. Create Enterprise Application

    Phase 4: Complete Auth0 Configuration

    Duration: 5 minutes

    Return to your SAML connection in Auth0:

    1. Navigate back to Authentication → Enterprise → SAML

    2. Click on your connection name

    3. Enter IdP details:

      • Sign In URL: The SSO URL from your IdP

    Phase 5: Enable Feature Flag

    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.

    Phase 6: Configure SAML in Duro Organization Settings

    Duration: 2 minutes

    This is the final step, performed by a Duro organization administrator.

    1. 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)

    Testing Your Setup

    Before announcing to users, thoroughly test the SAML flow:

    Test Checklist

    1. Open incognito/private browser (ensures clean session)

    2. Navigate to Duro and click "Sign in with SSO"

    3. Enter organization slug (e.g., acmecorp)

    4. Verify redirect chain:

    SAML configuration changes require Site Admin permissions and are typically managed through the Duro UI for security reasons.

    IdP sends SAML assertion back to Auth0

  • Auth0 returns user to Duro, fully authenticated

  • - Auth0 displays:
    • 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

  • admin.google.com
    entra.microsoft.com

    Category Registry

    Overview

    The category & specs registry is a powerful configuration system that helps you organize and validate your product data. It enables you to:

    • Define component categories with standardized attributes and specifications

    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.com

    Example: https://duro-dev.us.auth0.com/login/callback?connection=google-saml

  • Sign on URL: Same as Reply URL

  • Click Save

  • Import pre-built category templates from Duro's standard library
  • 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.

    Schema

    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 Default Category Sets

    Import pre-defined category sets from Duro to leverage existing definitions.

    Exclude Specific Categories

    Selectively exclude specific categories from imported sets that aren't relevant to your library.

    Define Common Specifications

    Create reusable specifications that can be referenced across multiple categories. Common specs are organized in a hierarchical structure:

    Define Custom Categories

    Create new categories with specific attributes and specifications.

    Define Category Type 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

    Implement Validation Rules

    Define validation rules for specifications using different data types and formats.

    Specify Lifecycle Requirements

    Define when specifications are required based on the item's lifecycle stage.

    Extend Existing Categories

    Extend pre-defined categories with additional specifications or modifications.

    Reference Common Specifications

    Reuse common specifications across different categories using references.

    Import External Specifications

    Use specifications defined in external libraries.

    Import Entire Specification Sets

    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

    Define Warning-Level Requirements

    Specify requirements that trigger warnings rather than errors when not met.

    Create Integration Points

    Define specifications for integration with external systems.

    Define Quality Control Requirements

    Specify quality-related specifications and their validation rules.

    Available Category Types

    Define the type of category being created. The type affects validation rules and available specifications.

    Validation Types Reference

    Define specifications using different validation types and formats.

    String Validation

    Integer Validation

    Paired Field Validation

    Conditional Validation

    The conditional validation supports two types of validation rules:

    1. Range-based conditions: Define minimum and maximum values based on another field's value

    2. 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".

    Version Format Reference

    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: Production
    specs:
      - 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: Design
    specs:
      # 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 status
    specs:
      - 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 components
    commonSpecs:
      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@v1

    Change Order Workflows

    Overview

    Change 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.

    Why Use Workflow Templates?

    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

    Getting Started

    Each Duro library comes with two default workflow templates:

    1. Default Template (default.yaml): A simple single-stage approval workflow

    2. 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.

    Basic Workflow Structure

    Every workflow template follows this structure:

    Creating Custom Fields

    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.

    Example: Adding Change Classification

    Supported Field Types

    • 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

    Configuring Approval Stages

    The stages.open section defines your review process. You can create multiple stages that execute sequentially.

    Single-Stage Approval

    Multi-Stage Approval

    Approval Types Explained

    • Unanimous: All reviewers must approve

    • Majority: More than 50% of reviewers must approve

    • Minimum: At least the minimum number of reviewers must approve

    Adding Validations

    Field validations ensure data quality and completeness:

    Automating Resolutions

    Configure automatic actions when change orders are approved, rejected, or withdrawn:

    Best Practices

    1. Start Simple

    Begin with a basic workflow and add complexity as needed. It's easier to expand a working workflow than debug a complex one.

    2. Use Descriptive Names

    Choose clear, meaningful names for stages and fields:

    • ✅ Good: manufacturing_impact_assessment

    • ❌ Avoid: field1, stage_a

    3. Group Related Fields

    Organize fields into logical groups to improve the user experience:

    4. Document Your Workflow

    Always include a clear description:

    5. Test Thoroughly

    Before deploying a new workflow:

    1. Create test change orders using the workflow

    2. Verify all stages execute correctly

    3. Confirm notifications reach the right people

    4. Test edge cases (rejections, withdrawals)

    Common Patterns

    Pattern 1: Cost-Based Escalation

    For organizations where higher-cost changes need additional approval:

    Pattern 2: Department-Specific Reviews

    When different departments need to review changes:

    Creating Templates via the GraphQL API

    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

    Prerequisites

    Before you begin, you'll need:

    1. 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.

    2. Your YAML workflow template: Either use one of the default templates or create your own custom configuration

    Step 1: Prepare Your YAML Configuration

    First, create your workflow template YAML file. Here's a starter template:

    Step 2: Minify Your YAML

    Apollo Studio requires the YAML to be provided as a single-line string. To convert your YAML:

    1. Visit the YAML Minifier: Go to https://onlineyamltools.com/minify-yaml

    2. Paste your YAML: Copy your YAML configuration and paste it into the left input box

    3. Copy the minified output: The right side will show your minified YAML as a single line. Copy this entire string.

    YAML Minifier showing conversion from formatted to minified YAML

    Step 3: Open Apollo Studio

    1. Navigate to Apollo Studio: Open https://api.durohub.com/graphql in your browser

    2. 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)

    Apollo Studio Headers tab showing x-api-key configuration

    Step 4: Create Your Template

    1. Paste the mutation: In the Operation panel, paste the following GraphQL mutation:

    1. 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.

    1. Execute the mutation: Click the "CreateTemplate" button to run the mutation

    Apollo Studio showing the complete setup for creating a change order template

    Step 5: Verify Your Template

    A successful response will show:

    Your template is now created and ready to use in your change order workflows!

    Troubleshooting Common Issues

    YAML Formatting Errors

    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

    Authentication Failed

    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

    Invalid Configuration

    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

    Next Steps

    1. Review the Change Order Workflow Reference for complete specification details

    2. Explore example templates in your library's configuration

    3. Start with a default template and customize it for your needs

    4. 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 configuration
    details:
      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: true
    stages:
      open:
        - name: Engineering Review
          types: ['Unanimous', 'Majority', 'Minimum']
          default: Majority
          minReviewers: 2
    stages:
      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: true
    fields:
      - 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: true
    resolved:
      resolutions:
        onapproval: [AUTO_CLOSE]      # Automatically close when approved
        onrejection: [MANUAL_CLOSE]   # Require manual closure when rejected
        onwithdrawal: [MANUAL_CLOSE]  # Require manual closure when withdrawn
    groups:
      - name: Impact Analysis
        icon: mdi-chart-line
        fields:
          - name: schedule_impact
          - name: cost_impact
          - name: quality_impact
    description: |
      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: true
    stages:
      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

    Overview

    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.

    Why Validations Matter

    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

    Understanding Validation Types

    The Duro platform provides two types of validations:

    System 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

    Custom Validations

    JavaScript-based rules you create to enforce your specific business logic:

    • Cost threshold checks

    • Part number format validation

    • Required approver verification

    • Impact assessment requirements

    Running Validations

    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:

    Basic Validation Query

    Example Response

    Accessing Validation History

    Every validation run is stored, allowing you to track how validation results change over time as issues are resolved.

    Viewing Latest Validation Results

    Viewing Historical Validation Runs

    Track how validation results have changed over time:

    Understanding Validation Results

    Each validation result provides detailed information to help you understand and resolve issues:

    Validation States

    • PASS: The validation succeeded without issues

    • FAIL: The validation failed and must be resolved

    • WARNING: The validation found potential issues that should be reviewed

    OnFailure Behavior

    • ERROR: Blocks the change order from proceeding (hard stop)

    • WARNING: Alerts reviewers but doesn't block progress (soft warning)

    Validation Logs

    Each validation generates detailed logs that help with debugging:

    Working with Custom Validations

    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:

    Practical Examples

    Example 1: Check Only Failed Validations

    When debugging validation failures, focus on just the problems:

    Then filter the results in your application code:

    Example 2: Monitor Validation Progress

    Track validation improvements over multiple runs:

    Example 3: Validation Integration Workflow

    Here's a complete workflow for integrating validations into your change order process:

    Best Practices

    1. Run Validations Early and Often

    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

    2. Provide Clear Error Messages

    When creating custom validations, ensure error messages are actionable:

    • ✅ Good: "Part number ABC-123 must have an associated drawing document"

    • ❌ Avoid: "Validation failed"

    3. Use Warnings Appropriately

    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

    4. Monitor Validation Trends

    Track validation patterns across change orders to identify:

    • Common failure points in your process

    • Training opportunities for users

    • Potential system improvements

    5. Leverage Validation Logs

    Use the detailed logs to:

    • Debug custom validation logic

    • Understand validation execution flow

    • Provide context to users about failures

    Troubleshooting

    Common Issues

    Validation Runs But Shows No Results

    Ensure you're requesting the results field with proper pagination:

    Custom Validation Not Running

    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

    Inconsistent Validation Results

    Check for:

    • Data changes between validation runs

    • Updates to validation rules

    • Different validation contexts (some validations may be conditional)

    Next Steps

    1. Review your organization's validation requirements

    2. Implement custom validations for your specific needs

    3. Integrate validation checks into your change order workflow

    4. 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

    Custom field dependencies
    Change Order Workflows
    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
          }
        }
      }
    }

    Change Order Workflow Reference

    Overview

    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 Information

    • Schema Version: 1.0

    • Schema URL:

    • Type: Object

    JSON Schema Validation

    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.

    Root Level Properties

    Property
    Type
    Required
    Description

    Example Root Structure

    Details Object

    The details object contains custom field definitions organized in groups.

    Structure

    Group Properties

    Property
    Type
    Required
    Description

    Field Definitions

    Each field in a group has the following properties:

    Property
    Type
    Required
    Description

    Field Types

    Type
    Description
    Additional Properties

    Field Validation Object

    Property
    Type
    Applies To
    Description

    Options for List/Enum Fields

    Property
    Type
    Required
    Description

    Validations Array

    Global validation rules (currently reserved for future use).

    Property
    Type
    Required
    Description

    Stages Object

    Defines the workflow stages and their behavior.

    Open Stages Array

    Array of sequential review stages. Each stage has:

    Property
    Type
    Required
    Description

    Approval Types

    Each stage must support one or more approval types:

    Type
    Description

    Reviewers Object

    User Reviewer Properties

    Property
    Type
    Required
    Description

    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.

    Stage Actions (resolved, closed, onHold)

    Resolution Types

    Value
    Description

    Notification List

    Note: For users in the notification list, use either id (UUID) or email, similar to reviewers.

    Complete Example

    Validation Rules

    The schema enforces these validation rules:

    1. Required Fields: All fields marked as required must have values

    2. Pattern Matching: Field names must match ^[a-zA-Z_][a-zA-Z0-9_]*$

    3. Length Limits:

    Best Practices

    1. Use Semantic Names: Choose descriptive names for fields and stages

    2. Provide Descriptions: Help users understand field purposes

    3. Set Appropriate Validations: Use min/max for numeric fields

    4. 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

    Descriptions: 500 characters
  • 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

    https://phoenix-production.durohub.com/static/schemes/change-orders/schema.json

    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.json
    version: "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: string
    stages:
      open: [ ]      # Required: Active review stages
      resolved: { }  # Optional: Resolution configuration
      closed: { }    # Optional: Closed state configuration
      onHold: { }    # Optional: On-hold state configuration
    reviewers:
      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: boolean
    resolved:
      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 users
    version: "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]"

    Searching and Filtering

    Learn how to effectively search and filter data in the Duro API using our powerful filtering system.

    Basic Query Structure

    All component searches use this base query structure:

    Query Parameters

    Our GraphQL API supports various filtering options:

    Available Filters

    • Text matching (contains, startsWith, endsWith)

    • Numeric comparisons (eq, gt, lt, gte, lte)

    • Date ranges

    • Enum values

    Sorting Results

    Pagination

    We use cursor-based pagination for optimal performance:

    Filter Operators

    The filtering system supports these operators:

    • eq: Exact match

    • in: Match any value in an array

    • contains: String contains

    Common Search Patterns

    1. Manufacturer and Status Filter

    Find components by manufacturer and status:

    2. Date Range and Part Number Filter

    Search by CPN and creation date:

    3. Category and Name Search

    Find components by category and name pattern:

    Advanced Filtering

    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

    Best Practices

    1. Start with simple filters and add complexity as needed

    2. Use appropriate operators for better performance

    3. Consider pagination for large result sets

    4. Test complex filters with smaller data sets first

    Complex filters can impact query performance. Consider breaking down complex searches into simpler queries if possible.

    Next Steps

    Learn about managing access with to control who can perform these searches.

    query FilterBOMComponents($filter: ComponentFilter) {
      componentSearch(filter: $filter) {
        nodes {
          id
          name
          status
        }
      }
    }
    Boolean flags
    gte/lte: Greater/Less than or equal
  • isNull: Check for null values

  • notIn: Exclude values in array

  • Datarooms & RBAC
    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"]
              }
            }
          }
        ]
      }
    }

    CPN Schema Reference

    Overview

    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 Information

    • Schema Version: 1.0

    • JSON Schema: https://json-schema.org/draft-07/schema#

    • Type: Object

    Root Level Properties

    Property
    Type
    Required
    Description

    Settings Object

    The settings object contains global configuration options for CPN generation.

    Property
    Type
    Required
    Default
    Description

    Freeform Validation Object

    When allow_freeform is true, you can optionally specify custom validation rules:

    Property
    Type
    Required
    Description

    Freeform Validation Examples

    Basic freeform with default validation:

    Custom pattern for company naming convention:

    Flexible pattern with length limit:

    Element-Level Override Control

    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.

    Override Elements Behavior

    override_elements
    Behavior

    Override Elements Examples

    Allow override of specific elements only:

    Allow all elements to be overridden (default behavior):

    Behavior Matrix

    allow_override
    allow_freeform
    override_elements
    Behavior

    Elements

    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

    Common Element Properties

    All elements include these base properties:

    Property
    Type
    Required
    Description

    About type

    Specifies 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.

    About name

    A 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.

    About required

    Indicates 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.

    About 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

    About 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).

    About attachedTo

    The 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

    List Element

    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.

    Property
    Type
    Required
    Description

    Values need to be one of:

    1. Array of strings

    2. Array of objects with id, name, and optional description

    3. Template reference in format ${{namespace.field}}

    List Element Examples

    Constant Element

    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.

    Property
    Type
    Required
    Description

    Constant Element Example

    This inserts a dash (-) as a separator between other CPN elements.

    This ensures every CPN begins with the prefix TMP-, e.g., TMP-10001.

    Numeric Counter

    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.

    Property
    Type
    Required
    Description

    Numeric Counter Example

    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

    Hex Counter

    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.

    Property
    Type
    Required
    Description

    Hex Counter Example

    2‑Digit Hex Sequence

    Generates values like 00, 01, … FE, FF. Always fixed at 2 hex digits with zero padding.

    Group Element

    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.

    Property
    Type
    Required
    Description

    Group elements can contain any other element type, including:

    • List elements

    • Constant elements

    • Free text elements (only available within groups)

    • Numeric counters

    Group Element Examples

    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.

    Free Text Element

    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.

    Property
    Type
    Required
    Description

    Examples Array

    The examples array must contain at least one example CPN that follows the defined scheme.

    Template References

    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.

    Complete Schema Examples

    Example 1: Basic Schema with Element-Level Overrides

    Example 2: Mixed Override Control

    Validation Rules

    1. Version must match pattern ^\d+\.\d+$

    2. Schema type must be exactly "id_generation_scheme"

    3. All referenced element names must be unique within their scope

    4. Counter ranges must be valid (min ≤ max) and min must be ≥ 0

    Best Practices

    • 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 range
  • free — 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-00003
  • Then the first CPN for prefix 101:

    • 101-00001

  • Returning to prefix 100, the next generated CPN continues from its own sequence:

    • 100-00004

  • Hex counters

    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 description
    settings:
      allow_override: true
      allow_freeform: true
      # Uses default pattern ^[a-zA-Z0-9\-_]+$ with 50 character limit
    settings:
      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 overridden
    settings:
      allow_override: true
      # override_elements not specified = all elements overrideable
    freeform_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: 99999
    type: "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: integer
    examples:
      - "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 override
    version: "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 override

    Webhooks

    Stay informed about important events in your Duro library with real-time webhook notifications.

    Webhooks follow a "Ping then Pull" pattern - you receive lightweight event notifications with key metadata, then fetch full resource details using our GraphQL API when needed.

    Overview

    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.

    Key Benefits

    • 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

    How Webhooks Work

    1. Event occurs - Something happens in Duro (e.g., a component is updated)

    2. Notification sent - Duro sends a lightweight JSON payload to your webhook URL

    3. 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.


    Available Events

    Webhooks are scoped to a specific library. Each webhook can subscribe to one or more event types.

    Component Events

    These events fire when components in your library are created, updated, or deleted:

    GraphQL Enum
    Payload Value
    Description

    When subscribing to events via GraphQL, use the enum name (e.g., COMPONENT_CREATED). When processing webhook payloads, the event field contains the string value (e.g., components.created).

    Change Order Events (Coming Soon)

    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.

    GraphQL Enum
    Payload Value
    Description

    Setting Up Webhooks

    Create a Webhook

    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

    Configuration Options

    Field
    Required
    Description
    Default

    List Your Webhooks

    Retrieve all webhooks for one or more libraries:

    The findAll query only returns active (non-archived) webhooks. Archived webhooks are excluded from results.

    Get a Specific Webhook

    The findOne query returns a webhook_not_found error if the webhook has been archived.

    Update a Webhook

    You can update any webhook configuration field:

    Add Events to a Webhook

    Add additional event subscriptions without removing existing ones:

    Remove Events from a Webhook

    Remove specific event subscriptions:

    Archive a Webhook

    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.

    When to Archive vs. Disable

    Action
    Use Case

    Webhook Payloads

    All webhook notifications follow a consistent JSON structure:

    Payload Fields Explained

    Field
    Type
    Description

    Component Event Metadata

    For component events, the metadata object contains:

    Field
    Type
    Description

    Example: Component Created

    Example: Component Updated

    HTTP Headers

    Each webhook request includes these headers:

    Header
    Value

    Security

    Webhook Signatures

    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.

    Verifying Signatures (Node.js)

    Verifying Signatures (Python)

    Security Best Practices

    • 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


    Fetching Full Resource Data

    After receiving a webhook notification, use the provided IDs to fetch complete resource details via GraphQL.

    Fetch Component Details

    Example: Complete Webhook Handler

    Here's a complete example showing how to receive a webhook and fetch the full component data:


    Retry Logic & Error Handling

    Duro automatically retries failed webhook deliveries using exponential backoff.

    Retry Schedule

    When a webhook delivery fails, Duro will retry with the following delays:

    Attempt
    Delay After Failure

    The retry schedule follows exponential backoff (2x multiplier) with a maximum delay of 5 minutes.

    What Counts as Success?

    • Success: HTTP status codes 200-299

    • Failure: All other status codes, timeouts, or connection errors

    Your Endpoint Should

    1. Respond quickly - Return a 200 status code immediately, then process asynchronously

    2. Handle duplicates - Use eventId for idempotency; you may receive the same event more than once

    3. Log failures - Track and investigate webhook processing failures

    Example: Idempotent Processing


    Monitoring Webhook Activity

    Query your webhook logs to monitor delivery status and troubleshoot issues:

    Log Status Values

    Status
    Description

    Example: Monitoring Script


    Common Use Cases

    ERP Integration

    Sync component and BOM changes to your ERP system in real-time:

    Slack Notifications

    Alert your team about important component changes:

    Audit Logging

    Maintain a detailed audit trail of all changes:


    Troubleshooting

    Common Issues

    Webhook not receiving events

    1. Check if webhook is enabled - Query the webhook and verify isEnabled: true

    2. Verify event subscriptions - Ensure the correct events are in the events array

    3. Check your endpoint - Verify your URL is accessible from the internet

    Signature verification failing

    1. Check the secret - Ensure you're using the exact same signingSecret you configured

    2. Use raw body - Signature is computed on the raw JSON string, not a parsed object

    3. Check encoding - Ensure UTF-8 encoding throughout

    Missing webhook deliveries

    1. Check retry status - Some deliveries may be queued for retry

    2. Verify library scope - Webhooks only fire for events in their configured library

    3. Check component filters - Events fire for all components in the library

    Testing Your Webhook Endpoint

    Before configuring a production webhook, test your endpoint:


    Next Steps

    • 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

    Cannot be unarchived - This action is permanent

    metadata

    object

    Event-specific data (see below)

    Keep secrets secure
    - Store your
    signingSecret
    in environment variables, never in code
  • Rotate 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)

    Be available
    - Ensure your endpoint is highly available to receive webhooks
    Review logs - Use the getLogs query to see delivery attempts and errors

    COMPONENT_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

    Authentication
    Searching and Filtering
    Change Orders
    Error Handling

    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', 200
    query 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
        }
      }'