Skip to content

Entity

The entity block defines what you're resolving: the type of identity and its compliance, hierarchy, and governance requirements. At its simplest, an entity is just a name. At its most powerful, it carries regulatory compliance rules, organizational hierarchies, and PII masking policies that flow through every stage of reconciliation.

Basic Configuration

yaml
entity:
  name: customer
  description: "B2B customer identity across CRM, billing, and support systems"
FieldTypeRequiredDescription
namestringYesEntity type identifier (alphanumeric + underscore)
descriptionstringNoHuman-readable description of what this entity represents

The entity name is used throughout the system as the primary identifier for this resolution configuration. It appears in API responses, audit logs, and the Python SDK's result objects. Choose a name that clearly describes the real-world concept you're resolving: customer, patient, product, employee, vendor.

python
from kanoniv import Spec

spec = Spec.from_file("customer-spec.yaml")
print(spec.entity)  # {"name": "customer"}

Compliance (Cloud)

Configure compliance metadata and enforcement for sensitive data. The compliance block controls PII masking, audit logging, and data retention. It is nested under the entity block.

yaml
entity:
  name: patient
  description: "Healthcare patient records"
  compliance:
    frameworks: [HIPAA, GDPR]
    pii_fields: [email, phone, ssn, date_of_birth, name]
    audit_required: true
    retention_days: 2555        # 7 years for HIPAA
FieldTypeRequiredDefaultDescription
frameworksarray[string]No[]Declarative compliance tags (see below)
pii_fieldsarray[string]No[]Fields containing PII, masked in non-admin API responses
audit_requiredbooleanNofalseWhether audit logging is required for this entity
retention_daysintegerNoN/AAuto-delete canonical records after N days

What Actually Enforces Compliance

The three fields that drive real runtime behavior are:

  • pii_fields: Fields listed here are masked in canonical API responses for non-admin users. This is the mechanism that protects sensitive data at the API layer.
  • audit_required: When true, enables audit logging for all operations on this entity's records.
  • retention_days: Drives the cleanup pipeline to auto-delete canonical records older than N days.

These fields are what you configure to meet your regulatory obligations. The engine enforces them regardless of which frameworks you list.

Frameworks (Declarative Tags)

The frameworks field is a list of declarative labels. The engine does not automatically enforce framework-specific rules. Listing HIPAA does not enable encryption, listing GDPR does not enable consent tracking, and listing SOC2 does not configure access controls.

Instead, frameworks serve as organizational tags that signal intent. They are useful for:

  • Audit logs and compliance reports: downstream tooling can filter or group by framework
  • Documentation: clearly communicates which regulations a spec is designed to satisfy
  • Policy enforcement in your CI/CD pipeline: external tools can read the spec and verify that appropriate pii_fields, retention_days, and audit_required settings are present for the declared frameworks

Common framework strings:

FrameworkTypical Use
GDPRTag specs handling EU personal data
HIPAATag specs handling protected health information
SOC2Tag specs subject to SOC 2 audit requirements
CCPATag specs handling California consumer data
PCITag specs handling payment card data
SOXTag specs subject to Sarbanes-Oxley financial controls

You can use any string value; these are conventions, not an enforced enum. Multiple frameworks can be combined freely.

PII Masking

When pii_fields are configured, non-admin API responses automatically mask those fields. This happens at the API layer. The underlying canonical data is stored unmasked for authorized access.

json
{
  "canonical_data": {
    "email": "j***@acme.com",
    "phone": "***-***-4567",
    "ssn": "***-**-6789",
    "date_of_birth": "****-**-15",
    "name": "John Doe",
    "company": "Acme Corp"
  }
}

Fields listed in pii_fields are masked using type-aware patterns:

  • Email: First character visible, domain partially visible (j***@acme.com)
  • Phone: Last four digits visible (***-***-4567)
  • SSN/Tax IDs: Last four digits visible (***-**-6789)
  • Dates: Day visible, month and year masked (****-**-15)
  • Other strings: First and last characters visible, middle replaced (J*** D**)

Admin API access (determined by JWT claims) returns the full unmasked data. This distinction is enforced at the API handler level, not the database level, so RLS policies still apply.

Retention Cleanup

The cleanup pipeline automatically deletes canonical records older than retention_days. This runs as a background worker job on a configurable schedule (default: daily at 02:00 UTC).

yaml
entity:
  name: patient
  compliance:
    frameworks: [HIPAA]
    pii_fields: [email, phone, ssn]
    retention_days: 2555  # 7 years

When a record is deleted by the retention pipeline:

  1. The canonical record is removed from the database
  2. Associated source mappings are deleted
  3. An audit log entry is created with reason retention_policy
  4. If audit logging is enabled, the deletion is logged for compliance reporting

WARNING

Setting retention_days is irreversible for deleted records. Ensure your retention period meets all applicable regulatory requirements before configuring this value.

Hierarchy (Cloud)

