@agent-native/core 0.145.8 → 0.146.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +37 -11
  3. package/corpus/templates/analytics/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  4. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +20 -4
  5. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +59 -24
  6. package/corpus/templates/analytics/changelog/2026-08-07-analytics-migrations-now-use-bounded-pressure-aware-backgrou.md +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-07-template-activity-charts-now-show-only-core-agent-native-app.md +6 -0
  8. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +110 -0
  9. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +13 -13
  10. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +448 -0
  11. package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +2 -2
  12. package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +4 -5
  13. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +35 -0
  14. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
  15. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +10 -4
  16. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +4 -11
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +74 -28
  18. package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +2 -5
  19. package/corpus/templates/analytics/server/plugins/db.ts +42 -0
  20. package/corpus/templates/analytics/server/routes/api/analytics-bigquery/backfill.post.ts +55 -0
  21. package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +22 -4
  22. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +37 -11
  23. package/corpus/templates/assets/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  24. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +37 -11
  25. package/corpus/templates/brain/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  26. package/corpus/templates/brain/server/lib/connectors.ts +38 -14
  27. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +37 -11
  28. package/corpus/templates/calendar/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  29. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +37 -11
  30. package/corpus/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  31. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +37 -11
  32. package/corpus/templates/clips/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  33. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +37 -11
  34. package/corpus/templates/content/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  35. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +37 -11
  36. package/corpus/templates/crm/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  37. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +37 -11
  38. package/corpus/templates/design/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  39. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +37 -11
  40. package/corpus/templates/dispatch/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  41. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +37 -11
  42. package/corpus/templates/factory/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  43. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +37 -11
  44. package/corpus/templates/forms/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  45. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +37 -11
  46. package/corpus/templates/macros/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  47. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +37 -11
  48. package/corpus/templates/mail/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  49. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +37 -11
  50. package/corpus/templates/plan/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  51. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +37 -11
  52. package/corpus/templates/slides/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  53. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +13 -0
  54. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -0
  55. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +5 -0
  56. package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +5 -0
  57. package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
  58. package/corpus/templates/slides/app/pages/Presentation.tsx +3 -0
  59. package/corpus/templates/slides/app/pages/SharedPresentation.tsx +5 -0
  60. package/corpus/templates/slides/app/routes/slide.tsx +10 -5
  61. package/corpus/templates/slides/changelog/2026-08-07-slide-previews-and-present-now-keep-deck-colors-consistent.md +6 -0
  62. package/corpus/templates/slides/server/db/schema.ts +1 -0
  63. package/corpus/templates/slides/server/handlers/share.ts +51 -3
  64. package/corpus/templates/slides/server/plugins/db.ts +5 -0
  65. package/corpus/templates/slides/shared/api.ts +2 -0
  66. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +37 -11
  67. package/corpus/templates/tasks/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  68. package/dist/cli/code-agent-executor.js +8 -3
  69. package/dist/client/ConnectBuilderCard.d.ts +7 -7
  70. package/dist/client/ConnectBuilderCard.js +45 -20
  71. package/dist/collab/struct-routes.d.ts +1 -1
  72. package/dist/config.d.ts +9 -2
  73. package/dist/config.js +38 -2
  74. package/dist/db/client.d.ts +13 -1
  75. package/dist/db/client.js +76 -2
  76. package/dist/db/create-get-db.js +2 -2
  77. package/dist/deploy/build.js +5 -1
  78. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  79. package/dist/notifications/routes.d.ts +2 -2
  80. package/dist/observability/routes.d.ts +1 -1
  81. package/dist/progress/routes.d.ts +1 -1
  82. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  83. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  84. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  85. package/dist/resources/handlers.d.ts +1 -1
  86. package/dist/scripts/docs/search.js +3 -2
  87. package/dist/server/agent-chat/prompt-resources.js +4 -3
  88. package/dist/server/agents-bundle.d.ts +20 -3
  89. package/dist/server/agents-bundle.js +57 -30
  90. package/dist/server/better-auth-instance.js +2 -2
  91. package/dist/shared/runtime-config.js +1 -1
  92. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +37 -11
  93. package/dist/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  94. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +37 -11
  95. package/dist/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  96. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +37 -11
  97. package/dist/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  98. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +37 -11
  99. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  100. package/dist/vite/agent-native-config-loader.d.ts +10 -0
  101. package/dist/vite/agent-native-config-loader.js +56 -0
  102. package/dist/vite/agents-bundle-plugin.d.ts +5 -1
  103. package/dist/vite/agents-bundle-plugin.js +29 -6
  104. package/dist/vite/client.d.ts +4 -2
  105. package/dist/vite/client.js +4 -49
  106. package/dist/vite/index.d.ts +2 -2
  107. package/dist/vite/index.js +1 -1
  108. package/docs/content/agent-native-config.mdx +43 -17
  109. package/docs/content/environment-variables.mdx +1 -1
  110. package/package.json +3 -3
  111. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +37 -11
  112. package/src/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  113. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +37 -11
  114. package/src/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  115. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +37 -11
  116. package/src/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
  117. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +37 -11
  118. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
