Skip to main content

Baseline Management & Script Maintenance

This guide explains how the CTMS provisioning scripts are structured, how to keep them up-to-date, and how to compare Frappe instances to ensure consistency.

Overview

The CTMS platform uses four provisioning scripts to bring a new Frappe instance to production-ready state:

┌──────────────────────────────────────────────────────────────────────────────┐
│ New Frappe Instance Setup │
├──────────────┬────────────────┬──────────────┬───────────────────────────────┤
│ Stage 1 │ Stage 2 │ Stage 3 │ Stage 4 │
│ DocTypes │ Custom Fields │ RBAC (539) │ Master Data (88) │
│ (34) │ (15) │ ────────── │ ────────── │
│ ────────── │ ────────── │ 4 native │ 6 lookup tables (31) │
│ 4 phases │ 5 DocTypes │ 4 CTMS roles│ 1 medical dept │
│ dependency- │ 15 fields │ 24 resources│ 4 laboratory items │
│ ordered │ │ 8 actions │ drug catalogue (52) │
│ │ │ 488 perms │ │
│ │ │ 11 nav items│ │
└──────────────┴────────────────┴──────────────┴───────────────────────────────┘
ScriptPurposeRecords
frappe_setup_custom_doctypes.pyCreates 34 custom DocTypes in dependency order34 schemas
frappe_setup_custom_fields.pyCreates 15 custom fields on 5 built-in DocTypes15 fields
frappe_seed_rbac.pySeeds RBAC configuration (Frappe native roles, CTMS roles, resources, permissions, nav)539 records
frappe_seed_master_data.pySeeds study-design lookups, medical departments, laboratory items, and drug catalogue88 records

Source of Truth

All provisioning data is embedded directly in the Python scripts as canonical dictionaries. This ensures:

  • Self-contained: No external file dependencies at runtime
  • Version-controlled: Every change is tracked via Git commits
  • Reproducible: Any instance can be provisioned from scratch
  • Auditabe: Full history of what changed and when

The external file scripts/frappe-migration/custom_doctypes_schema.json is a reference archive of all 34 DocType field schemas, but is not read by any script at runtime. It exists for documentation and comparison purposes.

Custom DocType Inventory (34 total)

Phase 1 — Standalone (15 DocTypes)

#DocTypeCategoryNaming
1CTMS RoleRBACfield: role_name
2CTMS ResourceRBACfield: resource_name
3CTMS ActionRBACfield: action_name
4Study StatusClinicalfield: study_status
5Study TypeClinicalfield: study_type_name
6Study PhaseClinicalfield: study_phase
7Event TypeClinicalfield: event_type_name
8Site StatusClinicalfield: site_status_name
9Intervention ModelStudy Designfield: intervention_model_name
10MaskingStudy Designfield: masking_name
11ControlStudy Designfield: control_name
12AllocationStudy Designfield: allocation_name
13ClassificationStudy Designfield: classification_name
14PurposeStudy Designfield: purpose_name
15zynomi_drug_masterDrug Masternaming_series: DRG-.####
#DocTypeCategory
16CTMS PermissionRBAC
17CTMS NavigationRBAC
18Study EventClinical
19Site LocationClinical
20StudyClinical
#DocTypeCategory
21CTMS Nav PermissionRBAC
22SiteClinical
23SubjectClinical
24ConsentClinical
25FamilyMedicalHistoryClinical
#DocTypeCategory
26Study SitesClinical
27study_personnelClinical
28PhysicalExaminationCRF
29AdverseEventsCRF
30CRF_FORM_DEMOGRAPHICSCRF
31CRF_FORM_VITAL_SIGNSCRF
32CRF_FORM_MEDICAL_HISTORYCRF
33Study CRFCRF
34CRF_FORM_MICRONUTRIENTS_ANTIOXIDANTS_HIV_INFECTIONCRF

Custom Fields Inventory (15 fields on 5 DocTypes)

The custom fields script adds CTMS-specific fields to Frappe's built-in DocTypes:

Built-in DocTypeFieldsDescription
Healthcare Practitioner4education, experience, biography, surgeries
Vital Signs2study_event (Link → Study Event), study (Link → Study)
Patient2height, weight
Drug Prescription4send_reminders, snooze_after, start_date, end_date
Nursing Task1clinical_trial (Select)

Master Data Inventory (88 records)

Study Design Lookups (31 records)

DocTypeRecordsValues
Intervention Model4Crossover, Factorial, Parallel, Single Group
Masking5Double Blind, Open Label, Quadruple Blind, Single Blind, Triple Blind
Control5Active, Dose Comparison, Historical, No Treatment, Placebo
Allocation3Non-Randomized, Not Applicable, Randomized
Classification7Bioavailability, Bioequivalence, Efficacy, Pharmacodynamics, Pharmacokinetics, Safety, Safety/Efficacy
Purpose7Basic Science, Diagnostic, Health Services Research, Prevention, Screening, Supportive Care, Treatment

Drug Master Catalogue (52 records)

The drug catalogue contains 52 pharmaceutical compounds with:

  • Drug Code (ATC classification or custom code)
  • Drug Name
  • Drug Status (Planning / Production)
  • Dosage Form (Tablet, Capsule, Injection, Cream)

Medical Department (1 record)

RecordDescription
Clinical TrialDepartment used for clinical trial laboratory items

Laboratory Items (4 records)

Laboratory items power the "Add Lab Test" dropdown in the Zynexa frontend. These are standard Frappe Item records with item_group = "Laboratory".