Hierarchy lets you model parent-child relationships within records of the same entity type. Many real-world datasets are naturally hierarchical: corporate structures (parent company, subsidiary, branch), geographic regions (country, state, city), product catalogs (category, subcategory, SKU), and org charts (department, team, individual). The hierarchy block tells the engine how these records relate to each other and, optionally, which field values should flow down from parent to child before matching begins.

Hierarchy is nested under the entity block.

Hierarchy vs. Inheritance

These are two distinct concepts configured together:

  • Hierarchy defines the structure: which field on a child record points to its parent's external_id. This is required. Without it, the engine has no way to know which records are parents and which are children.

  • Inheritance defines data propagation: which field values should fill down from parent to child when the child's value is missing or empty. This is optional. You can define a hierarchy without inheritance if you only need the structural relationship.

Inheritance runs in the worker pipeline before matching begins. This means inherited values are fully populated and available to matching rules, survivorship, and all downstream stages. A child record that inherits region from its parent will use that inherited value when evaluating match conditions that reference region.

Configuration

yaml
entity:
  name: company
  description: "Corporate entity hierarchy with regional subsidiaries"
  hierarchy:
    parent_field: parent_company_id
    depth: 3
    inheritance: [industry, region, country]

Field Reference

FieldTypeRequiredDefaultDescription
parent_fieldStringYesN/AField name on child records that contains the parent's external_id
depthu8No5Maximum number of traversal passes for multi-level propagation
inheritanceVec<String>No[]Flat list of field names to inherit from parent to child

parent_field: The name of the field on each child record that holds a reference to the parent record. The engine matches this field's value against each record's external_id to build the parent-child tree. Records where this field is missing or empty are treated as root nodes.

depth: Controls how many passes the engine makes when propagating inherited fields. Each pass pushes values one level deeper in the tree. A three-level hierarchy (grandparent, parent, child) needs at least 2 passes to fully propagate. Defaults to 5, which handles most real-world hierarchies. The engine stops early if a pass produces no updates, so setting this higher than needed has no performance cost.

inheritance: A flat list of field name strings. The inheritance behavior is always fill missing from parent: if a child record's value for a listed field is missing or empty, and the parent has a non-empty value, the parent's value is copied to the child. If the child already has a value, it keeps it. There are no strategy options (no parent_wins, child_wins, or merge). This is intentional; inheritance is meant for default propagation, not conflict resolution.

Example: Corporate Structure

Consider a corporate hierarchy where Acme Corp is the global parent, Acme Europe is a regional subsidiary, and Acme Germany is a local branch:

yaml
entity:
  name: company
  description: "Corporate entity hierarchy with regional subsidiaries"
  hierarchy:
    parent_field: parent_company_id
    depth: 3
    inheritance: [industry, region, country]

Source records before inheritance:

external_idnameparent_company_idindustryregioncountry
acme-corpAcme CorpTechnologyNorth AmericaUS
acme-euAcme Europeacme-corpUK
acme-deAcme Germanyacme-eu
  • acme-corp is the root (no parent_company_id). It has all fields populated.
  • acme-eu is a child of acme-corp. It has country set to UK but is missing industry and region.
  • acme-de is a child of acme-eu. It is missing industry, region, and country.

After inheritance runs:

external_idnameparent_company_idindustryregioncountry
acme-corpAcme CorpTechnologyNorth AmericaUS
acme-euAcme Europeacme-corpTechnologyNorth AmericaUK
acme-deAcme Germanyacme-euTechnologyNorth AmericaUK

What happened on each pass:

  • Pass 1: The engine scans all records. acme-eu has parent_company_id = acme-corp, so the engine checks each inherited field. industry is missing, so it is copied from parent (Technology). region is missing, so it is copied from parent (North America). country is UK, which means it already has a value, so it is not overwritten. Meanwhile, acme-de has parent_company_id = acme-eu. At the start of this pass, acme-eu has country = UK but no industry or region (updates are batched and applied at the end of the pass). So acme-de picks up country = UK from acme-eu, but cannot inherit industry or region yet.

  • Pass 2: The engine scans again. acme-eu already has all fields populated, so no updates. acme-de has parent_company_id = acme-eu. Now acme-eu has industry = Technology and region = North America (applied at the end of pass 1). The engine fills acme-de: industry from acme-eu (Technology), region from acme-eu (North America). country already has UK from pass 1, so it is not overwritten.

  • Pass 3: The engine scans again. No updates are made. Early termination kicks in.

Notice that acme-eu kept its original country value of UK even though its parent had US. Inheritance never overwrites existing values. And acme-de inherited UK from acme-eu (its direct parent), not US from acme-corp (its grandparent). Values propagate level by level, not from the root.

How It Works Step by Step

