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:
- Sent → Opened: industry typical 2-5%. The notification copy + timing.
- Opened → In-app action: industry typical 25-50%. The destination/in-app match.
- 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
Build a funnel: push_sent → push_opened → next_action. Per-campaign breakdown over the last 30 days.
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
}
]
}
Here’s the picture:
| Campaign | Sent | Open % | Action % | True conversion |
|---|---|---|---|---|
abandoned_cart_24h | 42k | 9.0% | 66.7% | 6.0% |
spring_sale_v3 | 180k | 7.0% | 54.3% | 3.8% |
re_engagement_30d | 280k | 3.0% | 25.0% | 0.75% |
weekly_digest | 640k | 2.0% | 25.0% | 0.50% |
The pattern is unmistakable:
weekly_digestis your worst-performing campaign by every metric but accounts for >50% of your send volume. You’re training users to ignore push.abandoned_cart_24his 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_v3is 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?”