Skip to main content

Work Order System — Prisma Data Model Reference

Classification: Internal — Engineering Reference Date: 2026-02-13 Status: Proposed CODITECT Alignment: PostgreSQL state store, RLS multi-tenancy, append-only audit


1. Schema Overview

The WO data model implements 22 entities across four domains: core work order lifecycle, job planning & resource requirements, registries (assets, tools, experience, materials), and compliance (approvals, audit trail, e-signatures). Every entity carries tenant_id for row-level security isolation and version for optimistic concurrency — both mandatory for CODITECT's multi-tenant regulated architecture.

Entity Count by Domain

DomainEntitiesTablesJoin Tables
Core LifecycleWorkOrder, ChangeItem, Party30
Job PlanningJobPlan, ToolReq, ExperienceReq, PersonReq, MaterialReq, MinReq60
RegistriesPerson, Team, Vendor, Asset, Tool, Experience, Material, PersonExperience81
ComplianceApproval, AuditTrail, ElectronicSignature, Schedule, TimeEntry50
Total221

2. CODITECT Platform Extensions

The source Prisma schema is extended with platform-level concerns that don't exist in a standalone QMS but are mandatory in CODITECT's multi-tenant, agent-orchestrated environment.

2.1 Multi-Tenancy (Row-Level Security)

Every table includes:

model WorkOrder {
// ... domain fields ...
tenantId String @map("tenant_id")

@@index([tenantId, status])
@@index([tenantId, masterId])
}

PostgreSQL RLS policy applied at connection level:

CREATE POLICY wo_tenant_isolation ON work_orders
USING (tenant_id = current_setting('app.tenant_id')::text);

ALTER TABLE work_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE work_orders FORCE ROW LEVEL SECURITY;

This pattern repeats for all 22 tables. CODITECT's API Gateway sets app.tenant_id on each connection from the pool, ensuring complete data isolation without application-level filtering.

2.2 Optimistic Concurrency

All mutable entities carry a version field:

model WorkOrder {
version Int @default(1)
// Update logic:
// UPDATE work_orders SET ..., version = version + 1
// WHERE id = $1 AND version = $2
// RETURNING *
// If 0 rows affected → 409 Conflict
}

2.3 Agent Attribution

Every state mutation records the acting agent or human:

model AuditTrail {
performedBy String // Person ID or Agent ID
performerType String // "HUMAN" | "AGENT" | "SYSTEM"
agentSessionId String? // CODITECT agent execution trace ID
correlationId String? // Distributed tracing correlation
}

This bridges the gap between the source spec (human-only performedBy) and CODITECT's autonomous agent model where an ExperienceMatchingNode or WorkOrderOrchestratorNode may initiate transitions.


3. Complete Prisma Schema (Annotated)

3.1 Enums

enum WorkOrderSourceType {
AUTOMATION // Agent-initiated (PM schedules, drift detection)
EXTERNAL_VENDOR // Vendor-originated (IQ/OQ, calibration)
MANUAL // Human-initiated (upgrade requests, corrections)
}

enum WorkOrderStatus {
DRAFT // Initial creation, metadata incomplete
PLANNED // Originator/item/summary set, ready for scheduling
SCHEDULED // JobPlan + Schedule + assignee locked
IN_PROGRESS // Assignee acknowledged, execution underway
PENDING_REVIEW // Execution complete, awaiting approval
APPROVED // System Owner (+ QA for reg) approved
REJECTED // Reviewer rejected, requires rework
COMPLETED // Post-approval tasks confirmed, terminal
CANCELLED // Cancelled with justification, terminal
}

enum PartyType {
PERSON // Individual human
VENDOR // External company
PROGRAM // Automated system / agent
TEAM // Group of persons
}

enum ApprovalRole {
SYSTEM_OWNER // Required for all WOs
QA // Required for regulatory WOs
OTHER // Additional approvers (department head, etc.)
}

enum PerformerType {
HUMAN // Authenticated user action
AGENT // CODITECT agent action
SYSTEM // Platform system action (timers, escalations)
}

