> ## Documentation Index
> Fetch the complete documentation index at: https://docs.allgoodhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Architecture

> How the allGood Datastore stores records, resolves them into people and companies, logs activity, and keeps every change as a decision you can trace back to its source.

The **Datastore** is the part of Marketing Kernel that holds your data: every row synced from Salesforce or Marketo, every table shared from your warehouse, every form submission and page view, and everything allGood itself decides about a person or company. This page explains how that data is organized so you can reason about your own. For how data gets in and out, see [Data Sharing](https://docs.allgoodhq.com/mk/data/sharing.md). For what acts on it, see [Orchestration](https://docs.allgoodhq.com/mk/data/orchestration.md).

## Overview

Three kinds of thing live in the Datastore:

* A **record** is one statement about one thing, from one source: a Salesforce contact row, a Marketo lead, a form submission, the Smart Fields a policy wrote.
* An **entity** is one real thing: a person or a company, made of the records that describe it.
* An **activity** is one timestamped event: a page view, an email click, a form submit.

Nothing in the Datastore is edited in place. Every change to a record is a **decision**, logged with who made it, what it was based on, and why, and a record's current values are simply the result of its decisions applied in order. That is what lets you open any field on a person and see where the value came from.

```mermaid theme={null}
flowchart LR
    subgraph sources["Sources"]
        S1["Salesforce, Marketo"]
        S2["Your warehouse"]
        S3["Forms, web, email"]
        S4["Policies and plans"]
    end
    LOG[("Decision log")]
    REC["Records"]
    ENT["People and companies"]
    ACT["Activities"]
    S1 --> LOG
    S2 --> LOG
    S3 --> ACT
    S4 --> LOG
    ACT -- "reducers" --> LOG
    LOG --> REC
    REC -- "identity resolution" --> ENT
```

Two rules follow from this, and they explain most of what you will see in the product. **Source data is never overwritten or merged.** A Salesforce contact and a Marketo lead for the same person both stay on file, field for field, each under its own record type; trusted values live on separate records that policies write. And **every value carries provenance.** You can always ask "why is this field this?" and get an answer.

If you use the [outbound share](https://docs.allgoodhq.com/ops/snowflake/outbound-sharing.md#data-schema), these are the `ENTITY`, `RECORD`, `ACTIVITY` and `DATA_CHANGE` views in your warehouse.

## Anatomy of a Record

A record is a typed, flat set of fields. A Salesforce contact looks like this on a person's page and in your `RECORD` view:

```json theme={null}
{
  "type": "sfdc_contact",
  "data": {
    "Id": "0031U00000AbCdEQAV",
    "Email": "priya.nair@acme.com",
    "FirstName": "Priya",
    "LastName": "Nair",
    "Title": "VP Engineering",
    "AccountId": "0011U00000XyZwVQAX"
  }
}
```

Field names come over from the source verbatim: a Salesforce field keeps its Salesforce casing, and a table shared from your warehouse keeps its column names. Matching is exact, so `Email` and `email` are different fields. Fields are the unit everything works at: identity resolution links on a field, policies read and write fields, and provenance is tracked per field.

Every record has a **type**, and the name tells you who writes it:

| Type | Written by |
| - | - |
| `sfdc_*`, `mkto_*` | That system's sync. Don't write to these; the next sync wins. |
| `person`, `company` | The **standard record**: a fixed set of fields every person and company has. Populated by a policy in your tenant. See [Person and Company](#person-and-company). |
| `person_s`, `company_s` | **Smart Fields**: whatever your policies compute, under whatever names you choose. |
| `ag_*` | allGood's own engines, such as `ag_plan_state` (a person's position in a plan). Read-only. |
| Anything else | Yours: a warehouse table bound to a record type, an upload, or a rollup of activity such as `person_forms`. |

Record types are listed under **Orchestration → Database → Record Types**. Each shows its fields, the linkages it takes part in, the policies that touch it, and (for synced types) its sync status. A sync never deletes a record: when a row disappears from its source, the record is marked deleted and its history stays.

### The decision log

A record never stores its own state directly. Its state is the result of its **decisions**, each one logged with who made it (the warehouse sync, a named policy, a plan, identity resolution, a person editing by hand), the fields it set, the input values it read, and a reason in plain language. Lay the decisions on top of each other oldest to newest and you have the record; a field's value is whatever the latest decision to touch it said.

Here is the life of one person's records, read from the log:

| Who | Record | Changed | Reason |
| - | - | - | - |
| Warehouse sync | `sfdc_contact` | `Email`, `Title = VP Engineering`, `AccountId`, … | Warehouse sync: Salesforce Contact |
| Activity rollup | `person_forms` | `email`, `last_form = cloud-security-guide` | Reducer: person\_form\_submits |
| Identity resolution | `person_forms` | — (placed on Priya) | `person_forms.email` matches `sfdc_contact.Email` |
| Job Level Normalization | `person_s` | `job_level = VP/Director` | Title "VP Engineering" maps to the VP/Director band under the normalization rules. |
| Cloud Security Guide | `ag_plan_state` | `state = delivered` | Admitted: submitted the gated-content form; delivery email sent. |

The policy row's reason is the agent's own explanation, stored next to the value it produced, so "why is this person VP/Director?" has an answer months later. A decision made based on the same input data can be applied to many records, if those records all share the same policy inputs: two people with the title "VP Engineering" get the same job-level decision, and each one's timeline shows it.

**Where you see it.** On a person's page, each field shows the decision that set it, and the timeline lists every decision that touched them. Each policy, plan and reducer under **Orchestration** has a **Decisions** tab. In your warehouse, the same rows are `DATA_CHANGE`.

## Anatomy of an Entity

An entity is one real thing, represented as the set of records that identity resolution decided describe it. A person might hold a Salesforce contact, two Marketo leads, a row from a list upload, a rollup of their form submits, the standard `person` record and their Smart Fields.

The entity itself stores almost nothing: an ID, a type, and which records belong to it. There is no merged copy of the fields. When you open a person, allGood reads the latest record of each type attached to them, so the "whole person" is always the records themselves, and nothing can be stale or lost. When identity resolution learns that two entities are one, the oldest entity's ID is kept, so IDs stay stable over time; records are only ever added to an entity, never split off.

Entity types are configured under **Orchestration → Database → Entities**. Each one's tabs show the record types it includes, its linkages (**Identity Resolution**), the relationships it takes part in, the policies that touch it, and how its name and description are composed from its records (**Appearance**). Person and Company are the two every tenant has, but anything with records that need resolving can be an entity type.

### Identity Resolution

Identity resolution is how records become people and companies. It is driven by **linkages**. A linkage says: a record of one type and a record of another type with the same value in a given field are the same thing. Each linkage names two record types and one field on each:

| Linkage | | | | | |
| - | - | - | - | - | - |
| Uploaded list row to its Marketo lead | `list_upload` | `mktoLeadId` | = | `mkto_lead` | `id` |
| Marketo lead to its Salesforce contact | `mkto_lead` | `sfdcContactId` | = | `sfdc_contact` | `Id` |
| Form submission to its person, by email | `person_forms` | `email` | = | `sfdc_contact` | `Email` |

Linkages chain. Any records connected through any sequence of linkages become one entity, however many hops apart they are, and whether or not they share a field directly. With the three linkages above, an uploaded row and a form submission that have nothing in common still land on one person:

```mermaid theme={null}
flowchart LR
    subgraph E["One person"]
        direction LR
        LU["list_upload<br/>mktoLeadId = 7731"]
        ML["mkto_lead<br/>id = 7731<br/>sfdcContactId = 0031…"]
        SC["sfdc_contact<br/>Id = 0031…<br/>Email = priya.nair@acme.com"]
        PF["person_forms<br/>email = priya.nair@acme.com"]
        LU -- "mktoLeadId = id" --> ML
        ML -- "sfdcContactId = Id" --> SC
        PF -- "email = Email" --> SC
    end
    style E fill:none,stroke-dasharray: 5 5
```

The uploaded row reaches the Salesforce contact through a Marketo ID and then a Salesforce ID; the form submission reaches it through an email. Three hops, four records, one person. Each placement is itself a decision on the person's timeline, labelled **Identity resolution**, with the linkage that justified it as the reason.

Three mechanics matter when you set up linkages:

* **Matching is exact.** No lowercasing, no trimming. `Priya.Nair@Acme.com` and `priya.nair@acme.com` do not match. Normalize at capture or in the source.
* **Placeholders are ignored.** A value that more than 100 records share on either side (`noreply@…`, `N/A`) links nothing. Without this, one bad default would fuse hundreds of people.
* **It runs on its own.** Resolution wakes whenever a linked field is written and after every warehouse sync. Linkages work on any record type, including tables shared from your warehouse, so you do not have to pre-join your data before sharing it.

**Checking it.** Open an entity type and select the **Identity Resolution** tab. Each linkage is a card showing its two record types, how many pairs match today, and a coverage meter for each side: how many records have the field filled in, and how many of those found a match. Low coverage almost always means the field is empty or formatted differently in one source.

> [!TIP]
>
> A common cause of poor matching is a masked field. A Salesforce sandbox with data masking replaces every email address, so no email-based linkage can match.

## Activities

An activity is an event: `page.viewed`, `form.submitted`, `email.clicked`, `email.bounced`, `list.unsubscribed`, `meeting.booked`, and so on, each with a timestamp, a payload, and where it was captured. Timestamps are stamped by allGood when the event arrives, never by the page. An entity's page shows its activity as a timeline; a company's timeline names the person behind each event. In your warehouse, activities are the `ACTIVITY` view.

Activities are not filed under an entity when they arrive. Each one carries identities, an email, a device ID (the first-party cookie on your branded subdomain), or a company domain, and allGood resolves it to whichever entity's records carry the same identity. This is what lets an activity belong to one person, several, or nobody yet, and follow a person through later merges.

Two consequences are worth knowing:

* **Email creates people; devices don't.** A form submit with an email nobody has seen creates a standard `person` record on the spot, so the person exists before any policy or plan runs. A device ID can only attach activity to a person who already exists. That stops every anonymous visitor becoming a person, and stops a shared laptop fusing two colleagues.
* **Anonymous activity catches up.** A page view from a device nobody has identified waits. When someone submits a form from that device, the email resolves them, and the earlier page views become theirs. A click on an email they forwarded resolves to the forwarder's device, never to them.

Activities become records through **reducers**, under **Orchestration → Config → Reducers**. A reducer folds a stream of events into one record per person or company: `person_forms` (last form, submit count), `person_web` (pages viewed), `company_web` (visits by domain). Those records take part in identity resolution and policies like any other, which is how "submitted the pricing form" becomes something a policy can read.

Tracking captures nothing until your consent tool permits it. See [Consent behavior](https://docs.allgoodhq.com/mk/developer/web-edge/consent-behavior.md).

## Person and Company

Person and Company are ordinary entity types with a **system role**. The product always says "Person", never "Lead" or "Contact": those are record types that live *inside* a person. The role does two things.

It gives the entity a **standard record** with a fixed set of fields, present on every person and company. For a person: `email`, `firstName`, `lastName`, `fullName`, `title`, `companyName`, `phone`, `consent`, `lifecycleStage`, `timezone`. For a company: `name`, `domain`, `website`, `industry`, `employeeCount`, `annualRevenue`, `city`, `state`, `country`, `lifecycleStage`. And it lets platform features find the entity: the email engine addresses a person through `person.email` and honors `person.consent`, lookups work by email or domain, and the `PERSON` and `COMPANY` views in your [outbound share](https://docs.allgoodhq.com/ops/snowflake/outbound-sharing.md#data-schema) read the standard record.

Everything else is the same machinery every entity type gets. The standard record is populated by a policy in your tenant, not by the platform: a "Populate Standard Person" policy coalesces `sfdc_contact`, `mkto_lead`, `list_upload` and `person_forms` fields into `person` in the precedence order you choose, and you can change it. **Smart Fields** (`person_s`, `company_s`) have no schema at all: persona, job level, region, account tier, whatever your policies compute. People and companies take part in identity resolution through their own linkages, hold any record types, and can be the ends of any relationship.

## Relationships

A relationship connects two entities without merging them: a person and their employer, an account and its owner, an email and its sender. Use one whenever a policy on one entity needs to read the other, or you want the company page to list its people.

A relationship is a linkage that does not merge: a field-equality match between two record types, with a slug to name it. Under **Orchestration → Database → Relationships**, the **Configure** tab for one shows:

| | |
| - | - |
| **Name** | Person to Company |
| **Slug** | `person_to_company` |
| **Description** | Relates a person to the company they work for. A person and their employer are related, never merged. |
| **Field-equality match** | `sfdc_contact` . `AccountId` = `sfdc_account` . `Id` |

Others follow the same pattern: `account_owner` (`sfdc_account.OwnerId` = `sfdc_user.Id`), `email_to_person` (`email_message.from_address` = `sfdc_contact.Email`). allGood sets these up with you.

Relationships are not stored; they are computed from the records whenever asked, so they are always current. The **Examples** tab shows live matched pairs. Because the match is between record types, not entities, a relationship follows the records: when identity resolution moves a record onto a different person, every relationship through it moves too. They are also undirected. A policy on a person reaches the related company, and a policy on a company reaches its people, through the same definition.

**Using one in a policy.** In the policy's **Inputs & Outputs**, add a related-record input, pick the relationship, and name the record and fields you want: "the `sfdc_account` of the company related by `person_to_company`, fields `Type` and `Industry`". The account's values then arrive as inputs, and the policy can write them onto the person's Smart Fields.

Note that the standard `person` record's `companyName` is a text field a policy copied in, not a link. The actual person-to-company connection is the relationship. If you want the company's own fields on a person, reach through the relationship rather than matching on the name.

When there is no clean key to match on, such as a lead whose free-text company is "Acme Corp." and an account named "ACME Corporation, Inc.", use a **Smart Relationship**. It matches with rules first and an agent for the rest, and its matches appear alongside ordinary relationships. See [Smart Relationships](https://docs.allgoodhq.com/mk/data/orchestration.md#smart-relationships).

## Good to know

* **Duplicates inside one source stay separate.** Identity resolution merges only across the record-type pairs your linkages name. Two `sfdc_contact` records with the same email stay two people unless a linkage covers that case. If your CRM has duplicates, say so during onboarding.
* **A policy cannot clear a field.** An empty answer leaves the old value in place. If "unknown" needs to exist, make it a real value.
* **Field names are exact.** A linkage or policy input naming `email` on a record whose field is `Email` reads nothing, silently. This is the usual reason a linkage matches nothing.
* **Lowercase emails in the policy that writes them.** Identity resolution and activity lookup both compare raw strings.
* **A test person is a record, not a field.** An entity is a test person because it has an `ag_test` record attached, which keeps "test" out of your real fields.
* **Two share views lag a little.** `ENTITY` and `RECORD` read a rollup that refreshes every few minutes; `ACTIVITY` and `DATA_CHANGE` are current. See [outbound sharing](https://docs.allgoodhq.com/ops/snowflake/outbound-sharing.md).

> [!NOTE]
>
> Questions? Reach out to [support@allgoodhq.com](mailto:support@allgoodhq.com), or ask your allGood implementation lead.

> [card] [Orchestration](https://docs.allgoodhq.com/mk/data/orchestration.md)
>
> How policies decide, how Smart Relationships infer links, and how plans move people through campaigns. Everything on this page is written by one of them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.