Docs/Getting started

For growth engineers

Build AI
growth funnels.

connect >

Your whole funnel becomes one live graph your agent runs from your editor: query conversion, trace any journey, wire automations. It keeps itself current from what actually happens, first-party on your domain, ad click to renewal.

A knowledge graph, not tablesEvery event queryable over MCPStages and scores derive themselvesclick_id survives hosted formsNamed events, webhooks anywhereFirst-party, on your own domain

How it works / one lead, end to end

The whole journey,
held for you.

One person, from the ad click to the signed deal. Tracking catches what they do; your AI staff work the thread around them, before and after the one moment that needs a human. Nobody updates a record. It all just happens.

lead · sarah k.themyour AIyou
tue 14:02clickfb-cold · click_id fnl_8h2k
tue 14:02pageview/webinar · then /pricing, twice
tue 14:06form_fillwebinar-intake · fnl_8h2k carried through the hosted form
tue 14:06agentlead created from the click trail · scored · #sales pinged
tue 14:31bookingstrategy-call · thu 2:00 pm
tue 14:31agentconfirmation sent · reminders queued · no-show watch armed
thu 13:45agentyour brief: who they are, the pages they read, what they asked on the form
thu 14:00meetingstrategy-call held · transcript captured
thu 14:41agentrecap drafted for your approval · stage moved itself: lead → proposal
fri 09:00agentno reply yet · follow-up queued · still watching
lead → proposal · every line above, one query away

And you can ask it anything

funal

you > which utm_source has the best click → booked rate this month?

utm_source      clicks   leads   booked   click→booked
newsletter         412      63       38         9.2%
google-brand     1,204     118       34         2.8%
fb-cold          2,987     201       41         1.4%

newsletter converts 6.6x better than fb-cold. Want me to trace where fb-cold's lander is losing people, or draft a budget shift?

Concepts / the data model

A graph,
not tables.

Tracking doesn't fill rows. It builds a knowledge graph, and every query on this page reads it. Three ideas make the rest legible.

  person · sarah k.
    ├─ click ────── fb-cold
    ├─ pageview ─── /webinar
    ├─ form_fill ── webinar-intake
    ├─ booking ──── strategy-call
    └─ email ────── recap

one node, its edges, the whole journey

01

Everything hangs off a person

Clicks, pageviews, form fills, emails, calls, bookings, payments. Not scattered across tables you join, but hung on the human they belong to. One record carries the whole story.

02

Relationships are first-class

The edge from a lead to the ad click that made them, from a client to their family, from a booking to its source, is a real object you walk. Not a foreign key you remember to set.

03

One hop from the answer

From a booked call, the ad click that caused it is a single step away. That is why "which ad produced clients" is a query, not a data project.

Step 01 / Tracking

Instrument the
whole journey.

page, track, identify: the Segment spec you already know, first-party on your own domain. One snippet instruments the whole journey, ad click to booked call to renewal, and stitches every anonymous session to the person behind it.

click_id and link_variant ride through redirects, hosted form tools, and cross-domain hops, so conversion by variant, or by anything you emit, is one query. First-party, invisible, never blocks a paint.

One line in your <head>

<script async src="https://go.funal.link/t.js" data-write-key="YOUR_WRITE_KEY"></script>

Paste into your site's global <head>, on every page. Your write key (starts with fnl_wk_) namespaces events to your org: any agent connected in step two can fetch it for you, or ask the Funal team. Wix: Settings > Custom Code, apply to all pages, load in head. Webflow: Site Settings > Custom Code > Head Code. Async and runs once, so it's safe to ship anywhere.

Or hand it to your agent

The full setup prompt covers the platform quirks, the hidden-field carriage through hosted form tools, and the checks that prove events are landing.

Setup prompt / tracking
# Setup prompt: install the Funal site tag

You are installing the Funal site tag on a business's marketing website. The
tag connects the site to Funal's ad-click attribution and journey tracking:
the ad click, page views, form submissions, and the eventual booking join
into one identity.

## What you need from the human before starting

1. Their Funal write key. It starts with fnl_wk_. If YOU have Funal's MCP
   server connected, fetch it yourself with the track_write_key tool (it
   also returns the exact snippet). Otherwise the human gets it from the
   Funal team. Never invent one.
2. Which website platform they use (Wix, Webflow, or other).
3. If their forms live on a hosted form tool with its own domain (FormCrafts,
   Typeform, etc.): which one.

## Install

Add this to the site's global <head>, on ALL pages, with their real write
key in data-write-key:

    <script async src="https://go.funal.link/t.js" data-write-key="THEIR_WRITE_KEY"></script>

