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
entity:
name: customer
description: "B2B customer identity across CRM, billing, and support systems"| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Entity type identifier (alphanumeric + underscore) |
description | string | No | Human-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.
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.
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| Field | Type | Required | Default | Description |
|---|---|---|---|---|
frameworks | array[string] | No | [] | Declarative compliance tags (see below) |
pii_fields | array[string] | No | [] | Fields containing PII, masked in non-admin API responses |
audit_required | boolean | No | false | Whether audit logging is required for this entity |
retention_days | integer | No | N/A | Auto-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: Whentrue, 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, andaudit_requiredsettings are present for the declared frameworks
Common framework strings:
| Framework | Typical Use |
|---|---|
GDPR | Tag specs handling EU personal data |
HIPAA | Tag specs handling protected health information |
SOC2 | Tag specs subject to SOC 2 audit requirements |
CCPA | Tag specs handling California consumer data |
PCI | Tag specs handling payment card data |
SOX | Tag 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.
{
"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).
entity:
name: patient
compliance:
frameworks: [HIPAA]
pii_fields: [email, phone, ssn]
retention_days: 2555 # 7 yearsWhen a record is deleted by the retention pipeline:
- The canonical record is removed from the database
- Associated source mappings are deleted
- An audit log entry is created with reason
retention_policy - 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
entity:
name: company
description: "Corporate entity hierarchy with regional subsidiaries"
hierarchy:
parent_field: parent_company_id
depth: 3
inheritance: [industry, region, country]Field Reference
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
parent_field | String | Yes | N/A | Field name on child records that contains the parent's external_id |
depth | u8 | No | 5 | Maximum number of traversal passes for multi-level propagation |
inheritance | Vec<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:
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_id | name | parent_company_id | industry | region | country |
|---|---|---|---|---|---|
| acme-corp | Acme Corp | Technology | North America | US | |
| acme-eu | Acme Europe | acme-corp | UK | ||
| acme-de | Acme Germany | acme-eu |
acme-corpis the root (noparent_company_id). It has all fields populated.acme-euis a child ofacme-corp. It hascountryset toUKbut is missingindustryandregion.acme-deis a child ofacme-eu. It is missingindustry,region, andcountry.
After inheritance runs:
| external_id | name | parent_company_id | industry | region | country |
|---|---|---|---|---|---|
| acme-corp | Acme Corp | Technology | North America | US | |
| acme-eu | Acme Europe | acme-corp | Technology | North America | UK |
| acme-de | Acme Germany | acme-eu | Technology | North America | UK |
What happened on each pass:
Pass 1: The engine scans all records.
acme-euhasparent_company_id = acme-corp, so the engine checks each inherited field.industryis missing, so it is copied from parent (Technology).regionis missing, so it is copied from parent (North America).countryisUK, which means it already has a value, so it is not overwritten. Meanwhile,acme-dehasparent_company_id = acme-eu. At the start of this pass,acme-euhascountry = UKbut noindustryorregion(updates are batched and applied at the end of the pass). Soacme-depicks upcountry = UKfromacme-eu, but cannot inheritindustryorregionyet.Pass 2: The engine scans again.
acme-eualready has all fields populated, so no updates.acme-dehasparent_company_id = acme-eu. Nowacme-euhasindustry = Technologyandregion = North America(applied at the end of pass 1). The engine fillsacme-de:industryfromacme-eu(Technology),regionfromacme-eu(North America).countryalready hasUKfrom 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:
Build lookup map. The engine constructs a
HashMapofexternal_idto entity index so parent records can be located in O(1) time.Iterate by depth. The engine loops up to
depthtimes (passes0throughdepth - 1). On each pass, it scans every entity.Resolve parent. For each entity that has a non-empty
parent_fieldvalue, the engine looks up the parent by matching that value against the lookup map ofexternal_idvalues.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.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
depthsetting.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():
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:
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 Levels | Minimum depth Needed | Example |
|---|---|---|
| 2 (parent → child) | 1 | Company → Subsidiary |
| 3 (grandparent → parent → child) | 2 | HQ → Region → Branch |
| 4+ | N - 1 | Deep 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
depthto the minimum value needed for your legitimate hierarchy levels to limit unnecessary passes.
Usage Notes
Hierarchy without inheritance. If you set
parent_fieldbut leaveinheritanceempty (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_fieldvalue does not match any record'sexternal_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:
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, survivorshipTier Requirements
| Feature | Required Tier | Description |
|---|---|---|
Basic entity name and description | Local | Available to all users |
Compliance frameworks | Cloud | Declarative compliance framework tags |
PII masking (pii_fields) | Cloud | Automatic field masking in API responses |
Data retention (retention_days) | Cloud | Automated record cleanup |
Audit required (audit_required) | Cloud | Mandatory audit logging |
Hierarchy (parent_field, inheritance) | Cloud | Parent-child entity relationships |
Attempting to use Cloud features on the Local tier results in a validation error:
from kanoniv import Spec, validate
spec = Spec.from_file("spec-with-compliance.yaml")
result = validate(spec)
# Error: 'compliance' requires Cloud tier