| name | zephyr |
| description | Zephyr - A Python REST API client for PTC Windchill PLM. Query parts, documents, change requests, suppliers, BOMs, CAD documents, workflows, and more. Supports OAuth 2.0 and Basic authentication. |
| tags | ["windchill","plm","ptc","odata","rest-api","enterprise","product-lifecycle-management","bom","change-management","supplier-management","cad","quality-management","regulatory-compliance","zephyr"] |
Zephyr - Windchill PLM REST API Client
Zephyr is a Python client for interacting with PTC Windchill PLM REST APIs. Supports OAuth 2.0 and Basic authentication.
Capabilities Summary (Read This First)
Zephyr provides comprehensive OData query support for Windchill PLM:
OData Filter Expressions - FULLY SUPPORTED:
- Data types: String, Int16/32/64, Boolean, DateTimeOffset, Single, Double
- Comparison:
eq, ne, gt, lt, ge, le
- Logical:
and, or, not
- String functions:
startswith, endswith, contains
- Type checking:
isof
- Special properties:
ID, CreatedBy, ModifiedBy, View
Domain Clients Available: 28 domains (ProdMgmt, DocMgmt, ChangeMgmt, QMS, CAPA, etc.)
Response Caching - BUILT-IN:
- Dual-backend: in-memory LRU + file-based persistence
- TTL-based expiration (default 300s, configurable per entity set)
- Auto-invalidation on write operations (POST/PATCH/DELETE)
- Cache stats tracking (hits, misses, hit rate)
- Zero external dependencies (stdlib only)
CLI - TERMINAL INTERFACE:
- 30+ subcommands: parts, documents, bom, search, count, domains, cache, etc.
- Auto-corrects common OData filter mistakes (enum /Value suffix)
- Tabular and JSON output formats
- Run:
python scripts/zephyr_cli.py <command> [options]
Async Query Manager - PARALLEL QUERIES:
- Parallel HTTP via aiohttp with semaphore concurrency control
- Retry with exponential backoff for transient failures
- Parallel BOM traversal and batch entity fetching
- Falls back to synchronous when aiohttp is unavailable
Key Files:
AGENT_NOTES.md - Critical learnings for AI agents (READ THIS)
scripts/zephyr_cli.py - Terminal CLI for quick queries
scripts/async_query_manager.py - Parallel query engine
scripts/odata_filter_builder.py - OData filter construction
scripts/cache_manager.py - Response caching (memory + file, TTL, LRU)
scripts/domains/<Domain>/client.py - Domain-specific helpers
references/CAPABILITIES.md - Full capability reference
Quick Start
- Copy
config.example.json to config.json in the skill directory
- Configure your Windchill server URL and authentication
- Use domain clients for best practices
import sys
sys.path.insert(0, '/home/ubuntu/.hermes/skills/zephyr/scripts')
from domains.ProdMgmt import ProdMgmtClient
client = ProdMgmtClient(config_path="config.json")
parts = client.query_entities('Parts', top=10)
results = client.search_parts('bracket', top=10)
part = client.get_part_by_number("PART-001")
CRITICAL WARNINGS
| Issue | Solution |
|---|
| NEW AGENTS: Read capabilities first | See references/CAPABILITIES.md for full feature reference |
| Use ODataFilter, not raw strings | from odata_filter_builder import ODataFilter |
| scripts/old/ is DEPRECATED | Use domain clients in scripts/domains/ instead |
| DO NOT create ad-hoc scripts | Use existing domain clients directly - they support all query patterns |
| URL double-slash bug | Client auto-fixes trailing slashes. If "Invalid domain request", check URLs. |
| CSRF token required | Header: CSRF_NONCE: <token> (NOT X-PTC-CSRF-Token) |
| OData properties case-sensitive | Properties are PascalCase: Number not number, Name not name, ContainerID not containerID. Wrong case = 400 error. |
| Enum properties need /Value | PTC.EnumType props (State, Status, Priority, Severity) require /Value: State/Value eq 'RELEASED' NOT State eq 'RELEASED'. CLI auto-corrects. |
| Navigation URL order | /{nav} segment must come BEFORE ?$expand= params. Wrong order = 400 error. |
| BOM child details via Uses | PartUse links lack child Number/Name by default. get_bom() auto-expands $expand=Uses. Read from item['Uses']['Number']. |
| GetBOM not exposed | Use Uses navigation: client.get_bom(part_id) (auto-expands child part details) |
| Slow API (7-8s/call) | PTC demo servers are slow - this is expected, not a bug |
| ManufacturerParts in ProdMgmt | ManufacturerParts/VendorParts are in ProdMgmt domain, NOT SupplierMgmt |
| search_ uses $search (full-text)* | search_parts(), search_documents() use full-text search across ALL fields. For field-specific filtering, use query_entities() with filter_expr=\"contains(Name, 'term')\" |
| Cache enabled by default | Set cache.enabled: false in config to disable. Use cache_clear() to reset. |
Response Caching
Zephyr includes built-in response caching that dramatically reduces latency for repeated queries. On PTC demo servers where each call takes 7-8 seconds, cached responses return in <1ms.
How It Works
- GET requests are automatically cached (memory + optional file)
- Write operations (POST/PATCH/DELETE) auto-invalidate affected entity set
- TTL expiration ensures stale data is eventually refreshed
- File persistence survives across sessions
Configuration
Add to config.json:
{
"cache": {
"enabled": true,
"ttl": 300,
"max_entries": 256,
"file_cache": true,
"max_size_mb": 50,
"default_ttls": {
"ProdMgmt:Parts": 600,
"ChangeMgmt:ChangeNotices": 120,
"Workflow:WorkItems": 60
}
}
}
| Config Key | Type | Default | Description |
|---|
cache.enabled | bool | true | Enable/disable caching |
cache.ttl | int | 300 | Default TTL in seconds |
cache.max_entries | int | 256 | Max in-memory entries (LRU eviction) |
cache.file_cache | bool | true | Enable file-based persistence |
cache.max_size_mb | int | 50 | Max file cache size in MB |
cache.default_ttls | dict | {} | Per entity-set TTL overrides |
Cache Control Methods
from domains.ProdMgmt import ProdMgmtClient
client = ProdMgmtClient(config_path="config.json")
stats = client.cache_stats()
client.cache_clear()
result = client.cache_cleanup()
count = client.cache_invalidate('ProdMgmt', 'Parts')
count = client.cache_invalidate_domain('ProdMgmt')
Performance Impact
| Scenario | Without Cache | With Cache | Improvement |
|---|
| Repeat same query | 7-8s | <1ms | 7000x faster |
| Query same entity set with different filters | 7-8s | 7-8s (miss) | Same (unique keys) |
| Query after write operation | 7-8s | 7-8s (invalidated) | Same (auto-invalidated) |
| Cross-session repeat | 7-8s | <5ms (file cache) | 1500x faster |
Domain Clients (28 domains)
| Domain | Client | Purpose |
|---|
| ProdMgmt | ProdMgmtClient | Parts, BOMs, product structures |
| ProdPlatformMgmt | ProdPlatformMgmtClient | Variant specifications, options, choices |
| PartListMgmt | PartListMgmtClient | Illustrated Parts Lists, illustrations, substitutes |
| ProjMgmt | ProjMgmtClient | Project plans, activities, milestones |
| NC | NCClient | Nonconformance tracking, quality issues |
| DocMgmt | DocMgmtClient | Documents, attachments |
| CADDocumentMgmt | CADDocumentMgmtClient | CAD documents, drawings |
| ChangeMgmt | ChangeMgmtClient | Change notices, requests, tasks |
| SupplierMgmt | SupplierMgmtClient | Suppliers, vendor parts |
| MfgProcMgmt | MfgProcMgmtClient | Process plans, operations |
| CEM | CEMClient | Customer experiences |
| BACMgmt | BACMgmtClient | Baselines, configurations |
| Workflow | WorkflowClient | Work items, activities |
| Audit | AuditClient | Audit records, compliance |
| DataAdmin | DataAdminClient | Containers, folders |
| ServiceInfoMgmt | ServiceInfoMgmtClient | Service documentation |
| UDI | UDIClient | Unique Device Identification |
| RegMstr | RegMstrClient | Regulatory Master |
| QMS | QMSClient | Quality Management (CAPA/NCR) |
| PrincipalMgmt | PrincipalMgmtClient | Users, groups, roles |
| PTC | PTCClient | Common entities, content download |
| CAPA | CAPAClient | Corrective and Preventive Actions |
| DocumentControl | DocumentControlClient | Controlled documents, training records |
| ClfStructure | ClfStructureClient | Classification hierarchy, nodes |
| DynamicDocMgmt | DynamicDocMgmtClient | Dynamic documents, burst configurations |
| EffectivityMgmt | EffectivityMgmtClient | Part effectivity, serial/lot/date effectivity |
| Factory | FactoryClient | Standard operations, procedures, SCC, resources |
| NavCriteria | NavCriteriaClient | Navigation criteria, config specs, filters |
Common Patterns
OData Filter Builder
Zephyr includes a comprehensive OData filter builder (odata_filter_builder.py) for constructing complex queries.
NEW: query_entities() now accepts ODataFilter objects directly!
import sys
sys.path.insert(0, '/home/ubuntu/.hermes/skills/zephyr/scripts')
from odata_filter_builder import ODataFilter
from domains.ProdMgmt import ProdMgmtClient
client = ProdMgmtClient(config_path="config.json")
f = ODataFilter().eq('State', 'RELEASED').and_contains('Name', 'Bracket')
parts = client.query_entities('Parts', filter_expr=f)
parts = client.query_entities('Parts', filter_expr="State eq 'RELEASED' and contains(Name, 'Bracket')")
f = ODataFilter().eq('Number', 'V0056726')
f = (ODataFilter()
.eq('State', 'RELEASED')
.and_gt('Quantity', 10))
f = ODataFilter().contains('Name', 'Engine')
f = (ODataFilter()
.eq('State', 'RELEASED')
.or_(ODataFilter().eq('State', 'APPROVED')))
f = ODataFilter().isof('WTPart')
f = ODataFilter().by_id('OR:wt.part.WTPart:12345')
f = ODataFilter().by_created_by('admin').and_by_modified_by('user1')
Supported OData Expressions:
| Category | Operators/Methods | Example |
|---|
| Comparison | eq, ne, gt, lt, ge, le | .eq('State', 'RELEASED') |
| Logical | and_(), or_(), not_() | .and_(ODataFilter().eq('State', 'APPROVED')) |
| String | startswith, endswith, contains | .contains('Name', 'Bracket') |
| Type Check | isof | .isof('WTPart') |
| Special Props | ID, CreatedBy, ModifiedBy, View | .by_id('OR:wt.part.WTPart:12345') |
| Data Types | String, Int16/32/64, Boolean, DateTimeOffset, Single, Double | .gt('Quantity', 100) |
Pagination for Large Datasets
Zephyr provides automatic pagination for handling large result sets:
import sys
sys.path.insert(0, '/home/ubuntu/.hermes/skills/zephyr/scripts')
from domains.ProdMgmt import ProdMgmtClient
client = ProdMgmtClient(config_path="config.json")
all_parts = client.query_all('Parts', filter_expr="State eq 'RELEASED'")
def progress(page, count, total):
print(f"Page {page}: {count} entities (total: {total})")
all_parts = client.query_all('Parts', progress_callback=progress)
for part in client.query_iter('Parts', filter_expr="State eq 'RELEASED'"):
process_part(part)
count = client.count_entities('Parts', filter_expr="State eq 'RELEASED'")
print(f"Found {count} released parts")
if count > 1000:
for part in client.query_iter('Parts'):
export_part(part)
else:
parts = client.query_all('Parts')
Pagination Methods:
| Method | Purpose | Memory Usage |
|---|
query_all() | Fetch all entities as list | O(n) - loads all into memory |
query_iter() | Generator yielding one at a time | O(1) - constant memory |
count_entities() | Get count without fetching data | O(1) - no entity data |
Query Parts and BOM
from domains.ProdMgmt import ProdMgmtClient
client = ProdMgmtClient(config_path="config.json")
parts = client.query_entities('Parts', top=50)
results = client.search_parts('bracket')
part = client.get_part_by_number("V0056726")
bom = client.get_bom(part_id)
for item in bom:
print(f"{item['child_part']['number']} | Qty: {item['quantity']}")
Query Change Notices
from domains.ChangeMgmt import ChangeMgmtClient
client = ChangeMgmtClient(config_path="config.json")
cns = client.query_entities('ChangeNotices', top=50)
results = client.search_change_notices('bracket')
results = client.search_change_requests('design update')
cn = client.get_change_notice_by_number("CN-2024-001")
Query CAD Documents
from domains.CADDocumentMgmt import CADDocumentMgmtClient
client = CADDocumentMgmtClient(config_path="config.json")
docs = client.query_entities('CADDocuments', top=50)
results = client.search_cad_documents('bracket')
Query Quality Records (CAPA/NCR)
from domains.QMS import QMSClient
client = QMSClient(config_path="config.json")
capas = client.get_capas()
open_capas = client.get_open_capas()
ncrs = client.get_ncrs()
Query CAPA Domain (Dedicated Client)
from domains.CAPA import CAPAClient
client = CAPAClient(config_path="config.json")
capa = client.get_capa_by_id(capa_id, expand=['Plan', 'PrimarySite'])
plan_actions = client.get_action_plan_actions(plan_id)
client.set_capa_state(capa_id, 'CLOSED')
Query DocumentControl Domain
from domains.DocumentControl import DocumentControlClient
client = DocumentControlClient(config_path="config.json")
docs = client.get_control_documents()
doc = client.get_control_document_by_number("DOC-001")
records = client.get_training_records_by_user(user_id)
Query ClfStructure (Classification Hierarchy)
from domains.ClfStructure import ClfStructureClient
client = ClfStructureClient(config_path="config.json")
root_nodes = client.get_root_clf_nodes()
children = client.get_child_clf_nodes(parent_id)
path = client.get_classification_path(node_id)
tree = client.get_classification_tree(max_depth=3)
Query Suppliers
from domains.SupplierMgmt import SupplierMgmtClient
client = SupplierMgmtClient(config_path="config.json")
suppliers = client.query_suppliers(top=50)
results = client.search_suppliers('amphenol')
supplier = client.get_supplier_by_name("Amphenol")
supplier = client.get_supplier_by_id("OR:com.ptc.windchill.suma.supplier.Manufacturer:2167863")
mfg_parts = client.query_manufacturer_parts(filter_expr="Name eq 'Connector'")
vendor_parts = client.query_vendor_parts(top=100)
Query Process Plans
from domains.MfgProcMgmt import MfgProcMgmtClient
client = MfgProcMgmtClient(config_path="config.json")
plans = client.query_entities('ProcessPlans', top=50)
results = client.search_process_plans('assembly')
Get Document Attachments
from domains.DocMgmt import DocMgmtClient
client = DocMgmtClient(config_path="config.json")
doc = client.get_document_by_number("DOC-001")
attachments = client.get_document_attachments(doc_id)
DynamicDocMgmt - Dynamic Documents
from domains.DynamicDocMgmt import DynamicDocMgmtClient
client = DynamicDocMgmtClient(config_path="config.json")
docs = client.get_dynamic_documents(top=50)
doc = client.get_dynamic_document_by_number("DYN-DOC-001")
doc = client.get_dynamic_document_by_id(doc_id, expand=["Creator", "Master", "Attachments"])
client.check_out_document(doc_id)
client.check_in_document(doc_id, check_in_note="Updated content")
client.revise_document(doc_id)
client.upload_stage1(doc_id, no_of_files=1)
client.upload_stage3(doc_id, content_info={"FileName": "drawing.pdf"})
client.set_document_state(doc_id, "RELEASED")
EffectivityMgmt - Part Effectivity
from domains.EffectivityMgmt import EffectivityMgmtClient
client = EffectivityMgmtClient(config_path="config.json")
contexts = client.get_part_effectivity_contexts(top=50)
by_part = client.get_contexts_by_part(part_id)
entities = client.get_effectivity_managed_entities(top=50)
effectivities = client.get_entity_effectivities(entity_id)
date_effs = client.get_date_effectivities_by_range("2024-01-01", "2024-12-31")
unit_effs = client.get_unit_effectivities_by_range(start_unit=100, end_unit=200)
serial_effs = client.get_serial_number_effectivities()
lot_effs = client.get_lot_effectivities()
client.set_effectivities_for_entity(entity_id, [
{"Type": "DateEffectivity", "StartDate": "2024-01-01", "EndDate": "2024-12-31"}
])
Factory - Standard Operations & Procedures
from domains.Factory import FactoryClient
client = FactoryClient(config_path="config.json")
operations = client.get_standard_operations(top=50)
op = client.get_standard_operation_by_number("OP-001")
op = client.get_standard_operation_by_id(op_id, expand=["Creator", "Master", "StandardProcedureUsages"])
client.check_out_operation(op_id, check_out_note="Updating operation")
client.check_in_operation(op_id, check_in_note="Changes complete")
client.revise_operation(op_id)
client.set_operation_state(op_id, "RELEASED")
client.set_operations_state_bulk([op_id1, op_id2], "INWORK")
sccs = client.get_standard_control_characteristics(top=50)
scc = client.get_scc_by_number("SCC-001")
scc = client.get_scc_by_id(scc_id, expand=["Master", "ResourceUsages", "StandardProcedureUsages"])
client.check_out_scc(scc_id)
client.check_in_scc(scc_id, check_in_note="Updated")
client.set_scc_state(scc_id, "RELEASED")
client.update_scc_related_links(scc_id, test_run="TR-001", update_requests=[
{"LinkType": "ResourceUsage", "Action": "Add", "ResourceId": "RES-001"}
])
procedures = client.get_standard_procedures(top=50)
proc = client.get_standard_procedure_by_number("PROC-001")
client.set_procedure_state(proc_id, "RELEASED")
resources = client.get_resources(top=50)
resource = client.get_resource_by_number("RES-001")
resource_usages = client.get_resource_usages(resource_id)
NavCriteria - Navigation Criteria & Config Specs
from domains.NavCriteria import NavCriteriaClient
client = NavCriteriaClient(config_path="config.json")
criteria = client.get_navigation_criteria(top=50)
criteria = client.get_navigation_criteria_by_name("MyConfig")
shared = client.get_shared_criteria()
standard_spec = client.build_standard_config_spec(view="Manufacturing")
baseline_spec = client.build_baseline_config_spec("OR:wt.proj.Baseline:12345")
effectivity_spec = client.build_effectivity_date_config_spec(
start_date="2024-01-01T00:00:00Z",
end_date="2024-12-31T23:59:59Z"
)
attr_filter = client.build_attribute_filter("Material", "Equals", "Steel")
box_filter = client.build_box_spatial_filter(0, 0, 0, 100, 100, 100)
option_filter = client.build_option_filter("Color", ["Red", "Blue"])
criteria = client.create_navigation_criteria(
name="ManufacturingView",
applicable_type="WTPart",
config_specs=[standard_spec],
filters=[attr_filter],
apply_to_top_level=True,
shared_to_all=True
)
epm_spec = client.build_epmdoc_standard_config_spec(view="Design")
client.update_navigation_criteria(criteria_id, config_specs=[baseline_spec])
NC - Nonconformance Management
from domains.NC import NCClient
client = NCClient(config_path="config.json")
ncs = client.get_nonconformances(top=50)
open_ncs = client.get_open_nonconformances()
nc = client.get_nonconformance_by_number("NC-2024-001")
by_state = client.get_nonconformances_by_state("OPEN")
by_priority = client.get_nonconformances_by_priority("HIGH")
by_severity = client.get_nonconformances_by_severity("CRITICAL")
nc = client.get_nonconformance_by_id(nc_id, expand=[
"AffectedObjects",
"ImmediateActions",
"Creator",
"Owner"
])
nc = client.create_nonconformance(
description="Material defect in batch A-123",
identified_date="2024-01-15T10:00:00Z",
source="Incoming Inspection",
priority="HIGH",
severity="MAJOR",
location="Warehouse A",
assigned_to="OR:wt.org.WTUser:12345"
)
affected = client.add_affected_object(
nc_id=nc["ID"],
name="Part-001",
number="PART-001",
quantity=100,
disposition="Scrap"
)
objects = client.get_affected_objects(nc_id)
action = client.add_immediate_action(
nc_id=nc["ID"],
description="Quarantine affected batch",
action_type="CONTAINMENT",
action_date="2024-01-15T12:00:00Z"
)
actions = client.get_immediate_actions(nc_id)
client.set_nonconformance_state(nc_id, "IN_REVIEW")
client.reserve_nonconformance(nc_id, reservation_note="Reviewing NC")
client.undo_reservation_nonconformance(nc_id)
client.upload_attachment(nc_id, file_name="inspection_report.pdf", file_path="/path/to/file.pdf")
ProdPlatformMgmt - Variant Specifications & Options
from domains.ProdPlatformMgmt import ProdPlatformMgmtClient
client = ProdPlatformMgmtClient(config_path="config.json")
specs = client.get_variant_specifications(top=50)
released = client.get_variant_specifications_by_state("RELEASED")
spec = client.get_variant_specification_by_number("VS-2024-001")
spec = client.get_variant_specification_by_id(spec_id, expand=[
"Options",
"OptionSets",
"Creator",
"Owner"
])
options = client.get_options(top=50)
option = client.get_option_by_number("OPT-001")
option_sets = client.get_option_sets(top=50)
option_set = client.get_option_set_by_number("OS-001")
options = client.get_options_for_variant_spec(spec_id)
option_sets = client.get_option_sets_for_variant_spec(spec_id)
choices = client.get_choices(top=50)
design_choices = client.get_design_choices_for_option(option_id)
sales_choices = client.get_sales_choices_for_option(option_id)
spec = client.create_variant_specification(
name="Product Family A",
description="Variant specification for Product Family A",
effectivity="2024-01-01 to 2024-12-31"
)
option_set = client.create_option_set(
name="Color Options",
expression="Color IN ('Red', 'Blue', 'Green')",
is_active=True
)
client.check_out_variant_specification(spec_id, check_out_note="Updating specification")
client.check_in_variant_specification(spec_id, check_in_note="Changes complete")
client.undo_check_out_variant_specification(spec_id)
client.revise_variant_specification(spec_id, description="New revision")
client.set_variant_specification_state(spec_id, "RELEASED")
client.set_variant_specifications_state_bulk([spec_id1, spec_id2], "INWORK")
client.set_option_set_state(option_set_id, "RELEASED")
client.set_choice_state(choice_id, "RELEASED")
PartListMgmt - Illustrated Parts Lists
from domains.PartListMgmt import PartListMgmtClient
client = PartListMgmtClient(config_path="config.json")
partlists = client.get_partlists(top=50)
released = client.get_partlists_by_state("RELEASED")
partlist = client.get_partlist_by_number("PL-001")
partlist = client.get_partlist_by_id(partlist_id, expand=[
"Uses",
"Creator",
"Organization",
"Versions"
])
items = client.get_partlist_items(top=50)
item = client.get_partlist_item_by_id(item_id)
items = client.get_partlist_items_for_partlist(partlist_id)
illustrations = client.get_illustrations(top=50)
ill = client.get_illustration_by_id(ill_id)
illustrations = client.get_illustrations_for_partlist(partlist_id)
substitutes = client.get_substitutes_for_item(item_id)
supplements = client.get_supplements_for_item(item_id)
client.check_out_partlist(partlist_id, check_out_note="Updating part list")
client.check_in_partlist(partlist_id, check_in_note="Changes complete")
client.undo_check_out_partlist(partlist_id)
client.revise_partlist(partlist_id)
client.check_out_partlists_bulk([pl_id1, pl_id2], check_out_note="Bulk update")
client.check_in_partlists_bulk([pl_id1, pl_id2], check_in_note="Bulk complete")
client.revise_partlists_bulk([pl_id1, pl_id2])
client.set_partlist_state(partlist_id, "RELEASED")
client.set_partlists_state_bulk([pl_id1, pl_id2], "RELEASED")
client.update_partlist_properties(partlist_id, updates=[
{"Name": "New Part List Name"}
])
client.update_partlist_item_properties(item_id, updates=[
{"Quantity": "10"}
])
ProjMgmt - Project Management
from domains.ProjMgmt import ProjMgmtClient
client = ProjMgmtClient(config_path="config.json")
plans = client.get_project_plans(top=50)
plan = client.get_project_plan_by_id(plan_id)
plan = client.get_project_plan_by_name("Product Development 2026")
plan = client.get_project_plan_by_id(plan_id, expand=[
"Activities",
"ImmediateChildren",
"Context"
])
activities = client.get_activities(top=50)
activity = client.get_activity_by_id(activity_id)
activities = client.get_activities_by_name("Design Review")
activities = client.get_activities_by_plan_name("Product Development 2026")
milestones = client.get_milestones(top=50)
summaries = client.get_summary_activities(top=50)
deliverables = client.get_deliverable_activities(top=50)
activities = client.get_activities_for_plan(plan_id)
top_level = client.get_immediate_children(plan_id)
children = client.get_activity_children(activity_id)
owner = client.get_activity_owner(activity_id)
deliverables = client.get_activity_deliverables(activity_id)
plan = client.create_project_plan(
name="New Product Launch",
deadline="2026-12-31",
estimated_start="2026-06-01",
estimated_finish="2026-12-31"
)
activity = client.create_activity(
name="Phase 1: Design",
start_date="2026-05-01T00:00:00Z",
finish_date="2026-05-31T00:00:00Z",
summary=True
)
milestone = client.create_activity(
name="Design Complete",
start_date="2026-05-31T00:00:00Z",
finish_date="2026-05-31T00:00:00Z",
milestone=True
)
client.add_activity_to_plan(plan_id, activity_id)
activity = client.create_activity_in_plan(
plan_id=plan_id,
name="Testing Phase",
start_date="2026-06-01T00:00:00Z",
finish_date="2026-06-30T00:00:00Z"
)
client.update_activity(activity_id, percent_work_complete=75.0)
client.update_activity(activity_id, status="IN_PROGRESS")
client.update_project_plan(plan_id, percent_work_complete=50.0)
BOM Query (Uses Navigation)
Since GetBOM may not be exposed, use the Uses navigation:
from domains.ProdMgmt import ProdMgmtClient
client = ProdMgmtClient(config_path="config.json")
bom = client.get_bom(part_id)
for item in bom:
child = item.get('Uses', {})
print(f"{child.get('Number')} | {child.get('Name')} | Qty: {item.get('Quantity')}")
IMPORTANT: PartUse links do NOT include child Number/Name by default. get_bom() uses $expand=Uses to inline the child Part. Always read child details from item['Uses'], not item['Part'].
File Structure
zephyr/
├── SKILL.md # This file (quick reference)
├── config.example.json # Configuration template
├── scripts/
│ ├── windchill_base.py # Base client
│ ├── windchill_odata_client.py # Comprehensive OData client
│ ├── property_resolver.py # Case-insensitive property resolution
│ ├── cache_manager.py # Response caching (memory + file, TTL, LRU)
│ ├── output_formatter.py # Output formatting
│ └── domains/ # Domain-specific clients (USE THESE)
│ ├── ProdMgmt/
│ ├── DocMgmt/
│ ├── ChangeMgmt/
│ ├── CAPA/
│ ├── DocumentControl/
│ ├── ClfStructure/
│ ├── DynamicDocMgmt/
│ ├── EffectivityMgmt/
│ ├── Factory/
│ ├── NavCriteria/
│ ├── NC/
│ ├── ProdPlatformMgmt/
│ ├── PartListMgmt/
│ ├── ProjMgmt/
│ └── ... (28 domains)
└── references/ # Detailed documentation by domain
├── ProdMgmt/
│ ├── ProdMgmt_REFERENCE.md
│ ├── ProdMgmt_Navigations.md
│ └── ProdMgmt_Entities.json
├── ChangeMgmt/
├── QMS/
├── CAPA/
├── DocumentControl/
├── ClfStructure/
├── DynamicDocMgmt/
├── EffectivityMgmt/
├── Factory/
├── NavCriteria/
└── ... (24 domains)
Reference Documentation
For detailed usage, see references/<Domain>/<Domain>_REFERENCE.md:
| Domain | Reference File | Key Entities |
|---|
| ProdMgmt | references/ProdMgmt/ProdMgmt_REFERENCE.md | Part, PartUse, BOM |
| ChangeMgmt | references/ChangeMgmt/ChangeMgmt_REFERENCE.md | ChangeNotice, ChangeTask |
| QMS | references/QMS/QMS_REFERENCE.md | CAPA, NCR, QualityAction |
| DocMgmt | references/DocMgmt/DocMgmt_REFERENCE.md | Document, Attachment |
| PTC | references/PTC/PTC_REFERENCE.md | Common types, OID format |
| CAPA | references/CAPA/CAPA_REFERENCE.md | CAPA, CAPAActionPlan, Action |
| DocumentControl | references/DocumentControl/DocumentControl_REFERENCE.md | ControlDocument, TrainingRecord |
| ClfStructure | references/ClfStructure/ClfStructure_REFERENCE.md | ClfNode, ClassifiedObject |
| DynamicDocMgmt | references/DynamicDocMgmt/DynamicDocMgmt_REFERENCE.md | DynamicDocument, BurstConfiguration |
| EffectivityMgmt | references/EffectivityMgmt/EffectivityMgmt_REFERENCE.md | PartEffectivityContext, Effectivity |
| Factory | references/Factory/Factory_REFERENCE.md | StandardOperation, StandardProcedure, StandardControlCharacteristic |
| NavCriteria | references/NavCriteria/NavCriteria_REFERENCE.md | NavigationCriteria, CachedNavigationCriteria |
| NC | references/NC/NC_REFERENCE.md | Nonconformance, AffectedObject, ImmediateAction |
| ProdPlatformMgmt | references/ProdPlatformMgmt/ProdPlatformMgmt_REFERENCE.md | VariantSpecification, OptionSet, Option, Choice |
| PartListMgmt | references/PartListMgmt/PartListMgmt_REFERENCE.md | PartList, PartListItem, Illustration, Substitute, Supplement |
| ProjMgmt | references/ProjMgmt/ProjMgmt_REFERENCE.md | ProjectPlan, Activity |
Configuration
Basic Auth Format:
{
"server_url": "https://windchill.example.com/Windchill",
"odata_base_url": "https://windchill.example.com/Windchill/servlet/odata/",
"auth_type": "basic",
"basic": {
"username": "your_username",
"password": "your_password"
},
"verify_ssl": true,
"timeout": 30
}
OAuth 2.0 Format:
{
"server_url": "https://windchill.example.com/Windchill",
"odata_base_url": "https://windchill.example.com/Windchill/servlet/odata/",
"auth_type": "oauth",
"oauth": {
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"token_url": "https://windchill.example.com/Windchill/oauth2/token",
"scope": "windchill"
},
"verify_ssl": true,
"timeout": 30
}
Important: Credentials are nested under basic or oauth object, not at root level. Config file location: /home/ubuntu/.hermes/skills/zephyr/config.json (gitignored).
Key Navigation Properties
| Entity | Navigation | Purpose |
|---|
| Part | Uses | BOM children |
| Part | UsedBy | Parent assemblies |
| Part | Versions | Version history |
| Document | Attachments | File attachments |
| ChangeNotice | Tasks | Change tasks |
| ChangeNotice | AffectedObjects | Affected items |
| CAPA | Plan | Action Plan |
| CAPA | PrimarySite | Primary location |
| CAPA | AffectedObjects | Affected items |
| ClfNode | Children | Child classifications |
| ClfNode | ClassifiedObjects | Objects in classification |
| DynamicDocument | Creator | Document creator |
| DynamicDocument | Master | Document master |
| DynamicDocument | Versions | Version history |
| DynamicDocument | Attachments | File attachments |
| DynamicDocument | Members | Document members |
| PartEffectivityContext | Part | Associated part |
| PartEffectivityContext | Effectivity | Associated effectivity |
| Effectivity | PartEffectivity | Part effectivity reference |
| StandardOperation | Folder | Operation folder |
| StandardOperation | Master | Operation master |
| StandardOperation | Versions | Version history |
| StandardOperation | StandardProcedureUsages | Procedure usages |
| StandardControlCharacteristic | Folder | SCC folder |
| StandardControlCharacteristic | Master | SCC master |
| StandardControlCharacteristic | ResourceUsages | Resource usages |
| StandardControlCharacteristic | StandardProcedureUsages | Procedure usages |
| StandardProcedure | Folder | Procedure folder |
| StandardProcedure | Master | Procedure master |
| Resource | Folder | Resource folder |
| Resource | ResourceUsages | Resource usages |
Actions (39 total)
See references/PTC/actions.json for full list. Common actions:
Unbound: CreateAssociations, SetStateParts, ReviseParts, CheckOutParts
Bound: CheckOut, CheckIn, UndoCheckOut, Revise, GetMultiLevelBOMRollup
Deprecated Scripts
DO NOT USE scripts/old/ - Use domain clients instead:
| Old Script | Use Instead |
|---|
| query_parts.py | ProdMgmtClient.query_entities('Parts') |
| query_change_notices.py | ChangeMgmtClient.query_entities('ChangeNotices') |
| query_qms.py | QMSClient.query_entities('Quality') or CAPAClient |
| generic_query.py | WindchillODataClient.query_entities() |
DO NOT CREATE AD-HOC SCRIPTS - Domain clients already support all query patterns:
from domains.DocMgmt import DocMgmtClient
client = DocMgmtClient(config_path="config.json")
docs = client.query_entities('Documents', filter_expr="contains(Number, '038')")
docs = client.search_documents('038')
Troubleshooting
| Error | Cause | Solution |
|---|
| "Invalid domain request" | Double slash in URL | Check odata_base_url trailing slash |
| "CSRF token missing" | POST without token | Get token from /PTC/GetCSRFToken() |
| "$top not supported" | Single-entity navigation | Remove pagination params |
| 7-8s response time | PTC demo server | Normal behavior, not a bug |
| 400 "types not compatible" | Enum compared to string | Use Property/Value eq 'X' (e.g. State/Value eq 'RELEASED') |
| 400 with filter | Property name wrong case | Use PascalCase: Number not number, Name not name |
| 400 on navigation with $expand | URL has params before nav segment | Navigation must come before ? in URL |
| BOM shows N/A for Number/Name | Missing child part expand | get_bom() auto-expands $expand=Uses; read item['Uses']['Number'] |
License
Apache License 2.0. See LICENSE file for full text.
Key Points:
- Free to use, modify, and distribute
- Must include license copy in distributions
- Must retain copyright notices
- Patent license granted by contributors
- See Apache 2.0 Summary