Item CodeItem NameDescription
LaboratoryLaboratoryLaboratory
ImagingImagingImaging
USG AbdomenUltrasound AbdomenUltrasound Abdomen
X-Ray ChestChest X-Ray (PA View)Chest X-Ray (PA View)

Adding a New Custom DocType

When a new custom DocType needs to be added to the provisioning scripts:

1. Determine the Phase

Check if the new DocType has Link fields referencing other custom DocTypes:

DependencyPhase
No custom DocType linksPhase 1
Links to Phase 1 DocTypesPhase 2
Links to Phase 2 DocTypesPhase 3
Links to Phase 3 DocTypesPhase 4

2. Fetch the Schema from Zynomi

import requests

url = "https://your-site.frappe.cloud"
headers = {"Authorization": "token <key>:<secret>"}

# Get the DocType schema
r = requests.get(f"{url}/api/doctype/YourDocType", headers=headers)
schema = r.json()["data"]
print(schema["fields"]) # Field definitions

3. Add to the Setup Script

Add the DocType definition to the appropriate PHASE_N list in frappe_setup_custom_doctypes.py:

PHASE_1.append({
"name": "Your DocType",
"module": "Healthcare",
"custom": 1,
"naming_rule": "By fieldname",
"autoname": "field:your_field",
"fields": [
{"fieldname": "your_field", "fieldtype": "Data", "label": "Your Field", "reqd": 1},
# ... more fields
],
})

4. Update Counts

Update the docstring header in the script to reflect new totals:

  • Total DocType count
  • Phase-specific count
  • Category breakdown (RBAC / Clinical / Study Design)

5. Update the External Schema

Add the DocType to scripts/frappe-migration/custom_doctypes_schema.json for reference:

# Fetch and append to the JSON file
python3 -c "
import requests, json
# ... fetch schema and update JSON
"

6. Test

# Dry run to verify
python frappe_setup_custom_doctypes.py --dry-run --env ../../.env.example

# Create on target instance
python frappe_setup_custom_doctypes.py --env ../../.env.example

Comparing Instances

To verify that two Frappe instances have identical custom DocTypes:

import requests

INSTANCES = {
"instance-a": ("https://your-site.frappe.cloud", "key1:secret1"),
"instance-b": ("https://your-other-site.frappe.cloud", "key2:secret2"),
}

for name, (url, token) in INSTANCES.items():
headers = {"Authorization": f"token {token}"}
r = requests.get(f"{url}/api/resource/DocType",
params={"filters": '[["custom","=",1]]', "limit_page_length": 0},
headers=headers)
doctypes = sorted(d["name"] for d in r.json()["data"])
print(f"{name}: {len(doctypes)} custom DocTypes")
for dt in doctypes:
print(f" - {dt}")

Expected Output

Both instances should report 34 custom DocTypes with identical names, plus 15 custom fields on 5 built-in DocTypes.

Keeping Scripts Up-to-Date

When to Update Scripts

TriggerAction
New DocType created in zynomiAdd to frappe_setup_custom_doctypes.py + schema JSON
New custom field on built-in DocTypeAdd to frappe_setup_custom_fields.py
New lookup values addedAdd to frappe_seed_master_data.py
New drugs added to catalogueAdd to frappe_seed_master_data.py
New laboratory items neededAdd to frappe_seed_master_data.py (LABORATORY_ITEM_DATA)
New medical departments neededAdd to frappe_seed_master_data.py (MEDICAL_DEPARTMENT_DATA)
RBAC roles/permissions changedUpdate frappe_seed_rbac.py
Field added/removed on DocTypeUpdate schema in setup script + JSON

Versioning Checklist

When making changes to provisioning scripts:

  • Update the script's docstring header (counts, dates)
  • Update custom_doctypes_schema.json if DocType fields changed
  • Update this documentation page
  • Update the Initial Setup Guide
  • Run --dry-run against a test instance
  • Verify on both both target instances
  • Commit with a descriptive message
  • Push to main branch

Git History

All provisioning script changes are tracked in the ctms.devops repository:

# View recent changes to provisioning scripts
git log --oneline scripts/frappe-seed/

# View what changed in a specific commit
git show <commit-hash> -- scripts/frappe-seed/

Troubleshooting

"DocType already exists"

This is normal — the setup script is idempotent and skips existing DocTypes. You'll see skipped=N in the output.

"Record already exists"

Same as above — seed scripts skip existing records. This is the expected behavior for re-runs.

Instance Drift

If instances have diverged (e.g., manual changes in Frappe UI):

  1. Run the comparison script above to identify differences
  2. Fetch the correct schema from zynomi (source of truth)
  3. Update the provisioning script
  4. Re-run the script against the drifted instance

If you get a "LinkValidationError" when seeding data, ensure the linked DocTypes and records exist:

  1. Run Stage 1 first (DocTypes)
  2. Then Stage 2 (Custom Fields)
  3. Then Stage 3 (RBAC — including Frappe native roles)
  4. Then Stage 4 (Master data)
  5. Then Stage 5 (Practitioner)

The stages must be run in order.

User Signup Fails with "Could not find Role"

If user signup via the API returns LinkValidationError: Could not find Row #1: Role: Platform Administrator, it means the Frappe native Role entries are missing. Stage 3 creates both:

  • Frappe native Role entries (required for User role assignment via Link fields)
  • CTMS Role entries (used by the CTMS permission matrix)

Re-run Stage 3 to create both:

CTMS_INIT_STAGES=3 docker compose --env-file .env.local --profile init run --rm ctms-init