--- name: clickfunnels-funnels description: > Build and modify funnel workflows programmatically - split tests, conditional splits, branch positioning, multi-page branches, and stored-filter wiring. Use this skill when you need to add A/B testing or audience-based routing to a funnel via the public API. version: "1.0" author: ClickFunnels tools: - http references: - path: https://accounts.myclickfunnels.com/llms.txt description: Quick-reference index - product overview, login, funnels, broadcasts, API docs. - path: https://accounts.myclickfunnels.com/.well-known/pages/skill.md description: Companion skill for building/editing pages programmatically via the Page Markup API. - path: https://accounts.myclickfunnels.com/.well-known/page-markup/skill.md description: PML reference - the DSL used to author the `markup` field on pages and blog posts. - path: https://accounts.myclickfunnels.com/.well-known/refine-filters/skill.md description: Companion skill for creating reusable Refine filters consumed by conditional splits and broadcasts. --- # ClickFunnels Funnels Skill A discovery hub for the funnel workflow APIs - split tests and conditional splits - and how they wire together with stored filters and pages. ## When to use this skill - Wrap an existing page in an A/B split test (variant page swap, weight tuning). - Insert a conditional split that routes contacts down a "matched" or "unmatched" branch based on a stored filter. - Grow a branch with multiple sequential pages (Welcome -> Onboarding -> CTA). - Position a split between specific steps of a funnel using `show_page_step_id`. - Read back the resulting tree (with the filter inline via `expand[]=filter`). ## Authentication OAuth2 password grant using an OAuth application's credentials. See the [Refine Filters Skill](https://accounts.myclickfunnels.com/.well-known/refine-filters/skill.md#authentication) for details - the same access token is used here. Reads (including `/structure`) and writes are available to any token authorized for the workspace. ## Endpoint reference | Action | Method | Path | |---------------------------|--------|---------------------------------------------------------------------| | List split tests | GET | `/api/v2/funnels/:funnel_id/split_test_steps` | | Create split test | POST | `/api/v2/funnels/:funnel_id/split_test_steps` | | Show split test | GET | `/api/v2/funnels/:funnel_id/split_test_steps/:id` | | Update split test | PATCH | `/api/v2/funnels/:funnel_id/split_test_steps/:id` | | Destroy split test | DELETE | `/api/v2/funnels/:funnel_id/split_test_steps/:id?branch=...` | | List conditional splits | GET | `/api/v2/funnels/:funnel_id/conditional_split_steps` | | Create conditional split | POST | `/api/v2/funnels/:funnel_id/conditional_split_steps` | | Show conditional split | GET | `/api/v2/funnels/:funnel_id/conditional_split_steps/:id` | | Update conditional split | PATCH | `/api/v2/funnels/:funnel_id/conditional_split_steps/:id` | | Destroy conditional split | DELETE | `/api/v2/funnels/:funnel_id/conditional_split_steps/:id?branch=...` | ## Split test steps A split test step wraps an existing page in a funnel into a 2-branch A/B test. Each variant carries the `show_page_step` + `page` attached to that branch (both `null` when the branch is empty), plus the branch's `weight` (0..100; the two weights must sum to 100). The first variant's `page_id` must already be attached to a step in this funnel's workflow (that page gets wrapped in place - no cloning). The second variant's `page_id` must be a page with **no existing show-page step** - an internal page not yet attached to any funnel, or a standalone external (SDK) page (see [Placing an external (SDK) page in a branch](#placing-an-external-sdk-page-in-a-branch)). Variant entries accept only `page_id` and `weight`; any other key is a 422. ### Positioning the split By default (no `show_page_step_id`), the split lands at the funnel entry - `sort_order 0` under the workflow root - with all surviving steps moved into a `Root Step after Sequence End` convergence sequence positioned right after the split. When `show_page_step_id` references an existing show-page step in this funnel, the split is inserted **after the workflow step that wraps it**, and steps N+1...end are migrated under the convergence sequence. `show_page_step_id` accepts a numeric or obfuscated `Workflows::Steps::ShowPageStep` id - pass `show_page_step.id` (or `show_page_step.public_id`) from any page response. **Positioning caution (applies to split tests AND conditional splits):** creating a split MOVES the wrapped step inside a branch sequence, so a step id you fetched earlier may no longer sit where you think. Read [Funnel Structure](#funnel-structure) immediately before creating a split to pick the anchor from the current tree, and re-read it immediately after to confirm placement. Anchoring on a step that sits inside another split's branch nests the new split inside that branch - if you want a split to apply to ALL traffic after an A/B test, create it before the A/B test (or anchor it on a step outside the branches), not on a page that is already a variant. ### Example - wrap step N+1 as the test variant ```http POST /api/v2/funnels/123/split_test_steps Authorization: Bearer Content-Type: application/json { "split_test_step": { "show_page_step_id": "aBcDeF", "variants": [ {"page_id": 500, "weight": 60}, {"page_id": 501, "weight": 40} ] } } ``` The response is the new `SplitTestStep` resource with both variants resolved. ### Destroying a split test ```http DELETE /api/v2/funnels/123/split_test_steps/9876?branch=left DELETE /api/v2/funnels/123/split_test_steps/9876?branch=right DELETE /api/v2/funnels/123/split_test_steps/9876?branch=both ``` `branch` defaults to `both`. **A 1-branch split is not a supported state**, so removing either single branch (`left` or `right`) collapses the entire split: the surviving branch's pages are promoted up under the split's parent (so they remain attached to the funnel's main workflow) and the split test wrapper is destroyed. `branch=both` is the same end-state with both sides discarded. ## Conditional split steps A conditional split routes contacts down one of two branches based on whether they match a stored Refine filter: - `branches[0]` - **matched** branch (filter matches the contact) - `branches[1]` - **unmatched** branch (filter does not match) Branches can be empty, hold a single page, or grow into multi-page sequences (see below). ### Empty create ```http POST /api/v2/funnels/123/conditional_split_steps Authorization: Bearer Content-Type: application/json {} ``` Creates an empty unconfigured split that the caller can fill in via PATCH. ### Multi-page branches via `page_ids` ```http POST /api/v2/funnels/123/conditional_split_steps Authorization: Bearer Content-Type: application/json { "conditional_split_step": { "branches": [ {"page_ids": [600, 601, 602]}, {"page_ids": [603]} ], "show_page_step_id": 100 } } ``` `page_id` (single) and `page_ids` (array) are mutually exclusive per branch, and branch entries accept **only** those two keys - anything else (e.g. `external_url`, `show_page_step_id`) is a 422. Each referenced page must be a workspace-owned page with **no existing show-page step**: an internal page not yet attached to any funnel, or a standalone external (SDK) page. ### Placing an external (SDK) page in a branch Branch entries never take a URL - they take a page id. To route a branch to a page you host yourself: 1. Create a **standalone** external page (no `funnel` block) - see [External (SDK) Pages](https://accounts.myclickfunnels.com/.well-known/pages/skill.md#standalone-external-pages): ```http POST /api/v2/workspaces/42/pages/external Authorization: Bearer Content-Type: application/json {"page": {"name": "External OTO", "external_url": "https://acme.example.com/oto"}} ``` 2. Pass the returned page id in the branch (or split-test variant) slot: ```http POST /api/v2/funnels/123/conditional_split_steps Authorization: Bearer Content-Type: application/json { "conditional_split_step": { "branches": [{}, {"page_id": ""}] } } ``` This works the same for a split test's second variant (`variants[1].page_id`). The same rule applies to internal pages: create the page first (unattached to any funnel), then reference it by id. ### Attaching a filter Once a stored filter exists (see the [Refine Filters Skill](https://accounts.myclickfunnels.com/.well-known/refine-filters/skill.md#applying-filters-to-conditional-split-steps)), attach it via `condition.filter_id`: ```http PATCH /api/v2/funnels/123/conditional_split_steps/9876 Authorization: Bearer Content-Type: application/json { "conditional_split_step": { "condition": {"filter_id": "AbCdEf"} } } ``` `filter_id` is the public id of a stored filter in the same workspace. Raw numeric ids are rejected for cross-tenant safety. Filters that were created under the standard public API are subject to the [safe-condition whitelist](https://accounts.myclickfunnels.com/.well-known/refine-filters/skill.md#safe-condition-whitelist) - once persisted, they can be referenced freely from this surface (no extra gating on the consumer side). ### Reading back with the inline filter ```http GET /api/v2/funnels/123/conditional_split_steps/9876 GET /api/v2/funnels/123/conditional_split_steps?expand[]=filter ``` On the show endpoint, `condition.filter` is always included as the full `RefineFilter` object. On the list endpoint it is included only when `expand[]=filter` is passed. ### Destroying a conditional split ```http DELETE /api/v2/funnels/123/conditional_split_steps/9876?branch=matched DELETE /api/v2/funnels/123/conditional_split_steps/9876?branch=unmatched DELETE /api/v2/funnels/123/conditional_split_steps/9876?branch=both ``` `branch` defaults to `both`. **A 1-branch conditional split is not a supported state**, so removing either single branch (`matched` or `unmatched`) collapses the entire split: the surviving branch's pages are promoted up under the split's parent (so they remain attached to the funnel's main workflow) and the conditional split wrapper is destroyed. `branch=both` is the same end-state with both sides discarded. ## Funnel structure ```http GET /api/v2/funnels/:id/structure ``` Returns a lean, flattened tree of the funnel's **visible** steps - show-page, conditional-split, and split-test steps, in order. The workflow's internal container steps (the root container, split-branch containers, and the post-split convergence) are spliced out, so `steps` is a flat ordered list you can walk directly. Useful for rendering an outline, finding a step, or picking a `sort_order` before inserting a page. ! **FORBIDDEN - a funnel step's path is its own standalone slug, never the funnel path + the step slug.** The `url` returned per step is composed **automatically** by ClickFunnels from the funnel/step hierarchy - do not reconstruct or guess it. When you create the pages that become these steps (via the [Pages skill](https://accounts.myclickfunnels.com/.well-known/pages/skill.md#setting-a-pages-path-current_path)), each step's `current_path` must be its **own** slug only - never a parent path concatenated with the child's slug. e.g. for funnel path `mangia-mangia-catering`, set the step's `current_path` to its own path (`mangia-mangia-catering`), **never** the nested `mangia-mangia-catering/catering`. A worked example - `Entry -> Conditional Split (matched: VIP Welcome / unmatched: Guest Welcome) -> Split Test (Premium Offer A 60% / Premium Offer B 40%)`. Note the flattening: split children are inline under `branches`/`variants`, internal sequence containers are gone, `sort_order` is container-local (it resets inside each branch), and split nodes carry a `convergence_step_id` (the step where every path rejoins): ```json { "funnel": { "id": 98, "public_id": "mYLmMN", "name": "Coaching Upsell Funnel" }, "steps": [ { "id": 1178, "public_id": "jxGkbJ", "step_type": "show_page_step", "name": "Entry", "sort_order": 0, "show_page_step_id": "WJmQqJ", "page": { "id": 1413, "public_id": "joXkgY", "name": "Entry", "url": "https://acme.myclickfunnels.com/entry", "external": false, "sdk_token": null } }, { "id": 1184, "public_id": "JEDdre", "step_type": "conditional_split_step", "name": "Conditional Split Step", "sort_order": 1, "convergence_step_id": 1195, "branches": [ { "branch": "matched", "steps": [ { "id": 1189, "public_id": "vbaRZj", "step_type": "show_page_step", "name": "VIP Welcome", "sort_order": 0, "show_page_step_id": "neonkv", "page": { "id": 1418, "public_id": "YGBWDj", "name": "VIP Welcome", "url": "https://acme.myclickfunnels.com/vip-welcome", "external": false, "sdk_token": null } } ] }, { "branch": "unmatched", "steps": [ { "id": 1190, "public_id": "vyggrJ", "step_type": "show_page_step", "name": "Guest Welcome", "sort_order": 0, "show_page_step_id": "RJgLRj", "page": { "id": 1419, "public_id": "ewMRAj", "name": "Guest Welcome", "url": "https://acme.myclickfunnels.com/guest-welcome", "external": false, "sdk_token": null } } ] } ] }, { "id": 1195, "public_id": "vXkkrj", "step_type": "split_test_step", "name": "Split Test Step", "sort_order": 1, "convergence_step_id": null, "variants": [ { "weight": 60, "steps": [ { "id": 1191, "public_id": "vpzzzJ", "step_type": "show_page_step", "name": "Premium Offer A", "sort_order": 0, "show_page_step_id": "MJnnnJ", "page": { "id": 1422, "public_id": "JygoNY", "name": "Premium Offer A", "url": "https://acme.myclickfunnels.com/premium-offer-a", "external": false, "sdk_token": null } } ] }, { "weight": 40, "steps": [ { "id": 1193, "public_id": "jQwwqe", "step_type": "show_page_step", "name": "Premium Offer B", "sort_order": 0, "show_page_step_id": "QeOPbJ", "page": { "id": 1420, "public_id": "eVNQme", "name": "Premium Offer B", "url": "https://acme.myclickfunnels.com/premium-offer-b", "external": false, "sdk_token": null } } ] } ] } ] } ``` Common node fields: `id`, `public_id`, `step_type` (short token, e.g. `show_page_step`), `name`, `sort_order`. Fields by `step_type`: | `step_type` | Extra fields | |--------|--------------| | `show_page_step` | `show_page_step_id` (pass this to `funnel.show_page_step_id` on Create/Update Page to swap), `page: { id, public_id, name, url, external, sdk_token }` | | `conditional_split_step` | `convergence_step_id` (where the branches rejoin), `branches: [{ branch: "matched"\|"unmatched", steps: [...] }]` | | `split_test_step` | `convergence_step_id`, `variants: [{ weight, steps: [...] }]` | Branch and variant `steps` arrays are flattened the same way (no nested container wrappers). `page.external` and `page.sdk_token` are always present on show-page nodes; `sdk_token` is `null` unless `external: true`. ## Common errors | Error | Cause | Fix | |-------|-------|-----| | 422 "condition.filter_id must be a stable identifier, not a raw numeric id" | Sent the database PK | Use the filter's public id (e.g. `AbCdEf`). | | 422 "condition.filter_id not found in this workspace" | Filter belongs to another workspace | Use a filter created in this workspace. | | 400 "conditional_split_step.branches[i].page_id not found in this workspace" | Page id from another workspace | Use a page in this workspace. | | 422 "conditional_split_step.branches[i].page_id is already used by another funnel step" | Page already wrapped in a `ShowPageStep` | Use a fresh page. | | 400 "conditional_split_step.branches[i] must provide either page_id or page_ids, not both" | Mixed shapes per entry | Pick one. | | 404 "Not found" | Funnel / step id does not belong to this workspace's accessible scope | Verify the funnel id and access token. | | 401 "API key missing or invalid" | Missing, revoked, or wrong token | Re-authenticate. |