The write key namespaces every event to their organization. It is public by
design (like a Segment write key) but revocable: if it ever leaks or gets
spammed, rotate it (track_write_key with rotate:true) and update the
snippet. Events whose click attribution proves a different org are dropped
by the server, so a wrong key means silently lost data: copy it exactly.

go.funal.link is Funal's shared link domain and works for every org. If the
human has a custom link domain configured in Funal (looks like
go.their-brand.com), swap it in for go.funal.link here and in every URL
below; otherwise do not ask, just use the default. Keep data-write-key
either way.

- Wix: Settings -> Custom Code -> Add Custom Code -> paste, apply to All
  Pages, load in Head.
- Webflow: Site Settings -> Custom Code -> Head Code -> paste -> Save and
  publish.
- If their call-to-action links out to a hosted form tool, add the tool's
  domain to the tag: data-decorate="formcrafts.com" (comma-separate several).

## If they use a hosted form tool

In the form builder, add two hidden fields named exactly click_id and
link_variant, each prefilled from the URL parameter of the same name. The
tag decorates the CTA link so those parameters arrive on the form URL; the
hidden fields carry them through the form's own webhooks back to Funal.

If the form tool redirects back to the site after submit with the
visitor's email as a query parameter (FormCrafts thank-you pages often
do), add data-identify-param="email" (the actual parameter name) to the
script tag. The tag then hashes that one parameter's value in the browser
(SHA-256; the raw email is never sent) and ties the visitor's session to
their lead. Only the named parameter is ever read, and only when this
attribute is present.

## Optional JavaScript API

The tag exposes window.funal for pages that know more than the tag can
observe. Add the queue line so calls made before the tag loads still work:

    <script>window.funal=window.funal||function(){(funal.q=funal.q||[]).push(arguments)}</script>

    funal('page')                                  // SPA route change
    funal('track', 'quiz_completed', { score: 12 }) // custom moment
    funal('identify', { email: userEmail })         // hashed in-browser

track names are stored namespaced (custom:quiz_completed). identify never
sends the raw email — it is hashed before any network call. Most installs
need none of this; the tag's automatic behavior covers normal sites.

## Verify (do not skip)

1. Open any page of the site with ?click_id=brief-test-123&link_variant=a
   appended to the URL.
2. In the browser console, confirm localStorage has _fnl_cid =
   "brief-test-123" and _fnl_anon = "c_brief-test-123".
3. Confirm the tag loaded: fetch https://go.funal.link/t.js and expect
   HTTP 200 with JavaScript.
4. Confirm the beacon sink accepts and namespaces: POST to
   https://go.funal.link/t with body
   {"writeKey":"THEIR_WRITE_KEY","events":[{"name":"page_viewed","anonymous_id":"a_brief_test"}]}
   and expect HTTP 202 with {"accepted":1,"dropped":0}. accepted:0 almost
   always means the write key is wrong or was rotated; re-check it.
5. If forms are native to the site: submit a test entry and confirm the
   submission contains hidden click_id / link_variant / anonymous_id fields.
6. Report each check's result to the human, then delete the test entry if
   one was created.

The tag is safe to install twice (it runs once) and never blocks or alters
the site; a failed network or blocked script is a silent no-op.

Reference / the tracking SDK

Three verbs,
nothing else.

With zero calls the tag already tracks pageviews, outbound clicks, and form submits, captures click_id, and decorates your links. When your own page code knows something the tag can't see, it speaks in three verbs.

Install with the command queue

<script async src="https://go.funal.link/t.js" data-write-key="fnl_wk_..."></script> <script>window.funal=window.funal||function(){(funal.q=funal.q||[]).push(arguments)}</script>

The second line is the command queue: calls made before the script loads accumulate and drain on arrival. No load-order bugs, no ready callbacks. window.funal is callable forever, even if the script never loads.

funal('page')report a pageview

Ambient on every document load, so multi-page sites never call it. Single-page apps call it on route change.

funal('track', name, properties?)a moment the tag cannot see

A quiz finished, a calculator used, a video watched. Stored namespaced as custom:{name}, so page code can never forge a reserved event. Properties are flat JSON.

funal('identify', traits)identity the page has learned

Pass an email; the SDK lowercases and hashes it in the browser with SHA-256 before anything leaves. The raw value never touches the wire. Stitches this browser’s anonymous trail to the known person.

Identity is asserted, never inferred

