Sources and event types
See what a source declares before any data arrives, and why each event type has a short name and a stored name.
Records and everything that hangs off them, plus the rules they obey: minting, event behavior, identity resolution, and what the registry enforces.
1. The record map
The records
- Person: one human, deduplicated globally by durable keys, LinkedIn URL first, then email. Exists once across every company they ever touch, keeping their history through job changes.
- Entity: one company, however many tools know it. Internal ULID id; a domain is one identifier on it, never the id itself.
The connections
- Contact: the person-to-company edge, one per affiliation. Title, department, and that person's activity at that company live on the edge, so the same human can hold different roles at different companies without colliding.
- Member: your own team: reps, CSMs, account owners. Managed by you over the API and never minted from source data, so a customer's Slack user can never become your teammate.
- Assignment: the member-to-record edge saying who owns the account or relationship right now. Serves
account_ownerandaccount_teamas live joins, never as stale copied fields. - Identity link: an external id (
domain,slack_channel_id,stripe_customer_id) attached to a person or an entity. It is how every incoming item finds its record, and the thing a delete freezes so a re-sync cannot re-create what you removed.
The data
- Event: one thing that happened, on one entity's timeline, stamped with the acting person when there is one. Append-only and immutable with provenance on every row: the primary data everything else derives from.
- Source / event type: the contract an event is validated against: fields, types, requiredness, scope. Predefined data models ship for Slack, Stripe, calendar and more; custom ones are declared over the API.
- Attribute: a static field you set directly on a record, like tier or region. Never written by ingest and never derived, so declared facts stay separate from observed history.
- File: an uploaded document on an entity, text-extracted, chunked, and embedded, so its content answers semantic search alongside messages.
Computed on read
- Score: your 0 to 100 definition, compiled live per record on every read. Edit it and the next query already serves the new score. Scores.
- Segment: a saved live filter over one grain. Membership is computed at read time, never snapshotted, and its changes feed is the loop you poll to act downstream. Segments.
2. Data models
A data model is the schema for one source: the contract every incoming item is validated against. One data model declares:
- Its event types, each with typed fields, requiredness, and a scope (
entityorperson). - Which fields are promoted, becoming filterable
emf__columns and scoring inputs. - The identity link types its items may carry, so its data can find its records.
- For person-scope events, the contact mapping naming which fields carry the acting person.
Four person-first data models ship enabled on a fresh deployment: job, education, web, signup. A custom one you declare lands in the same tables and is treated identically everywhere; deployments provisioned earlier may also carry previously shipped models such as slack, stripe, calendar, meetings, crm, and email.
One more belongs to ackDB itself: the system data model. Lifecycle events, record created, identity linked and unlinked, member assigned, attribute edited, score changed, are written through it by ackDB, and POST /ingest refuses it, so the audit trail cannot be forged.
The registry defends stored data: additions are free, while removing a field, changing its type, or making an optional field required is refused unless explicitly forced, and GET /schemas/sources/{sourceType}/editability says what is currently allowed. Creating and editing them lives in Build; what an event itself is lives in Events.
3. Attributes
Attributes are the schema for your own declared facts: fields you set directly on records, never written by ingest and never derived. Events are what happened; attributes are what you say is true.
- Each has a scope:
company,person(survives job changes), orcontact(one affiliation only). - A type (
string,number,boolean,date,string_array) and optionalallowedValuesthat validate every write. - A description, read by AI agents for semantic understanding.
Standard attributes ship and cannot be edited; custom ones are yours. Defining and setting them lives in Build.
4. Identity link types
Link types are the schema of identity: which external ids ackDB will accept and match on.
- Universal types (
domain, plus any you register) are valid from any source; a type flaggedcanMintmay create a company, which is how domainless companies get records. - Per-source types (
slack_channel_id,stripe_customer_id) are declared by a data model and are match-only: they attach data to existing records and can never mint, so a typo cannot create a phantom. - A record deletion freezes its identifiers, and a frozen identifier can neither mint nor attach again.
5. How records mint
A company mints two ways: an ingest item with a domain identity link that matches nothing, or an explicit POST /entities with a domain (or a registered mintable anchor type, for domainless companies). The domain is normalized and becomes the domain link; the name defaults to it.
A person mints from durable keys on incoming data: LinkedIn URL first, then email. One entity can hold many links of one type: two Slack channels, one company.
Source-specific ids never mint. A typo in a stripe_customer_id cannot create a phantom company.
6. How events behave
Events have their own page: the anatomy, the per-company timeseries, and examples. The schema-side rules: each event validates against its data model's field types and requiredness, its type is stored source-prefixed (slack_message), and its declared scope decides whether it may carry the acting person.
7. How people resolve
Person-first: durable keys find the human globally, then the contact edge at this company is found or created. A job change ends the old edge and starts a new one; the person and their history stay whole.
Two hard rules:
- ackDB never fuses two humans on a guess. A durable key owned by another person is a 409 plus a merge candidate, resolved deliberately.
- ackDB alone decides internal vs external, from the deployment's internal domains and users config. An
isExternalflag on pushed data is ignored.
8. Where an item lands
Nothing matched is not data lost. The item parks in the unresolved pool (GET /unresolved), and POST /entities/resolve promotes it once you know where it belongs.