Constructdocs
section · guides

Profile inheritance

How the extends field in profile.schema.json works, and a worked example showing a research-extended profile that adds an experiment-log intake type.

3 min read·guides / concepts / profile-inheritance

What inheritance does

A profile can declare "extends": "\<parent-id>" to inherit the parent's roles, departments, intake types, intake stages, doc templates, hooks, and default skills. The child profile then adds or overrides fields on top.

Inheritance is one level deep. A child cannot extend another child. Circular extension is rejected by the profile loader.

Resolution order

When Construct loads a profile with extends, it merges the two JSON objects with child fields winning over parent fields. For array fields (roles, intake.types, intake.stages, docTemplates, defaultSkills) the child's array is appended to the parent's array, not replaced. There is no remove key or other mechanism to drop a parent entry from a child — see Constraints.

The effective resolved profile is what every other subsystem sees. The raw extends field is only visible to the loader.

parent profile (research)
  └── child profile (research-extended)
        merged at load time → effective profile

Field-level resolution:

FieldMerge strategy
idchild wins (required, must differ from parent)
displayNamechild wins
taglinechild wins if present, else parent
customchild wins
rolesparent array + child array, deduped
departmentschild entries appended; same-id entries are replaced
intake.typesparent array + child array, deduped by value
intake.stageschild wins if present, else parent
docTemplatesparent array + child array, deduped
hookschild keys win; unset keys fall through to parent
defaultSkillsparent array + child array, deduped
rebrandchild keys win; unset keys fall through to parent

Worked example

The built-in research profile covers academic and applied research workflows. The example below shows a research-extended custom profile that adds an experiment-log intake type for teams that track experiments as first-class signals.

Parent: research (excerpt)

{
  "id": "research",
  "displayName": "Research",
  "extends": null,
  "roles": ["cx-researcher", "cx-analyst", "cx-product-manager"],
  "intake": {
    "types": ["signal", "finding", "requirement", "question"],
    "stages": ["inbox", "classify", "assign", "analyze", "synthesize", "archive"]
  },
  "docTemplates": ["research-brief", "research-finding", "evidence-brief"]
}

Child: research-extended

{
  "$schema": "https://geraldmaron.github.io/construct/schemas/scope.schema.json",
  "id": "research-extended",
  "displayName": "Research (extended)",
  "tagline": "Research workflow with experiment tracking.",
  "extends": "research",
  "custom": true,
  "roles": ["cx-experiment-lead"],
  "intake": {
    "types": ["experiment-log"],
    "stages": []
  },
  "docTemplates": ["runbook"]
}

Effective resolved profile

After the loader merges parent into child:

{
  "id": "research-extended",
  "displayName": "Research (extended)",
  "tagline": "Research workflow with experiment tracking.",
  "extends": "research",
  "custom": true,
  "roles": [
    "cx-researcher",
    "cx-analyst",
    "cx-product-manager",
    "cx-experiment-lead"
  ],
  "intake": {
    "types": ["signal", "finding", "requirement", "question", "experiment-log"],
    "stages": ["inbox", "classify", "assign", "analyze", "synthesize", "archive"]
  },
  "docTemplates": ["research-brief", "research-finding", "evidence-brief", "runbook"]
}

The child's roles array appended cx-experiment-lead to the parent's three roles. The child's intake.types appended experiment-log to the four parent types. Because the child left intake.stages as an empty array, the parent's stage sequence is carried through unchanged. The child's single new docTemplate was appended.

Where extends is defined

The extends field is declared in schemas/scope.schema.json:

"extends": { "type": ["string", "null"] }

The loader lives in lib/scopes/. Curated profiles (e.g. profiles/research.json) always have "extends": null. Custom profiles at .construct/scope.json may point at any curated id.

Constraints

  • Maximum inheritance depth: 1. A child profile cannot itself be a parent.
  • The parent must be a curated profile (exists in profiles/\<id>.json). Custom profiles cannot extend other custom profiles.
  • Array deduplication is string-equality based. Two entries that are logically the same but spelled differently are kept as two entries.
  • Removing a parent entry requires editing the parent directly or creating a new curated profile without that entry. There is no remove key or other selective-removal mechanism.