The tag never harvests emails from URLs on its own. To trust a redirect param from a hosted form tool, name it once: data-identify-param="email". Then it hashes client-side and fires identify.

Failure is silence

Every call is wrapped. A hostile DOM, a full localStorage, a blocked beacon: all no-ops. The tag can never break your page, and it never blocks a paint.

Step 02 / Connect MCP

Your funnel,
queryable.

Tracking doesn't fill tables. It builds a knowledge graph: clicks, pageviews, form fills, emails, calls, and bookings hang off the people they belong to, and relationships are first-class edges. One hop from a booked call sits the ad click that caused it.

One MCP endpoint hands that graph to Claude Code, Cursor, or Codex: search it, walk a lead's neighborhood, pull funnel analytics, wire automations, scoped to your org. Pull conversion numbers or trace a journey without leaving your editor.

Same move as step one: copy the setup prompt, paste it into the tool you want connected. It configures itself and proves the connection with a live call. You'll need your Funal API key from onboarding.

Setup prompt / MCP
# Setup prompt: connect Funal MCP to this coding agent

You are connecting Funal's MCP server to the agent environment you are
running in (Claude Code, Cursor, Codex, or another MCP client). Funal
exposes the business's working graph (records, search, analytics,
forms, workflows) as MCP tools, scoped to the business's organization.

## What you need from the human before starting

1. Their Funal API key. It is issued during onboarding; if they do not have
   one, they should ask the Funal team. Treat it like a password.
2. Which agent they run: Claude Code, Cursor, Codex, or something else.

## Connect

Endpoint (Streamable HTTP): https://funal-api.alex-boquis.workers.dev/mcp
Auth: an "Authorization: Bearer <API key>" header on every request.

- Claude Code:

      claude mcp add --transport http funal \
        https://funal-api.alex-boquis.workers.dev/mcp \
        --header "Authorization: Bearer THEIR_KEY"

- Cursor: add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):

      { "mcpServers": { "funal": {
        "url": "https://funal-api.alex-boquis.workers.dev/mcp",
        "headers": { "Authorization": "Bearer THEIR_KEY" } } } }

- Codex: add to ~/.codex/config.toml (Codex speaks stdio, so bridge the
  HTTP endpoint with mcp-remote):

      [mcp_servers.funal]
      command = "npx"
      args = ["-y", "mcp-remote",
        "https://funal-api.alex-boquis.workers.dev/mcp",
        "--header", "Authorization: Bearer THEIR_KEY"]

- Any other client without native Streamable HTTP support: use the same
  mcp-remote bridge as Codex.

## Verify (do not skip)

1. Restart or reload the agent so it picks up the new server.
2. Confirm the tool list includes atoms_list, atoms_search, and search.
3. Call atoms_count with kind "client". Any number, even 0, proves auth
   and org scoping are live. A 401 means the key is wrong or revoked; stop
   and tell the human.
4. Report the connected tool count and the atoms_count result to the human.

Never echo the API key back into chat output, logs, or committed files;
config files holding it (~/.cursor/mcp.json, ~/.codex/config.toml) should
stay out of version control.

Reference / tool surface

Everything your
agent can reach.

One endpoint, one API key, scoped to your org. The same guarded, validated tools Funal's own AI staff use. The full list loads the moment you connect.

Records & graph

Search, read, and write the whole book of business. Walk a lead’s neighborhood edge by edge.

atoms_searchatoms_getgraph_neighborhoodedges_listsearch

Analytics & funnels

Conversion, cohorts, and cause and effect down the chain. Trace exactly where a funnel leaks.

analytics_queryfunnels_comparefunnels_tracefunnels_segment

Links

Tracked links you own, split variants, attribution that survives every hop.

links_createlinks_listlinks_update

Forms

Author guided intake, preview it, activate it. The form is your funnel’s front door.

form_upsertform_previewform_activate

Scheduling

Availability, fresh booking links, booking-specific manage links, and no-show recovery, all attached to the source.

scheduling_linkscheduling_manage_linkscheduling_availabilityscheduling_book

Automations

Durable agent workflows — resolvers, one judgment pass, idempotent effects, self-checks — on a cron, an event, or on demand.

agent_workflows_upsertagent_workflows_runagent_workflows_runs

Events & webhooks

A catalog of named business events. Subscribe your other tools; every delivery replays.

relay_catalogrelay_eventsrelay_deliveries

And more: client memory, e-sign templates, pages, calendar, voice. Every tool is validated before it writes, attributed to whoever ran it, and inspectable after.

Cookbook / ask your agent

Say it,
ship it.