When the worker pipeline processes a reconciliation job, hierarchy inheritance runs as a pre-matching step:

  1. Build lookup map. The engine constructs a HashMap of external_id to entity index so parent records can be located in O(1) time.

  2. Iterate by depth. The engine loops up to depth times (passes 0 through depth - 1). On each pass, it scans every entity.

  3. Resolve parent. For each entity that has a non-empty parent_field value, the engine looks up the parent by matching that value against the lookup map of external_id values.

  4. Fill missing fields. For each field listed in inheritance, if the child's value is missing (None) or empty (zero-length string) and the parent has a non-empty value, the parent's value is copied to the child. All updates from a single pass are batched and applied at the end of that pass.

  5. Early termination. If a pass produces zero updates, the engine breaks out of the loop immediately. This means a flat dataset with no parent references completes in a single pass regardless of the depth setting.

  6. Matching proceeds. After all inheritance passes complete, the enriched entities are passed to the matching engine. All inherited values participate in matching rules, scoring, and survivorship as if they were original source data.

Python SDK

The hierarchy configuration lives in the YAML spec. The Python SDK loads it with Spec.from_file() or Spec.from_string():

python
from kanoniv import Spec, Source, reconcile

spec = Spec.from_file("company-spec.yaml")

# Hierarchy config is part of the parsed spec
print(spec.parsed["entity"]["hierarchy"])
# {'parent_field': 'parent_company_id', 'depth': 3, 'inheritance': ['industry', 'region', 'country']}

# Reconcile as usual -- inheritance runs automatically before matching
source = Source.from_csv("companies.csv", name="crm", primary_key="external_id")
result = reconcile([source], spec)

You can also define the spec inline:

python
from kanoniv import Spec

spec = Spec.from_string("""
api_version: kanoniv/v1
identity_version: company-v1
entity:
  name: company
  hierarchy:
    parent_field: parent_company_id
    depth: 3
    inheritance: [industry, region, country]
sources:
  crm:
    adapter: csv
    location: companies.csv
    primary_key: external_id
rules:
  - name: name_exact
    type: exact
    field: name
  - name: region_exact
    type: exact
    field: region
decision:
  thresholds:
    match: 0.9
    review: 0.7
""")

Depth Limits

The depth field controls how many passes the engine makes when propagating inherited fields. Each pass pushes values one level deeper in the hierarchy tree.

Hierarchy LevelsMinimum depth NeededExample
2 (parent → child)1Company → Subsidiary
3 (grandparent → parent → child)2HQ → Region → Branch
4+N - 1Deep org charts

The default depth of 5 handles hierarchies up to 6 levels deep. Setting depth higher than your actual hierarchy has no performance cost — the engine terminates early when a pass produces zero updates.

TIP

If you are unsure of your hierarchy depth, leave the default. The engine will stop as soon as there are no more fields to propagate, regardless of the depth setting.

Cycle Detection

The engine does not perform explicit cycle detection. If your source data contains circular parent references (A → B → A), the engine will not crash or loop infinitely — the depth cap guarantees termination after at most depth passes. However:

  • Inherited values in a cycle are not deterministic — the result depends on processing order
  • The engine will not warn you about cycles in the data
  • Recommendation: Clean circular references from your source data before reconciliation. If cycles are unavoidable, set depth to the minimum value needed for your legitimate hierarchy levels to limit unnecessary passes.

Usage Notes

  • Hierarchy without inheritance. If you set parent_field but leave inheritance empty (or omit it), the engine recognizes the parent-child structure but does not propagate any values. This is useful when you need the structural relationship for reporting or grouping but do not want data to flow between levels.

  • Performance. The lookup map is built once, and each pass is a linear scan of all entities. For a dataset of N entities with depth D, the worst-case cost is O(N * D * F) where F is the number of inherited fields. With early termination, the actual cost is typically much lower.

  • Missing parents. If a child's parent_field value does not match any record's external_id, that child is silently skipped for inheritance. No error is raised. This handles cases where the parent record is in a different source or was filtered out.

Complete Entity Example

A full entity configuration combining compliance and hierarchy for a healthcare use case:

yaml
api_version: kanoniv/v1
identity_version: "2.1"

entity:
  name: patient
  description: "Healthcare patient identity across EMR, billing, and pharmacy systems"
  compliance:
    frameworks: [HIPAA, SOC2]
    pii_fields: [email, phone, ssn, date_of_birth, name, address]
    audit_required: true
    retention_days: 2555
  hierarchy:
    parent_field: primary_care_provider_id
    depth: 2
    inheritance: [insurance_plan, network_tier]

sources:
  emr:
    adapter: postgres
    location: emr_db/patients
    primary_key: mrn
  billing:
    adapter: postgres
    location: billing_db/accounts
    primary_key: account_id

# ... rules, decision, survivorship

Tier Requirements

FeatureRequired TierDescription
Basic entity name and descriptionLocalAvailable to all users
Compliance frameworksCloudDeclarative compliance framework tags
PII masking (pii_fields)CloudAutomatic field masking in API responses
Data retention (retention_days)CloudAutomated record cleanup
Audit required (audit_required)CloudMandatory audit logging
Hierarchy (parent_field, inheritance)CloudParent-child entity relationships

Attempting to use Cloud features on the Local tier results in a validation error:

python
from kanoniv import Spec, validate

spec = Spec.from_file("spec-with-compliance.yaml")
result = validate(spec)
# Error: 'compliance' requires Cloud tier

The identity and delegation layer for AI agents.