@@ -115,10 +115,40 @@ function fixed(sql: string): (window?: MetricWindow) => string {
115
115
 
116
116
  const TEMPLATE_EXPR =
117
117
  "COALESCE(NULLIF(template, ''), NULLIF(properties::jsonb ->> 'templateId', ''), NULLIF(properties::jsonb ->> 'agent_native_template', ''), NULLIF(properties::jsonb ->> 'agentNativeTemplate', ''), NULLIF(app, ''), NULLIF(properties::jsonb ->> 'agent_native_app', ''), NULLIF(properties::jsonb ->> 'agentNativeApp', ''), 'unknown')";
118
+ /**
119
+ * The built-in traffic dashboard is about the shipped core apps, not every
120
+ * host or preview artifact that can share the browser analytics key.
121
+ *
122
+ * Keep this aligned with `coreTemplates()` in the shared app catalog. The
123
+ * analytics template is deliberately self-contained at runtime, so its
124
+ * production bundle does not depend on another workspace package being built
125
+ * before this template.
126
+ */
127
+ export const FIRST_PARTY_TEMPLATE_NAMES = [
128
+ "analytics",
129
+ "assets",
130
+ "brain",
131
+ "calendar",
132
+ "chat",
133
+ "clips",
134
+ "content",
135
+ "design",
136
+ "dispatch",
137
+ "forms",
138
+ "mail",
139
+ "plan",
140
+ "slides",
141
+ ] as const;
142
+ const FIRST_PARTY_TEMPLATE_SQL_LIST = FIRST_PARTY_TEMPLATE_NAMES.map(
143
+ (name) => `'${name}'`,
144
+ ).join(", ");
145
+ const FIRST_PARTY_TEMPLATE_FILTER = `lower(${TEMPLATE_EXPR}) IN (${FIRST_PARTY_TEMPLATE_SQL_LIST})`;
118
146
  const KNOWN_TEMPLATE_FILTER = `${TEMPLATE_EXPR} <> 'unknown'`;
119
147
  const PRODUCT_ACTIVITY_TEMPLATE_FILTER = `lower(${TEMPLATE_EXPR}) <> 'docs'`;
120
148
  const MARKETING_SITE_TEMPLATE_FILTER = `lower(${TEMPLATE_EXPR}) <> 'www'`;
121
149
  const KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER = `${KNOWN_TEMPLATE_FILTER} AND ${PRODUCT_ACTIVITY_TEMPLATE_FILTER}`;
150
+ const FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER = `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`;
151
+ const FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER = `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}`;
122
152
  const EVENT_DATE_SQL = "event_date";
123
153
  const EVENT_DATE_FILTER_SQL = EVENT_DATE_SQL;
124
154
  const USER_KEY_SQL = "NULLIF(user_key, '')";
@@ -225,7 +255,7 @@ const TOTAL_SIGNUPS_SQL = `SELECT COUNT(*) AS signups FROM analytics_events WHER
225
255
  const LEGACY_SEED_SIGNUPS_TIME_RANGE_FILTER = `('{{timeRange}}' IN ('', 'all') OR ('{{timeRange}}' = '7d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(7)}) OR ('{{timeRange}}' = '30d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(30)}) OR ('{{timeRange}}' = '90d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(90)}) OR ('{{timeRange}}' = '180d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(180)}) OR ('{{timeRange}}' = '365d' AND ${EVENT_DATE_SQL} >= ${daysAgoSql(365)}))`;
226
256
  export const LEGACY_SEED_SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${LEGACY_SEED_SIGNUPS_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
227
257
  export const LEGACY_SIGNUPS_OVER_TIME_SQL = `WITH offsets AS (SELECT (ROW_NUMBER() OVER (ORDER BY ${EVENT_DATE_SQL}) - 1)::int AS n FROM analytics_events LIMIT 800), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
228
- export const SIGNUPS_OVER_TIME_SQL = `WITH digits AS (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9), offsets AS (SELECT ones.n + tens.n * 10 + hundreds.n * 100 AS n FROM digits ones CROSS JOIN digits tens CROSS JOIN digits hundreds WHERE hundreds.n < 8), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
258
+ export const SIGNUPS_OVER_TIME_SQL = `WITH digits AS (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9), offsets AS (SELECT ones.n + tens.n * 10 + hundreds.n * 100 AS n FROM digits ones CROSS JOIN digits tens CROSS JOIN digits hundreds WHERE hundreds.n < 8), signup_events AS (SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER}), bounds AS (SELECT MIN(date::date) AS start_date, MAX(date::date) AS end_date FROM signup_events), dates AS (SELECT to_char(bounds.start_date + offsets.n, 'YYYY-MM-DD') AS date FROM bounds CROSS JOIN offsets WHERE bounds.start_date IS NOT NULL AND bounds.start_date + offsets.n <= bounds.end_date), templates AS (SELECT DISTINCT template FROM signup_events), daily AS (SELECT date, template, COUNT(*) AS count FROM signup_events GROUP BY date, template) SELECT dates.date, templates.template, COALESCE(daily.count, 0) AS count FROM dates CROSS JOIN templates LEFT JOIN daily ON daily.date = dates.date AND daily.template = templates.template ORDER BY dates.date, templates.template`;
229
259
  export const LEGACY_RETENTION_OVER_TIME_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS cohort_date FROM base GROUP BY user_key), anchor_dates AS (SELECT DISTINCT cohort_date AS date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_windows AS (SELECT a.date, f.user_key, f.cohort_date FROM anchor_dates a JOIN first_seen f ON f.cohort_date >= ${rollingWindowStartSql()} AND f.cohort_date <= a.date), cohort_sizes AS (SELECT date, COUNT(DISTINCT user_key) AS users FROM cohort_windows GROUP BY date), periods AS (SELECT '1-7d return' AS period UNION ALL SELECT '7-14d return' AS period), retained AS (SELECT cw.date, '1-7d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date > cw.cohort_date AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY cw.date UNION ALL SELECT cw.date, '7-14d return' AS period, COUNT(DISTINCT cw.user_key) AS retained FROM cohort_windows cw JOIN base b ON b.user_key = cw.user_key AND b.event_date >= to_char(cw.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(cw.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY cw.date) SELECT cs.date, p.period, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs CROSS JOIN periods p LEFT JOIN retained r ON r.date = cs.date AND r.period = p.period WHERE cs.users >= ${RETENTION_MIN_COHORT_SIZE} ORDER BY cs.date, p.period`;
230
260
  export const LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date > c.cohort_date AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