3.2 Core Lifecycle Entities

model WorkOrder {
id String @id @default(cuid())
tenantId String @map("tenant_id")

// Hierarchy
masterId String? @db.VarChar(191)
master WorkOrder? @relation("MasterChildWO", fields: [masterId], references: [id])
children WorkOrder[] @relation("MasterChildWO")
sequenceOrder Int? // Execution order within master (DAG position)

// Source & Origination
sourceType WorkOrderSourceType
originatorId String
originator Party @relation("OriginatedWOs", fields: [originatorId], references: [id])

// Change Target
itemId String
item ChangeItem @relation(fields: [itemId], references: [id])

// Description
summary String
detail String

// Assignment
assignerId String
assigner Party @relation("AssignedBy", fields: [assignerId], references: [id])
assigneeId String
assignee Party @relation("AssignedTo", fields: [assigneeId], references: [id])

// Planning
jobPlanId String?
jobPlan JobPlan? @relation(fields: [jobPlanId], references: [id])
scheduleId String?
schedule Schedule? @relation(fields: [scheduleId], references: [id])

// Classification
priority Int @default(3) // 1=Critical, 5=Low
status WorkOrderStatus @default(DRAFT)
regulatoryFlag Boolean @default(false)

// Compliance Metadata
complianceClass String? // "FDA_21CFR11" | "HIPAA" | "SOC2" | null
riskLevel String? // "HIGH" | "MEDIUM" | "LOW"

// Relations
approvals Approval[]
timeEntries TimeEntry[]
minRequirements WorkOrderMinimumRequirement[]
dependencies WorkOrderDependency[] @relation("DependentWO")
dependents WorkOrderDependency[] @relation("DependencyTarget")

// Versioning
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
version Int @default(1)

auditEvents AuditTrail[]

@@index([tenantId, status])
@@index([tenantId, masterId])
@@index([tenantId, assigneeId, status])
@@index([tenantId, regulatoryFlag, status])
}

model WorkOrderDependency {
id String @id @default(cuid())
tenantId String @map("tenant_id")

workOrderId String
workOrder WorkOrder @relation("DependentWO", fields: [workOrderId], references: [id])
dependsOnId String
dependsOn WorkOrder @relation("DependencyTarget", fields: [dependsOnId], references: [id])

dependencyType String // "BLOCKS" | "SOFT" | "INFORMATIONAL"

@@unique([workOrderId, dependsOnId])
@@index([tenantId])
}

model ChangeItem {
id String @id @default(cuid())
tenantId String @map("tenant_id")
type String // DEVICE | COMPUTER | SYSTEM | DOCUMENT
assetId String?
asset Asset? @relation(fields: [assetId], references: [id])
documentId String? // FK to external DMS
description String

workOrders WorkOrder[]

@@index([tenantId, type])
}

model Party {
id String @id @default(cuid())
tenantId String @map("tenant_id")
type PartyType
personId String?
person Person? @relation(fields: [personId], references: [id])
vendorId String?
vendor Vendor? @relation(fields: [vendorId], references: [id])
teamId String?
team Team? @relation(fields: [teamId], references: [id])
displayName String

originatedWOs WorkOrder[] @relation("OriginatedWOs")
assignedBy WorkOrder[] @relation("AssignedBy")
assignedTo WorkOrder[] @relation("AssignedTo")

@@index([tenantId, type])
}

3.3 Job Planning & Requirements

model JobPlan {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String // Template name for reusable plans
description String
workInstructionRef String? // Reference to DMS document ID
estimatedHours Float?
isTemplate Boolean @default(false) // Reusable template vs. instance
templateId String? // FK to template this was cloned from

tools JobPlanToolRequirement[]
experiences JobPlanExperienceRequirement[]
persons JobPlanPersonRequirement[]
materials JobPlanMaterialRequirement[]
workOrders WorkOrder[]

createdAt DateTime @default(now())
version Int @default(1)

@@index([tenantId, isTemplate])
}

