@graph8/sdk 0.303.0 → 0.357.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 (664) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +2220 -23
  390. package/dist/index.d.ts +2220 -23
  391. package/dist/index.js +64 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +64 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +2082 -20
  400. package/dist/react.d.ts +2082 -20
  401. package/dist/react.js +63 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +63 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +7 -3
  611. package/docs/api/appObjects.d.ts +3 -1
  612. package/docs/api/appObjects.md +2 -1
  613. package/docs/api/by-path.md +51 -0
  614. package/docs/api/dataPipelines.d.ts +149 -0
  615. package/docs/api/dataPipelines.md +232 -0
  616. package/docs/api/forms.d.ts +16 -0
  617. package/docs/api/forms.md +8 -0
  618. package/docs/api/objects.d.ts +705 -5
  619. package/docs/api/objects.md +1099 -14
  620. package/docs/api/ops.d.ts +1 -1
  621. package/docs/api/ops.md +1 -1
  622. package/docs/api/processLists.d.ts +247 -0
  623. package/docs/api/processLists.md +409 -0
  624. package/docs/api/quotes.d.ts +3 -1
  625. package/docs/api/quotes.md +7 -2
  626. package/docs/api/radar.d.ts +4 -0
  627. package/docs/api/radar.md +1 -0
  628. package/docs/api/reports.d.ts +476 -0
  629. package/docs/api/reports.md +539 -0
  630. package/docs/api/scheduleBuy.d.ts +12 -8
  631. package/docs/api/scheduleBuy.md +13 -10
  632. package/docs/api/seo.d.ts +406 -3
  633. package/docs/api/seo.md +505 -1
  634. package/docs/api/workspaceBackup.d.ts +324 -0
  635. package/docs/api/workspaceBackup.md +427 -0
  636. package/package.json +174 -5
  637. package/stacks/6sense.json +46 -0
  638. package/stacks/README.md +20 -0
  639. package/stacks/apollo.json +128 -0
  640. package/stacks/clay.json +59 -0
  641. package/stacks/gong.json +72 -0
  642. package/stacks/hubspot.json +162 -0
  643. package/stacks/intercom.json +53 -0
  644. package/stacks/mailchimp.json +71 -0
  645. package/stacks/outreach.json +86 -0
  646. package/stacks/salesforce.json +94 -0
  647. package/stacks/stack.schema.json +32 -0
  648. package/stacks/starter.json +22 -0
  649. package/styles/styles.css +2 -0
  650. package/styles/theme.css +181 -0
  651. package/styles/tokens.css +284 -0
  652. package/templates/next-app/README.md.tmpl +35 -0
  653. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  654. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  655. package/templates/next-app/app/globals.css.tmpl +8 -0
  656. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  657. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  658. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  659. package/templates/next-app/dot-gitignore.tmpl +7 -0
  660. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  661. package/templates/next-app/next.config.mjs.tmpl +4 -0
  662. package/templates/next-app/package.json.tmpl +25 -0
  663. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  664. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SaveToListDialog: save everything a search matches into a NEW list. This is also how locked