231
261
  export const LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen AS (SELECT user_key, template, event_date AS cohort_date, ROW_NUMBER() OVER (PARTITION BY user_key ORDER BY event_date, template) AS rn FROM base), first_seen AS (SELECT user_key, template, cohort_date FROM ranked_first_seen WHERE rn = 1), cohorts AS (SELECT user_key, template, cohort_date FROM first_seen WHERE cohort_date <= ${daysAgoSql(14)} AND ${dashboardTimeRangeFilter("cohort_date")}), cohort_sizes AS (SELECT template, COUNT(DISTINCT user_key) AS users FROM cohorts GROUP BY template), retained AS (SELECT c.template, COUNT(DISTINCT c.user_key) AS retained FROM cohorts c JOIN base b ON b.user_key = c.user_key AND b.event_date >= to_char(c.cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') AND b.event_date <= to_char(c.cohort_date::date + INTERVAL '14 days', 'YYYY-MM-DD') GROUP BY c.template) SELECT cs.template, COALESCE(r.retained, 0) AS retained_users, cs.users AS cohort_users, COALESCE(r.retained::float / NULLIF(cs.users, 0), 0) AS rate FROM cohort_sizes cs LEFT JOIN retained r ON r.template = cs.template WHERE cs.users >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cs.users DESC, cs.template`;
@@ -259,7 +289,11 @@ export const LEGACY_V0_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
259
289
  "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD'))))",
260
290
  "CURRENT_DATE - INTERVAL '365 days', 'YYYY-MM-DD')))",
261
291
  );
262
- const SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
292
+ export const LEGACY_SIGNUPS_BY_TEMPLATE_SQL = `SELECT ${TEMPLATE_EXPR} AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'signup' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${TEMPLATE_EXPR} ORDER BY count DESC`;
293
+ export const SIGNUPS_BY_TEMPLATE_SQL = `${LEGACY_SIGNUPS_BY_TEMPLATE_SQL.replace(
294
+ ` AND ${DASHBOARD_EMAIL_FILTER} GROUP BY`,
295
+ ` AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY`,
296
+ )}`;
263
297
  export const LEGACY_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
264
298
  export const LEGACY_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH all_users AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL}, user_id, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER}), first_seen AS (SELECT user_key, MIN(event_date) AS first_date FROM all_users GROUP BY user_key) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM all_users a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' AND ${dashboardTimeRangeFilter("a.event_date")} GROUP BY 1, 2 ORDER BY date, template`;
265
299
  const OBSERVED_ACTIVITY_LOOKBACK_FILTER = `${EVENT_DATE_SQL} >= ${daysAgoSql(OBSERVED_ACTIVITY_LOOKBACK_DAYS)}`;
@@ -291,7 +325,7 @@ export const PRE_MARKETING_SITE_RETENTION_OVER_TIME_SQL =
291
325
  const RETENTION_OVER_TIME_SQL =
292
326
  PRE_MARKETING_SITE_RETENTION_OVER_TIME_SQL.replace(
293
327
  PRODUCT_ACTIVITY_TEMPLATE_FILTER,
294
- `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
328
+ `${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
295
329
  );
296
330
  export const PRE_MARKETING_SITE_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
297
331
  LEGACY_ONE_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
@@ -303,7 +337,7 @@ export const MATERIALIZED_ONE_DAY_RETENTION_BY_TEMPLATE_SQL =
303
337
  KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER,
304
338
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
305
339
  );
306
- const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1, 2, 3), observed AS (SELECT user_key, event_date, FIRST_VALUE(template) OVER (PARTITION BY user_key ORDER BY event_date, template) AS starting_template, MIN(event_date) OVER (PARTITION BY user_key ORDER BY event_date, template) AS cohort_date FROM base), cohorts AS (SELECT user_key, starting_template AS template, cohort_date, MAX(CASE WHEN event_date > cohort_date AND event_date <= to_char(cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') THEN 1 ELSE 0 END) AS retained FROM observed WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")} GROUP BY user_key, starting_template, cohort_date) SELECT template, SUM(retained) AS retained_users, COUNT(*) AS cohort_users, SUM(retained)::float / NULLIF(COUNT(*), 0) AS rate FROM cohorts GROUP BY template HAVING COUNT(*) >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cohort_users DESC, template`;
340
+ const ONE_DAY_RETENTION_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1, 2, 3), observed AS (SELECT user_key, event_date, FIRST_VALUE(template) OVER (PARTITION BY user_key ORDER BY event_date, template) AS starting_template, MIN(event_date) OVER (PARTITION BY user_key ORDER BY event_date, template) AS cohort_date FROM base), cohorts AS (SELECT user_key, starting_template AS template, cohort_date, MAX(CASE WHEN event_date > cohort_date AND event_date <= to_char(cohort_date::date + INTERVAL '7 days', 'YYYY-MM-DD') THEN 1 ELSE 0 END) AS retained FROM observed WHERE cohort_date <= ${daysAgoSql(7)} AND ${dashboardTimeRangeFilter("cohort_date")} GROUP BY user_key, starting_template, cohort_date) SELECT template, SUM(retained) AS retained_users, COUNT(*) AS cohort_users, SUM(retained)::float / NULLIF(COUNT(*), 0) AS rate FROM cohorts GROUP BY template HAVING COUNT(*) >= ${PER_TEMPLATE_RETENTION_MIN_COHORT_SIZE} ORDER BY rate DESC, cohort_users DESC, template`;
307
341
  export const PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
308
342
  LEGACY_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
309
343
  `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER}), ranked_first_seen`,
@@ -312,7 +346,7 @@ export const PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
312
346
  const SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL =
313
347
  PRE_MARKETING_SITE_SEVEN_DAY_RETENTION_BY_TEMPLATE_SQL.replace(
314
348
  KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER,
315
- `${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
349
+ `${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
316
350
  );
317
351
  export const PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT a.event_date AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
318
352
  export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_SQL =
@@ -320,14 +354,14 @@ export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_SQL =
320
354
  PRODUCT_ACTIVITY_TEMPLATE_FILTER,
321
355
  `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
322
356
  );