model JobPlanToolRequirement {
id String @id @default(cuid())
jobPlanId String
jobPlan JobPlan @relation(fields: [jobPlanId], references: [id])
toolId String
tool Tool @relation(fields: [toolId], references: [id])
minQuantity Int @default(1)
calibrationRequired Boolean @default(false) // GxP: tool must be in calibration
}

model JobPlanExperienceRequirement {
id String @id @default(cuid())
jobPlanId String
jobPlan JobPlan @relation(fields: [jobPlanId], references: [id])
experienceId String
experience Experience @relation(fields: [experienceId], references: [id])
minRating Int // 1-5 scale
mandatory Boolean @default(true)
}

model JobPlanPersonRequirement {
id String @id @default(cuid())
jobPlanId String
jobPlan JobPlan @relation(fields: [jobPlanId], references: [id])
roleLabel String // "IT Technician", "Validation Engineer", etc.
headcount Int?
personId String?
person Person? @relation(fields: [personId], references: [id])
}

model JobPlanMaterialRequirement {
id String @id @default(cuid())
jobPlanId String
jobPlan JobPlan @relation(fields: [jobPlanId], references: [id])
materialId String
material Material @relation(fields: [materialId], references: [id])
quantity Int @default(1)
lotTracked Boolean @default(false) // GxP: material lot must be recorded
}

model WorkOrderMinimumRequirement {
id String @id @default(cuid())
workOrderId String
workOrder WorkOrder @relation(fields: [workOrderId], references: [id])
level Int // 1=mandatory, 2=recommended, 3=optional
requirementType String // PERSON | EXPERIENCE | TOOL | MATERIAL | INFO_SET
refPersonId String?
refExperienceId String?
refToolId String?
refMaterialId String?
infoDescription String?
satisfied Boolean @default(false)
satisfiedAt DateTime?
}

3.4 Registry Entities

model Person {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
email String
active Boolean @default(true)
department String?

experiences PersonExperience[]
timeEntries TimeEntry[]
approvals Approval[]
signatures ElectronicSignature[]
auditEvents AuditTrail[] @relation("PerformedByPerson")
parties Party[]
personReqs JobPlanPersonRequirement[]

@@unique([tenantId, email])
@@index([tenantId, active])
}

model Team {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
parties Party[]
members TeamMember[]
}

model TeamMember {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
personId String
person Person @relation(fields: [personId], references: [id])
role String? // "LEAD" | "MEMBER"

@@unique([teamId, personId])
}

model Vendor {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
contactInfo String?
qualifiedDate DateTime? // Date vendor was qualified (GxP requirement)
active Boolean @default(true)
parties Party[]
timeEntries TimeEntry[]
}

model Asset {
id String @id @default(cuid())
tenantId String @map("tenant_id")
type String // DEVICE | COMPUTER | INSTRUMENT | PART
name String
serialNumber String?
status String // IN_PRODUCTION | OUT_OF_PRODUCTION | DECOMMISSIONED
location String?
assetMgmtRef String? // External CMMS/ITSM reference
validationStatus String? // VALIDATED | PENDING_VALIDATION | NOT_APPLICABLE
lastValidatedAt DateTime?

changeItems ChangeItem[]

@@index([tenantId, status])
@@index([tenantId, type])
}

model Tool {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
type String // BASIC | SPECIALIZED
calibrationDue DateTime? // Next calibration date
status String @default("AVAILABLE") // AVAILABLE | IN_USE | MAINTENANCE

jobRequirements JobPlanToolRequirement[]

@@index([tenantId, status])
}

model Experience {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String // "Win11 lab workstation build"
description String?
category String? // "IT" | "VALIDATION" | "LAB_OPS" | "VENDOR_MGMT"

persons PersonExperience[]
jobRequirements JobPlanExperienceRequirement[]

@@index([tenantId, category])
}

model PersonExperience {
personId String
experienceId String
rating Int // 1-5 scale
certifiedAt DateTime? // Date of last certification/training
expiresAt DateTime? // Training expiration

person Person @relation(fields: [personId], references: [id])
experience Experience @relation(fields: [experienceId], references: [id])

@@id([personId, experienceId])
}

