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

# Event pipelines

> See and inspect the deterministic flows that turn inbound events — lead-form submissions, third-party webhooks — into enriched rows, emails, and integration actions.

# Event pipelines

An **event pipeline** is a deterministic flow that receives an inbound event and does fixed work
with it: a lead-form submission, a third-party webhook, or an in-app action arrives at the
pipeline's endpoint, gets normalized, and runs through an ordered list of **actions** — write the
row to a [dataset](/data), send an email, call a connected integration, broadcast a live update,
or return a custom response.

Pipelines are how a [page](/pages) captures leads, and how Erdo enriches and routes them without an
agent in the loop on every event. Each pipeline has:

* a **slug** and **name** — a stable, human-readable handle (e.g. `acme-leads-a1b2c3d4`),
* a **state** — `active`, `disabled`, or `proposed`,
* a **source kind** — `browser_form`, `third_party_webhook`, `custom_http`, `internal`, …,
* an **inbound endpoint URL** — where events are POSTed,
* an **auth mode** — how inbound events are verified (HMAC, shared secret, provider signature,
  signed-in viewer, or public),
* the **actions** it runs and the **datasets it writes to**, and
* an **execution log** — every event it received, with status (`accepted` / `rejected` /
  `failed`), a payload preview, the response, and any error.

## Where data lands

A pipeline's `dataset.write` actions persist rows to a **dataset** — per-row, SQL-queryable
storage. To see captured leads, query the dataset the pipeline writes to (shown as
`write_target_datasets`), not the pipeline itself. The pipeline's **execution log** tells you
whether events are arriving and being accepted; the **dataset** holds the rows.

## Emails a pipeline sends

A pipeline that emails on an event — a lead alert to your sales inbox, a confirmation back to the
person who submitted the form — has Erdo compose the message. The pipeline supplies the content
(a heading, the captured fields as label/value rows, a button through to the page the lead came
from) and Erdo renders it in the standard Erdo styling, with a plain-text version alongside for
clients that block HTML. Addresses and links in the fields become clickable, so a lead alert can
be replied to from the notification itself.

## Pipeline purpose

Every pipeline declares a **`purpose`** — its role — so you can tell capture flows apart from
analytics infrastructure without guessing from names:

* **`record_capture`** — a pipeline whose job is persisting submitted records to a dataset (it
  has at least one `dataset.write` action). This is how you find which datasets receive a flow's
  records: filter pipelines by `purpose = record_capture` and read their `write_target_datasets`
  (each entry is a fully-qualified `org-slug.dataset-slug`). You can set this explicitly when
  creating or updating a pipeline; a `custom` pipeline that writes to a dataset is also promoted
  to `record_capture` automatically at save time.
* **`page_events`** — the managed first-party page-analytics pipeline described below. Reserved
  for Erdo's own provisioning; you can't set it.
* **`custom`** — everything else: a webhook or enrichment flow that doesn't persist records.

To discover which dataset a campaign's leads land in, list your `record_capture` pipelines and
read their `write_target_datasets`, then query that dataset.

Every `dataset.write` row is stamped at ingestion with columns your transform doesn't set:
`variant`, `artifact_id`, `page_url` (from the page envelope), plus a set of request signals that
let you keep your metrics clean and segment them by who actually showed up:

