launch-watch
Skill0 starsChanged 14 days agoMITView on GitHub
Weekly launch-health check for a Pendo customer's own recently launched feature (adoption curve, guide engagement, activation funnel, friction signals, and NPS / Listen sentiment), pulled live via Pendo MCP. Renders a shareable dashboard artifact where the surface supports HTML artifacts, a self-contained HTML file where it doesn't, and a text block otherwise. Use whenever a PM asks to check in on how a launch is landing, wants a "launch health" or "launch watch" view, asks "how is [feature] adopting," wants to know if a new guide is working, or asks to track a launch week over week. Always confirm which features, pages, and guides to track before pulling data, since this skill has no built-in registry of what a customer has launched. Single owner of the launch-health read for a named launch. Not for a product area's weekly health (use pulse) or diagnosing one funnel drop (use funnel-dropoff-investigator).
1 other project uses a version of this.
Adopted by
Most recently taken up by
- mikezamayias/agent-skillstheir own version, first seen Oct 11, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- mikezamayias/agent-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository23 other files it gives its agents.
- attention-radarSkill
- competitor-release-producerSkill
- conviction-docSkill
- daily-digest-conductorSkill
- daily-digestSkill
- digest-composerSkill
- engineering-progress-producerSkill
- feature-creationSkill
- feature-validationSkill
- feedback-idea-producerSkill
- field-qual-producerSkill
- friction-finderSkill
- funnel-dropoff-investigatorSkill
- gtm-scoringSkill
- product-announcement-generatorSkill
- product-lead-weekly-updateSkill
- product-manager-weekly-updateSkill
- prototype-fidelitySkill
- pulseSkill
- release-kitSkill
- release-note-drafterSkill
- support-trends-producerSkill
- weekly-prioSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 8 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 26
Never manufacture a number, because this is a report people cite.
- Very specific: line 28
Never assume a Pendo host or subscription id, because the host differs by region.
- Distinctive: line 29
Never create a new artifact on a re-pin, because the share link must stay stable.
- Distinctive: line 32
Never delete archived snapshots without explicit approval, and never the latest snapshot per launch, because the archive is the …
- Very specific: line 45
Check each feedback item's source before counting it as independent signal, so a guide's own poll isn't counted as other feedback.
- Very specific: line 64
If subscriptionId or pendoHost is missing, the dashboard still renders.
- Distinctive: line 70
Before pulling anything, get from the user:
- Distinctive: line 87
Run these for whatever mix of entities was named in Step 1.
- Very specific: line 137
Session replay sample: if friction signals spike, sessionReplayList filtered to the feature/page with the relevant frustrationTypes gives a …
- Distinctive: line 145
Text block: If HTML can't be rendered or written at all (a text-only context, or an unattended digest), use the compact text block below, …
- Distinctive: line 167
Per-launch tabs: if more than one launch item is tracked, each gets its own tab.
- Distinctive: line 220
If an export library is blocked, the page says PDF and PNG aren't available and points to the HTML export.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Launch Watch
A recurring health check for one or more recent feature launches, built on Pendo MCP analytics tools. Its best output is a persistent dashboard artifact the PM reopens week over week, not a one-time text digest. This skill is generic: it has no knowledge of what any given customer has shipped, so the first thing it always does is ask.
Done
Result: one launch-health dashboard per run, covering every tracked launch: a published artifact where the surface supports it, else a self-contained HTML file, else the text block below. Plus a one-line read in chat.
Done: every section shows data, "no signal yet", its unavailable caveat, or "not in scope"; every number links to its Pendo source, or links are left out because the host or subscription id is unknown; the "As of" date is set; on a re-pin the outgoing snapshot is archived and a row is appended to history/index.json; the one-line read ("landing well / narrow / with friction" plus the single reason) is posted in chat.
Next consumer: leadership and teammates who open the pinned artifact or its export; the re-pin task, which updates the same artifact; a daily digest or unattended run, which reads the one-line chat read.
Intent: catch a launch that looks adopted but is in trouble, not just report a number that went up (see "Why this shape").
Boundaries and authority
- Never guess an ID, because a wrong ID silently reports on the wrong entity; resolve every name with
listPagesFeaturesAndEvents,listGuides, orsearchEntitiesfirst, bake the resolved IDs into the artifact, and if a name doesn't resolve, say so and offer the closest matches. A user-supplied ID that doesn't resolve is tracked only if the user confirms: name it by its ID, tag it "unresolved, user-supplied" in its row's note or read, and expect its calls to come back unavailable. - In this skill "pin" means the snapshot of record; it never means the Artifact tool's sidebar pin unless the user says sidebar.
- Never manufacture a number, because this is a report people cite; a missing signal renders as "-" or "no signal yet", in the dashboard and the text alike.
- Never run the funnel stand-in or the proxy NPS without the user's yes, because neither is the real metric (the stand-in isn't sequence-verified, the proxy isn't real NPS movement); offer them, and on unattended runs mark the funnel unavailable with its caveat instead (Step 2).
- Never assume a Pendo host or subscription id, because the host differs by region; build links from
CONFIGand leave them out when either is unknown. - Never create a new artifact on a re-pin, because the share link must stay stable; update the same artifact, and if the surface can't update in place, produce a new dated file and say so.
- Never show old numbers or an old read under a new date, because the page would misstate what was measured when. A live refresh may save a partial snapshot: each call that still fails after 3 tries marks only its own section unavailable, and the read is re-derived over what came back. If nothing comes back, or the read can't be re-derived, keep the pinned snapshot and its original "As of" date and show "refresh failed:
<which call>" (see "Sharing, refresh & archive"). - Never point a page control or link at a local server, a file path, or a workspace URL (for example
localhostor127.0.0.1), because a shared page would send viewers to someone's machine or expose local files; the page links only to https hosts, and re-pinning happens in chat, never from a page link. - Never delete archived snapshots without explicit approval, and never the latest snapshot per launch, because the archive is the point-in-time record of what was reported; show the count and date range and ask (see "Quarterly cleanup").
Authority: this skill may publish the dashboard artifact and later update that same artifact in place, or write a self-contained HTML file; write <workspace>/.launch-watch/history/<launch-key>/<YYYY-MM-DD>.html and append a row (with the artifact URL) to <workspace>/.launch-watch/history/index.json; write the artifact URL to <workspace>/.launch-watch/artifact.json at first pin; register a re-pin scheduled task, only if the user accepts at setup; run buildPendoSegment, only after the user says yes to the proxy NPS or a segment cut (its write semantics aren't documented, so treat it as a write); mirror snapshots to Notion, only if the user asks; and delete archived snapshots, only on explicit approval. Inside the page, a viewer's own clicks can store that viewer's own unpinned snapshots in browser storage (each keeps a copy of the pinned themes and quotes; the pinned snapshot itself is never changed), call HOST.ask for a re-derived read, and save exports through the surface's download prompt. Every other Pendo call is a read.
Judgment vs mechanics
The model judges: scoping the launch with the user, which feedback items are really about the launch, the health level against the user's targets, the verdict, the narrative read, captions, per-guide and per-surface reads, and when to offer the deeper cuts. Pendo MCP tools compute the counts, rates, funnel conversion, retention curves, and feedback topics. The template's code owns the page structure, empty states, deep links, rate-unit formatting (ratePct), and, on a live refresh, the mapping from tool responses to sections, the zero-filled adoption weeks, the dead clicks per 100 events, the guide status labels, the average time and funnel conversion formatting, the week number, the summary cards, the retries, and marking a failed call's section unavailable. On the main run the model computes those same numbers by the definitions in "Numbers the pin shares with a local snapshot" (Step 2), plus, in prose: the funnel stand-in ratio, the frustration-per-visitor trend, and the archive and index.json writes. No code gates the done check above.
Honesty
- An empty result is valid: "no sentiment signal for this launch yet" is a legitimate finding, and if nothing looks off, say so plainly rather than manufacturing a watch item.
- Couldn't check is not nothing there: a tool that errors or times out is "unavailable right now", not "no signal"; a rate whose unit isn't stated is "unit unclear"; a live refresh call that still fails after its retries marks its section unavailable and the page names it, and a refresh with nothing back shows "refresh failed:
<which call>"; no Pendo MCP connected is said in one line. - Proxies and small samples are labeled: the funnel stand-in "isn't sequence-verified"; proxy NPS is a proxy, always shown with its response count, and under roughly 20 responses is directional at best and stays out of the health summary (the 20 is a rule of thumb, not a statistical test); pasted numbers are labeled user-provided; a live view is labeled as not the pinned snapshot.
- Check each feedback item's
sourcebefore counting it as independent signal, so a guide's own poll isn't counted as other feedback. - Read back each write before reporting it done: re-open the published or updated artifact and check its "As of" date; re-read the archived snapshot file and the new
index.jsonrow; if a weekly re-pin task was registered, list scheduled tasks to confirm it exists; re-fetch the Notion mirror page if one was written.
Why this shape
A launch can look "adopted" and still be in trouble: a guide gets views but nobody finishes it, a feature gets clicks but they're mostly rage clicks, or usage is real but concentrated in three accounts. The point of this skill is to catch that, not just report a number that went up. The dashboard makes the shape of a launch legible at a glance, and can be refreshed so the PM watches the curve move, not a frozen snapshot.
Setup (config)
Nothing here is hardcoded. Collect these once (from the connector's own context where possible, otherwise ask) and bake them into the dashboard's CONFIG:
| Value | Where it comes from |
subscriptionId | The Pendo subscription the data belongs to. It's the id the Pendo MCP tools take for the subscription, and it appears in the Pendo app URL after /s/. |
pendoHost | The host the user signs in to Pendo on, without https:// or a port. It differs by region, so copy it from the browser's address bar rather than assuming one. |
appId | The app, if the subscription has more than one (listAllApplications). If every resolved entity is in one app, use that app without asking. acquisitionTrend requires an appId, so when the user picks "all apps", take it from the primary entity's app and say so in chat. An entity in another app carries its own appId in entities; acquisitionTrend returns 0, not an error, when it's asked about the wrong app. So if it reads 0 for a launch whose entities show usage, check the app before reporting 0. |
mcpServer | The Pendo connector's display name as the surface reports it at publish (for example Pendo), needed only for in-page refresh. Ask at publish time, before setting it: granting the page connector access can limit sharing to the owner's organization. Leave it empty for a page anyone you share it with can open. |
targets | Per launch, the launch targets the user sets (for example "20 new accounts a week by week 4", "guide completion 10%"), each as {metric, target, status} (a weekly active target may also carry entityId, see Step 1). target is the goal only; put where it stands on the pin (the measured value, its week, met or missed) in status, never in metric or target. A local snapshot drops status and judges the targets again from its own numbers, so a pinned result is never shown under a new date. Ask at setup. Optional: with no target, the health read is not colored (see Step 1). |
If subscriptionId or pendoHost is missing, the dashboard still renders; it just leaves out the deep links.
No Pendo MCP connected? Say so in one line and point the user to connecting it (the rest of this skill assumes it). Offer to continue from numbers they paste or export from Pendo instead (usage, guide metrics, feedback): run the same analysis and output on that data, label every figure as user-provided, mark what's missing as "no signal yet," and skip in-page refresh.
Step 1: Scope the launch (always ask, don't assume)
Before pulling anything, get from the user:
- What shipped. One or more of: a page, a feature, a track event, a guide (walkthrough, tooltip, or banner announcing the launch), or a combination. Get names, then resolve to Pendo IDs with
listPagesFeaturesAndEvents(pages/features/trackEvents) andlistGuides(guides). If the user only has a vague description ("the new sync button"), uselistPagesFeaturesAndEvents(itssearchwithsearchType) orsearchEntitieswithsearchType='fuzzy'or'semantic'to help them find it rather than guessing an ID. - App, if the subscription has more than one (
listAllApplications) and the resolved entities span more than one app. If they're all in one app, use it without asking. - Launch date, or "when did this go out". This anchors the before/after comparison. If they don't know exactly, a rough week is fine. If they don't know at all, propose the week where
acquisitionTrendfirst-use steps up and ask them to confirm it. - Cadence: one-off check or recurring weekly. If recurring and the surface supports scheduled tasks, this is a candidate for a scheduled run (see "Sharing, refresh & archive"); otherwise just run it once.
- Launch targets: ask what the launch is meant to hit (adoption, guide completion, funnel conversion, or anything else the user tracks). Record each as a metric plus a target in the launch's
targets. If a target is for weekly active visitors or accounts and the launch has more than one page, feature or track event, ask which one it is measured on and store its id on the target asentityId; if the user doesn't say, leaveentityIdout and the target is measured on the first usage entity. If the user has none, that's fine: say the health read will be a verdict without a green, amber, or red color, and continue. Don't suggest or invent default targets. - Optional: a segment to cut by (for example beta cohort vs. GA, a specific account tier) if adoption breadth is a concern. Don't ask this unless the user brings it up or the numbers below suggest it's worth checking.
What gets tracked by default: the five Step 2 signals (adoption curve, guide engagement, activation funnel, friction, sentiment) for every entity named in #1, skipping any that don't apply. Guides aren't only announcements: walkthroughs, tooltips and onboarding support a launch, and a schedule can deactivate one before the watch is made. Add every guide that's clearly relevant, announcement or not, whatever its state. Recommend relevant inactive guides to the user (say they're inactive and why they're probably relevant, for example a schedule ended) and include them unless the user says no. Only when no guide is relevant does the guide signal stay empty. If you ask the user what to track, name these five; don't offer a "core set" or any other label without saying what it covers.
Don't proceed to pulling data until you have at least #1. Everything else has reasonable defaults (all visitors, last 7 vs. prior 7 days, one-off, no targets). Capture every resolved ID, the app (per entity, when they differ), the launch date, the targets, and any segment: these get baked into the artifact so it can re-pull on its own. In-page refresh always pulls all visitors, so for a dashboard cut by a segment, leave in-page refresh off (empty mcpServer): a local snapshot would show all-visitor numbers under the pin's labels.
Health color comes only from targets. Green, amber, or red on the health read means "measured against the user's launch targets." With targets, judge the level against them and say which target drove it. Without any, set the level to none: the banner shows the verdict in a neutral style, and the read still says whether the launch is landing well, narrow, or with friction. "New accts, latest wk" is a complete week of accounts using the launch for the first time, so judge only a new-accounts-per-week target against it. A target for weekly active visitors or accounts is a different measure: on the main run judge it from aggregateEntityUsage over the last of the 8 weeks acquisitionTrend returns (the same start and end dates as the last of spans, as an absolute dateRange), for the target's own entityId entity, else the first usage entity, and put the entity's name, those dates and the number in the target's status. With no page, feature or track event, a weekly active target is "not judged". A local snapshot judges the same way: it runs the same aggregateEntityUsage over the same last week, once per distinct entity (each weekly active target's own entityId, else the first usage entity; the first-entity call is made only when a weekly active target without its own entity exists, so a launch with no weekly active target makes none), and hands the visitor and account counts to the re-derived read, which names the entity, the dates and the number when it judges a weekly active target. A funnel target is judged against the funnel's conversion. A target a local snapshot has no number for (for example the weekly-active call failed, or no funnel is stored) is named as not judged, and the read goes neutral only if no target can be judged.
Step 2: Pull the launch signals
Run these for whatever mix of entities was named in Step 1. Skip any that don't apply (for example no guide was involved, or there's no NPS survey tied to this launch).
Adoption curve: acquisitionTrend on the launch's first page, feature or track event, exactly as defined in "Numbers the pin shares with a local snapshot" below; that call alone feeds the chart and the two adoption cards. aggregateEntityUsage with compareToDateRange (current week vs. prior week, or since-launch vs. an equivalent pre-launch window) may inform the read (total events, unique visitors, unique accounts), but none of its numbers go on a card or the chart, because the page can't re-pull them.
Guide engagement: if a guide is in scope, one guideUsage call per guide, as defined below, fills the guide rows. aggregateGuideMetrics (several guides compared) or guideEffectivenessMetrics (dashboard-standard engagement and completion rates) may inform the read, but not the rows or cards. Guide rates (completionRate, dismissalRate) can come back as a fraction or a percent. Take the unit from the tool's response or its schema, never from the size of the value: if it says percent, show the value as a percent; if it says fraction, multiply by 100. If neither states the unit, show the number as returned, labeled "unit unclear", rather than guessing. For guideUsage the template's rateUnit defaults to percent (percent of total views); switch it to fraction if the tool's response or schema says so (see "How to build it").
Activation funnel: if both a guide and a feature/page are in scope, run queryFunnel from guide-seen to feature/page-used. This answers the sharper question underneath "guide engagement": of the people who saw the launch guide, how many actually went on to use the thing. A guide with high views but a weak funnel conversion means the announcement worked but the feature didn't land. The first step is the launch guide seen ({kind:"guide", id, eventType:"guideSeen"}) and the last is the launch's first usage entity used ({kind:"page"|"feature"|"trackEvent", id}); put the arguments you send, besides subId and dateRange, in that launch's funnel in the page config (see "How to build it"), so Generate re-runs the same funnel.
Zero is a finding, not a rate. A funnel that succeeds with 0 entrants at its first step has no conversion to read: show "no signal yet: nobody reached <step 1>" (the template does this when the first step's n is 0), not 0%, and say in the read whether that is because the guide never showed (check its state and views). A guide with 0 views gets "-" for completion and dismissal, never 0%. Neither means the funnel is unavailable, so don't offer the stand-in. Usage elsewhere (for example accounts using the feature) still stands on its own.
If queryFunnel errors or times out, retry it once (a shorter window is fine) before calling it unavailable. If the retry fails too, don't just drop the metric. Tell the user it's unavailable right now, then offer a rougher stand-in: pull entityUsage/aggregateEntityUsage unique-visitor counts for the two steps separately (for example visitors who clicked "start" vs. visitors who completed "finish") and present the ratio as an approximate conversion. Flag clearly that this isn't sequence-verified the way a real funnel is (it's "how many touched each step," not "how many went from one to the next in order"), and only run it if the user wants that instead of waiting or retrying.
Unattended runs (a scheduled re-pin with no one to approve): retry once the same way, but don't run the stand-in. Mark the funnel unavailable, with the caveat "queryFunnel unavailable; stand-in not run (unattended run)", and say so in the one-line chat read. On the next attended run for this launch, if the current pin carries that caveat and queryFunnel still fails, offer the stand-in then.
Friction signals: one entityUsage call per page, feature or track event, as defined below, fills the friction rows (rage clicks, dead clicks, u-turns for pages). A launch with rising adoption and rising frustration-per-visitor is not a clean win. Flag it.
Sentiment: check Listen feedback before reaching for a survey score. Pull qualitative feedback tied to the launch first: getFeedbackItems/getFeedbackInsights/generateFeedbackTopics with similaritySearchTerms matching the launch name, or scoped to its productAreaIds if one exists. This catches feedback from every source (support tickets, sales and CS call transcripts, embedded polls), not just a guide's own survey, and it's usually more informative than a score this early in a launch. Check each item's source field (PollResponse is usually an in-app guide or poll, while ZendeskTickets and call-transcript sources are independent signal) so a guide's own poll doesn't get counted as "other feedback." A product-area filter returns everything tagged to that area, not only items about this launch, so hand-pick what's actually about the launch rather than reporting the whole batch.
In the dashboard, lead the sentiment section with clustered themes, not raw titles. Use generateFeedbackTopics (scoped to the launch's product area, then hand-picked like the items) for the "why" (theme name, description, and count), then show a few real verbatims: in-app poll responses (the title is the quote, with its 1 to 5 rating) and substantive form or ticket items (real descriptions). Leave out call-sourced feedback pointers, and say so: show only items that carry a usable quote. Treat a call-sourced item as a pointer when it has fewer than 8 words once the opening pleasantry is removed (everything before the first clause that names the launch, and all of it if no clause names the launch, so 0 words and a pointer). Count the ones left out and set the snapshot's sentimentNote to "<n> call items without a usable quote, transcript not checked" (the read says the same), so nothing is dropped silently. Real call quotes would need a call-transcript source, which most PMs won't have connected; don't fake them. Then, whether or not Listen found items, check whether there's a dedicated survey tied to the launch (listSurveys) and pull surveyScores/surveyResponses if one exists with responses. If neither the survey nor Listen turns up anything, say so plainly rather than manufacturing a number: "no sentiment signal for this launch yet" is a legitimate finding. If the survey was not checked, say so in sentimentNote.
Last resort: if there's genuinely nothing else, offer (don't run unprompted) scoring the general product NPS survey for a segment of people who've used the launched feature (buildPendoSegment on the feature's used metric, then surveyScores with that segmentPipeline) against the unscoped baseline. This is a proxy, not real NPS movement. Say so, and only run it once the user says yes. Always report the response count alongside the score. Anything under about 20 responses is directional at best, not a number to put in the health summary. The 20 is a rule of thumb, not a statistical test.
Numbers the pin shares with a local snapshot
A viewer who presses Generate new snapshot gets these numbers from fetchLive() in the template, under the same labels as the pin. So compute the pinned ones exactly this way: same tool, same arguments, same window, same label and caption. A different measure under the same label reads as a real change between the two. <launch> is the launch's launchDate (ISO) and <asOf> is the pin's "As of" date; for a local snapshot it is the day Generate ran. "First usage entity" is the first page, feature or track event in the launch's entities, and "launch guide" is the first guide there, so put the launch's main surface and main guide first.
| Shown as | Call and arguments | Computed as |
| Header "Week N since launch" | none | floor((<asOf> - <launch>) in days / 7) + 1; the launch date is day 1 of week 1. |
Chart "New accounts per week", and adoption.weeks / newAccounts / spans | acquisitionTrend with subId, appId (CONFIG), scope = the first usage entity's type, entityId its id, unit:"account", periodType:"week", periodCount:8 | 8 weeks ending at the last complete week. The tool leaves out the week in progress and omits a week with no new accounts, so fill each omitted week with 0 (8 contiguous weeks). Weeks are labelled by their start date ("Mon D"); spans holds "Mon D - Mon D". Count only accounts whose first-ever use of the entity falls in that week. If the tool's week labels can't be placed on one 7-day grid, the weeks are shown as returned, without zero-fill, the primary card reads "New accounts, N wks" with N the number of weeks returned, and "New accts, latest wk" shows the last week returned (its sub names that week's label as returned) with no weekly active dates. |
| Card "New accounts, 8 wks" (primary) | same call | summary.totalNew (equals the sum of the 8 weeks). Sub: "first use, weeks of <first week start> - <last week end>", or "first use, none in the last 8 complete weeks" when the call returned no weeks (the chart then says "No new accounts in the last 8 complete weeks"). If the call failed, both adoption cards show "-" with "unavailable: <which call>", never "none". Not "since launch" and not "accounts reached": the weeks don't start at the launch date, and an account that used the entity before them isn't counted. |
| Card "New accts, latest wk" | same call | The last of the 8 weeks (0 if it was omitted); prev is the week before it, goodUp:true. Sub: "week of <Mon D - Mon D>". This is the only card with a delta. |
| Guide rows (views, visitors, completion, dismissal) | guideUsage with subId, guideId, dateRange:{type:"absolute", startDate:<launch>, endDate:<asOf>}, limit:1 | summary.totalViews, uniqueVisitors, completionRate, dismissalRate (rates through ratePct and rateUnit; "-" when views are 0). |
| Guide status pill | listGuides with subId and guideId (the single-guide form; it returns the guide with its state) | state as returned (public, staged, draft, _pendingReview_, disabled, _archived_); the page shows the Pendo UI label (disabled reads "Inactive"). A guide listGuides doesn't know ("not found") has no pill; a failed call shows "Status unavailable". |
| Card "Launch guide completion" | the launch guide's row | Its completion. Sub: "<guide name>, <views> views since launch". |
| Friction rows (visitors, dead clicks, rage clicks, u-turns) | entityUsage with subId, entityType, entityId, the same dateRange, limit:1, per page, feature or track event | summary.totalNumVisitors; dead clicks as round(totalDeadClickCount / totalNumEvents * 1000) / 10 (one decimal) written "N per 100 views" (page) or "N per 100 clicks" (feature), also stored as the number deadPer100; totalRageClickCount; totalUTurnCount for pages. Track events have none of these, so they show "-". Never write dead clicks as a percent: on a page they can pass 100. |
| Friction row "Avg time" (pages only) | aggregateEntityUsage with subId, appId (the entity's own app, else CONFIG.appId), the same dateRange as the friction call, productArea:[{entityType:"page", ids:[<id>]}], limit:1 | summary.avgTimePerVisitor, which is in minutes, as round(minutes * 10) / 10 followed by " min" ("4.9 min"; "<0.1 min" when positive but under 0.05). Features and track events show "-". |
| Weekly active visitors and accounts (not on a card; they go to the read, which names the entity, the dates and the number when it judges a weekly active target) | aggregateEntityUsage with subId, appId, dateRange:{type:"absolute", startDate, endDate} of the last of the 8 weeks (the same dates as the last of spans), productArea:[{entityType, ids:[<the target's entityId, else the first usage entity>]}], limit:1; one call per distinct entity, made only for weekly active visitors/accounts/users targets (a target whose metric names "active" and visitors, accounts or users); with no such target there is no call | summary.uniqueVisitors and summary.uniqueAccounts, written with thousands separators. No usage entity: not measured. |
| Card "Friction hotspot" | the friction rows | The surface with the highest deadPer100; a tie goes to more rage clicks, then entity order. No card when no surface has a deadPer100 above 0. Value: the surface name. Sub: "N dead clicks per 100 views" (or "clicks"); "1 dead click per 100" when N is exactly 1. |
| Activation funnel section | queryFunnel with subId, the launch's stored funnel.args (guide seen to first usage entity used, plus appId), and dateRange:{type:"absolute", startDate:<launch>, endDate:<asOf>} | One step per summary.steps entry, n = that step's visitors, named from funnel.stepNames; conversion = the last step's n over the first step's, to one decimal ("38.2%"; "<0.1%" when positive but under 0.05; "0%" for none). 0 at step 1: "no signal yet: nobody reached <step 1>" (see "Zero is a finding"). |
| Card for the funnel (only with entrants at step 1) | the funnel above | Label: funnel.name, the funnel's own name. Value: the conversion. Sub: "funnel, <last n> of <first n> completed every step". Always the last card. |
Show the four cards in that order, and the two adoption cards only when the launch has a page, feature or track event. The funnel card follows them when the launch stored a funnel and the funnel has entrants at step 1 (label it with the funnel's own name, sub starting "funnel,"). Put no other number on a card. A funnel stand-in is never stored or re-run, so a launch that has only a stand-in has no stored funnel, and a local snapshot shows its funnel as not refreshed. Only the friction summary note and the themes and quotes are pinned-only: a local snapshot shows no summary note and carries the themes and quotes over, labelled as the pinned set. A launch with no guide, or no page, feature or track event, has no funnel; that is not a gap.
Step 3: Optional deeper cuts (offer, don't force)
These add real signal but need more setup. Offer them after the first pass rather than running them by default:
- Retention and stickiness:
cohortRetentionCurvewithcohortMode='first'on the launched feature/page. Tells you whether first-time users came back, that is, whether adoption is durable or a one-time try. - Segment comparison: rerun the launch signals with a
segmentPipeline(built viabuildPendoSegment) to check whether adoption is broad or concentrated in a handful of accounts. - Session replay sample: if friction signals spike,
sessionReplayListfiltered to the feature/page with the relevantfrustrationTypesgives a small qualitative sample of what's actually going wrong, without having to guess from the numbers alone.
Output: a launch-health dashboard
Pick the richest form the current surface supports, in this order:
- Dashboard artifact. If the surface can publish HTML artifacts that keep their own URL across sessions, build the dashboard there. It's a pinned snapshot of record that carries the FULL analysis (not a summary) and, where the surface allows the page to call connector tools, also supports an on-demand live refresh. It renders its baked snapshot on open with no connector call, so it can be shared with your team, including leadership who have no Pendo connector, and the numbers a viewer was shown never shift under them. It includes customer quotes, so check it before sharing it outside your team. How refreshing and re-pinning work is in Sharing, refresh & archive below.
- Self-contained HTML file. If the surface can't publish artifacts but you can write files, build the same dashboard as a static HTML file with this run's numbers baked in (no live re-pull) wrapped in a full page (
<!doctype html>,<meta charset="utf-8">, the template inside<body>), and hand it over, noting it's a snapshot. In a static file the Generate button only shows a hint to re-run the skill, and the snapshot hint says to re-run the skill to update (a local file has no artifact toolbar or live link); exports still work. - Text block. If HTML can't be rendered or written at all (a text-only context, or an unattended digest), use the compact text block below, which still carries every number and the read.
In the snapshot, a section that couldn't be checked carries unavailable: "<which call, why>" (adoption, funnel, friction) or guidesUnavailable, and sentimentChecked: false when Listen wasn't checked, so the page says so instead of hiding the panel or claiming there's no signal. A section that does not apply (adoption and friction with no page, feature or track event) sets notInScope:"<why>" instead, and the page says "Not in scope: <why>".
Every number links back to its source in Pendo. This is a report people cite, so make each element traceable: guides link to the guide (/s/<subscriptionId>/guides/<guideId>); friction surfaces and the adoption and usage figures link to the page, feature, track event, or object detail (/pages/<id>, /features/<id>, /trackevents/<id>, /objects/<id>); each feedback verbatim links to its Listen item (/s/<subscriptionId>/listen/validate?openItem=<feedbackId>). Build these from CONFIG.subscriptionId + CONFIG.pendoHost (see Setup; never assume a host). The page drops any link whose host isn't exactly CONFIG.pendoHost. Pendo's API does not expose a bespoke "report" URL for a metric, so the entity's own detail page, which shows that metric, is the honest source-of-record link. Anywhere a link is possible, include it. If the host or subscription id is unknown, omit links rather than guessing.
The dashboard must preserve every layer of the analysis. The failure mode to avoid is collapsing a rich read into a few single-number cards. Concretely, keep: each guide as its own row with its own views, visitors, completion, dismissal, and status; each friction surface (every page, feature, or action in scope) as its own row with visitors, dead clicks per 100 views or clicks, rage clicks, and dwell time; the funnel with any caveat (for example "stand-in: counts per step, order not verified"); the adoption trend as a chart plus a caption that says what it's doing ("flat for three weeks, stepped up after the walkthrough"); and the analytical Read, always present, as interpretation rather than a recap: it says what the banner and cards can't, without restating their values (see item 3 below). If the source analysis has a nuance ("most rage clicks on Reconnect follow an error message, so it reads as a failure rather than impatience"), the dashboard carries that nuance.
How to build it:
Start from references/dashboard-template.html, a detail-preserving skeleton whose SNAPSHOT data model already has slots for all of the above (banner verdict + full narrative, summary cards, adoption series + caption, a guides[] array, a funnel with caveat, a friction.surfaces[] array + summary, themes[] of {name, description, count}, and sentiment[]). Its SNAPSHOT and HISTORY ship with fictional demo data; always replace both. Fill its CONFIG with the values from Setup and Step 1 (subscriptionId, pendoHost, appId, mcpServer (only with in-page refresh), launchDate (the fallback; each launch also carries its own launchDate, which sets that launch's refresh window), and rateUnit: percent by default for guideUsage, meaning completion and dismissal on a 0 to 100 scale of total views; use fraction only if the tool's response or schema says so, or leave it empty if you're using another tool that doesn't state a unit, so the page labels rates "unit unclear") and each launch's entities as [{type:'page'|'feature'|'trackEvent'|'guide', id, name, appId}] (appId only when the entity isn't in CONFIG.appId's app) (fetchLive uses these to re-pull on refresh: pages, features, and track events are each queried with their own type, guides through guideUsage and listGuides; track events have no frustration counts, so their dead and rage cells show "-"). When the pin ran queryFunnel (a launch with a guide and a page, feature or track event), also set that launch's funnel to {name, stepNames, args}: name the funnel's own name (it labels the funnel card), stepNames one display name per step, and args the exact queryFunnel arguments the pin used apart from subId and dateRange, which the page sets (the launch date to today), for example {steps:[{kind:"guide", id:"<guideId>", eventType:"guideSeen"}, {kind:"feature", id:"<featureId>"}], appId:"<appId>"}. Without a stored funnel Generate cannot re-run it and shows the funnel as not refreshed, so leave it out only when the pin ran no queryFunnel (unavailable, or a stand-in, which is never stored). Replace the SNAPSHOT object with THIS run's full data, keeping every sub-metric, status, dwell, caveat, and read. Put each launch's targets in two places: its SNAPSHOT entry's targets (the goal and where it stands on the pin; the banner and target lines render from these) and its CONFIG launch's targets (the goal and any entityId only; Generate reads these). Write CONFIG, SNAPSHOT and HISTORY as JSON with every < escaped as \u003c (and U+2028 and U+2029 as \u2028 and \u2029): they carry text from outside the team, such as feedback quotes and guide names, and a </script> inside one would end the script block and run as code in the viewer's page. Before publishing, check that the data block contains no </. A guide's status takes its API state (public, staged, draft, disabled, ...); the page shows the Pendo UI label (disabled reads "Inactive"), so don't write the API word into a read either. The template's HOST block already speaks the claude.use(name) page runtime that Claude artifacts use, Cowork included: mcp for in-page Pendo calls (it reads each result's payload), sample for the re-derived read, downloads for PDF, HTML and PNG exports. Declare downloads on every publish; all three export formats need it. Declare sample and mcp (the Pendo connector with exactly the seven tools fetchLive() calls: acquisitionTrend, guideUsage, entityUsage, getFeedbackItems, listGuides, aggregateEntityUsage, queryFunnel) only if the user wants in-page refresh, and set CONFIG.mcpServer to the display name the publish result reports. Before publishing, probe each of those tools once in chat with the exact arguments fetchLive() will send (for getFeedbackItems, startDate is the pin's "As of" date (the launch date if "As of" isn't an ISO date) and endDate the day after the later of that date and today, which on the pin date is the day after "As of", because Pendo treats endDate as exclusive; for listGuides, the single-guide form with subId and guideId; for aggregateEntityUsage, one probe for an average time and one for the weekly active week; for queryFunnel, the stored funnel.args, which can be slow, so allow it time) to confirm the call is accepted and its response still matches what fetchLive() reads. If a probe fails, say in chat which tool failed and why, then publish anyway: the page retries each call and marks a section unavailable if its call still fails. A getFeedbackItems error saying "no relevant insights" means no matching items in that window; the page reads it as 0 new items. A page can't run a scheduled task, so there is no page-side re-pin. On another surface, map its functions into HOST; with no bridge, leave it alone and the page is a static snapshot, which is fine. Never wire a control to a local URL or a file (see Boundaries). If the surface's artifact view already has its own reload control, don't add another.
The page's structure (already laid out in the template):
- Header: feature name, launch date, "Week N since launch" (computed as in Step 2's shared definitions), and an "As of" date.
- Health banner: one-line verdict, colored green, amber, or red only when the launch has targets (neutral otherwise), with the targets listed under it or a note that none are set.
- The read: interpretation only, up top and prominent. The one-line status (landing well, narrow, or with friction), then only the findings no card or section shows on its own (usage concentrated in a few accounts, the funnel step people stop at, a guide that stopped showing, how two signals explain each other), in 3 to 5 sentences, without restating the banner or the card values. This is the heart of the output; write it from the numbers and never drop it.
- Summary cards: exactly the cards defined in Step 2's "Numbers the pin shares with a local snapshot", in that order, with their labels and captions (plus the funnel card when the launch has a funnel). Only "New accts, latest wk" carries a delta, against the week before it (direction colored by whether the move is good). These are a summary; the detail lives in the sections below. A card with no data renders "-", never a fabricated number.
- New accounts per week: the 8 complete weeks as a chart (Chart.js, with an inline-SVG fallback) plus the trend caption.
- Activation funnel: the steps, the conversion %, and any caveat as a visible badge.
- Guide engagement: one row per guide with its four metrics and a status pill; a one-line read under each.
- Friction: one row per surface (visitors, dead clicks per 100 views or clicks, rage clicks, u-turns for pages, avg time) plus the "where the friction isn't" summary note. Avg time comes from
aggregateEntityUsage'savgTimePerVisitor(pages only) over the same window, as defined in "Numbers the pin shares with a local snapshot"; a local snapshot refreshes it too. - Sentiment: themes and hand-picked launch feedback with its
sourcetag, or "no signal yet." - Per-launch tabs: if more than one launch item is tracked, each gets its own tab.
Styling. Clean, analytical, data-dense, in the style of a product-analytics adoption dashboard: white cards, soft rounded corners, subtle shadows, muted grays for secondary labels, one accent color (the template ships with pink; swap the CSS variables at the top of its <style> for your own brand), and green, amber, and red reserved for the health read (only when targets are set) and deltas.
Persistence and storage. Browser storage holds only that viewer's own unpinned snapshots (the browser-local Generate fallback, the 12 most recent), wrapped in try/catch. Each keeps a copy of the pinned themes and quotes, which the viewer already has on the page, so customer quotes stay in that viewer's browser until they clear site data or 12 newer snapshots push it out; the pinned snapshot itself is never changed. Keep any text baked into a raw .html file ASCII-safe (HTML entities or \u escapes) so it renders anywhere.
Whichever form it takes, finish with a one-line read in chat ("landing well / narrow / with friction" plus the single reason) so a daily digest or an unattended run stays legible without opening the artifact.
Text fallback block (per tracked launch item):
## [Feature/Page/Guide name] - launched [date]
**Adoption:** [trend in one line - e.g. "6 weeks in: slow start, stepped up after the wk4 walkthrough, 34 new accounts latest (111 total) - trending up"]
**Guide engagement:** [per guide: views / visitors / completion % / dismissal % + status]
**Activation funnel:** [step-to-step conversion %, with caveat if a proxy]
**Friction:** [per surface: visitors, dead clicks per 100 views or clicks, rage clicks, dwell - flag the hotspot]
**NPS/sentiment:** [score + n + direction, or "no signal yet"]
**Targets:** [each target and where it stands, or "none set - health not colored"]
**Read:** [the full narrative - is this landing well, narrow, or with friction, and why]Don't pad the dashboard (or the text) with prose where the number speaks for itself, but never drop a real finding to make it tidy. If nothing looks off, say so plainly rather than manufacturing a "watch item." If something is genuinely concerning (adoption declining, funnel weak, the launch guide no longer showing), the health banner and the read lead with that instead of burying it under green metrics.
Sharing, refresh & archive
This is a report people cite, so it behaves like a pinned document, not a query that re-runs under the reader. What follows depends on what the surface supports; use what's there and skip the rest.
- Pinned snapshot = the shareable record. On open, the artifact renders only its baked-in data and analysis, stamped "As of
<date>." No connector is called, so it opens identically for the PM, a teammate, or a VP who has never used Pendo, and what someone was shown stays put. This is what you send to leadership. The dashboard carries a split Export control: primary click is PDF, built in the page and saved through the surface's download prompt (a local file uses the browser's Print > Save as PDF); a caret lists all three: PDF, HTML (self-contained file, chart baked to an image) and PNG (rasterized dashboard), saved the same way. All three go throughdownloads, because a plain download link or print call does nothing in an artifact view; where the view has no download prompt, the page says so and suggests asking Claude for the HTML file. PDF and PNG load their rendering libraries (pinned versions with integrity hashes) on the first export click, never on open; if they're blocked, the page points to HTML. The chart library does load on open (a pinned version with an integrity hash, from a public CDN), so a viewer's browser contacts that CDN; if it's blocked, the page draws the chart without it. The exported file is what you attach to a leadership email and what gets written to the archive; a live share link, if any, comes from the surface's own sharing controls, so the page does not reimplement it. - Refresh is explicit and connector-gated. Where the page can call the Pendo connector (via
HOST), the refresh control re-pulls the numbers (fetchLive maps acquisitionTrend to adoption, guideUsage and listGuides to the guide rows and their status pills, entityUsage and aggregateEntityUsage to the friction rows and their average time, queryFunnel to the funnel and its card, and aggregateEntityUsage over the last complete week to the weekly active counts the read judges targets with, and recomputes the week number and the summary cards, all as defined in Step 2's "Numbers the pin shares with a local snapshot"; the funnel is re-run only from the arguments the pin stored in the page config, and a launch with a guide and a page, feature or track event but no stored funnel shows it as not refreshed (the page never runs the stand-in), while a launch with no guide or no page, feature or track event has no funnel and that is not a gap; it marks adoption and friction not in scope for a launch with no page, feature or track event; sentiment keeps the pinned, hand-picked themes and quotes and only counts new items matching the launch name since the pin, labelled as such, because a page can't judge which items are about the launch) AND re-derives the analysis over those fresh numbers (viaHOST.ask), so the catch (verdict, narrative read, per-guide and per-surface reads, captions) refreshes with the data, never going stale against new numbers. The live view is clearly labelled as not the pinned snapshot, and a viewer without the Pendo connector simply never triggers it and keeps the record. Refreshing numbers without re-deriving the judgment is the failure mode to avoid: the judgment is the point of this skill. Each live call is tried up to 3 times. If one still fails, the page still saves the local snapshot: that section shows "unavailable" (never the pinned value under the new date; a failedlistGuidesleaves that guide's numbers and shows "Status unavailable" on its row, a failed average time shows "unavailable" in that cell, a failed weekly active call leaves those targets not judged), a "Partial snapshot" note names it, and the read is re-derived over what came back and says the section is unavailable rather than reasoning from it. If no usage number came back for a launch (only the new-feedback count), the page skips the analysis and the read says in one line that there is nothing to read yet, naming the unavailable calls; that launch alone doesn't make the refresh a success. A pin with no usage numbers for a launch does the same: one line, no interpretation. One Generate refreshes every launch on the page, and switching tabs stays on the snapshot being viewed. A launch where no call comes back, or whose read can't be re-derived (noHOST.ask, or it fails), is left out of the local snapshot, and its tab says "Refresh failed for this launch" with the calls that failed; the other launches are kept. If no launch refreshes, the page keeps the pinned snapshot and its original "As of" date and shows a visible "refresh failed:<which call>" note. Exports leave out that note and the "pinned snapshot" label; an export of a local or archived snapshot keeps its "local (unpinned)" or "archived snapshot" label, so it can't pass for the pin. Exports are disabled while a tab shows "not tracked in this snapshot". The PDF is drawn in the light theme with page breaks between panels. - Re-pinning updates the shared version. Running the skill again ("refresh my launch watch," or a weekly scheduled task) re-pulls, re-analyses, and updates the same artifact in place (same URL) with a new pinned snapshot and a bumped "As of" date, carrying the prior week's numbers for the week-over-week deltas. The share link is stable; its contents advance only when the owner re-pins, never mid-read for a viewer. If the surface can't update an artifact in place, produce a new dated file instead and say so.
- In-dashboard controls and the re-pin model. The dashboard carries a Generate new snapshot button and a History dropdown. A page can't rewrite the shared artifact or run a scheduled task, so the button only makes a live snapshot for that viewer's browser (labelled "local (unpinned)", needs
mcpandsample, see "How to build it") and tells them to ask Claude to "refresh my launch watch" to pin it for everyone. Re-pinning is always a run of this skill: by hand, or from a scheduled re-pin task. Who schedules a weekly re-pin depends on how the skill runs: under thedaily-digestconductor, the conductor does, so register no weekly task here; standalone, offer one at setup and register it only if the user accepts. The task's prompt must update the SAME artifact (never create a new one), bump "As of", move the prior snapshot intoHISTORY, and archive the outgoing one. Record the artifact's URL at first pin in<workspace>/.launch-watch/artifact.json(see Archive) so every refresh, scheduled or manual, targets the same one. History reads a bakedHISTORYarray of prior pins (baked inline, so they render offline; every pin is also kept in the archive, see Archive), appended on every re-pin. If the surface asks for tool permissions on scheduled runs, tell the user to run the task once by hand to approve its Pendo and artifact tools. - Archive every pinned snapshot. The first pin writes the artifact URL to
<workspace>/.launch-watch/artifact.json({launch-key: url}, one entry per launch for a multi-launch page); a re-pin reads it and updates that artifact. The first pin writes no archive file orindex.jsonrow, because nothing was outgoing; its row is written when it is replaced. Before overwriting a pin, save the outgoing snapshot as a dated, self-contained HTML file (renders offline, no connector) under a history store,<workspace>/.launch-watch/history/<launch-key>/<YYYY-MM-DD>.html, and append a row to<workspace>/.launch-watch/history/index.json({launch, asOf, pinnedAt, level, verdict, path, artifactUrl}). Every pin carriespinnedAt(ISO time) in its snapshot, and History shows it. A second pin on the same day gets a suffixed file (<YYYY-MM-DD>-2.html,-3, ...), never overwrites the first. When one pin tracks several launches, write the same snapshot file into each launch's folder and append oneindex.jsonrow per launch, so each launch's history is complete on its own. That gives leadership a durable, point-in-time record of exactly what was reported each week, independent of the live artifact. If the surface can't write files, say so and offer the HTML export instead. If the user keeps their work in Notion, offer to also mirror each snapshot to a "Launch Watch Snapshots" page or database; don't assume it.
Quarterly cleanup
The history store grows, so prune it, but never silently.
- On a quarterly cadence (or whenever the user says "clean up launch-watch history"), read
index.jsonand check for snapshots older than the recommended cutoff. - If there are any, ask before deleting, showing the count and the exact date range you propose to clear. Recommended cutoff: keep the most recent snapshot for every launch no matter its age, plus everything from the trailing two full quarters; recommend clearing snapshots dated before the start of the quarter two quarters back. State the recommendation and the kept and removed counts plainly, for example "You have 23 launch-watch snapshots across 3 launches. Recommend clearing the 12 dated before the start of the quarter two quarters back (keeping the latest per launch + everything from the last two full quarters). Clear them?"
- Only delete on explicit approval, never remove the latest snapshot per launch, and report what was removed and what was kept. Let the user override the range.
Third-party libraries
The dashboard template loads these from public CDNs, each pinned to an exact version with a subresource integrity hash, so a changed file won't run. None is bundled. When you bump one, update its URL and hash together in references/dashboard-template.html and in this table.
| Library | Version | License | Loaded from | When | Used for |
| Chart.js | 4.5.1 | MIT | cdnjs.cloudflare.com | On open | The adoption chart. If it's blocked, the page draws an inline SVG chart instead. |
| html2canvas | 1.4.1 | MIT | cdn.jsdelivr.net | First PDF or PNG export | Renders the page to an image. |
| jsPDF | 4.2.1 | MIT | cdn.jsdelivr.net | First PDF export | Builds the PDF from that image. |
If an export library is blocked, the page says PDF and PNG aren't available and points to the HTML export.
Notes
- The artifact is the memory: with a live refresh or a re-pin, and
compareToDateRangeon each pull, week-over-week works without the skill keeping its own log. For a static snapshot instead, rely oncompareToDateRangeeach run, or have the user keep a running log. - ID resolution and the no-manufactured-numbers rule live in "Boundaries and authority".
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

