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.

Updated · published

markdown for agents →

difficulty intermediate · time to value 5 minutes · execution on demand

Start from 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: measures delivery-to-open behavior for this product and campaign.
  2. Opened → In-app action: measures whether the destination matches the notification promise.
  3. In-app action → conversion goal: measures the predefined product outcome with its own denominator.

Stitching these together means joining three event streams (the provider’s webhook + your app’s events + your conversion events). Agentry’s funnel workflow 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 protected metric report for the lifecycle marketing team; publish a browser-safe version later only after the user asks and a real query blueprint backs it

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.

Endpoint: POST /v1/projects/:project_id/query-blueprints/:blueprint_id/run
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fquery-blueprints%2F%7Bblueprint_id%7D%2Frun&method=post
Blueprint: funnel_3_step
Fetch GET /v1/query-blueprints/funnel_3_step for the allowed params before running it.
Concept fields:
  params: {
    step1: "push_sent",
    step2: "push_opened",
    step3: "in_app_action",
    days: 30
  }
{
  "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.

Endpoint: POST /v1/projects/:project_id/public-queries
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fpublic-queries&method=post
Blueprint: funnel_3_step
Publish only real query-blueprint results; use Agentry's returned `public_url`.
Concept fields:
  blueprint_id: "funnel_3_step",
  description: "Push notification funnel (30d, per campaign)",
  params: { step1: "push_sent", step2: "push_opened", step3: "in_app_action", days: 30 }
{ "id": "<publication_id>", "public_url": "https://api.agentry.sh/v1/public/q/<publication_id>" }

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 view:
  Use Agentry's returned public_url after publishing a real query blueprint.

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/", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.AGENTRY_SERVER_API_KEY}`,
        "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?”

Adapt this workflow 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.

Agentry gives coding agents production context from errors, product analytics, and deploys through one HTTP API. Fetch and read https://agentry.sh/install.md completely, then install Agentry's full first-time telemetry baseline in this repository. I authorize device-code login, saving the owner key only to ~/.agentry/credentials.json, creating one Agentry project for this repository, and read-only inspection of shipped source plus hidden CI/provider configuration. Deeply analyze the product's business logic and complete value flow before choosing telemetry. Save the complete source-backed plan and show me its exact contents and hash. Until I explicitly approve that plan or request a replacement, do not edit app/CI source, place runtime/CI credentials, commit, exercise product traffic, start proof, push, or deploy. After approval, I authorize only the reviewed targets: place the required scoped browser/server/CI credentials through the established environment or secret mechanism, preserve existing telemetry, implement and test the baseline, commit it, push that reviewed commit when the shipped CI/provider path requires it, exercise safe proof paths with test/non-customer data, and perform one deployment through the reviewed shipped CI/provider path. Ask first if proof would charge money, contact a third party, change real customer data, or require new external access. After the plan is saved, immediately before every onboarding state-changing POST, GET current onboarding state, perform only its single returned next_action, then read state again; do not batch or infer later stages. Continue until status is verified, installation_complete is true, and next_action is null. Keep all secrets, source snapshots, proof markers, and scratch files outside the repository.

+ 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.