323
- const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT event_date AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
357
+ const RECURRING_USERS_BY_TEMPLATE_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT event_date AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
324
358
  export const PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH first_seen AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, MIN(${EVENT_DATE_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} GROUP BY 1), activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER}) SELECT to_char(date_trunc('week', a.event_date::date), 'YYYY-MM-DD') AS date, a.template AS template, COUNT(DISTINCT a.user_key) AS users FROM activity a JOIN first_seen f ON f.user_key = a.user_key WHERE a.event_date <> f.first_date AND a.template <> 'unknown' GROUP BY 1, 2 ORDER BY date, template`;
325
359
  export const DOUBLE_SCAN_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL =
326
360
  PRE_MARKETING_SITE_RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL.replace(
327
361
  PRODUCT_ACTIVITY_TEMPLATE_FILTER,
328
362
  `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
329
363
  );
330
- const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT to_char(date_trunc('week', event_date::date), 'YYYY-MM-DD') AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
364
+ const RECURRING_USERS_BY_TEMPLATE_WEEKLY_SQL = `WITH activity AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, ${EVENT_DATE_SQL} AS event_date, ${TEMPLATE_EXPR} AS template, MIN(${EVENT_DATE_SQL}) OVER (PARTITION BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS first_date FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${OBSERVED_ACTIVITY_LOOKBACK_FILTER}) SELECT to_char(date_trunc('week', event_date::date), 'YYYY-MM-DD') AS date, template, COUNT(DISTINCT user_key) AS users FROM activity WHERE event_date <> first_date AND template <> 'unknown' AND ${DASHBOARD_TIME_RANGE_FILTER} GROUP BY 1, 2 ORDER BY date, template`;
331
365
  const LEGACY_RECURRING_USERS_DESCRIPTION =
332
366
  "Daily signed-in visitors who are NOT on their all-time first active day (Recurring only), stacked by inferred template/app used that day. Docs traffic and unknown template are excluded.";
333
367
  const LEGACY_RECURRING_USERS_WEEKLY_DESCRIPTION =
@@ -509,20 +543,44 @@ export const LEGACY_DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${T
509
543
  export const PRE_MARKETING_SITE_DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT user_key) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
510
544
  const DAU_BY_TEMPLATE_SQL = PRE_MARKETING_SITE_DAU_BY_TEMPLATE_SQL.replace(
511
545
  ` AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY`,
512
- ` AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} GROUP BY`,
546
+ ` AND ${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} GROUP BY`,
513
547
  );
514
548
  export const LEGACY_WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
515
549
  export const PRE_MARKETING_SITE_WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT user_key AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
516
550
  const WAU_BY_TEMPLATE_SQL = PRE_MARKETING_SITE_WAU_BY_TEMPLATE_SQL.replace(
517
551
  ` AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}`,
518
- ` AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}`,
552
+ ` AND ${FIRST_PARTY_KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}`,
519
553
  );
520
554
  const PRE_MARKETING_SITE_REPEAT_USERS_SQL = `WITH user_days AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS user_key, COUNT(DISTINCT ${EVENT_DATE_SQL}) AS active_days FROM analytics_events WHERE ${SIGNED_IN_PRODUCT_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${SIGNED_IN_ACTIVITY_KEY_SQL}) SELECT COUNT(*) AS count FROM user_days WHERE active_days >= 2`;
