For growth engineers
Build AI
growth funnels.
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.
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.
And you can ask it anything
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 ────── recapone node, its edges, the whole journey
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.
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.
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: 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 pageviewAmbient 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 seeA 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 learnedPass 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: 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.
Analytics & funnels
Conversion, cohorts, and cause and effect down the chain. Trace exactly where a funnel leaks.
Links
Tracked links you own, split variants, attribution that survives every hop.
Forms
Author guided intake, preview it, activate it. The form is your funnel’s front door.
Scheduling
Availability, fresh booking links, booking-specific manage links, and no-show recovery, all attached to the source.
Automations
Durable agent workflows — resolvers, one judgment pass, idempotent effects, self-checks — on a cron, an event, or on demand.
Events & webhooks
A catalog of named business events. Subscribe your other tools; every delivery replays.
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.
you > track a custom event when someone finishes the pricing calculator
funal('track', 'calculator_done', { plan: 'pro' })
stored custom:calculator_doneNow on their journey. Ask later: who used the calculator but never booked?
you > identify a visitor by email after they log in
funal('identify', { email })
hashed in-browser, sha-256The raw email never leaves the page. This browser’s anonymous trail joins the known person.
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.
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.
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.
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?
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.