Skip to main content

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.

Pages accept both GET and POST requests. Combined with backend scripts, this lets you build data-collection endpoints, custom forms, and mini-apps using regular ElasticFunnels pages — no external server needed.

The pattern

  1. Create a page (e.g. /weight/checkin)
  2. Add a <script scope="backend"> that checks for POST, validates request.body with the Schema validator, and stores the data
  3. Your frontend uses fetch() to POST JSON data to that page
  4. The backend script responds with JSON — the page HTML is skipped when response.json() is called
For reusable validation logic across multiple pages, use shared backend script modules — create a module with exported functions and import them anywhere.

Full example: Weight check-in

1. Create a shared validation module

Create a backend script module (code: validators) with shared schemas:
<script scope="backend">
  export var WeightCheckinSchema = Schema.object({
    weight:  Schema.number().required().min(50).max(500),
    unit:    Schema.string().valid('lbs', 'kg').default('lbs'),
    notes:   Schema.string().max(500).default(''),
  });

  export function requireCustomer() {
    if (!is_customer) {
      response.status(401);
      response.json({ ok: false, error: 'not authenticated' });
    }
  }
</script>

2. Create the page /weight/checkin

<script scope="backend">
  import { WeightCheckinSchema, requireCustomer } from "validators";

  if (request.method === 'POST') {
    requireCustomer();

    var result = WeightCheckinSchema.validate(request.body);

    if (!result.valid) {
      response.status(400);
      response.json({ ok: false, errors: result.errors });
    }

    var data = result.value;

    var entitySlug = 'pattern-tracker'; // your CRM entity slug from settings
    var fields = [
      { fieldKey: 'current_weight', value: data.weight },
      { fieldKey: 'weight_unit', value: data.unit },
      { fieldKey: 'last_checkin', value: new Date().toISOString() }
    ];

    if (data.notes) {
      fields.push({ fieldKey: 'checkin_notes', value: data.notes });
    }

    setCrmFields({ slug: entitySlug, fields: fields });

    response.json({ ok: true, data: data });
  }

  setVariable('page_title', 'Weight Check-in');
</script>

<div>
  <h1>{{ var.page_title }}</h1>
  <form id="checkin-form">
    <input type="number" id="weight" placeholder="Current weight" step="0.1" required />
    <select id="unit">
      <option value="lbs">lbs</option>
      <option value="kg">kg</option>
    </select>
    <textarea id="notes" placeholder="Notes (optional)"></textarea>
    <button type="submit">Log Weight</button>
  </form>
  <div id="result"></div>
</div>

<script>
document.getElementById('checkin-form').addEventListener('submit', async function(e) {
  e.preventDefault();

  var res = await fetch('/weight/checkin', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      weight: parseFloat(document.getElementById('weight').value),
      unit: document.getElementById('unit').value,
      notes: document.getElementById('notes').value
    })
  });

  var data = await res.json();
  document.getElementById('result').textContent = data.ok
    ? 'Logged: ' + data.data.weight + ' ' + data.data.unit
    : 'Error: ' + (data.errors || [data.error]).join(', ');
});
</script>

3. Read the data back on another page

On any page (e.g. /dashboard), read the CRM data:
<script scope="backend">
  import { requireCustomer } from "validators";
  requireCustomer();

  var entitySlug = 'pattern-tracker';
  var fields = getCrmFields({ slug: entitySlug, fieldKeys: ['current_weight', 'goal_weight'] });
  var weight = fields.current_weight;
  var goal   = fields.goal_weight;
  setVariable('current_weight', weight || 'Not logged yet');
  setVariable('goal_weight', goal || 'Not set');
  setVariable('on_track', weight && goal && Number(weight) <= Number(goal));
</script>

<h2>Current weight: {{ var.current_weight }}</h2>
<h2>Goal: {{ var.goal_weight }}</h2>
<p v-if="var.on_track">You've reached your goal!</p>

How it fits together

Frontend (browser)             Backend (sandbox)
─────────────────              ─────────────────
fetch('/weight/checkin', {     → request.method = 'POST'
  method: 'POST',               request.body = { weight: 182, ... }
  body: JSON.stringify(data)
})                             → Schema.validate(request.body)
                              → setCrmFields({ slug: 'pattern-tracker', fields: [...] })
← { ok: true, data: {...} }   ← response.json({ ok: true, data })

CRM functions reference