model Material {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
description String?
partNumber String?
lotTracking Boolean @default(false)

requirements JobPlanMaterialRequirement[]
}

3.5 Scheduling & Time Tracking

model Schedule {
id String @id @default(cuid())
tenantId String @map("tenant_id")
expectedStartAt DateTime?
expectedEndAt DateTime?
expectedTotalHours Float?
actualStartAt DateTime?
actualEndAt DateTime?
gapsNotes String?
slaDueAt DateTime? // SLA deadline for escalation

workOrders WorkOrder[]
}

model TimeEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
workOrderId String
workOrder WorkOrder @relation(fields: [workOrderId], references: [id])
personId String
person Person @relation(fields: [personId], references: [id])
vendorId String?
vendor Vendor? @relation(fields: [vendorId], references: [id])
actualStartAt DateTime
actualEndAt DateTime?
billableHours Float?
costCenter String?
rateType String? // "STANDARD" | "OVERTIME" | "VENDOR"

@@index([tenantId, workOrderId])
@@index([tenantId, personId])
}

3.6 Compliance: Approvals, Signatures, Audit

model Approval {
id String @id @default(cuid())
tenantId String @map("tenant_id")
workOrderId String
workOrder WorkOrder @relation(fields: [workOrderId], references: [id])
role ApprovalRole
approverId String
approver Person @relation(fields: [approverId], references: [id])
decision String // APPROVED | REJECTED
decisionTs DateTime
comment String?
signatureId String?
signature ElectronicSignature? @relation(fields: [signatureId], references: [id])

@@index([tenantId, workOrderId])
}

model ElectronicSignature {
id String @id @default(cuid())
tenantId String @map("tenant_id")
signerId String
signer Person @relation(fields: [signerId], references: [id])
meaning String // "Approval of Work Order {id}", "Review of record"
signedAt DateTime @default(now())
reason String?
authMethod String // "password" | "smartcard" | "sso_reauth"
sessionId String?
ipAddress String?

// Part 11 §11.50: Signature manifestations
// - signerId links to unique individual
// - meaning captures what was signed and why
// - signedAt provides timestamp
// - authMethod documents authentication context

approvals Approval[]

@@index([tenantId, signerId])
}

model AuditTrail {
id String @id @default(cuid())
tenantId String @map("tenant_id")
entityType String // WORK_ORDER | JOB_PLAN | SCHEDULE | APPROVAL | ASSET
entityId String
action String // CREATE | UPDATE | STATUS_CHANGE | APPROVAL | SIGNATURE | LOGIN
performedBy String
performer Person @relation("PerformedByPerson", fields: [performedBy], references: [id])
performerType PerformerType @default(HUMAN)
agentSessionId String? // CODITECT agent trace ID
correlationId String? // Distributed tracing correlation
performedAt DateTime @default(now())
previousVal Json?
newVal Json?

// Part 11 §11.10(e): Audit trail must be computer-generated,
// independent of operator, and not modifiable.
// Enforced via DB trigger: no UPDATE or DELETE allowed.

@@index([tenantId, entityType, entityId])
@@index([tenantId, performedAt])
@@index([tenantId, performedBy])
}

4. Database Constraints & Triggers

4.1 Immutable Audit Trail (Part 11 §11.10(e))

-- Prevent modification of audit records
CREATE OR REPLACE FUNCTION prevent_audit_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Audit trail records cannot be modified or deleted (21 CFR Part 11 §11.10(e))';
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER audit_trail_immutable
BEFORE UPDATE OR DELETE ON audit_trail
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();

4.2 Auto-Versioning on WorkOrder Update

CREATE OR REPLACE FUNCTION auto_version_work_order()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.version != NEW.version - 1 THEN
RAISE EXCEPTION 'Optimistic lock violation: expected version %, got %',
OLD.version + 1, NEW.version;
END IF;
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER wo_auto_version
BEFORE UPDATE ON work_orders
FOR EACH ROW EXECUTE FUNCTION auto_version_work_order();

