Push notification: sent → opened → action conversion

Joined funnel from push provider's send webhook through open through in-app action. Per-campaign breakdown — what marketing platforms refuse to show you.

Published

difficulty intermediate · time to value 5 minutes · tools used 3

Just say this

Build a funnel from push_sent (from our push provider webhook) → push_opened → next_action. Per-campaign breakdown.

Why this matters

Push marketing platforms (Braze, OneSignal, Iterable, Customer.io) grade themselves by send rate. They’ll tell you they sent 1.2M notifications. Their dashboard shows “98% delivery rate.” Nobody opened those notifications and nobody acted on them after opening.

The metrics that matter for push are:

  1. Sent → Opened: industry typical 2-5%. The notification copy + timing.
  2. Opened → In-app action: industry typical 25-50%. The destination/in-app match.
  3. In-app action → conversion goal: depends on the campaign. The actual ROI.

Stitching these together means joining three event streams (the provider’s webhook + your app’s events + your conversion events). Agentry’s funnel recipe does it in one prompt because everything lives in the same event table.

What you get

  • A funnel: push_sent → push_opened → next_action with absolute counts and conversion %
  • Per-campaign breakdown so you can rank the campaigns by real impact (not just send count)
  • A published dashboard for the lifecycle marketing team

Walk through it

You

Build a funnel: push_sent → push_opened → next_action. Per-campaign breakdown over the last 30 days.

Agent

Aggregate funnel first, then a per-campaign cut.

agentry_run_recipe {
  recipe_id: "funnel",
  params: {
    project_id: "default",
    steps: ["push_sent", "push_opened", "in_app_action"],
    window_days: 30,
    conversion_window_hours: 24,
    group_by: "properties.campaign_id"
  }
}
{
  "overall": {
    "push_sent":       1242000,
    "push_opened":       42180,
    "in_app_action":     16842
  },
  "per_group": [
    {
      "campaign_id": "spring_sale_v3",
      "push_sent":   180000, "push_opened": 12600, "in_app_action":  6840,
      "open_rate": 0.070, "action_rate": 0.543
    },
    {
      "campaign_id": "abandoned_cart_24h",
      "push_sent":   42000, "push_opened":  3780, "in_app_action":  2520,
      "open_rate": 0.090, "action_rate": 0.667
    },
    {
      "campaign_id": "weekly_digest",
      "push_sent":  640000, "push_opened": 12800, "in_app_action":  3200,
      "open_rate": 0.020, "action_rate": 0.250
    },
    {
      "campaign_id": "re_engagement_30d",
      "push_sent":  280000, "push_opened":  8400, "in_app_action":  2100,
      "open_rate": 0.030, "action_rate": 0.250
    }
  ]
}
Agent

Here’s the picture:

CampaignSentOpen %Action %True conversion
abandoned_cart_24h42k9.0%66.7%6.0%
spring_sale_v3180k7.0%54.3%3.8%
re_engagement_30d280k3.0%25.0%0.75%
weekly_digest640k2.0%25.0%0.50%

The pattern is unmistakable:

  • weekly_digest is your worst-performing campaign by every metric but accounts for >50% of your send volume. You’re training users to ignore push.
  • abandoned_cart_24h is your hero: 9% open rate, 67% action rate — 12× more effective per send than the digest. Send more of these (within reason — increase the trigger criteria, not the frequency per user).
  • spring_sale_v3 is good — keep the format for future promo pushes.

Recommendation: cut weekly_digest send frequency from daily to weekly. The aggregate sent volume drops 80% but conversions only drop ~50% (because most of those sends weren’t converting anyway). And critically, you stop training users that “this app’s notifications aren’t worth opening.”

Let me publish.

agentry_publish_query {
  recipe_id: "funnel",
  description: "Push notification funnel (30d, per campaign)",
  params: { steps: ["push_sent", "push_opened", "in_app_action"], window_days: 30, group_by: "properties.campaign_id" }
}
{ "publication_id": "pub_push_fun", "public_url": "https://agentry.sh/q/pub_push_fun?key=agp_…" }

The output

Push notification funnel (30d)

CAMPAIGN              SENT      OPEN %   ACTION %   END CONV   $/SEND
─────────────────────────────────────────────────────────────────────
abandoned_cart_24h    42,000     9.0%     66.7%      6.00%     $1.42 ★★★
spring_sale_v3       180,000     7.0%     54.3%      3.80%     $0.91 ★★
re_engagement_30d    280,000     3.0%     25.0%      0.75%     $0.18
weekly_digest        640,000     2.0%     25.0%      0.50%     $0.12 ★ training users to ignore

Total sent:        1,242,000
Total opened:         42,180  (3.4%)
Total in-app:         16,842  (1.4%)

Top recommendations:
  1. Cut weekly_digest from daily to weekly (-80% volume, -50% conv)
  2. Trigger more abandoned_cart_24h (highest ROI per send)
  3. A/B test re_engagement_30d copy — open rate is at app-floor

Public dashboard:
  https://agentry.sh/q/pub_push_fun?key=agp_xxx

Setting it up

The trick is making sure distinct_id matches across the three event types.

1. Push provider sends push_sent event via webhook to /v1/analytics/. Configure your push provider’s webhook to POST send events to Agentry:

// Cloudflare Worker / Lambda that receives the provider's webhook
export default {
  async fetch(req: Request, env: Env) {
    const event = await req.json();  // provider's payload
    await fetch(`https://api.agentry.sh/v1/analytics/${env.PROJECT_ID}/`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.AGENTRY_DSN}`,
        "Content-Type": "application/json",
        "User-Agent": "push-proxy/1.0",  // REQUIRED — Cloudflare 403s default UAs
      },
      body: JSON.stringify({
        event: "push_sent",
        distinct_id: event.user_email,  // critical: match the mobile-app distinct_id
        properties: {
          campaign_id: event.campaign,
          message_id: event.id,
          template: event.template,
        },
      }),
    });
    return new Response("ok");
  },
};

2. Mobile app fires push_opened with the matching campaign_id. Most push payloads include a data: { campaign_id } block — forward it:

// FirebaseMessagingService.onMessageReceived → if the user taps the notification
fun onNotificationTap(payload: Map<String, String>) {
  postAnalytics("push_opened", mapOf(
    "campaign_id" to payload["campaign_id"],
    "message_id"  to payload["message_id"]
  ))
  // ... navigate to deep link
}

3. Fire in_app_action on the next meaningful event. This is usually a page view, product view, or purchase — whatever the campaign is steering toward.

Variations

  • “Same funnel, but per platform — does iOS open rate beat Android?”
  • “Just the abandoned_cart_24h campaign: open rate by hour of day. Is timing the lever?”
  • “Per-campaign refund rate of resulting purchases — are ‘free trial’ pushes driving low-quality conversions that refund?”
  • “Compare push opens vs email opens for the same campaign — when both arrive, which wins?”

Try this recipe in your own agent.

Paste the prompt above into your agent. It'll set up Agentry against your data, then run the recipe and show you the real output.

Install https://agentry.sh/agentry.md and set it up