5
+ * ("***") emails and phones are revealed: contacts that were masked in the search results are
6
+ * real, unlocked records once they sit in a list.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. saveContactSearch | saveCompanySearch POST /search/contacts/save | /search/companies/save (billable)
10
+ * { filters, list_title, max_results } -> 202 { list_id, list_title, estimated_total, status: "processing" }
11
+ * Goes through ConfirmSpendDialog first. Answers 402 when a trial's contact cap is reached.
12
+ * 2. listLists GET /lists?search=<list_title> (read)
13
+ * polled with useJobPoll until the list with that list_id is `completed` or `failed`
14
+ * (the save runs in the background; there is no stream to wait on).
15
+ *
16
+ * The API can only save into a new list. To add chosen people to an existing list, save first and
17
+ * then use AddToListButton from the lists block on the saved list's members.
18
+ */
19
+
20
+ import { useState } from "react";
21
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label } from "@graph8/sdk/ui";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
26
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
27
+ import { SEARCH_WINDOW, completeFilters } from "../lib/search-types";
28
+ import type { SearchCondition, SearchResource } from "../lib/search-types";
29
+
30
+ export interface SavedListInfo {
31
+ id: number;
32
+ title: string;
33
+ status?: string | null;
34
+ total?: number;
35
+ contact_count?: number | null;
36
+ company_count?: number | null;
37
+ status_detail?: string | null;
38
+ }
39
+
40
+ export interface SaveToListDialogProps {
41
+ open: boolean;
42
+ onOpenChange: (open: boolean) => void;
43
+ resource?: SearchResource;
44
+ filters: SearchCondition[];
45
+ /** What the search reported as matching; used for the default size and the confirmation. */
46
+ estimatedTotal?: number;
47
+ defaultTitle?: string;
48
+ /** Credits one saved record costs on your plan, if you know it; shows an estimate in the confirmation. */
49
+ creditsPerRecord?: number;
50
+ /** Called once the list finished building. Route to your ContactTable listId={list.id}. */
51
+ onSaved?: (list: SavedListInfo) => void;
52
+ }
53
+
54
+ interface SaveResult { list_id: number; list_title: string; estimated_total: number; status: string }
55
+
56
+ export function SaveToListDialog({ open, onOpenChange, resource = "contacts", filters, estimatedTotal, defaultTitle, creditsPerRecord, onSaved }: SaveToListDialogProps) {
57
+ const [title, setTitle] = useState(defaultTitle ?? "");
58
+ const [max, setMax] = useState(String(Math.max(1, Math.min(estimatedTotal ?? 1000, 1000))));
59
+ const [confirmOpen, setConfirmOpen] = useState(false);
60
+ const [saved, setSaved] = useState<SaveResult | null>(null);
61
+ // Seed the size from the match count each time the dialog opens, never leave it at 0.
62
+ const [seeded, setSeeded] = useState(open);
63
+ if (open && !seeded) { setSeeded(true); setMax(String(Math.max(1, Math.min(estimatedTotal ?? 1000, 1000)))); }
64
+ if (!open && seeded) setSeeded(false);
65
+
66
+ const count = Math.max(1, Math.min(Number(max) || 0, SEARCH_WINDOW));
67
+ const effective = Math.min(count, estimatedTotal ?? count);
68
+ const valid = title.trim().length > 0 && Number(max) >= 1 && Number(max) <= SEARCH_WINDOW && completeFilters(filters).length > 0;
69
+
70
+ const save = useG8Mutation(
71
+ () => g8Post<SaveResult>(`/search/${resource}/save`, { filters: completeFilters(filters), list_title: title.trim(), max_results: count }),
72
+ { onSuccess: (res) => { setConfirmOpen(false); setSaved(res.data); } },
73
+ );
74
+ const progress = useJobPoll<SavedListInfo | null>(
75
+ async (signal) => {
76
+ const res = await g8Get<SavedListInfo[]>("/lists", { search: saved!.list_title, limit: 50 }, { signal });
77
+ return ((res.data ?? []) as SavedListInfo[]).find((l) => l.id === saved!.list_id) ?? null;
78
+ },
79
+ { enabled: saved != null, intervalMs: 2000, isTerminal: (l) => l?.status === "completed" || l?.status === "failed" },
80
+ );
81
+
82
+ const list = progress.status;
83
+ const failed = list?.status === "failed";
84
+ const finished = progress.done && !failed;
85
+ const close = () => { onOpenChange(false); setSaved(null); setConfirmOpen(false); save.reset(); };
86
+
87
+ return (
88
+ <Dialog open={open} onOpenChange={(o) => (o ? onOpenChange(true) : close())}>
89
+ <DialogContent>
90
+ <DialogHeader>
91
+ <DialogTitle>Save results to a new list</DialogTitle>
92
+ <DialogDescription>Saving adds the matches to your CRM and unlocks their emails and phones.</DialogDescription>
93
+ </DialogHeader>
94
+
95
+ {saved ? (
96
+ <div role="status" className="flex flex-col gap-1 text-sm">
97
+ {failed ? <p className="font-medium text-destructive">The list could not be built{list?.status_detail ? `: ${list.status_detail}` : "."}</p>
98
+ : finished ? <p className="font-medium">Saved {(list?.contact_count ?? list?.company_count ?? list?.total ?? 0).toLocaleString()} {resource} to "{saved.list_title}".</p>
99
+ : <p>Building "{saved.list_title}"... about {saved.estimated_total.toLocaleString()} records. This keeps running if you close this window.</p>}
100
+ {progress.error && !progress.done ? <ApiError error={progress.error} title="Could not check progress" onRetry={() => progress.start()} /> : null}
101
+ </div>
102
+ ) : (
103
+ <div className="flex flex-col gap-3">
104
+ <div className="flex flex-col gap-1"><Label htmlFor="save-title">List name</Label><Input id="save-title" value={title} onChange={(e) => setTitle(e.target.value)} /></div>
105
+ <div className="flex flex-col gap-1">
106
+ <Label htmlFor="save-max">How many to save (up to {SEARCH_WINDOW.toLocaleString()})</Label>
107
+ <Input id="save-max" inputMode="numeric" value={max} onChange={(e) => setMax(e.target.value)} />
108
+ {estimatedTotal != null ? <p className="text-xs text-muted-foreground">The search matches about {estimatedTotal.toLocaleString()}.</p> : null}
109
+ </div>
110
+ {save.error ? <ApiError error={save.error} title="Could not start the save" /> : null}
111
+ </div>
112
+ )}
113
+
114
+ <DialogFooter>
115
+ <Button variant="ghost" onClick={close}>{saved ? (finished || failed ? "Done" : "Close") : "Cancel"}</Button>
116
+ {!saved ? <Button disabled={!valid} onClick={() => setConfirmOpen(true)}>Save {effective.toLocaleString()} {resource}</Button> : null}
117
+ {finished && list && onSaved ? <Button onClick={() => { onSaved(list); close(); }}>Open list</Button> : null}
118
+ </DialogFooter>
119
+
120
+ <ConfirmSpendDialog
121
+ open={confirmOpen}
122
+ tier="billable"
123
+ title={`Save up to ${effective.toLocaleString()} ${resource} to "${title.trim()}"`}
124
+ description="The list is created in your workspace and filled in the background. Revealing contact details can use credits on your plan."
125
+ estimatedCredits={creditsPerRecord != null ? effective * creditsPerRecord : undefined}
126
+ affected={effective}
127
+ confirmLabel="Save"
128
+ confirming={save.loading}
129
+ onConfirm={() => void save.mutate(undefined)}
130
+ onCancel={() => setConfirmOpen(false)}
131
+ />
132
+ </DialogContent>
133
+ </Dialog>
134
+ );
135
+ }
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SavedSearches: keep searches you run often. Save the current filters under a name, load one back
5
+ * into FilterBuilder, delete the ones you no longer need.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listSavedSearches GET /search/saved-searches?scope=<resource> (read) the key's OWN saved searches
9
+ * 2. saveSearch POST /search/saved-searches { title, scope, icp_query } (write)
10
+ * 3. deleteSavedSearch DELETE /search/saved-searches/{query_id} (destructive, ConfirmSpendDialog)
11
+ *
12
+ * What is stored: `icp_query = { format: "g8-sdk-v1", resource, filters }` with the PUBLIC filter names, so a
13
+ * search saved here loads back here. Searches saved in the graph8 app use the app's own format; they are
14
+ * listed but cannot be loaded (shown as "Saved in the graph8 app").
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
24
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
25
+ import { completeFilters } from "../lib/search-types";
26
+ import type { SearchCondition, SearchResource } from "../lib/search-types";
27
+
28
+ export const SAVED_FORMAT = "g8-sdk-v1";
29
+
30
+ interface SavedSearch {
31
+ id: number;
32
+ title: string;
33
+ scope: string;
34
+ icp_query: { format?: string; filters?: SearchCondition[] } & Record<string, unknown>;
35
+ created_at?: string | null;
36
+ }
37
+
38
+ export interface SavedSearchesProps {
39
+ resource?: SearchResource;
40
+ /** The filters being edited now: what "Save current" stores. */
41
+ current: SearchCondition[];
42
+ /** Load a saved search into your FilterBuilder state (and usually run it). */
43
+ onLoad: (filters: SearchCondition[], saved: { id: number; title: string }) => void;
44
+ className?: string;
45
+ }
46
+
47
+ function loadable(s: SavedSearch): boolean {
48
+ return s.icp_query?.format === SAVED_FORMAT && Array.isArray(s.icp_query.filters) && s.icp_query.filters.length > 0;
49
+ }
50
+
51
+ export function SavedSearches({ resource = "contacts", current, onLoad, className }: SavedSearchesProps) {
52
+ const [title, setTitle] = useState("");
53
+ const [deleting, setDeleting] = useState<SavedSearch | null>(null);
54
+ const ready = completeFilters(current);
55
+
56
+ const list = useG8Query((signal) => g8Get<SavedSearch[]>("/search/saved-searches", { scope: resource }, { signal }), [resource]);
57
+ const save = useG8Mutation(
58
+ () => g8Post("/search/saved-searches", { title: title.trim(), scope: resource, icp_query: { format: SAVED_FORMAT, resource, filters: ready } }),
59
+ { onSuccess: () => { setTitle(""); void list.refetch(); } },
60
+ );
61
+ const remove = useG8Mutation((id: number) => g8Delete(`/search/saved-searches/${id}`), { onSuccess: () => { setDeleting(null); void list.refetch(); } });
62
+
63
+ const items = (list.data?.data ?? []) as SavedSearch[];
64
+ return (
65
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
66
+ <form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (title.trim() && ready.length) void save.mutate(undefined); }}>
67
+ <Input aria-label="Name this search" placeholder="Name this search" value={title} onChange={(e) => setTitle(e.target.value)} />
68
+ <Button type="submit" size="sm" variant="outline" loading={save.loading} disabled={!title.trim() || ready.length === 0}>Save current</Button>
69
+ </form>
70
+ {save.error ? <ApiError error={save.error} title="Could not save the search" /> : null}
71
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the search" /> : null}
72
+ {list.error ? <ApiError error={list.error} title="Could not load saved searches" onRetry={() => void list.refetch()} /> : null}
73
+ {list.loading && !list.data ? <Skeleton className="h-10 w-full" /> : null}
74
+ {!list.loading && !list.error && items.length === 0 ? <EmptyState title="No saved searches" description="Save the filters you use often." /> : null}
75
+
76
+ <ul className="flex flex-col gap-2">
77
+ {items.map((s) => (
78
+ <li key={s.id} className="flex items-center justify-between gap-2 rounded-md border p-3" data-testid="saved-search">
79
+ <div className="min-w-0">
80
+ <p className="truncate text-sm font-medium">{s.title}</p>
81
+ <p className="text-xs text-muted-foreground">{loadable(s) ? `${s.icp_query.filters!.length} filter${s.icp_query.filters!.length === 1 ? "" : "s"}` : "Saved in the graph8 app"}</p>
82
+ </div>
83
+ <div className="flex gap-1">
84
+ <Button size="sm" variant="outline" disabled={!loadable(s)} onClick={() => onLoad(s.icp_query.filters!, { id: s.id, title: s.title })}>Load</Button>
85
+ <Button size="sm" variant="ghost" onClick={() => setDeleting(s)}>Delete</Button>
86
+ </div>
87
+ </li>
88
+ ))}
89
+ </ul>
90
+
91
+ <ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.title ?? ""}"`} description="Lists already built from this search are not affected."
92
+ affected={1} confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
4
+ import { g8Get } from "@/g8/lib/g8-client";
5
+ import type { SearchResource } from "../lib/search-types";
6
+
7
+ export interface FilterOption {
8
+ /** Normalised value: send this back as the filter value. */
9
+ id: string;
10
+ label: string;
11
+ count?: number | null;
12
+ }
13
+
14
+ const cache = new Map<string, FilterOption[]>();
15
+
16
+ /**
17
+ * The distinct values a field holds (`GET /search/filter-options`), so a filter can offer real
18
+ * choices instead of guessed strings (a guessed industry silently returns zero rows). Cached per
19
+ * resource and field for the life of the page. `enabled: false` makes no request.
20
+ */
21
+ export function useFilterOptions(resource: SearchResource, field: string, enabled = true) {
22
+ const key = `${resource}:${field}`;
23
+ const query = useG8Query(
24
+ async (signal) => {
25
+ const hit = cache.get(key);
26
+ if (hit) return hit;
27
+ const res = await g8Get<Record<string, FilterOption[]>>("/search/filter-options", { resource, fields: [field] }, { signal });
28
+ const options = res.data?.[field] ?? [];
29
+ cache.set(key, options);
30
+ return options;
31
+ },
32
+ [key],
33
+ { enabled },
34
+ );
35
+ return { options: query.data ?? [], loading: query.loading, error: query.error };
36
+ }
37
+
38
+ /** Test hook: forget cached option lists. */
39
+ export function clearFilterOptionsCache(): void {
40
+ cache.clear();
41
+ }
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Filter vocabulary and result shapes for prospect search. Field names are the PUBLIC ones
5
+ * `POST /search/contacts` and `POST /search/companies` take (job_title, company_industry...), not the
6
+ * app's raw index names.
7
+ *
8
+ * Masked results: search sends "***" in place of a locked email/phone and lists the field in
9
+ * `locked_fields`. `isLocked` is the one check every component uses.
10
+ */
11
+
12
+ export type SearchResource = "contacts" | "companies";
13
+ export type SearchOperator = "any_of" | "contains" | "all_of" | "none_of" | "is_empty" | "is_not_empty" | "between" | "exists";
14
+
15
+ export interface SearchCondition {
16
+ field: string;
17
+ operator: SearchOperator;
18
+ value?: unknown[];
19
+ }
20
+
21
+ export type FieldKind = "text" | "enum" | "number" | "presence";
22
+ /** Where a value picker gets its suggestions: the fixed value set of the field, or type-ahead. */
23
+ export type Suggest = "options" | "autocomplete" | "none";
24
+
25
+ export interface FieldMeta {
26
+ key: string;
27
+ label: string;
28
+ kind: FieldKind;
29
+ suggest: Suggest;
30
+ }
31
+
32
+ export const CONTACT_FIELDS: FieldMeta[] = [
33
+ { key: "job_title", label: "Job title", kind: "text", suggest: "autocomplete" },
34
+ { key: "seniority_level", label: "Seniority", kind: "enum", suggest: "options" },
35
+ { key: "job_department", label: "Department", kind: "enum", suggest: "options" },
36
+ { key: "role", label: "Role", kind: "text", suggest: "none" },
37
+ { key: "skills", label: "Skills", kind: "text", suggest: "none" },
38
+ { key: "country", label: "Country", kind: "enum", suggest: "options" },
39
+ { key: "state", label: "State", kind: "text", suggest: "none" },
40
+ { key: "city", label: "City", kind: "text", suggest: "none" },
41
+ { key: "company_name", label: "Company name", kind: "text", suggest: "autocomplete" },
42
+ { key: "company_domain", label: "Company domain", kind: "text", suggest: "autocomplete" },
43
+ { key: "company_industry", label: "Industry", kind: "enum", suggest: "options" },
44
+ { key: "company_employee_count", label: "Company size", kind: "enum", suggest: "options" },
45
+ { key: "company_country", label: "Company country", kind: "enum", suggest: "options" },
46
+ { key: "company_revenue", label: "Company revenue", kind: "text", suggest: "none" },
47
+ { key: "company_founded_year", label: "Company founded", kind: "number", suggest: "none" },
48
+ { key: "work_email", label: "Has work email", kind: "presence", suggest: "none" },
49
+ { key: "direct_phone", label: "Has direct phone", kind: "presence", suggest: "none" },
50
+ { key: "mobile_phone", label: "Has mobile phone", kind: "presence", suggest: "none" },
51
+ { key: "linkedin_url", label: "Has LinkedIn", kind: "presence", suggest: "none" },
52
+ ];
53
+
54
+ export const COMPANY_FIELDS: FieldMeta[] = [
55
+ { key: "name", label: "Name", kind: "text", suggest: "autocomplete" },
56
+ { key: "domain", label: "Domain", kind: "text", suggest: "autocomplete" },
57
+ { key: "industry", label: "Industry", kind: "enum", suggest: "options" },
58
+ { key: "industry_group", label: "Industry group", kind: "enum", suggest: "options" },
59
+ { key: "employee_count", label: "Employees", kind: "enum", suggest: "options" },
60
+ { key: "revenue", label: "Revenue", kind: "text", suggest: "none" },
61
+ { key: "founded_year", label: "Founded", kind: "number", suggest: "none" },
62
+ { key: "country", label: "Country", kind: "enum", suggest: "options" },
63
+ { key: "state", label: "State", kind: "text", suggest: "none" },
64
+ { key: "city", label: "City", kind: "text", suggest: "none" },
65
+ ];
66
+
67
+ export const OPERATORS_BY_KIND: Record<FieldKind, SearchOperator[]> = {
68
+ text: ["any_of", "contains", "all_of", "none_of", "is_empty", "is_not_empty"],
69
+ enum: ["any_of", "none_of", "is_empty", "is_not_empty"],
70
+ number: ["between", "any_of", "is_empty", "is_not_empty"],
71
+ presence: ["exists", "is_empty"],
72
+ };
73
+
74
+ export const OPERATOR_LABEL: Record<SearchOperator, string> = {
75
+ any_of: "is any of", contains: "contains", all_of: "has all of", none_of: "is none of",
76
+ is_empty: "is empty", is_not_empty: "is not empty", between: "is between", exists: "exists",
77
+ };
78
+
79
+ export const NO_VALUE_OPERATORS: SearchOperator[] = ["is_empty", "is_not_empty", "exists"];
80
+
81
+ export function fieldsFor(resource: SearchResource): FieldMeta[] {
82
+ return resource === "contacts" ? CONTACT_FIELDS : COMPANY_FIELDS;
83
+ }
84
+
85
+ /** A condition the API will accept: value-less operators need no value, `between` needs two, others at least one. */
86
+ export function isComplete(c: SearchCondition): boolean {
87
+ if (NO_VALUE_OPERATORS.includes(c.operator)) return true;
88
+ const values = (c.value ?? []).filter((v) => String(v).trim() !== "");
89
+ return c.operator === "between" ? values.length === 2 : values.length > 0;
90
+ }
91
+
92
+ export function completeFilters(filters: SearchCondition[]): SearchCondition[] {
93
+ return filters.filter(isComplete);
94
+ }
95
+
96
+ export interface SearchPagination {
97
+ page: number;
98
+ limit: number;
99
+ total: number;
100
+ has_next: boolean;
101
+ total_available?: number | null;
102
+ total_is_lower_bound?: boolean | null;
103
+ notes?: string[] | null;
104
+ }
105
+
106
+ export interface ProspectContact {
107
+ first_name?: string | null;
108
+ last_name?: string | null;
109
+ work_email?: string | null;
110
+ personal_emails?: string | null;
111
+ direct_phone?: string | null;
112
+ mobile_phone?: string | null;
113
+ job_title?: string | null;
114
+ seniority_level?: string | null;
115
+ linkedin_url?: string | null;
116
+ city?: string | null;
117
+ state?: string | null;
118
+ country?: string | null;
119
+ company_name?: string | null;
120
+ company_domain?: string | null;
121
+ company_industry?: string | null;
122
+ locked_fields?: string[];
123
+ }
124
+
125
+ export interface ProspectCompany {
126
+ name?: string | null;
127
+ domain?: string | null;
128
+ industry?: string | null;
129
+ employee_count?: string | null;
130
+ revenue?: string | null;
131
+ city?: string | null;
132
+ state?: string | null;
133
+ country?: string | null;
134
+ linkedin_url?: string | null;
135
+ }
136
+
137
+ /** True when a value exists but is locked ("***", or named in `locked_fields`) until the contact is saved to a list. */
138
+ export function isLocked(item: ProspectContact, field: keyof ProspectContact): boolean {
139
+ const v = item[field];
140
+ return item.locked_fields?.includes(field as string) === true || (typeof v === "string" && v.trim() === "***");
141
+ }
142
+
143
+ /** Never put the placeholder on screen as a value. */
144
+ export function visible(item: ProspectContact, field: keyof ProspectContact): string {
145
+ return isLocked(item, field) ? "" : String(item[field] ?? "");
146
+ }
147
+
148
+ /** The most records one search can page through (page x limit). */
149
+ export const SEARCH_WINDOW = 10_000;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @title Look up valid filter values before searching
3
+ * @when Writing a filter on a field with a fixed value set (industry, seniority, country, company size) or a
4
+ * name you have to spell exactly (company, domain). A guessed value returns zero rows, indistinguishable from a
5
+ * correct filter that matches nothing. The `id` of each option is exactly what the search expects as a filter value.
6
+ */
7
+ import { g8 } from "@graph8/sdk";
8
+
9
+ g8.init({ apiKey: process.env.G8_API_KEY! });
10
+
11
+ export async function industryOptions() {
12
+ const res = await g8.api.search.getFilterOptions({ query: { resource: "contacts", fields: ["company_industry", "seniority_level"] } });
13
+ return res.data; // { company_industry: [{ id, label, count }], seniority_level: [...] }
14
+ }
15
+
16
+ export async function suggestCompanies(typed: string) {
17
+ const res = await g8.api.search.getAutocompleteSuggestions({ query: { resource: "contacts", field: "company_name", value: typed, size: 8 } });
18
+ return res.data;
19
+ }
20
+
21
+ /** What exists among a subset: industries among companies in Spain (needs at least one filter). */
22
+ export async function industriesIn(country: string) {
23
+ const res = await g8.api.search.getFilteredFilterOptions({
24
+ body: { resource: "companies", fields: ["industry"], filters: [{ field: "country", operator: "any_of", value: [country] }] },
25
+ });
26
+ return res.data;
27
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @title Save, list and delete saved searches
3
+ * @when A search is run again and again (a weekly ICP pull). A saved search belongs to the API key's own
4
+ * identity, not the whole org. `icp_query` is stored as-is: keep your own format marker so you can load it back.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+
10
+ export async function saveIcp() {
11
+ const saved = await g8.api.search.saveSearch({
12
+ body: {
13
+ title: "VP Sales at software companies",
14
+ scope: "contacts",
15
+ icp_query: {
16
+ format: "g8-sdk-v1",
17
+ resource: "contacts",
18
+ filters: [{ field: "job_title", operator: "any_of", value: ["VP Sales"] }],
19
+ },
20
+ },
21
+ });
22
+ return saved.data;
23
+ }
24
+
25
+ export async function mySearches() {
26
+ const res = await g8.api.search.listSavedSearches({ query: { scope: "contacts" } });
27
+ return res.data;
28
+ }
29
+
30
+ /** Soft delete; lists already built from the search are untouched. */
31
+ export async function remove(queryId: number) {
32
+ const res = await g8.api.search.deleteSavedSearch({ path: { query_id: queryId } });
33
+ return res.data;
34
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @title Preview a prospect search, save it to a list and read the unlocked contacts
3
+ * @when Building an audience from the open-data index. Search results carry no contact id and mask
4
+ * locked emails and phones as "***" (named in `locked_fields`): never treat "***" as a real email. Order:
5
+ * search (preview) -> save to a new list -> wait for the list to finish building -> read the list's contacts,
6
+ * which are real, unlocked records with ids -> enrich/verify -> enroll. Saving can spend credits, so ask a human first.
7
+ */
8
+ import { g8 } from "@graph8/sdk";
9
+
10
+ g8.init({ apiKey: process.env.G8_API_KEY! });
11
+
12
+ const FILTERS = [
13
+ { field: "job_title", operator: "any_of" as const, value: ["VP Sales", "Head of Sales"] },
14
+ { field: "company_industry", operator: "any_of" as const, value: ["computer software"] },
15
+ ];
16
+
17
+ export async function preview() {
18
+ const { data, pagination } = await g8.search.contacts({ filters: FILTERS, page: 1, limit: 25 });
19
+ const usable = data.filter((c) => c.work_email && c.work_email !== "***" && !c.locked_fields?.includes("work_email"));
20
+ return { matches: pagination?.total_available ?? pagination?.total, withVisibleEmail: usable.length, sample: data.slice(0, 3) };
21
+ }
22
+
23
+ /** Only after a person approved the spend. The API saves into a NEW list; it answers 202 and builds in the background. */
24
+ export async function saveAndRead(listTitle: string, maxResults = 200) {
25
+ const started = await g8.search.saveContacts({ filters: FILTERS, list_title: listTitle, max_results: maxResults });
26
+
27
+ let status = started.status;
28
+ for (let i = 0; i < 60 && status === "processing"; i++) {
29
+ await new Promise((r) => setTimeout(r, 2000));
30
+ const lists = await g8.api.lists.listLists({ query: { search: listTitle, limit: 50 } });
31
+ status = (lists.data.find((l) => l.id === started.list_id)?.status as string | undefined) ?? status;
32
+ }
33
+ if (status !== "completed") throw new Error(`list ${started.list_id} is ${status}`);
34
+
35
+ const members = await g8.lists.contacts(started.list_id, 1, 100);
36
+ return { listId: started.list_id, total: members.total, contactIds: members.data.map((c) => c.id) };
37
+ }
@@ -0,0 +1,87 @@
1
+ # sequencer
2
+
3
+ Build multi-channel outreach (email, LinkedIn, calls), launch it, enroll contacts, and watch results. The
4
+ Apollo / Outreach "sequences" screen.
5
+
6
+ Depends on: `lists` (the audience) and `mailboxes` (the sender). Install: `npx @graph8/sdk add sequencer`.
7
+
8
+ ## Components
9
+
10
+ | Component | What it does | Tiers |
11
+ |---|---|---|
12
+ | `SequenceList` | Sequences with status. Launch (after a copy preview), pause, resume, duplicate, archive. | read, write, external, destructive |
13
+ | `SequenceBuilder` | Build a draft (email, LinkedIn, call, call task + a sender), or edit an existing sequence's steps. | read, write, external, destructive |
14
+ | `StepEditor` | One step's form, with the rules the API only enforces at send time. | read |
15
+ | `EnrollDialog` | Confirm and enroll contacts, with the recipient count and a deliverability warning. | read, external |
16
+ | `SequenceStats` | Totals, per-step progress, skip reasons, per-contact state with pause and resume. | read, write, external |
17
+
18
+ Launch, pause, resume, enroll and step edits reach or change what real people receive, so each opens
19
+ `ConfirmSpendDialog` first (with the recipient count where it applies). Archive is destructive and the API cannot
20
+ un-archive.
21
+
22
+ ## The flow, in order
23
+
24
+ 1. **Pick a sender and an audience.** `SequenceBuilder` -> `listMailboxes` (the chosen mailbox `id` and `email`
25
+ become the sequence channel) and `listLists` (the list `id` becomes `associated_list_id`).
26
+ 2. **Create the draft.** `createSequence` with all steps and the channel in one call (confirmed; nobody is contacted).
27
+ It returns the sequence `id`.
28
+ 3. **Check the copy.** `SequenceList` -> `previewSequence` before launch: `will_send_as_shown` and `content_issues`
29
+ catch broken tokens that would otherwise be deleted silently or block the launch with a 400.
30
+ 4. **Launch.** `runSequence` (confirmed with the contact count). Draft -> live.
31
+ 5. **Enroll more contacts.** `EnrollDialog` -> `addContactsToSequence` with `contact_ids` AND the `list_id` they
32
+ came from (both required). Already-enrolled contacts are skipped, so a retry never duplicates.
33
+ 6. **Watch.** `SequenceStats` -> `getSequenceStats`, `getSequenceAnalytics`, `listSequenceContacts`. Pause one
34
+ contact (`pauseSequenceContact`, write) or resume them (`resumeSequenceContact`, confirmed).
35
+
36
+ ## Step rules (why `StepEditor` exists)
37
+
38
+ - **Email**: use `input_type: "MANUAL_TEMPLATE"` for your own subject and body. `ON_DEMAND` ignores them (the AI
39
+ writes each email from `instructions`).
40
+ - **LinkedIn** is the `HEYREACH` step type: `linkedin_action_type` is required, and the copy goes in that action's
41
+ own key (`linkedin_connection_message`, `linkedin_message_text`, `linkedin_inmail_body`), never `body`.
42
+ - **Call** is `PHONE`; **call task** is `MANUAL_DIALER` (a person works it in the dialer). Do-not-call numbers are never dialed by default.
43
+ - `time_interval` is in seconds (the UI works in days). A step's `step_data` is replaced wholesale on update, so the
44
+ editor always sends the complete object.
45
+ - Search results are masked until saved to a list: enroll contacts that already live in a list.
46
+
47
+ ## Prerequisites
48
+
49
+ - A connected sending mailbox (`where: "api"`). Google and Microsoft are connected in the graph8 app.
50
+ - An audience list with the contacts (`where: "api"`).
51
+ - A LinkedIn sender account for LinkedIn steps (`where: "app"`): a sequence with LinkedIn steps and no sender cannot launch.
52
+
53
+ ## Defaults (do not ask the human)
54
+
55
+ - Two days between steps; stop on reply; the person writes the email copy.
56
+ - **Ask the human**: who to enroll and when to launch. Never launch or enroll without approval of the count.
57
+ - SMS and WhatsApp steps exist in the API (`SMS`, `WHATSAPP`, `message_body`) but this block's builder covers email, LinkedIn, call and call task.
58
+
59
+ ## Operations
60
+
61
+ | Operation | Method and path | Tier | Used by |
62
+ |---|---|---|---|
63
+ | `list_sequences_sequences_get` | GET /sequences | read | SequenceList |
64
+ | `preview_sequence_sequences__sequence_id__preview_get` | GET /sequences/{sequence_id}/preview | read | SequenceList |
65
+ | `run_sequence_sequences__sequence_id__run_post` | POST /sequences/{sequence_id}/run | external | SequenceList |
66
+ | `pause_sequence_sequences__sequence_id__pause_post` | POST /sequences/{sequence_id}/pause | external | SequenceList |
67
+ | `resume_sequence_sequences__sequence_id__resume_post` | POST /sequences/{sequence_id}/resume | external | SequenceList |
68
+ | `duplicate_sequence_sequences__sequence_id__duplicate_post` | POST /sequences/{sequence_id}/duplicate | write | SequenceList |
69
+ | `archive_sequence_sequences__sequence_id__archive_post` | POST /sequences/{sequence_id}/archive | destructive | SequenceList |
70
+ | `list_mailboxes_mailboxes_get` | GET /mailboxes | read | SequenceBuilder |
71
+ | `list_lists_lists_get` | GET /lists | read | SequenceBuilder |
72
+ | `create_sequence_sequences_post` | POST /sequences | external | SequenceBuilder |
73
+ | `list_sequence_steps_sequences__sequence_id__steps_get` | GET /sequences/{sequence_id}/steps | read | SequenceBuilder |
74
+ | `get_sequence_sequences__sequence_id__get` | GET /sequences/{sequence_id} | read | SequenceBuilder, EnrollDialog |
75
+ | `add_sequence_steps_sequences__sequence_id__steps_post` | POST /sequences/{sequence_id}/steps | write | SequenceBuilder |
76
+ | `update_sequence_step_sequences__sequence_id__steps__step_id__patch` | PATCH /sequences/{sequence_id}/steps/{step_id} | external | SequenceBuilder |
77
+ | `delete_sequence_step_sequences__sequence_id__steps__step_id__delete` | DELETE /sequences/{sequence_id}/steps/{step_id} | destructive | SequenceBuilder |
78
+ | `get_outreach_connection_integrations_linkedin_outreach_connection_get` | GET /integrations/linkedin-outreach/connection | read | StepEditor |
79
+ | `get_safe_to_send_deliverability_safe_to_send_get` | GET /deliverability/safe-to-send | read | EnrollDialog |
80
+ | `add_contacts_to_sequence_sequences__sequence_id__contacts_post` | POST /sequences/{sequence_id}/contacts | external | EnrollDialog |
81
+ | `get_sequence_stats_sequences__sequence_id__stats_get` | GET /sequences/{sequence_id}/stats | read | SequenceStats |
82
+ | `get_sequence_analytics_sequences__sequence_id__analytics_get` | GET /sequences/{sequence_id}/analytics | read | SequenceStats |
83
+ | `list_sequence_contacts_sequences__sequence_id__contacts_get` | GET /sequences/{sequence_id}/contacts | read | SequenceStats |
84
+ | `pause_sequence_contact_sequences__sequence_id__contacts__contact_id__pause_post` | POST /sequences/{sequence_id}/contacts/{contact_id}/pause | write | SequenceStats |
85
+ | `resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post` | POST /sequences/{sequence_id}/contacts/{contact_id}/resume | external | SequenceStats |
86
+
87
+ Recipes (`recipes/`): `create-multichannel-sequence.ts`, `launch-and-enroll.ts`, `sequence-report.ts`, `pause-when-unsafe.ts`.