FunctionDescription
getCrmEntries({ slug, ... })Fetch entries — slug required (Data Functions)
getCrmField({ slug, fieldKey, ... })Get one field
getCrmFields({ slug, fieldKeys, ... })Get multiple fields at once
setCrmField({ slug, fieldKey, value, ... })Set one field
setCrmFields({ slug, fields, ... })Set multiple fields in one call
moveCrmToStage({ slug, stageId | stageSlug, ... })Move an entry to a specific stage
moveCrmToPipeline({ slug, pipelineId | pipelineSlug, stageId | stageSlug?, ... })Move an entry into a pipeline
moveCrmToEntity({ fromSlug, toSlug, fieldMap, ... })Move mapped fields to another CRM entity
moveCrmLead({ slug, pipelineId | pipelineSlug, stageId | stageSlug, ... })Direct workflow move (compatibility helper)
reassignCrmReference({ slug, fromReferenceType, fromReferenceId, ... })Move entries from a visitor reference to the logged-in customer (Data Functions — CRM)

slug (CRM entity) vs reference

slug — required on every call. This is the CRM entity slug from entity settings (e.g. pattern-tracker). It is not the same as referenceType (customer, order, …). referenceType / referenceId — which record entries attach to. Omit or null for 'customer' and the session customer_id.
setCrmField({ slug: 'pattern-tracker', fieldKey: 'score', value: 85 });

setCrmField({
  slug: 'pattern-tracker',
  referenceType: 'order',
  referenceId: '12345',
  fieldKey: 'followup',
  value: 'done',
});
Calls must use a single options object (no positional lists).

Parameter types

PropertyAllowed types / notes
slugentity slug string — required; must exist for the brand
referenceTypestring (alphanumeric, -, _) or omit / null
referenceIdstring (max 512 chars) or omit / null
fieldKeystring (alphanumeric, _, ., -) — required
valuestring, number, boolean, or JSON object — required for set (0 / false valid)
pipelineId / stageIdinteger id for workflow move targets
pipelineSlug / stageSlugslug string for workflow move targets

Moving through CRM workflow

moveCrmToStage and moveCrmToPipeline are the workflow transition functions. You can move:
  • Stage to stage in the same pipeline
  • Pipeline to pipeline (by setting a stage that belongs to the new pipeline)
moveCrmToStage({
  slug: 'pattern-tracker',
  stageSlug: 'qualified'
});

moveCrmToPipeline({
  slug: 'pattern-tracker',
  pipelineSlug: 'sales',
  stageSlug: 'proposal-sent'
});

moveCrmToPipeline({
  slug: 'pattern-tracker',
  pipelineSlug: 'sales',
  stageSlug: 'proposal-sent',
  async: true
});
These only change workflow placement (pipeline_id, stage_id). They do not map one field key into another. For field mapping (for example quiz_email -> email), do it explicitly with reads/writes:
var quizEmail = getCrmField({ slug: 'pattern-tracker', fieldKey: 'quiz_email' });
if (quizEmail) {
  setCrmField({ slug: 'pattern-tracker', fieldKey: 'email', value: quizEmail, force: true });
}
Or move across entities in one call:
moveCrmToEntity({
  fromSlug: 'quiz-tracker',
  toSlug: 'sales-leads',
  fieldMap: {
    quiz_email: 'email',
    quiz_score: 'lead_score'
  },
  pipelineSlug: 'sales',
  stageSlug: 'new',
  removeFromOld: true
});

moveCrmToEntity({
  fromSlug: 'quiz-tracker',
  toSlug: 'sales-leads',
  fieldMap: { quiz_email: 'email' },
  async: true
});

Input validation

All inputs are strictly validated before data is stored:
  • slug must match a CRM entity configured for your brand; unknown slug → TypeError
  • fieldKey must match /^[a-zA-Z0-9_.-]+$/
  • referenceType must match /^[a-zA-Z0-9_-]+$/
  • referenceId rejects {}[]"\ characters
  • String values are capped at 10,000 characters
  • Numbers must be finite (no NaN, Infinity)
  • Objects are JSON-serialized and capped at 10 KB
  • Missing slug, fieldKey, or value (set) throws TypeError
  • Other invalid inputs still cause false / null / [] where applicable

Quiz / anonymous visitor → customer CRM migration

A common pattern: save a quiz or funnel payload with setCrmField({ referenceType: 'quiz_visitor', referenceId: sessionUuid, ... }) while the visitor is anonymous, then after members login call reassignCrmReference so the same CRM entries attach to customer + customer_id. fromReferenceId must match session.get('quiz_visitor_id') and the target must be the current session customer — see ReassignCrmReference on the Data Functions page. Clear quiz_visitor_id from the session after a successful migration to keep the flow idempotent.

Security checklist

Always validate request.body in your backend script. The body comes from the visitor’s browser and can contain anything.
  1. Validate with Schema — use the built-in Schema validator to enforce types, lengths, and allowed values
  2. Check authentication — verify is_customer before storing data
  3. Use response.json() — this stops page rendering and returns JSON directly
  4. Use response.status() — set proper HTTP status codes (400 for validation errors, 401 for auth)
  5. Share validation logic — create a backend script module for reusable schemas and auth checks

POST body limits

ConstraintValue
Max body size64 KB (larger payloads are dropped)
Content typeapplication/json only
Body available asrequest.body (parsed object or null)