@method-ai-hq/sdk 0.0.0-stage → 0.14.3

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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -2
  3. package/dist/packages/contracts/src/identity.d.ts +16 -0
  4. package/dist/packages/contracts/src/identity.js +41 -0
  5. package/dist/packages/contracts/src/method-package.d.ts +71 -0
  6. package/dist/packages/contracts/src/method-package.js +22 -0
  7. package/dist/packages/method-document/src/conversion-report.d.ts +138 -0
  8. package/dist/packages/method-document/src/conversion-report.js +23 -0
  9. package/dist/packages/sdk/examples/daily-briefing/README.md +31 -0
  10. package/dist/packages/sdk/examples/daily-briefing/TASK.md +3 -0
  11. package/dist/packages/sdk/examples/daily-briefing/approved-report.md +138 -0
  12. package/dist/packages/sdk/examples/daily-briefing/briefing_artifacts.py +39 -0
  13. package/dist/packages/sdk/examples/daily-briefing/briefing_check_inputs.py +47 -0
  14. package/dist/packages/sdk/examples/daily-briefing/briefing_files.py +31 -0
  15. package/dist/packages/sdk/examples/daily-briefing/briefing_manifest.py +23 -0
  16. package/dist/packages/sdk/examples/daily-briefing/briefing_markdown.py +67 -0
  17. package/dist/packages/sdk/examples/daily-briefing/briefing_progress.py +18 -0
  18. package/dist/packages/sdk/examples/daily-briefing/briefing_render.py +71 -0
  19. package/dist/packages/sdk/examples/daily-briefing/briefing_sessions.py +37 -0
  20. package/dist/packages/sdk/examples/daily-briefing/briefing_times.py +158 -0
  21. package/dist/packages/sdk/examples/daily-briefing/briefing_validation.py +101 -0
  22. package/dist/packages/sdk/examples/daily-briefing/briefing_website.py +215 -0
  23. package/dist/packages/sdk/examples/daily-briefing/checks.json +19 -0
  24. package/dist/packages/sdk/examples/daily-briefing/daily-briefing.method +158 -0
  25. package/dist/packages/sdk/examples/daily-briefing/inputs.json +1 -0
  26. package/dist/packages/sdk/examples/daily-briefing/package-lock.json +10 -0
  27. package/dist/packages/sdk/examples/daily-briefing/package.json +4 -0
  28. package/dist/packages/sdk/examples/daily-briefing/pyproject.toml +8 -0
  29. package/dist/packages/sdk/examples/daily-briefing/reader/reader.css +35 -0
  30. package/dist/packages/sdk/examples/daily-briefing/reader/reader.js +42 -0
  31. package/dist/packages/sdk/examples/daily-briefing/sample/context.json +1 -0
  32. package/dist/packages/sdk/examples/daily-briefing/sample/documents/document-001.md +219 -0
  33. package/dist/packages/sdk/examples/daily-briefing/sample/media.json +1 -0
  34. package/dist/packages/sdk/examples/daily-briefing/sample/metadata/documents.json +39 -0
  35. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0001.json +49 -0
  36. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0002.json +49 -0
  37. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0003.json +49 -0
  38. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0004.json +49 -0
  39. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0005.json +49 -0
  40. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0006.json +49 -0
  41. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0007.json +49 -0
  42. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0008.json +49 -0
  43. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0009.json +49 -0
  44. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0010.json +49 -0
  45. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0011.json +49 -0
  46. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0012.json +49 -0
  47. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0013.json +49 -0
  48. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0014.json +49 -0
  49. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0015.json +49 -0
  50. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0016.json +49 -0
  51. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0017.json +49 -0
  52. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0018.json +49 -0
  53. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0019.json +49 -0
  54. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0020.json +49 -0
  55. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0021.json +49 -0
  56. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0022.json +49 -0
  57. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0023.json +49 -0
  58. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0024.json +49 -0
  59. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0025.json +49 -0
  60. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0026.json +49 -0
  61. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0027.json +49 -0
  62. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0028.json +49 -0
  63. package/dist/packages/sdk/examples/daily-briefing/sample/sessions.json +1 -0
  64. package/dist/packages/sdk/examples/daily-briefing/sample/timeline.jsonl +28 -0
  65. package/dist/packages/sdk/examples/daily-briefing/sample/untimed.jsonl +0 -0
  66. package/dist/packages/sdk/examples/daily-briefing/sample-output.zip +0 -0
  67. package/dist/packages/sdk/examples/daily-briefing/sample-report.md +51 -0
  68. package/dist/packages/sdk/examples/daily-briefing/uv.lock +128 -0
  69. package/dist/packages/sdk/examples/daily-briefing/vendor/marked-LICENSE.md +44 -0
  70. package/dist/packages/sdk/examples/daily-briefing/vendor/marked.mjs +77 -0
  71. package/dist/packages/sdk/examples/files.json +126 -0
  72. package/dist/packages/sdk/examples/message-routing/README.md +23 -0
  73. package/dist/packages/sdk/examples/message-routing/TASK.md +5 -0
  74. package/dist/packages/sdk/examples/message-routing/choose-destination.mjs +10 -0
  75. package/dist/packages/sdk/examples/message-routing/create-ticket.mjs +15 -0
  76. package/dist/packages/sdk/examples/message-routing/inputs.json +1 -0
  77. package/dist/packages/sdk/examples/message-routing/message-routing.method +45 -0
  78. package/dist/packages/sdk/examples/message-routing/result.fixture.json +6 -0
  79. package/dist/packages/sdk/examples/message-routing/routing.mjs +29 -0
  80. package/dist/packages/sdk/examples/message-routing/routing.test.mjs +27 -0
  81. package/dist/packages/sdk/examples/message-routing/ticket.method +88 -0
  82. package/dist/packages/sdk/examples/message-routing/ticket.test.mjs +64 -0
  83. package/dist/packages/sdk/examples/notes-summary/README.md +23 -0
  84. package/dist/packages/sdk/examples/notes-summary/TASK.md +3 -0
  85. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/case.json +36 -0
  86. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/examples.json +16 -0
  87. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/files/0 +10 -0
  88. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/recording.json +195 -0
  89. package/dist/packages/sdk/examples/notes-summary/notes/2026-10-05-standup.md +4 -0
  90. package/dist/packages/sdk/examples/notes-summary/notes/2026-10-07-planning.md +3 -0
  91. package/dist/packages/sdk/examples/notes-summary/notes-summary.method +54 -0
  92. package/dist/packages/sdk/examples/notes-summary/out/.keep +0 -0
  93. package/dist/packages/sdk/examples/notes-summary/read-notes.mjs +4 -0
  94. package/dist/packages/sdk/examples/notes-summary/save.mjs +7 -0
  95. package/dist/packages/sdk/examples/outbound-management/README.md +119 -0
  96. package/dist/packages/sdk/examples/outbound-management/TASK.md +5 -0
  97. package/dist/packages/sdk/examples/outbound-management/check_plan.py +16 -0
  98. package/dist/packages/sdk/examples/outbound-management/check_sources.py +14 -0
  99. package/dist/packages/sdk/examples/outbound-management/crm.py +153 -0
  100. package/dist/packages/sdk/examples/outbound-management/crm_test.py +141 -0
  101. package/dist/packages/sdk/examples/outbound-management/fixtures/README.md +15 -0
  102. package/dist/packages/sdk/examples/outbound-management/fixtures/crm.json +54 -0
  103. package/dist/packages/sdk/examples/outbound-management/fixtures/day-1/messages.json +29 -0
  104. package/dist/packages/sdk/examples/outbound-management/fixtures/day-2/messages.json +38 -0
  105. package/dist/packages/sdk/examples/outbound-management/fixtures/observations.json +12 -0
  106. package/dist/packages/sdk/examples/outbound-management/fixtures/plan.json +69 -0
  107. package/dist/packages/sdk/examples/outbound-management/fixtures/receipt.json +1 -0
  108. package/dist/packages/sdk/examples/outbound-management/fixtures/state.json +99 -0
  109. package/dist/packages/sdk/examples/outbound-management/fixtures/tasks.md +16 -0
  110. package/dist/packages/sdk/examples/outbound-management/inputs.json +4 -0
  111. package/dist/packages/sdk/examples/outbound-management/outbound.method +277 -0
  112. package/dist/packages/sdk/examples/outbound-management/read_crm.py +4 -0
  113. package/dist/packages/sdk/examples/outbound-management/save_day.py +4 -0
  114. package/dist/packages/sdk/examples/outbound-management/starter/crm.json +11 -0
  115. package/dist/packages/sdk/examples/social-briefing/README.md +41 -0
  116. package/dist/packages/sdk/examples/social-briefing/TASK.md +5 -0
  117. package/dist/packages/sdk/examples/social-briefing/inputs.json +5 -0
  118. package/dist/packages/sdk/examples/social-briefing/result.fixture.json +14 -0
  119. package/dist/packages/sdk/examples/social-briefing/social-briefing.method +110 -0
  120. package/dist/packages/sdk/examples/support-triage/README.md +77 -0
  121. package/dist/packages/sdk/examples/support-triage/TASK.md +3 -0
  122. package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/case.json +40 -0
  123. package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json +460 -0
  124. package/dist/packages/sdk/examples/support-triage/fake-helpdesk.mjs +38 -0
  125. package/dist/packages/sdk/examples/support-triage/helpdesk.mjs +33 -0
  126. package/dist/packages/sdk/examples/support-triage/inputs.json +1 -0
  127. package/dist/packages/sdk/examples/support-triage/read-ticket.mjs +8 -0
  128. package/dist/packages/sdk/examples/support-triage/route.mjs +4 -0
  129. package/dist/packages/sdk/examples/support-triage/sample-tickets.json +5 -0
  130. package/dist/packages/sdk/examples/support-triage/set-ticket.mjs +9 -0
  131. package/dist/packages/sdk/examples/support-triage/support-triage.method +165 -0
  132. package/dist/packages/sdk/examples/support-triage/triage.mjs +38 -0
  133. package/dist/packages/sdk/examples/support-triage/triage.test.mjs +68 -0
  134. package/dist/packages/sdk/observers/mail.delivery/fetch.mjs +87 -0
  135. package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce-by-recipient.json +1 -0
  136. package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce.json +1 -0
  137. package/dist/packages/sdk/observers/mail.delivery/fixtures/delayed.json +1 -0
  138. package/dist/packages/sdk/observers/mail.delivery/fixtures/delivered.json +1 -0
  139. package/dist/packages/sdk/observers/mail.delivery/fixtures/empty.json +1 -0
  140. package/dist/packages/sdk/observers/mail.delivery/fixtures/other-message.json +1 -0
  141. package/dist/packages/sdk/observers/mail.delivery/judge.mjs +13 -0
  142. package/dist/packages/sdk/observers/mail.delivery/observer.json +14 -0
  143. package/dist/packages/sdk/package.json +232 -0
  144. package/dist/packages/sdk/src/authoring-example.d.ts +91 -0
  145. package/dist/packages/sdk/src/authoring-example.js +50 -0
  146. package/dist/packages/sdk/src/authoring-instructions.d.ts +20 -0
  147. package/dist/packages/sdk/src/authoring-instructions.js +151 -0
  148. package/dist/packages/sdk/src/authoring-prompt.d.ts +2 -0
  149. package/dist/packages/sdk/src/authoring-prompt.js +8 -0
  150. package/dist/packages/sdk/src/authoring.d.ts +10 -0
  151. package/dist/packages/sdk/src/authoring.js +287 -0
  152. package/dist/packages/sdk/src/bindings.d.ts +4 -0
  153. package/dist/packages/sdk/src/bindings.js +124 -0
  154. package/dist/packages/sdk/src/bridge.d.ts +2 -0
  155. package/dist/packages/sdk/src/bridge.js +23 -0
  156. package/dist/packages/sdk/src/browser-runtime/pyproject.toml +5 -0
  157. package/dist/packages/sdk/src/browser-runtime/tools.json +189 -0
  158. package/dist/packages/sdk/src/browser-runtime/uv.lock +3894 -0
  159. package/dist/packages/sdk/src/browser-service.py +127 -0
  160. package/dist/packages/sdk/src/browser.d.ts +38 -0
  161. package/dist/packages/sdk/src/browser.js +203 -0
  162. package/dist/packages/sdk/src/capabilities.d.ts +13 -0
  163. package/dist/packages/sdk/src/capabilities.js +68 -0
  164. package/dist/packages/sdk/src/classification-client.d.ts +4 -0
  165. package/dist/packages/sdk/src/classification-client.js +27 -0
  166. package/dist/packages/sdk/src/cloud.d.ts +23 -0
  167. package/dist/packages/sdk/src/cloud.js +43 -0
  168. package/dist/packages/sdk/src/computer-settings.d.ts +28 -0
  169. package/dist/packages/sdk/src/computer-settings.js +57 -0
  170. package/dist/packages/sdk/src/connect.d.ts +47 -0
  171. package/dist/packages/sdk/src/connect.js +203 -0
  172. package/dist/packages/sdk/src/current-inspection.d.ts +3 -0
  173. package/dist/packages/sdk/src/current-inspection.js +146 -0
  174. package/dist/packages/sdk/src/current-runtime.d.ts +14 -0
  175. package/dist/packages/sdk/src/current-runtime.js +167 -0
  176. package/dist/packages/sdk/src/doctor.d.ts +17 -0
  177. package/dist/packages/sdk/src/doctor.js +68 -0
  178. package/dist/packages/sdk/src/executable.d.ts +2 -0
  179. package/dist/packages/sdk/src/executable.js +22 -0
  180. package/dist/packages/sdk/src/explain-effects.d.ts +32 -0
  181. package/dist/packages/sdk/src/explain-effects.js +192 -0
  182. package/dist/packages/sdk/src/explain.d.ts +115 -0
  183. package/dist/packages/sdk/src/explain.js +441 -0
  184. package/dist/packages/sdk/src/files.d.ts +9 -0
  185. package/dist/packages/sdk/src/files.js +50 -0
  186. package/dist/packages/sdk/src/guard-keys.d.ts +5 -0
  187. package/dist/packages/sdk/src/guard-keys.js +42 -0
  188. package/dist/packages/sdk/src/hosted-models.d.ts +49 -0
  189. package/dist/packages/sdk/src/hosted-models.js +81 -0
  190. package/dist/packages/sdk/src/improve.d.ts +55 -0
  191. package/dist/packages/sdk/src/improve.js +159 -0
  192. package/dist/packages/sdk/src/index.d.ts +13 -0
  193. package/dist/packages/sdk/src/index.js +13 -0
  194. package/dist/packages/sdk/src/inspect.d.ts +6 -0
  195. package/dist/packages/sdk/src/inspect.js +17 -0
  196. package/dist/packages/sdk/src/link.d.ts +12 -0
  197. package/dist/packages/sdk/src/link.js +53 -0
  198. package/dist/packages/sdk/src/local-cli.d.ts +24 -0
  199. package/dist/packages/sdk/src/local-cli.js +146 -0
  200. package/dist/packages/sdk/src/local-setup.d.ts +15 -0
  201. package/dist/packages/sdk/src/local-setup.js +42 -0
  202. package/dist/packages/sdk/src/method-client.d.ts +30 -0
  203. package/dist/packages/sdk/src/method-client.js +250 -0
  204. package/dist/packages/sdk/src/method-files.d.ts +29 -0
  205. package/dist/packages/sdk/src/method-files.js +93 -0
  206. package/dist/packages/sdk/src/method-help.d.ts +19 -0
  207. package/dist/packages/sdk/src/method-help.js +404 -0
  208. package/dist/packages/sdk/src/method-issues.d.ts +86 -0
  209. package/dist/packages/sdk/src/method-issues.js +192 -0
  210. package/dist/packages/sdk/src/method-sync.d.ts +46 -0
  211. package/dist/packages/sdk/src/method-sync.js +293 -0
  212. package/dist/packages/sdk/src/method.d.ts +5 -0
  213. package/dist/packages/sdk/src/method.js +359 -0
  214. package/dist/packages/sdk/src/model-checks.d.ts +96 -0
  215. package/dist/packages/sdk/src/model-checks.js +181 -0
  216. package/dist/packages/sdk/src/outbox.d.ts +120 -0
  217. package/dist/packages/sdk/src/outbox.js +297 -0
  218. package/dist/packages/sdk/src/own-model-key.d.ts +22 -0
  219. package/dist/packages/sdk/src/own-model-key.js +76 -0
  220. package/dist/packages/sdk/src/prepare.d.ts +24 -0
  221. package/dist/packages/sdk/src/prepare.js +245 -0
  222. package/dist/packages/sdk/src/process.d.ts +15 -0
  223. package/dist/packages/sdk/src/process.js +107 -0
  224. package/dist/packages/sdk/src/production.d.ts +182 -0
  225. package/dist/packages/sdk/src/production.js +284 -0
  226. package/dist/packages/sdk/src/progress.d.ts +12 -0
  227. package/dist/packages/sdk/src/progress.js +48 -0
  228. package/dist/packages/sdk/src/proposals.d.ts +73 -0
  229. package/dist/packages/sdk/src/proposals.js +284 -0
  230. package/dist/packages/sdk/src/quality.d.ts +82 -0
  231. package/dist/packages/sdk/src/quality.js +294 -0
  232. package/dist/packages/sdk/src/result-files.d.ts +5 -0
  233. package/dist/packages/sdk/src/result-files.js +149 -0
  234. package/dist/packages/sdk/src/result-transfer.d.ts +21 -0
  235. package/dist/packages/sdk/src/result-transfer.js +37 -0
  236. package/dist/packages/sdk/src/run-data.d.ts +11 -0
  237. package/dist/packages/sdk/src/run-data.js +80 -0
  238. package/dist/packages/sdk/src/run-method.d.ts +2 -0
  239. package/dist/packages/sdk/src/run-method.js +2 -0
  240. package/dist/packages/sdk/src/run-saved.d.ts +5 -0
  241. package/dist/packages/sdk/src/run-saved.js +138 -0
  242. package/dist/packages/sdk/src/run-worker.d.ts +6 -0
  243. package/dist/packages/sdk/src/run-worker.js +131 -0
  244. package/dist/packages/sdk/src/secrets.d.ts +37 -0
  245. package/dist/packages/sdk/src/secrets.js +196 -0
  246. package/dist/packages/sdk/src/update.d.ts +5 -0
  247. package/dist/packages/sdk/src/update.js +39 -0
  248. package/dist/packages/sdk/src/versions.d.ts +58 -0
  249. package/dist/packages/sdk/src/versions.js +144 -0
  250. package/dist/packages/sdk/src/worker.d.ts +120 -0
  251. package/dist/packages/sdk/src/worker.js +443 -0
  252. package/dist/packages/workflow-language/src/execution.d.ts +1 -0
  253. package/dist/packages/workflow-language/src/execution.js +1 -0
  254. package/dist/packages/workflow-language/src/inspection.d.ts +990 -0
  255. package/dist/packages/workflow-language/src/inspection.js +130 -0
  256. package/dist/packages/workflow-language/src/result-files.d.ts +160 -0
  257. package/dist/packages/workflow-language/src/result-files.js +67 -0
  258. package/dist/packages/workflow-language/src/schema.d.ts +207 -0
  259. package/dist/packages/workflow-language/src/schema.js +56 -0
  260. package/dist/packages/workflow-language/src/validate.d.ts +11 -0
  261. package/dist/packages/workflow-language/src/validate.js +77 -0
  262. package/package.json +230 -4