* **`is_test`** — a boolean, always present. `true` when the submission was tagged as test traffic
  (see [Marking test traffic](#marking-test-traffic) below), otherwise `false`. Filter it out to
  count real leads and conversions.
* **`device_type`** — `"mobile"`, `"tablet"`, `"desktop"`, or empty, classified from the request's
  `User-Agent` at ingestion. Segment funnels by it to compare mobile vs desktop (a page can take
  most of your spend yet convert zero mobile visitors — this column surfaces that without parsing
  User-Agents by hand).
* **`browser`** — the coarse browser family (`"chrome"`, `"safari"`, `"firefox"`, `"edge"`,
  `"samsung"`, `"opera"`, `"other"`, or empty), also from the `User-Agent`.
* **`in_app`** — the in-app webview container the visit came from (`"instagram"`, `"facebook"`,
  `"tiktok"`, and similar), or empty for a normal standalone browser. In-app webviews routinely
  break modals and payment flows, so this separates "converts badly on mobile" from "converts badly
  inside Instagram's browser".
* **`language`** — the visitor's preferred language, the primary tag of the `Accept-Language`
  header (e.g. `"pt-BR"`), or empty. Check that a Brazilian, Colombian, or French campaign is
  actually reaching PT/ES/FR speakers.
* **`country`**, **`region`**, **`city`** — the visitor's coarse location: ISO-2 country (e.g.
  `"US"`), ISO-3166-2 region code (e.g. `"NY"`), and city name, from the serving edge. Empty when
  the request reaches the pipeline through an ingress that carries no edge geo. Segment by them to
  verify geo-targeted campaigns (e.g. "New Yorkers relocating to Miami") landed where you paid for.

These are stamped only where your transform left the column unset, so a page that maps its own
column of the same name always wins. Each is **epoch-bound** — rows written before the column
existed carry it blank, which means "no signal", not a real value like desktop or zero visitors.

## Marking test traffic

QA checks and automated evaluations submit to the same pipelines your real visitors do, so their
rows would otherwise inflate lead and funnel counts. Tag a submission as test traffic and it lands
with `is_test = true`, keeping it out of your real-traffic metrics. Two equivalent markers:

* Send the request header **`X-Erdo-Test: 1`** (`1` or `true`), or
* Include a top-level **`"is_test": true`** field in the JSON body.

The envelope's marker always wins over the transform, so a transform can never clear a test mark.
The marker is trusted as-is: mismarking real traffic as test only hides it from your own dashboards,
so there's no signature to manage. Auto-provisioned **page-events** datasets ship with a default
filter that hides `is_test = true` rows, so their funnel counts exclude test traffic automatically;
add the same `is_test` `not_equals` `true` default filter to your own lead datasets to get the same
behaviour on reads.

## Auto-provisioned page-events pipelines

Alongside the pipelines you or your agents create, every published page gets one
**page-events pipeline** automatically (its `purpose` is `page_events` — see
[Pipeline purpose](#pipeline-purpose) above). It receives the page's first-party analytics events — pageviews, scroll
milestones, section views, CTA clicks, form starts, leads, video plays — and writes them to your
organization's shared **Page events** dataset, stamped with the page, experiment variant, session,
campaign attribution, `is_test`, `device_type`, `browser`, `in_app`, `language`, and geo
(`country`/`region`/`city`). Events with names outside that fixed vocabulary are rejected, which you
can see in the pipeline's execution log. Disabling a page-events pipeline turns first-party
analytics off for that page; Erdo won't re-create it. See
[Pages → First-party page events](/pages#first-party-page-events) for what's captured and how to
query it.

## Inspecting your pipelines

You can list your pipelines, read one's configuration, and review its run history over **MCP**,
the **REST API**, or the **CLI**. Everything is **org-scoped** — a caller only ever sees their own
organization's pipelines — and pipelines are referenced by **slug** (a UUID is also accepted).

### CLI

```bash theme={null}
# list pipelines in the active org
erdo event-pipelines list

# inspect one — state, source, endpoint URL, actions, write targets
erdo event-pipelines get acme-leads-a1b2c3d4

# review run history (newest first) — is it firing? are events accepted?
erdo event-pipelines executions acme-leads-a1b2c3d4 --limit 50
```

The CLI authenticates with your Erdo token and the active organization (`erdo login`, `erdo org`).

### MCP tools

| Tool                                  | What it does                                                                              |
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
| `erdo_list_event_pipelines`           | List your org's event pipelines — slug, name, state, source, endpoint URL, write targets. |
| `erdo_get_event_pipeline`             | One pipeline by slug — full config: auth mode, transform, actions, datasets it writes to. |
| `erdo_list_event_pipeline_executions` | Recent run history (newest first) — per-event status, payload preview, response, error.   |

Identity and RBAC ride the request context, so a caller can only reach their own org's pipelines.

### REST

Base URL `https://api.erdo.ai`. Authenticate with `Authorization: Bearer <token>` and select the
org with `X-Organization-ID`.

| Method | Path                                   |
| ------ | -------------------------------------- |
| `GET`  | `/v1/event-pipelines`                  |
| `GET`  | `/v1/event-pipelines/:slug`            |
| `GET`  | `/v1/event-pipelines/:slug/executions` |

These are read endpoints — pipelines are created and edited from the page that owns them. To
confirm a pipeline is capturing events end-to-end, check its executions here, then query the
dataset it writes to.
