Email click to first in-app action conversion

Join UTM-tagged email opens to the in-app action they were meant to drive. Grade onboarding emails by behavior change, not click rate.

Published

difficulty intermediate · time to value 5 minutes

Start from this

Build a funnel from onboarding email opens (UTM-tagged) to the in-app action they were supposed to trigger. Per-template breakdown.

Why this matters

Lifecycle emails are graded by open rate, click rate, maybe unsubscribe rate. The metric that actually matters — “did they do the thing the email asked?” — requires joining your ESP data to your product data and almost no team does it.

Customer.io, Loops, Resend all give you “X people clicked.” None of them say “X people clicked AND THEN created their first project within 24 hours.” That join needs both sides in one database.

Agentry holds both: send every email open/click as an analytics event (one fetch from your ESP webhook), and join against your in-app events using distinct_id. The agent ships the funnel as a per-template scorecard.

What you get

  • A per-email-template funnel: sent → opened → clicked → in-app target action
  • Templates ranked by action-conversion rate (not click rate)
  • Surprise findings: the email everyone opens but no one acts on, vs the low-CTR email that drives 40% of activations

Walk through it

You

Build a funnel from onboarding email opens (UTM-tagged) to the in-app action they were supposed to trigger. Per-template breakdown.

Agent

Let me see what email events you’ve got and what target actions you’ve defined.

Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "SELECT properties.email_template AS template, count() AS sent FROM events WHERE event = 'email_sent' AND timestamp >= now() - INTERVAL 30 DAY GROUP BY template ORDER BY sent DESC"
{
  "rows": [
    { "template": "welcome_day0",       "sent": 4200 },
    { "template": "create_first_project_day1", "sent": 4180 },
    { "template": "invite_teammate_day3",      "sent": 4100 },
    { "template": "feature_overview_day5",     "sent": 4050 },
    { "template": "trial_ending_day12",        "sent": 3800 }
  ]
}
Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "WITH sends AS (SELECT distinct_id, properties.template AS template, timestamp AS sent_at FROM events WHERE event = 'email_sent' AND timestamp >= now() - INTERVAL 30 DAY), clicks AS (SELECT distinct_id, properties.template AS template, min(timestamp) AS clicked_at FROM events WHERE event = 'email_clicked' AND timestamp >= now() - INTERVAL 30 DAY GROUP BY distinct_id, template), actions AS (SELECT distinct_id, min(timestamp) AS acted_at FROM events WHERE event IN ('signup_completed','first_project_created','team_member_invited','integration_connected','subscription_started') AND timestamp >= now() - INTERVAL 30 DAY GROUP BY distinct_id) SELECT s.template, count() AS sent, countIf(c.clicked_at >= s.sent_at) AS clicked, countIf(a.acted_at >= s.sent_at AND a.acted_at <= s.sent_at + INTERVAL 72 HOUR) AS target_action FROM sends s LEFT JOIN clicks c ON c.distinct_id = s.distinct_id AND c.template = s.template LEFT JOIN actions a ON a.distinct_id = s.distinct_id GROUP BY s.template ORDER BY sent DESC"
{
  "templates": [
    { "name": "welcome_day0",
      "sent": 4200, "opened": 2940, "clicked": 1188, "target_action": 1080,
      "open_rate": 0.70, "ctr": 0.40, "action_rate": 0.92 },
    { "name": "create_first_project_day1",
      "sent": 4180, "opened": 2090, "clicked": 418,  "target_action": 209,
      "open_rate": 0.50, "ctr": 0.20, "action_rate": 0.50 },
    { "name": "invite_teammate_day3",
      "sent": 4100, "opened": 2050, "clicked": 615,  "target_action": 41,
      "open_rate": 0.50, "ctr": 0.30, "action_rate": 0.067 },
    { "name": "feature_overview_day5",
      "sent": 4050, "opened": 1620, "clicked": 162,  "target_action": 162,
      "open_rate": 0.40, "ctr": 0.10, "action_rate": 1.00 },
    { "name": "trial_ending_day12",
      "sent": 3800, "opened": 2660, "clicked": 1140, "target_action": 380,
      "open_rate": 0.70, "ctr": 0.30, "action_rate": 0.33 }
  ]
}
Agent

