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

# CRM Overview

> Build custom data applications on top of ElasticFunnels with the CRM module.

The CRM module lets you define custom entity types, fields, pipelines, and stages — then store structured data entries linked to any record in the system. Think of it as both a **customer relations management** tool and a **general-purpose data platform** (like Webflow CMS Collections). You can create traditional pipeline-based entities (deals, contacts) or standalone data collections (properties, inventory, cities).

## How entries attach to records

Each CRM entry is linked to a record in your funnel or store through two values:

| Field            | Description                                           |
| ---------------- | ----------------------------------------------------- |
| `reference_type` | The type of record (e.g. `customer`, `order`, `lead`) |
| `reference_id`   | The ID of that record                                 |

This pattern lets you attach structured data to customers, orders, or custom reference types you define.

## Concepts

<Note>
  **Entries vs. Fields**: An **entry** is a single record or document stored in the CRM (like a row in a database, representing e.g. a specific check-in for a customer). A **field** is a specific piece of data that lives inside an entry (like a column, e.g. "current\_weight" or "deal\_value").
</Note>

### Entity Modes

Entities operate in one of two modes:

| Mode              | Description                                                                                                                            |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **CRM** (default) | Traditional pipeline-based entity with stages, suitable for deals, leads, contacts                                                     |
| **Data**          | Standalone data collection without pipelines — ideal for storing structured records like properties, inventory items, cities, products |

When creating an entity, choose "Data Collection" to skip pipelines entirely. Data-mode entities focus purely on fields and entries.

### Entities

An **entity** defines a type of CRM record (e.g. "Deals", "Contacts", "Properties"). Each entity has:

* A **name** and **slug** (machine-readable identifier)
* An **entity mode** (`crm` or `data`)
* **Fields** that define the data structure
* **Pipelines** with **stages** for workflow tracking (CRM mode only)

### Fields

Fields define what data can be stored on entries. Each field has:

| Property           | Description                                                       |
| ------------------ | ----------------------------------------------------------------- |
| `label`            | Human-readable label                                              |
| `key`              | Machine-readable key (used in API and templates)                  |
| `type`             | See supported types below                                         |
| `ui_editable`      | Whether this field can be edited in the admin UI (default `true`) |
| `reporting_config` | Optional analytics metadata for CRM reports                       |

#### Supported field types

| Type                   | Renders as           | Notes                                       |
| ---------------------- | -------------------- | ------------------------------------------- |
| `text`                 | Text input           | Default type                                |
| `textarea`             | Multi-line text      |                                             |
| `rich_text`, `wysiwyg` | Rich text            | Formatted content stored as HTML            |
| `email`                | Email input          | Rendered as mailto link                     |
| `phone`                | Phone input          | Rendered as tel link                        |
| `url`                  | URL input            | Rendered as clickable link                  |
| `number`               | Numeric input        | Stored as double                            |
| `date`                 | Date picker          | ISO 8601 string                             |
| `datetime`             | Date+time picker     | ISO 8601 string                             |
| `boolean`              | Toggle switch        | true/false                                  |
| `select`               | Dropdown             | Requires `options` array                    |
| `multiselect`          | Multi-select tags    | Requires `options` array                    |
| `color`                | Color picker         | Hex color code                              |
| `image`                | Image URL / uploader | Renders as thumbnail                        |
| `file`                 | File URL / uploader  | Renders as download link                    |
| `json`                 | JSON editor          | Renders as collapsible tree                 |
| `reference`            | Entity reference     | Links to single entry in another entity     |
| `multi_reference`      | Multi-reference      | Links to multiple entries in another entity |

The **type** in CRM settings controls how the field appears and behaves in the admin UI. It does **not** change what you pass from code: `setCrmField` always sends a JavaScript **value** — a **string**, **number**, **boolean**, or a **JSON object**. For fields configured as **date** in the CRM, pass a **date-time string** from scripts, typically **ISO 8601** (e.g. `new Date().toISOString()` or `'2026-03-15'`).

#### UI-editable fields

Set `ui_editable: false` on fields that should only be updated programmatically (via backend scripts). These fields display with a lock icon in the admin UI and cannot be inline-edited.