16 prompts to paste at your agent once you're connected. Every one lands in the same graph tracking feeds, attributed the moment it exists.

tracking / custom event

you > track a custom event when someone finishes the pricing calculator

funal('track', 'calculator_done', { plan: 'pro' })
stored   custom:calculator_done

Now on their journey. Ask later: who used the calculator but never booked?

tracking / identify

you > identify a visitor by email after they log in

funal('identify', { email })
hashed   in-browser, sha-256

The raw email never leaves the page. This browser’s anonymous trail joins the known person.

links / variants

you > spin up two link variants for the fb campaign, split 50/50

go.funal.link/fb-webinar        live
  A  "book a call"    50%
  B  "get the plan"   50%

Both carry click_id end to end. Conversion by variant is one query, whenever.

links / swap

you > point my main ad link at the new lander without touching the ad

go.funal.link/q4-ad  →  /lander-v2
  swapped   0 ad edits, attribution intact

The ad platform never resets its learning. You moved the destination, not the link.

funnels / ab test

you > run deposit-required against no-deposit and call the winner

funnel intake-ab             live
  A  deposit     12 booked / 41
  B  no-deposit  15 booked / 44

Too early to call: roughly 38 more leads to significance. I’ll ping you when it’s decided.

funnels / trace

you > trace where fb-cold leads drop off between click and booking

click 2,987  →  form 201  →  booked 41
  biggest leak   click to form, 93%

The lander is losing them before the form. Want me to diff it against newsletter’s?

funnels / what-if

you > what if we cut the intake from 8 fields to 4

whatif   fields 8 → 4
  modeled completion   +18%

Modeled on your real drop-off curve. A projection, not a promise. Ship it as a test?

forms / build

you > build a 5-question intake form for the webinar lander and give me the link

form webinar-intake          live
  5 fields · funal.ai/f/webinar-intake

Wired to tracking: every submission joins the click that brought it.

forms / route

you > add a qualifying question that routes low-fit leads to a nurture page

form webinar-intake          updated
  budget < $2k  →  /nurture

High-fit leads still hit book-now. The form behaves like your best intake rep.

scheduling / window

you > give hot leads a booking link that only offers this week

go.funal.link/strategy-call  live
  window   mon-fri, 30 min holds

Bookings land in the graph attached to their source. No Calendly, no sync.

scheduling / protect

you > protect my 9-11am slots for leads scored above 80

target prime-am
  reserved   score 80+, else after 11

Your best hours go to your best leads, automatically.

automations / on booking

you > when someone books, post to #sales and email a confirmation

workflow booking-alert       live
  trigger  booking.created
  steps    slack #sales → email

Runs on every booking, logs every run. Change it by asking.

automations / daily brief

you > every morning, draft me a brief on leads that went quiet

routine quiet-leads          8:00 am
  reads memory + last touch → brief

Lands in your inbox before you’re at your desk. A draft, for your approval.

automations / webhook

you > send every booking to our ops webhook

subscription  booking.*  →  ops
  every delivery   logged, replayable

Named events, one catalog. Your other tools subscribe; nothing is a mystery.

analysis / real roi

you > which ad produced actual clients, not just clicks

source         clicks   clients
  google-brand   1,204      11
  fb-cold        2,987       3

Only the system that holds the bookings and payments can answer this. Clicks lie; clients don’t.

analysis / no-shows

you > show me leads who booked but never showed, grouped by source

no-shows this month   22
  fb-cold 14 · google 5 · ref 3

fb-cold no-shows 3x the rest. Want the confirmation flow tightened for that source?

Ship / verify it works

Prove it,
then ship.

Two setups, two proofs. Don't trust either until you've seen it land.

Tracking is live

  • Load your site and watch the network tab: a POST to /t returns 202.
  • Or just ask your agent: did events land for my write key in the last hour?
  • Submit a test lead and confirm click_id rode through to the record, hosted form and all.

The connection is live

  • The tool list now includes atoms_search, funnels_compare, and search.
  • atoms_count with kind "client" returns a number, even 0. A 401 means the key is wrong or revoked.

Three ways it bites, and the fix

A wrong write key loses data silently

Events whose click proves a different org are dropped by the server, with no error. Copy the key exactly.

Hosted form tools need decoration

Add the tool’s domain with data-decorate and let the tag inject the hidden click_id field, or the journey breaks at the form hop.

Custom domain, everywhere or nowhere

If go.yourbrand.com is configured, swap it into the snippet and every URL below. Otherwise use go.funal.link. Never mix the two.