Two findings worth your attention:

  1. invite_teammate_day3 is the worst. 615 people click, only 41 actually invite anyone. The CTA is broken — likely the click sends them to the workspace home, not to the invite modal. Fix the deep link.
  2. feature_overview_day5 is the surprise. Only 10% CTR but 100% of clickers do the target action. The email is filtering out un-interested users; those who click are pre-qualified. Don’t fix the CTR, leverage it.
  3. trial_ending_day12 has good open/click but only 33% conversion to subscription. Pricing or friction at checkout. Worth a separate investigation.
# This email-to-action funnel is custom HogQL, not a built-in public-query
# blueprint. Keep it server-side in a protected view, or add a real query
# blueprint for this analysis before publishing a public-query URL.
Agent

Analysis saved in the protected handoff. The actionable list: fix invite_teammate_day3’s deep link first.

The output

Onboarding Email Funnel (30d) — graded by ACTION, not click

TEMPLATE                     SENT   OPEN   CTR    ACTION   ACTION-RATE
welcome_day0                4,200  70%   40%   1,080         92%   ★ excellent
create_first_project_day1   4,180  50%   20%     209         50%   ok
invite_teammate_day3        4,100  50%   30%      41        6.7%  ✗ FIX
feature_overview_day5       4,050  40%   10%     162        100%  ★ pre-qualified
trial_ending_day12          3,800  70%   30%     380         33%  ✗ pricing/friction

KEY INSIGHTS:
  invite_teammate_day3 — 615 clicks → 41 invites. Deep link is broken.
                         Probably routing to workspace home instead of invite modal.
  feature_overview_day5 — 10% CTR is fine; 100% action rate means the CTA
                         self-selects. Don't optimize for opens.
  trial_ending_day12   — Click-to-paid conversion is only 33%. Friction at
                         checkout, not in the email. Investigate separately.

Setting it up

You need email events flowing in from your ESP, with distinct_id matching the in-app one (= user email). Most ESPs (Customer.io, Loops, Resend, Postmark, Sendgrid) ship webhooks — pipe those into a tiny handler that re-emits as Agentry analytics events.

// Webhook handler — e.g. Customer.io or Resend
app.post("/webhooks/email", async (req) => {
  const evt = req.body;
  // evt.event = "sent" | "opened" | "clicked"
  await fetch(`https://api.agentry.sh/v1/analytics/`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.AGENTRY_PUBLIC_API_KEY}`,
      "Content-Type": "application/json",
      "User-Agent": "myapp-email-relay/1.0",  // REQUIRED — Cloudflare 403s default UAs
    },
    body: JSON.stringify({
      event: `email_${evt.event}`,  // → email_sent, email_opened, email_clicked
      distinct_id: evt.recipient,   // ← MUST match in-app distinct_id (= email)
      properties: {
        email_template: evt.template_name,
        utm_campaign:   evt.template_name,  // mirror for HogQL convenience
        link:           evt.link_url,       // for clicked events
      },
      timestamp: evt.timestamp,
    }),
  });
});

For in-app target_action events, you’re presumably already capturing first_project_created, team_member_invited, etc. for signup-funnel. The playbook just joins both sides on distinct_id within the time window.

Variations

  • “Same funnel but only for trial users on the Enterprise pricing path.”
  • “Compare onboarding email performance across two ESPs — we A/B’d Customer.io vs Loops last quarter.”
  • “For the worst-performing template, draft 3 subject-line / CTA variants based on what the winning ones do.”
  • “For each clicker who DIDN’T do the target action within 24h, send a follow-up. List them.”

Adapt this playbook in your own agent.

Ask your agent to map the starter prompt to your saved signal map, live events, and the current HTTP API before answering.

Set up Agentry in this repo by following https://agentry.sh/install.md. I authorize you to: run its device-code login and save the key to ~/.agentry/credentials.json, read this repo, propose an instrumentation plan for my approval, and edit code after I approve it.

+ Full access
5.5 Extra High
  1. 1. Open your repo in Codex, Claude Code, Cursor etc.
  2. 2. Paste the install prompt.
  3. 3. Your agent reads the install doc and shows you an implementation plan for approval.