Use `reporting_config` to mark fields as reportable and suggest how the Reports tab should aggregate and chart them. Supported values include `reportable`, `aggregation_type` (`numeric`, `categorical`, `date`, `boolean`), `baseline` (`min`/`max`), `bucket_size`, and `preferred_chart` (`kpi`, `histogram`, `pie`, `bar`, `line`).

### Relations

Relations link entries across entities. A field of type `reference` or `multi_reference` creates a relation between the current entity and a target entity. Relations are stored in Elasticsearch alongside the entry data.

#### Configuring reference fields

When creating a reference field, specify:

* **Target entity**: Which entity the reference points to
* **Display field**: Which field from the target entity to show as the label (defaults to title)

#### Working with relations programmatically

Use `SetCrmRelation` and `RemoveCrmRelation` to manage relations from backend scripts:

```javascript theme={null}
SetCrmRelation({
  slug: 'properties',
  relationKey: 'city',
  targetEntryId: 'abc123'
});

RemoveCrmRelation({
  slug: 'properties',
  relationKey: 'city',
  targetEntryId: 'abc123'
});
```

### Pipelines & Stages

Pipelines represent workflows (CRM mode only). Each pipeline contains ordered stages with optional colors and semantic statuses (e.g. `won`, `lost`, `active`). Entries move through stages as they progress.

## Customer CRM Data

The most common pattern is linking CRM entries to customers. When a customer visits a page, their session supplies the customer context so CRM defaults attach to the right person (creating the customer record when needed).

### Example: Weight Loss Tracker

1. Create a CRM entity called "Weight Tracker" with slug `weight-tracker` and fields like `current_weight`, `goal_weight`, `last_checkin`
2. Create a pipeline "Progress" with stages "Starting", "In Progress", "Goal Reached"
3. Store data via backend scripts or the template engine:

```javascript theme={null}
setCrmFields({
  slug: 'weight-tracker',
  fields: [
    { fieldKey: 'current_weight', value: 185 },
    { fieldKey: 'goal_weight', value: 165 },
    { fieldKey: 'last_checkin', value: '2026-03-15' }
  ]
});
```