521
555
  const REPEAT_USERS_SQL = PRE_MARKETING_SITE_REPEAT_USERS_SQL.replace(
522
556
  PRODUCT_ACTIVITY_TEMPLATE_FILTER,
523
- `${PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
557
+ `${FIRST_PARTY_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${MARKETING_SITE_TEMPLATE_FILTER}`,
524
558
  );
525
559
 
560
+ export const LEGACY_TEMPLATE_INTEREST_OVER_TIME_SQL = `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`;
561
+ export const TEMPLATE_INTEREST_OVER_TIME_SQL = `${LEGACY_TEMPLATE_INTEREST_OVER_TIME_SQL.replace(
562
+ ` AND ${DASHBOARD_EMAIL_FILTER} GROUP BY`,
563
+ ` AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY`,
564
+ )}`;
565
+ export const LEGACY_CLICKS_BY_TEMPLATE_SQL =
566
+ LEGACY_TEMPLATE_INTEREST_OVER_TIME_SQL;
567
+ const CLICKS_BY_TEMPLATE_SQL = TEMPLATE_INTEREST_OVER_TIME_SQL;
568
+ export const LEGACY_DEMO_CLICKS_OVER_TIME_SQL = `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`;
569
+ export const DEMO_CLICKS_OVER_TIME_SQL = `${LEGACY_DEMO_CLICKS_OVER_TIME_SQL.replace(
570
+ ` AND ${DASHBOARD_EMAIL_FILTER} GROUP BY`,
571
+ ` AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY`,
572
+ )}`;
573
+ export const LEGACY_CLI_COPIES_OVER_TIME_SQL = `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`;
574
+ export const CLI_COPIES_OVER_TIME_SQL = `${LEGACY_CLI_COPIES_OVER_TIME_SQL.replace(
575
+ ` AND ${DASHBOARD_EMAIL_FILTER} GROUP BY`,
576
+ ` AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY`,
577
+ )}`;
578
+ export const LEGACY_PAGEVIEWS_OVER_TIME_SQL = `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template`;
579
+ export const PAGEVIEWS_OVER_TIME_SQL = `${LEGACY_PAGEVIEWS_OVER_TIME_SQL.replace(
580
+ ` AND ${DASHBOARD_EMAIL_FILTER} GROUP BY`,
581
+ ` AND ${DASHBOARD_EMAIL_FILTER} AND ${FIRST_PARTY_TEMPLATE_FILTER} GROUP BY`,
582
+ )}`;
583
+
526
584
  /**
527
585
  * Catalog entries. Order here is the default panel order when a caller passes
528
586
  * metrics; callers can reorder by listing keys in the order they want.
@@ -745,9 +803,7 @@ const ENTRIES: FirstPartyMetric[] = [
745
803
  source: "first-party",
746
804
  width: 2,
747
805
  windowed: false,
748
- buildSql: fixed(
749
- `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
750
- ),
806
+ buildSql: fixed(TEMPLATE_INTEREST_OVER_TIME_SQL),
751
807
  config: {
752
808
  xKey: "date",
753
809
  yKey: "count",
@@ -767,9 +823,7 @@ const ENTRIES: FirstPartyMetric[] = [
767
823
  source: "first-party",
768
824
  width: 2,
769
825
  windowed: false,
770
- buildSql: fixed(
771
- `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click template' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
772
- ),
826
+ buildSql: fixed(CLICKS_BY_TEMPLATE_SQL),
773
827
  config: {
774
828
  xKey: "date",
775
829
  yKey: "count",
@@ -789,9 +843,7 @@ const ENTRIES: FirstPartyMetric[] = [
789
843
  source: "first-party",
790
844
  width: 2,
791
845
  windowed: false,
792
- buildSql: fixed(
793
- `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'click try demo' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
794
- ),
846
+ buildSql: fixed(DEMO_CLICKS_OVER_TIME_SQL),
795
847
  config: {
796
848
  xKey: "date",
797
849
  yKey: "count",
@@ -811,9 +863,7 @@ const ENTRIES: FirstPartyMetric[] = [
811
863
  source: "first-party",
812
864
  width: 2,
813
865
  windowed: false,
814
- buildSql: fixed(
815
- `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
816
- ),
866
+ buildSql: fixed(CLI_COPIES_OVER_TIME_SQL),
817
867
  config: {
818
868
  xKey: "date",
819
869
  yKey: "count",
@@ -833,9 +883,7 @@ const ENTRIES: FirstPartyMetric[] = [
833
883
  source: "first-party",
834
884
  width: 2,
835
885
  windowed: false,
836
- buildSql: fixed(
837
- `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'copy cli command' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), 'unknown') ORDER BY date, template`,
838
- ),
886
+ buildSql: fixed(CLI_COPIES_OVER_TIME_SQL),
839
887
  config: {
840
888
  xKey: "date",
841
889
  yKey: "count",
@@ -859,9 +907,7 @@ const ENTRIES: FirstPartyMetric[] = [
859
907
  windowed: false,
860
908
  // Browser telemetry emits explicit `pageview` events with URL context, so
861
909
  // keep pageview panels scoped to that event instead of all tracked events.
862
- buildSql: fixed(
863
- `SELECT ${EVENT_DATE_SQL} AS date, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') AS template, COUNT(*) AS count FROM analytics_events WHERE event_name = 'pageview' AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} GROUP BY ${EVENT_DATE_SQL}, COALESCE(NULLIF(template, ''), NULLIF(app, ''), 'unknown') ORDER BY date, template`,
864
- ),
910
+ buildSql: fixed(PAGEVIEWS_OVER_TIME_SQL),
865
911
  config: {
866
912
  xKey: "date",
867
913
  yKey: "count",
@@ -29,20 +29,17 @@ function intervalMs(): number {
29
29
  }
30
30
 
31
31
  export default function registerAnalyticsRollupBackfillJobs(): void {
32
- const isProd = process.env.NODE_ENV === "production";
33
32
  const flag =
34
33
  process.env.ANALYTICS_ROLLUP_BACKFILL_JOBS ??
35
34
  process.env.RUN_BACKGROUND_JOBS;
36
- const enabled =
37
- !platformSchedulerOwnsBackfill() &&
38
- (flag === "1" || (isProd && flag !== "0"));
35
+ const enabled = !platformSchedulerOwnsBackfill() && flag === "1";
39
36
 
40
37
  if (!enabled) {
41
38
  if (!skippingLogged) {
42
39
  console.log(
43
40
  platformSchedulerOwnsBackfill()
44
41
  ? "[analytics-rollups] Skipping in-process cron because the platform scheduler owns historical rollup backfills."
45
- : "[analytics-rollups] Skipping historical rollup backfill (set ANALYTICS_ROLLUP_BACKFILL_JOBS=1 or RUN_BACKGROUND_JOBS=1 to enable in dev; on by default in production)",
42
+ : "[analytics-rollups] Skipping historical rollup backfill (set ANALYTICS_ROLLUP_BACKFILL_JOBS=1 or RUN_BACKGROUND_JOBS=1 to enable explicitly)",
46
43
  );
47
44
  skippingLogged = true;
48
45
  }
@@ -1492,6 +1492,48 @@ export const runAnalyticsMigrations = runMigrations(
1492
1492
  CREATE INDEX IF NOT EXISTS dashboards_folder_idx ON dashboards (folder_id)`,
1493
1493
  },
1494
1494
  },
1495
+ {
1496
+ version: 139,
1497
+ name: "analytics-bigquery-backfill-jobs",
1498
+ sql: {
1499
+ postgres: `CREATE TABLE IF NOT EXISTS analytics_bigquery_backfill_jobs (
1500
+ id TEXT PRIMARY KEY,
1501
+ org_id TEXT NOT NULL,
1502
+ owner_email TEXT NOT NULL,
1503
+ table_ref TEXT NOT NULL,
1504
+ batch_size INTEGER NOT NULL DEFAULT 250,
1505
+ backfill_cursor TEXT,
1506
+ status TEXT NOT NULL DEFAULT 'pending',
1507
+ copied_count INTEGER NOT NULL DEFAULT 0,
1508
+ lease_token TEXT,
1509
+ lease_expires_at TEXT,
1510
+ next_run_at TEXT NOT NULL DEFAULT (now()::text),
1511
+ last_error TEXT,
1512
+ completed_at TEXT,
1513
+ updated_at TEXT NOT NULL DEFAULT (now()::text)
1514
+ );
1515
+ CREATE INDEX IF NOT EXISTS analytics_bigquery_backfill_jobs_due_idx
1516
+ ON analytics_bigquery_backfill_jobs (status, next_run_at, lease_expires_at, updated_at)`,
1517
+ sqlite: `CREATE TABLE IF NOT EXISTS analytics_bigquery_backfill_jobs (
1518
+ id TEXT PRIMARY KEY,
1519
+ org_id TEXT NOT NULL,
1520
+ owner_email TEXT NOT NULL,
1521
+ table_ref TEXT NOT NULL,
1522
+ batch_size INTEGER NOT NULL DEFAULT 250,
1523
+ backfill_cursor TEXT,
1524
+ status TEXT NOT NULL DEFAULT 'pending',
1525
+ copied_count INTEGER NOT NULL DEFAULT 0,
1526
+ lease_token TEXT,
1527
+ lease_expires_at TEXT,
1528
+ next_run_at TEXT NOT NULL DEFAULT (datetime('now')),
1529
+ last_error TEXT,
1530
+ completed_at TEXT,
1531
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1532
+ );
1533
+ CREATE INDEX IF NOT EXISTS analytics_bigquery_backfill_jobs_due_idx
1534
+ ON analytics_bigquery_backfill_jobs (status, next_run_at, lease_expires_at, updated_at)`,
1535
+ },
1536
+ },
1495
1537
  ],
1496
1538
  { table: "analytics_migrations" },
1497
1539
  );
@@ -0,0 +1,55 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+
3
+ import { createError, defineEventHandler, getHeader } from "h3";
4
+
5
+ // guard:allow-action-twin — the generated scheduled worker authenticates before invoking this job route.
6
+ import { runFirstPartyAnalyticsBigQueryBackfillOnce } from "../../../jobs/analytics-bigquery-backfill.js";
7
+
8
+ declare global {
9
+ var __AGENT_NATIVE_ANALYTICS_BIGQUERY_BACKFILL_SCHEDULED_RUNTIME__:
10
+ | boolean
11
+ | undefined;
12
+ }
13
+
14
+ function scheduledFunctionRuntime(): boolean {
15
+ return (
16
+ globalThis.__AGENT_NATIVE_ANALYTICS_BIGQUERY_BACKFILL_SCHEDULED_RUNTIME__ ===
17
+ true
18
+ );
19
+ }
20
+
21
+ function cronSecret(): string | null {
22
+ const secret = process.env.ANALYTICS_BIGQUERY_BACKFILL_CRON_SECRET?.trim();
23
+ return secret ? secret : null;
24
+ }
25
+
26
+ function headerMatchesSecret(
27
+ header: string | undefined,
28
+ secret: string,
29
+ ): boolean {
30
+ const expected = `Bearer ${secret}`;
31
+ const value = header?.trim() ?? "";
32
+ if (value.length !== expected.length) return false;
33
+ return timingSafeEqual(Buffer.from(value), Buffer.from(expected));
34
+ }
35
+
36
+ export default defineEventHandler(async (event) => {
37
+ const secret = cronSecret();
38
+ const scheduledRuntime = scheduledFunctionRuntime();
39
+ const authenticated =
40
+ secret !== null &&
41
+ headerMatchesSecret(getHeader(event, "authorization"), secret);
42
+ if (!scheduledRuntime && !authenticated) {
43
+ throw createError({
44
+ statusCode: secret ? 401 : 503,
45
+ statusMessage: secret
46
+ ? "Unauthorized"
47
+ : "ANALYTICS_BIGQUERY_BACKFILL_CRON_SECRET is required",
48
+ });
49
+ }
50
+
51
+ return {
52
+ ok: true,
53
+ ...(await runFirstPartyAnalyticsBigQueryBackfillOnce()),
54
+ };
55
+ });
@@ -16,8 +16,10 @@ import {
16
16
  buildDashboardAgentContext,
17
17
  buildDashboardSeedAgentContext,
18
18
  } from "../../lib/agent-readable-resource-context.js";
19
+ import { repairCanonicalFirstPartyDashboardQueries } from "../../lib/canonical-first-party-dashboard-repair.js";
19
20
  import { loadDashboardSeed } from "../../lib/dashboard-seeds.js";
20
21
  import type { DashboardRecord } from "../../lib/dashboards-store.js";
22
+ import { FIRST_PARTY_DASHBOARD_ID } from "../../lib/first-party-metric-catalog.js";
21
23
 
22
24
  function queryString(value: unknown): string {
23
25
  if (typeof value === "string") return value;
@@ -91,7 +93,13 @@ export default defineEventHandler(async (event) => {
91
93
  setResponseStatus(event, 403);
92
94
  return { error: "Invalid or expired agent access token" };
93
95
  }
94
- return buildDashboardSeedAgentContext(id, seed, { includeConfig: true });
96
+ return buildDashboardSeedAgentContext(
97
+ id,
98
+ id === FIRST_PARTY_DASHBOARD_ID
99
+ ? repairCanonicalFirstPartyDashboardQueries(seed).config
100
+ : seed,
101
+ { includeConfig: true },
102
+ );
95
103
  }
96
104
 
97
105
  if (!row) {
@@ -104,7 +112,17 @@ export default defineEventHandler(async (event) => {
104
112
  return { error: "Invalid or expired agent access token" };
105
113
  }
106
114
 
107
- return buildDashboardAgentContext(rowToDashboard(row), {
108
- includeConfig: true,
109
- });
115
+ const dashboard = rowToDashboard(row);
116
+ return buildDashboardAgentContext(
117
+ id === FIRST_PARTY_DASHBOARD_ID
118
+ ? {
119
+ ...dashboard,
120
+ config: repairCanonicalFirstPartyDashboardQueries(dashboard.config)
121
+ .config,
122
+ }
123
+ : dashboard,
124
+ {
125
+ includeConfig: true,
126
+ },
127
+ );
110
128
  });
@@ -4,7 +4,7 @@ description: >-
4
4
  Turn visible project context, a proven thread, skill, or workflow into a
5
5
  runnable Agent-Native app with simple buttons, visible agent steps, preview,
6
6
  and deployment handoff. Use when a user invokes `/turn-into-app` or asks to
7
- make a workflow into an app.
7
+ make a workflow into an app, including from Claude or ChatGPT on the web.
8
8
  user-invocable: true
9
9
  scope: both
10
10
  metadata:
@@ -13,9 +13,32 @@ metadata:
13
13
 
14
14
  # Turn Into App
15
15
 
16
+ ## Host execution boundary
17
+
18
+ The build location depends on the host. Treat this as a hard routing rule:
19
+
20
+ - In Claude Web, ChatGPT Web, or a Claude/ChatGPT Project on the web, you are
21
+ the source analyst and handoff orchestrator. You must not build the app in
22
+ the host sandbox. Do not run `npm`, `pnpm`, `npx`, `agent-native create`, or
23
+ `add-app`; do not edit files, create artifacts, or start a local dev server.
24
+ After writing the bounded source brief, call the connected Dispatch action
25
+ `start-workspace-app-creation`. Pass the brief and repeatable workflow in
26
+ `prompt`, plus the inferred `appId`, `description`, `template`, and selected
27
+ `resourceIds` when available. This is the Builder handoff.
28
+ - Do not substitute the generic `create_workspace_app` MCP tool in an online
29
+ host. That tool is a local workspace scaffolder, not the Builder handoff.
30
+ - If `start-workspace-app-creation` is not available or Dispatch is not
31
+ authenticated, stop with the connector setup needed. Do not fall back to a
32
+ local build or claim that the app exists.
33
+ - In Claude Code, Codex Code, or another local code-agent runtime with a target
34
+ workspace, follow the local implementation steps below. The local agent may
35
+ scaffold, edit, run, and verify the app there.
36
+
16
37
  ## Default behavior
17
38
 
18
- This is an end-to-end build skill, not a request for an app proposal.
39
+ For a local code-agent runtime this is an end-to-end build skill, not a request
40
+ for an app proposal. For an online host, the end-to-end result is a verified
41
+ Builder handoff and the resulting workspace app, not code written in the host.
19
42
 
20
43
  - With no argument, choose the source in this order: visible project context,
21
44
  then the current thread. A fresh Claude or ChatGPT Project is a valid source
@@ -47,9 +70,12 @@ private web access, invent an importer, add fake OAuth, or scrape a logged-in
47
70
  page. If the needed context is not visible, ask for an export, transcript, or
48
71
  attachment and treat that artifact as imported source material.
49
72
 
50
- Deliver a fresh app in a new directory, implement the repeatable workflow with
51
- buttons and agent handoffs, start its dev server, verify the main path, and
52
- continue through build/deployment handoff. Do not stop at a plan.
73
+ For local code-agent runtimes, deliver a fresh app in a new directory,
74
+ implement the repeatable workflow with buttons and agent handoffs, start its dev
75
+ server, verify the main path, and continue through build/deployment handoff. Do
76
+ not stop at a plan. For online hosts, call Dispatch first after the source brief,
77
+ then report the returned Builder branch/path and verify the workspace app through
78
+ Dispatch when it becomes available.
53
79
 
54
80
  ## Fresh project context mode
55
81
 
@@ -190,12 +216,12 @@ Do not use `create` for an existing workspace; it scaffolds a new standalone
190
216
  workspace rather than adding an app to the current one.
191
217
 
192
218
  When the source came from a fresh external Project and the target is a Builder
193
- workspace, prefer the available Dispatch MCP workspace handoff after writing
194
- the source brief. Call `start-workspace-app-creation` with a concise prompt,
195
- the inferred app id, and the brief's repeatable workflow. Pass selected
196
- workspace resource IDs when they are available, rather than pasting entire
197
- knowledge files. Continue from the returned workspace path or Builder handoff
198
- and report the actual verification result.
219
+ workspace, the online host path is mandatory: after writing the source brief,
220
+ call `start-workspace-app-creation` with a concise prompt, the inferred app id,
221
+ and the brief's repeatable workflow. Pass selected workspace resource IDs when
222
+ they are available, rather than pasting entire knowledge files. Continue from
223
+ the returned workspace path or Builder branch handoff and report the actual
224
+ verification result. Do not run the local scaffold commands in this host mode.
199
225
 
200
226
  Use a first-party template only when it materially fits the workflow. Keep the
201
227
  new app independent from the source thread's working tree unless the user
@@ -51,13 +51,24 @@ also place the skill instructions or this reference in the Project
51
51
  instructions/knowledge files. Then add and authenticate the Dispatch MCP
52
52
  connector from the packaged `adapters/chatgpt-mcp/connector.json`.
53
53
 
54
+ For Claude Web, ChatGPT Web, and their web Projects, the host is an
55
+ orchestrator, not the build environment. After forming the source brief, call
56
+ the Dispatch `start-workspace-app-creation` action so Builder creates the app
57
+ in the connected workspace. Do not use the host's code interpreter, shell,
58
+ artifact editor, `create_workspace_app`, or local `pnpm`/`npm` commands. If the
59
+ handoff action is unavailable, stop and ask the user to authenticate Dispatch;
60
+ do not fall back to a local sandbox build. Claude Code and Codex Code are the
61
+ local-agent exception and may follow the implementation steps in the main
62
+ skill.
63
+
54
64
  In a new Project chat, say:
55
65
 
56
66
  ```text
57
67
  Turn this project into an app. Use the visible Project instructions,
58
68
  knowledge files, and any selected successful runs as the source. Create the
59
- app in the connected Agent-Native workspace, keep the source brief bounded,
60
- and report the real path and verification result.
69
+ app in the connected Agent-Native workspace through Dispatch and Builder, keep
70
+ the source brief bounded, and report the real Builder branch/path and
71
+ verification result. Do not build it in this chat's sandbox.
61
72
  ```
62
73
 
63
74
  If the host does not show the Project instructions or files to the model, stop
@@ -4,7 +4,7 @@ description: >-
4
4
  Turn visible project context, a proven thread, skill, or workflow into a
5
5
  runnable Agent-Native app with simple buttons, visible agent steps, preview,
6
6
  and deployment handoff. Use when a user invokes `/turn-into-app` or asks to
7
- make a workflow into an app.
7
+ make a workflow into an app, including from Claude or ChatGPT on the web.
8
8
  user-invocable: true
9
9
  scope: both
10
10
  metadata:
@@ -13,9 +13,32 @@ metadata:
13
13
 
14
14
  # Turn Into App
15
15
 
16
+ ## Host execution boundary
17
+
18
+ The build location depends on the host. Treat this as a hard routing rule:
19
+
20
+ - In Claude Web, ChatGPT Web, or a Claude/ChatGPT Project on the web, you are
21
+ the source analyst and handoff orchestrator. You must not build the app in
22
+ the host sandbox. Do not run `npm`, `pnpm`, `npx`, `agent-native create`, or
23
+ `add-app`; do not edit files, create artifacts, or start a local dev server.
24
+ After writing the bounded source brief, call the connected Dispatch action
25
+ `start-workspace-app-creation`. Pass the brief and repeatable workflow in
26
+ `prompt`, plus the inferred `appId`, `description`, `template`, and selected
27
+ `resourceIds` when available. This is the Builder handoff.
28
+ - Do not substitute the generic `create_workspace_app` MCP tool in an online
29
+ host. That tool is a local workspace scaffolder, not the Builder handoff.
30
+ - If `start-workspace-app-creation` is not available or Dispatch is not
31
+ authenticated, stop with the connector setup needed. Do not fall back to a
32
+ local build or claim that the app exists.
33
+ - In Claude Code, Codex Code, or another local code-agent runtime with a target
34
+ workspace, follow the local implementation steps below. The local agent may
35
+ scaffold, edit, run, and verify the app there.
36
+
16
37
  ## Default behavior
17
38
 
18
- This is an end-to-end build skill, not a request for an app proposal.
39
+ For a local code-agent runtime this is an end-to-end build skill, not a request
40
+ for an app proposal. For an online host, the end-to-end result is a verified
41
+ Builder handoff and the resulting workspace app, not code written in the host.
19
42
 
20
43
  - With no argument, choose the source in this order: visible project context,
21
44
  then the current thread. A fresh Claude or ChatGPT Project is a valid source
@@ -47,9 +70,12 @@ private web access, invent an importer, add fake OAuth, or scrape a logged-in
47
70
  page. If the needed context is not visible, ask for an export, transcript, or
48
71
  attachment and treat that artifact as imported source material.
49
72
 
50
- Deliver a fresh app in a new directory, implement the repeatable workflow with
51
- buttons and agent handoffs, start its dev server, verify the main path, and
52
- continue through build/deployment handoff. Do not stop at a plan.
73
+ For local code-agent runtimes, deliver a fresh app in a new directory,
74
+ implement the repeatable workflow with buttons and agent handoffs, start its dev
75
+ server, verify the main path, and continue through build/deployment handoff. Do
76
+ not stop at a plan. For online hosts, call Dispatch first after the source brief,
77
+ then report the returned Builder branch/path and verify the workspace app through
78
+ Dispatch when it becomes available.
53
79
 
54
80
  ## Fresh project context mode
55
81
 
@@ -190,12 +216,12 @@ Do not use `create` for an existing workspace; it scaffolds a new standalone
190
216
  workspace rather than adding an app to the current one.
191
217
 
192
218
  When the source came from a fresh external Project and the target is a Builder
193
- workspace, prefer the available Dispatch MCP workspace handoff after writing
194
- the source brief. Call `start-workspace-app-creation` with a concise prompt,
195
- the inferred app id, and the brief's repeatable workflow. Pass selected
196
- workspace resource IDs when they are available, rather than pasting entire
197
- knowledge files. Continue from the returned workspace path or Builder handoff
198
- and report the actual verification result.
219
+ workspace, the online host path is mandatory: after writing the source brief,
220
+ call `start-workspace-app-creation` with a concise prompt, the inferred app id,
221
+ and the brief's repeatable workflow. Pass selected workspace resource IDs when
222
+ they are available, rather than pasting entire knowledge files. Continue from
223
+ the returned workspace path or Builder branch handoff and report the actual
224
+ verification result. Do not run the local scaffold commands in this host mode.
199
225
 
200
226
  Use a first-party template only when it materially fits the workflow. Keep the
201
227
  new app independent from the source thread's working tree unless the user
@@ -51,13 +51,24 @@ also place the skill instructions or this reference in the Project
51
51
  instructions/knowledge files. Then add and authenticate the Dispatch MCP
52
52
  connector from the packaged `adapters/chatgpt-mcp/connector.json`.
53
53
 
54
+ For Claude Web, ChatGPT Web, and their web Projects, the host is an
55
+ orchestrator, not the build environment. After forming the source brief, call
56
+ the Dispatch `start-workspace-app-creation` action so Builder creates the app
57
+ in the connected workspace. Do not use the host's code interpreter, shell,
58
+ artifact editor, `create_workspace_app`, or local `pnpm`/`npm` commands. If the
59
+ handoff action is unavailable, stop and ask the user to authenticate Dispatch;
60
+ do not fall back to a local sandbox build. Claude Code and Codex Code are the
61
+ local-agent exception and may follow the implementation steps in the main
62
+ skill.
63
+
54
64
  In a new Project chat, say:
55
65
 
56
66
  ```text
57
67
  Turn this project into an app. Use the visible Project instructions,
58
68
  knowledge files, and any selected successful runs as the source. Create the
59
- app in the connected Agent-Native workspace, keep the source brief bounded,
60
- and report the real path and verification result.
69
+ app in the connected Agent-Native workspace through Dispatch and Builder, keep
70
+ the source brief bounded, and report the real Builder branch/path and
71
+ verification result. Do not build it in this chat's sandbox.
61
72
  ```
62
73
 
63
74
  If the host does not show the Project instructions or files to the model, stop