4.3 DAG Cycle Detection on Dependency Insert

CREATE OR REPLACE FUNCTION check_dependency_cycle()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
WITH RECURSIVE dep_chain AS (
SELECT depends_on_id FROM work_order_dependencies
WHERE work_order_id = NEW.depends_on_id
UNION ALL
SELECT d.depends_on_id FROM work_order_dependencies d
JOIN dep_chain dc ON dc.depends_on_id = d.work_order_id
)
SELECT 1 FROM dep_chain WHERE depends_on_id = NEW.work_order_id
) THEN
RAISE EXCEPTION 'Dependency cycle detected: WO % → WO % would create a cycle',
NEW.work_order_id, NEW.depends_on_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER dependency_cycle_check
BEFORE INSERT ON work_order_dependencies
FOR EACH ROW EXECUTE FUNCTION check_dependency_cycle();

5. Index Strategy

TableIndexPurpose
work_orders(tenant_id, status)Dashboard queries, queue views
work_orders(tenant_id, master_id)Hierarchy traversal
work_orders(tenant_id, assignee_id, status)My work queue
work_orders(tenant_id, regulatory_flag, status)Compliance dashboard
audit_trail(tenant_id, entity_type, entity_id)Entity audit history
audit_trail(tenant_id, performed_at)Time-range audit queries
approvals(tenant_id, work_order_id)Approval status lookup
person_experience(person_id, experience_id)Resource matching
time_entries(tenant_id, work_order_id)WO time accumulation

Partitioning Strategy (>10M WOs/tenant/year)

-- Partition audit_trail by month for high-volume tenants
CREATE TABLE audit_trail (
-- ... columns ...
) PARTITION BY RANGE (performed_at);

CREATE TABLE audit_trail_2026_01 PARTITION OF audit_trail
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- Auto-create partitions via pg_partman or CODITECT scheduler

6. CODITECT Agent ↔ Prisma Integration Points

Agent NodePrimary TablesOperationsRBAC
WorkOrderOrchestratorWorkOrder, WorkOrderDependencyCRUD, transitionsSYSTEM
AssetManagementAsset, ChangeItemStatus updatesAGENT
SchedulingSchedule, TimeEntryCreate/updateAGENT
ExperienceMatchingPersonExperience, Person, ExperienceRead-only queriesAGENT
QAReviewApproval, ElectronicSignature, AuditTrailCreate approvalsQA (delegated)
VendorCoordinatorVendor, TimeEntry, WorkOrderUpdate vendor WOsVENDOR (delegated)
DocumentationChangeItem (documentId)Link documentsAGENT

Critical Rule: Agent nodes that create approvals or signatures MUST operate "on behalf of" an authenticated human. The performerType=AGENT + agentSessionId in AuditTrail captures this delegation, but the Approval.approverId MUST reference a real Person who authorized the action.


7. Migration File Inventory

OrderFilePurpose
001create_enums.sqlAll enum types
002create_persons_teams_vendors.sqlRegistry: people
003create_assets_tools_materials.sqlRegistry: physical
004create_experience.sqlRegistry: skills
005create_parties.sqlPolymorphic party abstraction
006create_schedules_time_entries.sqlScheduling
007create_job_plans.sqlJob planning with requirements
008create_change_items.sqlChange targets
009create_work_orders.sqlCore WO table
010create_wo_dependencies.sqlDAG dependencies
011create_wo_min_requirements.sqlMulti-level requirements
012create_signatures.sqlElectronic signatures
013create_approvals.sqlApproval records
014create_audit_trail.sqlImmutable audit
015create_rls_policies.sqlRow-level security for all tables
016create_triggers.sqlImmutability, versioning, cycle detection
017create_indexes.sqlPerformance indexes
018seed_experiences.sqlDefault experience categories

Copyright 2026 AZ1.AI Inc. All rights reserved. Developer: Hal Casteel, CEO/CTO Product: CODITECT-BIO-QMS | Part of the CODITECT Product Suite Classification: Internal - Confidential