<Tip>
  `slug` identifies the **CRM entity** (board/type). `referenceType` / `referenceId` default to the current **customer** when omitted. For another record: `setCrmField({ slug: 'weight-tracker', referenceType: 'order', referenceId: '12345', fieldKey: 'status', value: 'shipped' })`. See [Data Functions](/backend-scripts/data-functions#crm-data).
</Tip>

4. Read it back on any page:

```javascript theme={null}
var weight = getCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight' });
var goal = getCrmField({ slug: 'weight-tracker', fieldKey: 'goal_weight' });
if (weight && goal && Number(weight) <= Number(goal)) {
  setVariable('show_congrats', true);
}
```

### Viewing Customer CRM Data

Customer CRM entries are visible in the **Customer Details** page under the **CRM** tab. This shows all CRM entries linked to that customer across all entity types.

## Functions

The following functions are available in both the template engine and backend scripts. Write functions accept an optional `async` parameter (default `false`). When `true`, the write runs in the background for faster response times. Read functions are always synchronous.

| Function                                                                 | Description                                                                               |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `EnsureCrmEntity({ slug, ... })`                                         | Idempotently create an entity with fields (returns `{ id, slug, created, fields_added }`) |
| `CreateCrmEntry({ slug, title?, values?, ... })`                         | Create a new entry and return its ID                                                      |
| `getCrmEntries({ slug, ... })`                                           | Fetch all entries for one entity **slug** + reference                                     |
| `getCrmField({ slug, fieldKey, ... })`                                   | Get one field value (`slug` + `fieldKey` required)                                        |
| `getCrmFields({ slug, fieldKeys, ... })`                                 | Get multiple field values (`slug` + `fieldKeys` required)                                 |
| `setCrmField({ slug, fieldKey, value, ... })`                            | Set one field value — upserts a single value per key                                      |
| `setCrmFields({ slug, fields, ... })`                                    | Set multiple field values in one read/write cycle                                         |
| `addCrmFieldValue({ slug, fieldKey, value, ... })`                       | Append a new entry with a field value — enables multiple values per key                   |
| `getCrmFieldValues({ slug, fieldKey, ... })`                             | Get all values for a field key across entries (returns array)                             |
| `clearCrmEntries({ slug, ... })`                                         | Delete all entries for an entity slug + reference                                         |
| `SetCrmRelation({ slug, relationKey, targetEntryId, ... })`              | Add a relation from entry to another entry                                                |
| `RemoveCrmRelation({ slug, relationKey, targetEntryId, ... })`           | Remove a specific relation                                                                |
| `moveCrmLead({ slug, pipelineId/pipelineSlug, stageId/stageSlug, ... })` | Move entry to a specific pipeline + stage                                                 |
| `moveCrmToStage({ slug, stageId/stageSlug, ... })`                       | Move entry to a stage (pipeline resolved from stage)                                      |
| `moveCrmToPipeline({ slug, pipelineId/pipelineSlug, ... })`              | Move entry to a pipeline (lands on first stage)                                           |
| `moveCrmToEntity({ fromSlug, toSlug, fieldMap, ... })`                   | Copy mapped fields across entity types                                                    |

`slug` is always required (the entity's slug from CRM settings). Reference defaults: `customer` + session customer id.

### Common options

Every CRM function accepts these optional properties:

| Property        | Type   | Default               | Description        |
| --------------- | ------ | --------------------- | ------------------ |
| `referenceType` | string | `'customer'`          | Linked record type |
| `referenceId`   | string | session `customer_id` | Linked record id   |

Write functions also accept:

| Property | Type    | Default | Description                                                                 |
| -------- | ------- | ------- | --------------------------------------------------------------------------- |
| `async`  | boolean | `false` | When `true`, writes run in the background — faster but eventual consistency |

### Single-value fields

Use `setCrmField` / `getCrmField` for fields that hold one value per key (e.g. weight, goal, last check-in date):

```javascript theme={null}
setCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight', value: 182 });
var weight = getCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight' });
```

When you need to read multiple fields at once, use `getCrmFields` and pass an array of `fieldKeys`:

```javascript theme={null}
var fields = getCrmFields({ slug: 'weight-tracker', fieldKeys: ['current_weight', 'goal_weight'] });
var weight = fields.current_weight;
var goal = fields.goal_weight;
```

When writing multiple fields in one script, prefer `setCrmFields` so all values are merged in one read/write cycle:

```javascript theme={null}
setCrmFields({
  slug: 'weight-tracker',
  fields: [
    { fieldKey: 'current_weight', value: 182 },
    { fieldKey: 'goal_weight', value: 165 },
    { fieldKey: 'last_checkin', value: today }
  ]
});
```

If you need to keep separate `setCrmField` calls, pass `force: true` to skip the per-key scan and merge directly into the newest entry — much faster for sequential writes:

```javascript theme={null}
setCrmField({ slug: 'weight-tracker', fieldKey: 'current_weight', value: 182, force: true });
setCrmField({ slug: 'weight-tracker', fieldKey: 'goal_weight', value: 165, force: true });
setCrmField({ slug: 'weight-tracker', fieldKey: 'last_checkin', value: today, force: true });
```

### Multi-value fields

Use `addCrmFieldValue` / `getCrmFieldValues` when a key can have multiple values over time (e.g. weigh-in history). Each `addCrmFieldValue` call creates a separate CRM entry:

```javascript theme={null}
addCrmFieldValue({
  slug: 'weight-tracker',
  fieldKey: 'weighin_history',
  value: { date: '2026-03-31', weight: 182, notes: 'post-workout' }
});

var history = getCrmFieldValues({ slug: 'weight-tracker', fieldKey: 'weighin_history', limit: 10 });
// Returns an array of values, newest first
```

### Clearing entries

Use `clearCrmEntries` to delete all entries for an entity + reference:

```javascript theme={null}
var result = clearCrmEntries({ slug: 'weight-tracker' });
// { ok: true, deleted: 3 }
```

### Pipeline and stage movement

Use `moveCrmLead` to move a CRM entry to a specific pipeline and stage by id or slug:

```javascript theme={null}
// By slug (recommended — works across environments)
moveCrmLead({ slug: 'deals', pipelineSlug: 'sales', stageSlug: 'qualified' });

// By id
moveCrmLead({ slug: 'deals', pipelineId: 3, stageId: 12 });
```

Returns `{ ok, moved, totalMatched, pipeline_id, stage_id }`. `moved` is the number of entries updated (1 unless `allEntries: true`).

Use `moveCrmToStage` when you only know the stage — the pipeline is resolved automatically:

```javascript theme={null}
moveCrmToStage({ slug: 'deals', stageSlug: 'won' });
```

Use `moveCrmToPipeline` to move an entry into a pipeline; it lands on the first stage:

```javascript theme={null}
moveCrmToPipeline({ slug: 'deals', pipelineSlug: 'onboarding' });
```

Use `moveCrmToEntity` to copy field values from one entity type to another (e.g. promoting a lead to a deal):

```javascript theme={null}
moveCrmToEntity({
  fromSlug: 'leads',
  toSlug: 'deals',
  fieldMap: { lead_score: 'initial_score', source: 'lead_source' },
  pipelineSlug: 'sales',
  stageSlug: 'new',
  removeFromOld: false  // set true to delete source entries after copy
});
```

Returns `{ ok, moved, removed_from_old }`.

Pass `allEntries: true` to any move function to update all matching entries instead of just the newest one.

### Ensuring entities programmatically

Use `EnsureCrmEntity` at the top of a backend script to idempotently provision an entity. Re-runs never overwrite admin edits — only missing fields are added.

```javascript theme={null}
EnsureCrmEntity({
  slug: 'properties',
  name: 'Properties',
  singularName: 'Property',
  pluralName: 'Properties',
  entityMode: 'data',  // 'crm' (default) or 'data'
  icon: 'building',
  fields: [
    { key: 'address', label: 'Address', type: 'text' },
    { key: 'price', label: 'Price', type: 'number' },
    { key: 'bedrooms', label: 'Bedrooms', type: 'number' },
    { key: 'photo', label: 'Photo', type: 'image', ui_editable: false },
    { key: 'city', label: 'City', type: 'reference', options: { target_entity_id: 5 } },
    { key: 'status', label: 'Status', type: 'select', options: ['available', 'sold', 'pending'] },
    {
      key: 'listing_date',
      label: 'Listing Date',
      type: 'date',
      reportingConfig: { reportable: true, aggregationType: 'date' }
    }
  ]
});
```

### Creating entries

Use `CreateCrmEntry` to create a new entry and get back its ID (useful for setting relations):

```javascript theme={null}
var result = CreateCrmEntry({
  slug: 'properties',
  title: '123 Main St',
  values: {
    address: '123 Main Street, Springfield',
    price: 450000,
    bedrooms: 3,
    status: 'available'
  }
});
// result = { ok: true, id: 'abc123' }
```

### Managing relations

Use `SetCrmRelation` to link entries:

```javascript theme={null}
// Link a property to a city
SetCrmRelation({
  slug: 'properties',
  relationKey: 'city',
  targetEntryId: 'city-entry-id-here'
});
```

Use `RemoveCrmRelation` to unlink:

```javascript theme={null}
RemoveCrmRelation({
  slug: 'properties',
  relationKey: 'city',
  targetEntryId: 'city-entry-id-here'
});
```

### Async writes

For non-critical writes where you don't need immediate consistency, pass `async: true` so the write runs in the background:

```javascript theme={null}
setCrmField({ slug: 'weight-tracker', fieldKey: 'last_page_view', value: today, async: true });
setCrmFields({ slug: 'weight-tracker', fields: [{ fieldKey: 'current_weight', value: 182 }, { fieldKey: 'last_checkin', value: today }], async: true });
addCrmFieldValue({ slug: 'weight-tracker', fieldKey: 'page_views', value: { url: '/dashboard', ts: today }, async: true });
moveCrmLead({ slug: 'deals', pipelineSlug: 'sales', stageSlug: 'contacted', async: true });
```

See [Data Functions](/backend-scripts/data-functions#crm-data) for full parameter details and examples.