@@ -0,0 +1,119 @@
1
+ # Manage daily outbound
2
+
3
+ Each run reads email, finds prospects in Happenstance, checks their company and
4
+ LinkedIn pages, updates the CRM, and writes today's tasks with outreach drafts.
5
+ Messages stay unsent until you review and act on the tasks.
6
+
7
+ ## Steps
8
+
9
+ | Step | Type | Output |
10
+ | --- | --- | --- |
11
+ | `read_crm` | run | target settings, contacts, resolved message IDs, CRM hash |
12
+ | `read_email` | agent, browser | `messages`, `email_search` |
13
+ | `find_prospects` | agent, browser | `prospects`, `prospect_search` |
14
+ | `enrich` | agent, browser, `each` prospect | `enrichment` for each prospect |
15
+ | `update_contacts` | call | `updated_contacts`, `resolutions` |
16
+ | `draft_tasks` | call | `day_tasks` |
17
+ | `save_day` | run, changes `state.crm` | `tasks.md`, `receipt.json` |
18
+
19
+ Each model step does one task. The shape of each output is declared in `out`, so
20
+ the prompts say only what to do and what to leave empty when a source does not
21
+ have the answer.
22
+
23
+ The `draft_tasks` prompt shows two tasks written the way they should be, for
24
+ contacts that are not in the CRM: a reply to a pricing question and an
25
+ introduction request. Replace them with two drafts that you sent and liked.
26
+
27
+ The three browser steps accept the `untrusted_content_can_act` warning: each
28
+ agent reads pages with a signed-in browser that could also send, and the
29
+ `accept:` line on each step gives the reason to keep it. Nothing is sent until
30
+ you review the tasks.
31
+
32
+ ## Sources
33
+
34
+ - **Email:** opens your mailbox in the browser and reads received and sent
35
+ messages for the campaign. Saves message IDs and thread links with the CRM.
36
+ - **Happenstance:** searches your connected network for people who fit your
37
+ target customer, including introduction paths. See its
38
+ [people search guide](https://happenstance.ai/guides/ai-people-search).
39
+ - **Company websites and LinkedIn:** checks roles, company activity, and the
40
+ reason your offer fits. Saves page links and the facts read there. This uses
41
+ the existing browser and does not require a separate enrichment subscription.
42
+
43
+ Each browser step has a check. It stops the run if the agent reports blocked
44
+ access or returns a record without its link. A complete search may return no
45
+ matches. Checks require source records; they do not prove that the agent found
46
+ every email or that every source is accurate.
47
+
48
+ ## Set up and run
49
+
50
+ Run from this folder. Copy the empty starter CRM once to set up the account.
51
+ Set `target.customer`, `target.offer`, `target.new_prospects_per_day`, and
52
+ `target.mailbox_url` for your business. The starter uses Gmail; replace its URL
53
+ with your webmail URL if needed. Use the intended signed-in account.
54
+
55
+ ```sh
56
+ mkdir -p work/crm
57
+ cp starter/crm.json work/crm/crm.json
58
+ method validate outbound.method
59
+ method publish outbound.method --reason "First version" # prints the Method ID
60
+ python3 - <<'PY' > work/state.json
61
+ import json
62
+ from pathlib import Path
63
+ print(json.dumps({'crm': Path('work/crm/crm.json').read_text()}))
64
+ PY
65
+ chmod 600 work/state.json
66
+ method state METHOD_ID --enable --file work/state.json
67
+ method run outbound.method --inputs inputs.json
68
+ ```
69
+
70
+ Use the Method ID that `publish` printed; it is also the `id:` line in the file. Set `day` and `from_date` in `inputs.json` before
71
+ each run. Use an overlap with the last run so late replies are included.
72
+ Method connects the browser. Follow its setup request
73
+ if email, Happenstance, or LinkedIn needs sign-in.
74
+
75
+ Open the returned `tasks.md` and `receipt.json`. Updates are saved in shared
76
+ Method account state. The setup file is a snapshot, not the current CRM. Run the saved Method daily
77
+ or use your scheduler; this Method does not create a schedule itself.
78
+
79
+ ## Saved state
80
+
81
+ The CRM keeps contacts, owner notes, message history, introduction paths, and
82
+ daily plans. Already resolved email IDs are skipped. Unclear senders remain for
83
+ review. Customers and opt-outs stay out of outreach tasks.
84
+
85
+ A checked plan returns a replacement for `state.crm`. Method accepts that state
86
+ only after the save check passes. Shared account state uses revision checks, so local
87
+ and production runs use one current CRM. If the CRM changed during
88
+ research, the save stops. Repeating the same save does not repeat its updates.
89
+ The receipt records the saved state hash. Keep shared state enabled for normal use.
90
+
91
+ `update_contacts` and `draft_tasks` each have a check that `save_day` runs again
92
+ before it changes the CRM. The research notes for the day are the notes from the
93
+ email search, the Happenstance search, and the pages that could not be opened.
94
+
95
+ ## Production
96
+
97
+ Publish the Method and run it from an app with `method connect APP_FOLDER`, from
98
+ `method worker`, or on Method Cloud (`method publish --cloud`). Production runs use
99
+ the same account CRM; they do not need a writable local folder. Publishing does
100
+ not schedule or send outreach.
101
+
102
+ ## Files and checks
103
+
104
+ - `outbound.method`: read CRM, read email, find and enrich prospects, update contacts, draft tasks, save.
105
+ - `crm.py` and entry scripts: state reads, source checks (`check_sources.py email|prospects|enrichment`),
106
+ contact and task checks (`check_plan.py contacts|tasks`), state replacements.
107
+ - `starter/crm.json`: empty CRM with editable target settings.
108
+ - `inputs.json`: email date range and date to plan.
109
+
110
+ Fictional records are only in `fixtures/` for local state
111
+ tests. They are not inputs to this Method. The tests cover two-day history,
112
+ repeated saves, stale writes, owner notes, opt-outs, unclear senders, and blocked
113
+ sources. A live run is still needed to check signed-in access and research quality.
114
+
115
+ Run the state tests offline with `python3 -m unittest crm_test.py`.
116
+ `fixtures/observations.json` and `fixtures/plan.json` hold step outputs for the
117
+ fictional first day. `fixtures/tasks.md`, `fixtures/receipt.json`, and
118
+ `fixtures/state.json` are the outputs that `save_day` makes from them, not a live
119
+ account run. A test checks that they are current.
@@ -0,0 +1,5 @@
1
+ # Request
2
+
3
+ Read campaign email and prospect sources each day. Keep the CRM current, preserve
4
+ owner notes and opt-outs, and prepare due tasks with outreach drafts for review.
5
+ Keep messages unsent. Reuse saved state on the next run.
@@ -0,0 +1,16 @@
1
+ import json
2
+ import sys
3
+ from crm import check_contacts, check_tasks
4
+ args = json.load(sys.stdin)
5
+ given, out = args['inputs'], args['outputs']
6
+ try:
7
+ if sys.argv[1] == 'contacts':
8
+ check_contacts(given['contacts'], given['resolved'], given['messages'], given['enrichment'],
9
+ out['updated_contacts'], out['resolutions'])
10
+ else:
11
+ contacts = {c['id']: c for c in given['contacts'] + given['updated_contacts']}
12
+ check_tasks(given['day'], contacts, out['day_tasks'])
13
+ result = {'status': 'pass', 'reason': 'The ' + sys.argv[1] + ' are valid.', 'evidence': []}
14
+ except (ValueError, KeyError, TypeError) as error:
15
+ result = {'status': 'fail', 'reason': str(error), 'evidence': []}
16
+ print(json.dumps(result))
@@ -0,0 +1,14 @@
1
+ import json
2
+ import sys
3
+ from crm import check_email, check_enrichment, check_search
4
+ args = json.load(sys.stdin)
5
+ out = args['outputs']
6
+ checks = {'email': lambda: check_email(out['email_search'], out['messages']),
7
+ 'prospects': lambda: check_search(out['prospect_search'], 'happenstance'),
8
+ 'enrichment': lambda: check_enrichment(out['enrichment'])}
9
+ try:
10
+ checks[sys.argv[1]]()
11
+ result = {'status': 'pass', 'reason': 'Source records are complete.', 'evidence': []}
12
+ except (ValueError, KeyError, TypeError) as error:
13
+ result = {'status': 'fail', 'reason': str(error), 'evidence': []}
14
+ print(json.dumps(result))
@@ -0,0 +1,153 @@
1
+ """Read the CRM, check each step's records, and save the day's plan."""
2
+ import datetime
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from urllib.parse import urlparse
8
+
9
+ STATUSES = ('new', 'contacted', 'replied', 'qualified', 'meeting', 'customer', 'closed', 'opted_out')
10
+ FIELDS = ('id', 'name', 'email', 'company', 'role', 'fit', 'sources', 'status',
11
+ 'next_action', 'next_action_date', 'introduction_path')
12
+
13
+
14
+ def digest(data):
15
+ return hashlib.sha256(data).hexdigest()
16
+
17
+
18
+ def read(args):
19
+ if datetime.date.fromisoformat(args['from_date']) > datetime.date.fromisoformat(args['day']):
20
+ raise ValueError('Email start date is after the run date')
21
+ crm = json.loads(args['crm'])
22
+ return {'target': crm['target'], 'contacts': crm['contacts'],
23
+ 'resolved': [m['id'] for m in crm['messages'] if m['contact_id']],
24
+ 'crm_sha256': digest(args['crm'].encode())}
25
+
26
+
27
+ def check_search(search, name):
28
+ if search['status'] != 'complete':
29
+ raise ValueError('Source access is incomplete: ' + name)
30
+
31
+
32
+ def check_email(search, messages):
33
+ check_search(search, 'email')
34
+ ids = set()
35
+ for m in messages:
36
+ if not m['id'] or m['id'] in ids:
37
+ raise ValueError('Duplicate or empty email ID')
38
+ ids.add(m['id'])
39
+ if m['direction'] not in ('received', 'sent') or not m['text']:
40
+ raise ValueError('Invalid email record')
41
+ datetime.datetime.fromisoformat(m['at'].replace('Z', '+00:00'))
42
+ if urlparse(m['url']).scheme != 'https':
43
+ raise ValueError('Email needs a source link')
44
+
45
+
46
+ def check_enrichment(enrichment):
47
+ if not enrichment['sources']:
48
+ raise ValueError('Prospect needs enrichment sources')
49
+ for source in enrichment['sources']:
50
+ if urlparse(source['url']).scheme != 'https' or not source['facts']:
51
+ raise ValueError('Enrichment needs a page link and facts')
52
+
53
+
54
+ def check_contacts(contacts, resolved, messages, enrichment, updated, resolutions):
55
+ """Return all contacts by ID after the updates, keeping owner notes."""
56
+ old = {c['id']: c for c in contacts}
57
+ collected = {m['url'] for m in messages} | {s['url'] for e in enrichment for s in e['sources']}
58
+ collected |= {u for c in contacts for u in c['sources']}
59
+ emails = {c['email'].lower(): c['id'] for c in contacts if c['email']}
60
+ merged, seen = dict(old), set()
61
+ for c in updated:
62
+ if not c['id'] or c['id'] in seen:
63
+ raise ValueError('Invalid or duplicate contact')
64
+ seen.add(c['id'])
65
+ if c['status'] not in STATUSES:
66
+ raise ValueError('Invalid contact status')
67
+ if c['next_action_date']:
68
+ datetime.date.fromisoformat(c['next_action_date'])
69
+ if any(url not in collected for url in c['sources']):
70
+ raise ValueError('Contact source was not collected')
71
+ if c['id'] not in old and (not c['sources'] or not c['fit']):
72
+ raise ValueError('New prospects need sources and a reason they fit')
73
+ if c['email']:
74
+ if emails.setdefault(c['email'].lower(), c['id']) != c['id']:
75
+ raise ValueError('Email belongs to an existing contact')
76
+ before = old.get(c['id'], {})
77
+ if before.get('status') in ('customer', 'opted_out') and c['status'] != before['status']:
78
+ raise ValueError('Keep customer and opt-out status')
79
+ merged[c['id']] = {**before, **{k: c[k] for k in FIELDS}, 'manual_notes': before.get('manual_notes', '')}
80
+ incoming = {m['id']: m for m in messages if m['id'] not in resolved}
81
+ if len(resolutions) != len(incoming) or {r['id'] for r in resolutions} != set(incoming):
82
+ raise ValueError('Resolve each new message once')
83
+ for r in resolutions:
84
+ if not r['resolution']:
85
+ raise ValueError('Each message needs a resolution')
86
+ if r['contact_id']:
87
+ if r['contact_id'] not in merged:
88
+ raise ValueError('Message refers to an unknown contact')
89
+ message, contact = incoming[r['id']], merged[r['contact_id']]
90
+ if message['contact_id'] != contact['id'] and not (message['email'] and message['email'].lower() == contact['email'].lower()):
91
+ raise ValueError('Message identity is not established')
92
+ return merged
93
+
94
+
95
+ def check_tasks(day, contacts, tasks):
96
+ keys = set()
97
+ for task in tasks:
98
+ contact = contacts.get(task['contact_id'])
99
+ if not contact:
100
+ raise ValueError('Task refers to an unknown contact')
101
+ if contact['status'] in ('customer', 'opted_out', 'closed'):
102
+ raise ValueError('Task targets an inactive contact')
103
+ if datetime.date.fromisoformat(task['due_date']) > datetime.date.fromisoformat(day):
104
+ raise ValueError('Daily task is not due yet')
105
+ key = (task['contact_id'], task['action'])
106
+ if key in keys or not task['action'] or not task['reason']:
107
+ raise ValueError('Empty or duplicate task')
108
+ keys.add(key)
109
+
110
+
111
+ def research_notes(args):
112
+ notes = [('Email', args['email_search']['notes']), ('Happenstance', args['prospect_search']['notes'])]
113
+ notes += [('Pages', e['notes']) for e in args['enrichment']]
114
+ return '\n\n'.join(f'{name}: {text}' for name, text in notes if text)
115
+
116
+
117
+ def save(args):
118
+ contacts = check_contacts(args['contacts'], args['resolved'], args['messages'], args['enrichment'],
119
+ args['updated_contacts'], args['resolutions'])
120
+ check_tasks(args['day'], contacts, args['day_tasks'])
121
+ notes = research_notes(args)
122
+ plan_id = digest(json.dumps({k: v for k, v in args.items() if k != 'crm'}, sort_keys=True).encode())
123
+ current = args['crm']
124
+ state = json.loads(current)
125
+ if state.get('last_plan') != plan_id:
126
+ if digest(current.encode()) != args['crm_sha256']:
127
+ raise ValueError('CRM changed after it was read; start a new run')
128
+ saved = {m['id']: m for m in state['messages']}
129
+ incoming = {m['id']: m for m in args['messages']}
130
+ for r in args['resolutions']:
131
+ saved[r['id']] = {**incoming[r['id']], **r}
132
+ state.update(contacts=list(contacts.values()), messages=list(saved.values()), last_plan=plan_id)
133
+ state.setdefault('days', {})[args['day']] = {'tasks': args['day_tasks'], 'research_notes': notes}
134
+ current = json.dumps(state, indent=2) + '\n'
135
+ lines = ['# Outbound tasks — ' + args['day'], '']
136
+ for task in args['day_tasks']:
137
+ contact = contacts[task['contact_id']]
138
+ lines += [f"## {task['action']}: {contact['name']} · {contact['company']}",
139
+ 'Due: ' + task['due_date'], '', task['reason'], '', task['draft'], '']
140
+ if not args['day_tasks']:
141
+ lines += ['No outreach tasks are due today.', '']
142
+ unresolved = [m for m in state['messages'] if not m['contact_id']]
143
+ lines += ['## Messages to review', ''] + [m['id'] + ': ' + m['resolution'] for m in unresolved]
144
+ lines += ['', '## Research notes', '', notes]
145
+ output = Path(os.environ['METHOD_OUTPUT_DIR'])
146
+ output.mkdir(parents=True, exist_ok=True)
147
+ def artifact(name, data):
148
+ (output / name).write_bytes(data)
149
+ return {'path': name, 'sha256': digest(data)}
150
+ return {'tasks': artifact('tasks.md', ('\n'.join(lines) + '\n').encode()),
151
+ 'receipt': artifact('receipt.json', json.dumps({'day': args['day'], 'crm_sha256': digest(current.encode()),
152
+ 'contacts': len(state['contacts']), 'messages': len(state['messages'])}).encode()),
153
+ 'state': {'crm': current}}
@@ -0,0 +1,141 @@
1
+ import copy
2
+ import importlib.util
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import tempfile
7
+ import unittest
8
+
9
+ EXAMPLE = Path(__file__).resolve().parent
10
+ FIXTURES = EXAMPLE / 'fixtures'
11
+ spec = importlib.util.spec_from_file_location('outbound_crm', EXAMPLE / 'crm.py')
12
+ crm = importlib.util.module_from_spec(spec)
13
+ spec.loader.exec_module(crm)
14
+
15
+
16
+ def fixture(name):
17
+ return json.loads((FIXTURES / name).read_text())
18
+
19
+
20
+ class OutboundTest(unittest.TestCase):
21
+ def setUp(self):
22
+ self.temp = tempfile.TemporaryDirectory()
23
+ self.addCleanup(self.temp.cleanup)
24
+ self.root = Path(self.temp.name)
25
+ self.crm = (FIXTURES / 'crm.json').read_text()
26
+ self.previous_output = os.environ.get('METHOD_OUTPUT_DIR')
27
+ os.environ['METHOD_OUTPUT_DIR'] = str(self.root / 'output')
28
+ self.addCleanup(self.restore_env)
29
+
30
+ def restore_env(self):
31
+ if self.previous_output is None:
32
+ os.environ.pop('METHOD_OUTPUT_DIR', None)
33
+ else:
34
+ os.environ['METHOD_OUTPUT_DIR'] = self.previous_output
35
+
36
+ def args(self, day=1):
37
+ """Return the save_day inputs: read_crm outputs, source outputs, and a plan."""
38
+ date = f'2026-09-{16+day}'
39
+ read = crm.read(dict(crm=self.crm, from_date='2026-09-10', day=date))
40
+ read.pop('target')
41
+ sources = {**fixture('observations.json'), 'messages': fixture(f'day-{day}/messages.json')}
42
+ if day == 1:
43
+ plan = fixture('plan.json')
44
+ else:
45
+ contacts = copy.deepcopy(read['contacts'])
46
+ contacts[0].update(next_action='Propose meeting times', next_action_date=date)
47
+ new = [m for m in sources['messages'] if m['id'] not in read['resolved']]
48
+ plan = dict(updated_contacts=[contacts[0]],
49
+ resolutions=[dict(id=m['id'], contact_id=m['contact_id'],
50
+ resolution='Recorded reply' if m['contact_id'] else 'Review sender') for m in new],
51
+ day_tasks=[dict(contact_id='c1', action='Propose meeting times', due_date=date,
52
+ reason='Alex asked to meet', draft='Hello Alex, here are some times.')])
53
+ return dict(crm=self.crm, day=date, **read, **sources, **plan)
54
+
55
+ def test_fixture_outputs_are_current(self):
56
+ result = crm.save(self.args())
57
+ self.assertEqual(result['state']['crm'], (FIXTURES / 'state.json').read_text())
58
+ self.assertEqual((self.root / 'output/tasks.md').read_text(), (FIXTURES / 'tasks.md').read_text())
59
+ self.assertEqual((self.root / 'output/receipt.json').read_text(), (FIXTURES / 'receipt.json').read_text())
60
+
61
+ def test_two_days_and_repeated_save(self):
62
+ first = self.args()
63
+ self.crm = crm.save(first)['state']['crm']
64
+ before = self.crm
65
+ self.crm = crm.save({**first, 'crm': self.crm})['state']['crm']
66
+ self.assertEqual(before, self.crm)
67
+ self.crm = crm.save(self.args(2))['state']['crm']
68
+ state = json.loads(self.crm)
69
+ self.assertEqual(len(state['messages']), 4)
70
+ self.assertEqual(len(state['days']), 2)
71
+ self.assertEqual(state['contacts'][2]['status'], 'opted_out')
72
+ self.assertEqual(state['contacts'][1]['status'], 'customer')
73
+ self.assertEqual(state['contacts'][0]['manual_notes'], 'Keep this owner note.')
74
+ self.assertIn('Propose meeting times', (self.root / 'output/tasks.md').read_text())
75
+
76
+ def test_stale_write_does_not_change_state(self):
77
+ args = self.args()
78
+ args['crm'] = self.crm + '\n'
79
+ with self.assertRaisesRegex(ValueError, 'CRM changed'):
80
+ crm.save(args)
81
+ self.assertFalse((self.root / 'output/tasks.md').exists())
82
+
83
+ def test_reject_opt_out_task_and_guessed_identity(self):
84
+ args = self.args()
85
+ contacts = crm.check_contacts(args['contacts'], args['resolved'], args['messages'],
86
+ args['enrichment'], args['updated_contacts'], args['resolutions'])
87
+ task = dict(args['day_tasks'][0], contact_id='c3')
88
+ with self.assertRaisesRegex(ValueError, 'inactive'):
89
+ crm.check_tasks(args['day'], contacts, [task])
90
+ args['resolutions'][2]['contact_id'] = 'c1'
91
+ with self.assertRaisesRegex(ValueError, 'identity'):
92
+ crm.save(args)
93
+
94
+ def test_keep_owner_notes_opt_outs_and_message_coverage(self):
95
+ args = self.args()
96
+ args['updated_contacts'][0]['manual_notes'] = 'Changed by agent'
97
+ contacts = crm.check_contacts(args['contacts'], args['resolved'], args['messages'],
98
+ args['enrichment'], args['updated_contacts'], args['resolutions'])
99
+ self.assertEqual(contacts['c1']['manual_notes'], 'Keep this owner note.')
100
+ args['updated_contacts'][1]['status'] = 'contacted'
101
+ with self.assertRaisesRegex(ValueError, 'customer and opt-out'):
102
+ crm.save(args)
103
+ args['updated_contacts'][1]['status'] = 'customer'
104
+ args['resolutions'].pop()
105
+ with self.assertRaisesRegex(ValueError, 'each new message'):
106
+ crm.save(args)
107
+
108
+ def test_new_prospect_is_saved_with_sources(self):
109
+ args = self.args()
110
+ url = 'https://west.example/team'
111
+ args['prospects'].append(dict(name='Pat Jones', email='', company='West', role='',
112
+ profile_url='https://www.linkedin.com/in/pat-jones', introduction_path='Through Alex Kim'))
113
+ args['enrichment'].append(dict(email='pat@west.example', role='Operations lead',
114
+ fit='Runs a logistics team', sources=[dict(url=url, facts='Operations lead at West')], notes=''))
115
+ new = dict(id='email:pat@west.example', name='Pat Jones', email='pat@west.example',
116
+ introduction_path='Through Alex Kim', company='West', role='Operations lead',
117
+ fit='Runs a logistics team', sources=[url], status='new', next_action='Review introduction',
118
+ next_action_date='2026-09-17')
119
+ args['updated_contacts'].append(new)
120
+ args['day_tasks'].append(dict(contact_id=new['id'], action='Review introduction',
121
+ due_date='2026-09-17', reason='Matches the target customer', draft='Hello Pat.'))
122
+ state = json.loads(crm.save(args)['state']['crm'])
123
+ self.assertEqual(state['contacts'][-1]['sources'], [url])
124
+ self.assertEqual(state['contacts'][-1]['manual_notes'], '')
125
+ self.assertIn('Pat Jones', (self.root / 'output/tasks.md').read_text())
126
+ new['sources'] = ['https://west.example/other']
127
+ with self.assertRaisesRegex(ValueError, 'not collected'):
128
+ crm.save(args)
129
+
130
+ def test_blocked_or_unsourced_records_fail(self):
131
+ args = self.args()
132
+ with self.assertRaisesRegex(ValueError, 'Source access'):
133
+ crm.check_email(dict(status='blocked', notes='Sign-in required'), args['messages'])
134
+ with self.assertRaisesRegex(ValueError, 'Duplicate'):
135
+ crm.check_email(args['email_search'], args['messages'] + args['messages'][:1])
136
+ with self.assertRaisesRegex(ValueError, 'enrichment sources'):
137
+ crm.check_enrichment(dict(email='', role='', fit='', sources=[], notes='LinkedIn blocked'))
138
+
139
+
140
+ if __name__ == '__main__':
141
+ unittest.main()
@@ -0,0 +1,15 @@
1
+ # Outbound state test records
2
+
3
+ These people, companies, messages, and links are fictional. The Python tests use
4
+ these records to check state writes without browser access.
5
+
6
+ - `crm.json`: the CRM before the first day.
7
+ - `day-1/messages.json`, `day-2/messages.json`: `read_email` output. Day two
8
+ repeats the first day's messages and adds a request for a meeting.
9
+ - `observations.json`: the other source step outputs for day one
10
+ (`email_search`, `prospects`, `prospect_search`, `enrichment`).
11
+ - `plan.json`: `update_contacts` and `draft_tasks` output for day one.
12
+ - `state.json`, `tasks.md`, `receipt.json`: what `save_day` makes from them.
13
+
14
+ The real outbound Method uses an empty starter CRM and gathers from email,
15
+ Happenstance, company websites, and LinkedIn. It does not read these fixtures.
@@ -0,0 +1,54 @@
1
+ {
2
+ "target": {
3
+ "customer": "Operations leaders at small US logistics companies who still process customer requests manually.",
4
+ "offer": "A service that turns repeated work into reusable automated workflows.",
5
+ "new_prospects_per_day": 3,
6
+ "mailbox_url": "https://mail.google.com/mail/u/0/"
7
+ },
8
+ "contacts": [
9
+ {
10
+ "id": "c1",
11
+ "name": "Alex Kim",
12
+ "email": "alex@north.example",
13
+ "company": "North",
14
+ "role": "Operations lead",
15
+ "fit": "Manages a small logistics team",
16
+ "sources": [],
17
+ "status": "contacted",
18
+ "next_action": "Review follow-up",
19
+ "next_action_date": "2026-09-17",
20
+ "manual_notes": "Keep this owner note.",
21
+ "introduction_path": ""
22
+ },
23
+ {
24
+ "id": "c2",
25
+ "name": "Morgan Bell",
26
+ "email": "morgan@south.example",
27
+ "company": "South",
28
+ "role": "Operations lead",
29
+ "fit": "Manages a small logistics team",
30
+ "sources": [],
31
+ "status": "customer",
32
+ "next_action": "",
33
+ "next_action_date": "",
34
+ "manual_notes": "Keep this owner note.",
35
+ "introduction_path": ""
36
+ },
37
+ {
38
+ "id": "c3",
39
+ "name": "Sam Lee",
40
+ "email": "sam@east.example",
41
+ "company": "East",
42
+ "role": "Operations lead",
43
+ "fit": "Manages a small logistics team",
44
+ "sources": [],
45
+ "status": "contacted",
46
+ "next_action": "",
47
+ "next_action_date": "",
48
+ "manual_notes": "Keep this owner note.",
49
+ "introduction_path": ""
50
+ }
51
+ ],
52
+ "messages": [],
53
+ "days": {}
54
+ }
@@ -0,0 +1,29 @@
1
+ [
2
+ {
3
+ "id": "m1",
4
+ "url": "https://mail.google.com/mail/u/0/#inbox/m1",
5
+ "email": "alex@north.example",
6
+ "at": "2026-09-17T09:00:00-04:00",
7
+ "direction": "received",
8
+ "text": "Can you send pricing today?",
9
+ "contact_id": "c1"
10
+ },
11
+ {
12
+ "id": "m2",
13
+ "url": "https://mail.google.com/mail/u/0/#inbox/m2",
14
+ "email": "sam@east.example",
15
+ "at": "2026-09-17T10:00:00-04:00",
16
+ "direction": "received",
17
+ "text": "Please stop contacting me.",
18
+ "contact_id": "c3"
19
+ },
20
+ {
21
+ "id": "m3",
22
+ "url": "https://mail.google.com/mail/u/0/#inbox/m3",
23
+ "email": "",
24
+ "at": "2026-09-17T11:00:00-04:00",
25
+ "direction": "received",
26
+ "text": "Alex: send the contract. Sender address was not captured.",
27
+ "contact_id": ""
28
+ }
29
+ ]
@@ -0,0 +1,38 @@
1
+ [
2
+ {
3
+ "id": "m1",
4
+ "url": "https://mail.google.com/mail/u/0/#inbox/m1",
5
+ "email": "alex@north.example",
6
+ "at": "2026-09-17T09:00:00-04:00",
7
+ "direction": "received",
8
+ "text": "Can you send pricing today?",
9
+ "contact_id": "c1"
10
+ },
11
+ {
12
+ "id": "m2",
13
+ "url": "https://mail.google.com/mail/u/0/#inbox/m2",
14
+ "email": "sam@east.example",
15
+ "at": "2026-09-17T10:00:00-04:00",
16
+ "direction": "received",
17
+ "text": "Please stop contacting me.",
18
+ "contact_id": "c3"
19
+ },
20
+ {
21
+ "id": "m3",
22
+ "url": "https://mail.google.com/mail/u/0/#inbox/m3",
23
+ "email": "",
24
+ "at": "2026-09-17T11:00:00-04:00",
25
+ "direction": "received",
26
+ "text": "Alex: send the contract. Sender address was not captured.",
27
+ "contact_id": ""
28
+ },
29
+ {
30
+ "id": "m4",
31
+ "url": "https://mail.google.com/mail/u/0/#inbox/m4",
32
+ "email": "alex@north.example",
33
+ "at": "2026-09-18T09:00:00-04:00",
34
+ "direction": "received",
35
+ "text": "Thanks for the pricing. Can we meet on September 21? Please send some times.",
36
+ "contact_id": "c1"
37
+ }
38
+ ]
@@ -0,0 +1,12 @@
1
+ {
2
+ "email_search": {
3
+ "status": "complete",
4
+ "notes": ""
5
+ },
6
+ "prospects": [],
7
+ "prospect_search": {
8
+ "status": "complete",
9
+ "notes": "No new prospects in this state test."
10
+ },
11
+ "enrichment": []
12
+ }