@plotday/twister 0.56.0 → 0.58.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 (331) hide show
  1. package/README.md +53 -44
  2. package/bin/commands/create.js +9 -14
  3. package/bin/commands/create.js.map +1 -1
  4. package/bin/commands/deploy.js +2 -0
  5. package/bin/commands/deploy.js.map +1 -1
  6. package/bin/commands/generate.js +8 -5
  7. package/bin/commands/generate.js.map +1 -1
  8. package/bin/index.js +2 -2
  9. package/bin/index.js.map +1 -1
  10. package/bin/templates/AGENTS.template.md +110 -94
  11. package/bin/templates/README.template.md +36 -33
  12. package/cli/templates/AGENTS.template.md +110 -94
  13. package/cli/templates/README.template.md +36 -33
  14. package/dist/connector.d.ts +24 -17
  15. package/dist/connector.d.ts.map +1 -1
  16. package/dist/connector.js +19 -12
  17. package/dist/connector.js.map +1 -1
  18. package/dist/docs/assets/hierarchy.js +1 -1
  19. package/dist/docs/assets/navigation.js +1 -1
  20. package/dist/docs/assets/search.js +1 -1
  21. package/dist/docs/classes/index.Connector.html +66 -60
  22. package/dist/docs/classes/index.FileNotFoundError.html +2 -2
  23. package/dist/docs/classes/index.Files.html +4 -4
  24. package/dist/docs/classes/index.Imap.html +10 -10
  25. package/dist/docs/classes/index.Options.html +2 -2
  26. package/dist/docs/classes/index.Smtp.html +6 -6
  27. package/dist/docs/classes/tool.ITool.html +2 -2
  28. package/dist/docs/classes/tool.Tool.html +23 -23
  29. package/dist/docs/classes/tools_ai.AI.html +5 -5
  30. package/dist/docs/classes/tools_callbacks.Callbacks.html +8 -8
  31. package/dist/docs/classes/tools_integrations.Integrations.html +26 -12
  32. package/dist/docs/classes/tools_network.Network.html +9 -9
  33. package/dist/docs/classes/tools_plot.Plot.html +34 -33
  34. package/dist/docs/classes/tools_store.Store.html +8 -8
  35. package/dist/docs/classes/tools_tasks.Tasks.html +6 -6
  36. package/dist/docs/classes/tools_twists.Twists.html +12 -11
  37. package/dist/docs/classes/twist.Twist.html +28 -28
  38. package/dist/docs/documents/Building_Connectors.html +42 -28
  39. package/dist/docs/documents/Built-in_Tools.html +170 -67
  40. package/dist/docs/documents/CLI_Reference.html +68 -47
  41. package/dist/docs/documents/Core_Concepts.html +52 -81
  42. package/dist/docs/documents/Getting_Started.html +28 -31
  43. package/dist/docs/documents/MULTI_USER_AUTH.html +45 -0
  44. package/dist/docs/documents/Runtime_Environment.html +13 -12
  45. package/dist/docs/documents/SYNC_STRATEGIES.html +373 -0
  46. package/dist/docs/enums/plot.ActionType.html +9 -9
  47. package/dist/docs/enums/plot.ActorType.html +4 -4
  48. package/dist/docs/enums/plot.ConferencingProvider.html +6 -6
  49. package/dist/docs/enums/plot.ThemeColor.html +9 -9
  50. package/dist/docs/enums/tag.Tag.html +3 -3
  51. package/dist/docs/enums/tools_ai.AIModel.html +3 -3
  52. package/dist/docs/enums/tools_integrations.AuthProvider.html +13 -13
  53. package/dist/docs/enums/tools_plot.ContactAccess.html +2 -2
  54. package/dist/docs/enums/tools_plot.FocusAccess.html +3 -3
  55. package/dist/docs/enums/tools_plot.LinkAccess.html +3 -3
  56. package/dist/docs/enums/tools_plot.ThreadAccess.html +4 -4
  57. package/dist/docs/functions/index.Uuid.Generate.html +1 -1
  58. package/dist/docs/functions/utils_hash.quickHash.html +1 -1
  59. package/dist/docs/hierarchy.html +1 -1
  60. package/dist/docs/index.html +7 -8
  61. package/dist/docs/interfaces/tools_ai.AIRequest.html +13 -13
  62. package/dist/docs/interfaces/tools_ai.AIResponse.html +9 -9
  63. package/dist/docs/interfaces/tools_ai.FilePart.html +5 -5
  64. package/dist/docs/interfaces/tools_ai.ImagePart.html +4 -4
  65. package/dist/docs/interfaces/tools_ai.ReasoningPart.html +4 -4
  66. package/dist/docs/interfaces/tools_ai.RedactedReasoningPart.html +3 -3
  67. package/dist/docs/interfaces/tools_ai.TextPart.html +3 -3
  68. package/dist/docs/interfaces/tools_ai.ToolCallPart.html +5 -5
  69. package/dist/docs/interfaces/tools_ai.ToolExecutionOptions.html +4 -4
  70. package/dist/docs/interfaces/tools_ai.ToolResultPart.html +5 -5
  71. package/dist/docs/interfaces/tools_twists.TwistSource.html +3 -3
  72. package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
  73. package/dist/docs/media/AGENTS.md +101 -74
  74. package/dist/docs/modules/index.html +1 -1
  75. package/dist/docs/modules/tools_integrations.html +1 -1
  76. package/dist/docs/modules.html +1 -1
  77. package/dist/docs/types/index.BooleanDef.html +2 -2
  78. package/dist/docs/types/index.CreateLinkDraft.html +9 -9
  79. package/dist/docs/types/index.ImapAddress.html +3 -3
  80. package/dist/docs/types/index.ImapConnectOptions.html +6 -6
  81. package/dist/docs/types/index.ImapFetchOptions.html +4 -4
  82. package/dist/docs/types/index.ImapFlagOperation.html +1 -1
  83. package/dist/docs/types/index.ImapMailbox.html +5 -5
  84. package/dist/docs/types/index.ImapMailboxStatus.html +7 -7
  85. package/dist/docs/types/index.ImapMessage.html +14 -14
  86. package/dist/docs/types/index.ImapSearchCriteria.html +9 -9
  87. package/dist/docs/types/index.ImapSession.html +1 -1
  88. package/dist/docs/types/index.NewSchedule.html +13 -13
  89. package/dist/docs/types/index.NewScheduleContact.html +2 -2
  90. package/dist/docs/types/index.NewScheduleOccurrence.html +1 -1
  91. package/dist/docs/types/index.NoteWriteBackResult.html +3 -3
  92. package/dist/docs/types/index.NumberDef.html +2 -2
  93. package/dist/docs/types/index.OptionDef.html +1 -1
  94. package/dist/docs/types/index.OptionalScopeGroup.html +6 -6
  95. package/dist/docs/types/index.OptionsSchema.html +1 -1
  96. package/dist/docs/types/index.ReactionCapabilities.html +1 -1
  97. package/dist/docs/types/index.ResolvedOptions.html +1 -1
  98. package/dist/docs/types/index.ResolvedRecipient.html +5 -5
  99. package/dist/docs/types/index.Schedule.html +12 -12
  100. package/dist/docs/types/index.ScheduleContact.html +2 -2
  101. package/dist/docs/types/index.ScheduleContactRole.html +1 -1
  102. package/dist/docs/types/index.ScheduleContactStatus.html +1 -1
  103. package/dist/docs/types/index.ScheduleOccurrence.html +6 -6
  104. package/dist/docs/types/index.ScheduleOccurrenceUpdate.html +1 -1
  105. package/dist/docs/types/index.ScopeConfig.html +3 -3
  106. package/dist/docs/types/index.SelectDef.html +2 -2
  107. package/dist/docs/types/index.Serializable.html +1 -1
  108. package/dist/docs/types/index.SmtpAddress.html +3 -3
  109. package/dist/docs/types/index.SmtpConnectOptions.html +7 -7
  110. package/dist/docs/types/index.SmtpMessage.html +12 -12
  111. package/dist/docs/types/index.SmtpSendResult.html +4 -4
  112. package/dist/docs/types/index.SmtpSession.html +1 -1
  113. package/dist/docs/types/index.TextDef.html +2 -2
  114. package/dist/docs/types/index.Uuid.html +1 -1
  115. package/dist/docs/types/plot.Action.html +1 -1
  116. package/dist/docs/types/plot.Actor.html +5 -5
  117. package/dist/docs/types/plot.ActorId.html +4 -4
  118. package/dist/docs/types/plot.Contact.html +4 -4
  119. package/dist/docs/types/plot.ContentType.html +1 -1
  120. package/dist/docs/types/plot.Focus.html +8 -8
  121. package/dist/docs/types/plot.FocusUpdate.html +1 -1
  122. package/dist/docs/types/plot.Link.html +17 -17
  123. package/dist/docs/types/plot.LinkUpdate.html +1 -1
  124. package/dist/docs/types/plot.NewActor.html +1 -1
  125. package/dist/docs/types/plot.NewContact.html +1 -1
  126. package/dist/docs/types/plot.NewFocus.html +1 -1
  127. package/dist/docs/types/plot.NewLink.html +5 -2
  128. package/dist/docs/types/plot.NewLinkWithNotes.html +1 -1
  129. package/dist/docs/types/plot.NewNote.html +1 -1
  130. package/dist/docs/types/plot.NewReactions.html +1 -1
  131. package/dist/docs/types/plot.NewTags.html +1 -1
  132. package/dist/docs/types/plot.NewThread.html +1 -1
  133. package/dist/docs/types/plot.NewThreadWithNotes.html +1 -1
  134. package/dist/docs/types/plot.Note.html +1 -1
  135. package/dist/docs/types/plot.NoteUpdate.html +1 -1
  136. package/dist/docs/types/plot.PlanOperation.html +1 -1
  137. package/dist/docs/types/plot.Reaction.html +3 -3
  138. package/dist/docs/types/plot.Reactions.html +1 -1
  139. package/dist/docs/types/plot.Tags.html +1 -1
  140. package/dist/docs/types/plot.Thread.html +1 -1
  141. package/dist/docs/types/plot.ThreadAccessLevel.html +1 -1
  142. package/dist/docs/types/plot.ThreadCommon.html +6 -6
  143. package/dist/docs/types/plot.ThreadFilter.html +2 -2
  144. package/dist/docs/types/plot.ThreadMeta.html +1 -1
  145. package/dist/docs/types/plot.ThreadType.html +1 -1
  146. package/dist/docs/types/plot.ThreadUpdate.html +1 -1
  147. package/dist/docs/types/plot.ThreadWithNotes.html +1 -1
  148. package/dist/docs/types/tools_ai.AIAssistantMessage.html +2 -2
  149. package/dist/docs/types/tools_ai.AICapabilities.html +4 -4
  150. package/dist/docs/types/tools_ai.AIMessage.html +1 -1
  151. package/dist/docs/types/tools_ai.AIOptions.html +2 -2
  152. package/dist/docs/types/tools_ai.AISource.html +1 -1
  153. package/dist/docs/types/tools_ai.AISystemMessage.html +2 -2
  154. package/dist/docs/types/tools_ai.AITool.html +1 -1
  155. package/dist/docs/types/tools_ai.AIToolMessage.html +2 -2
  156. package/dist/docs/types/tools_ai.AIToolSet.html +1 -1
  157. package/dist/docs/types/tools_ai.AIUsage.html +5 -5
  158. package/dist/docs/types/tools_ai.AIUserMessage.html +2 -2
  159. package/dist/docs/types/tools_ai.DataContent.html +1 -1
  160. package/dist/docs/types/tools_ai.ModelPreferences.html +5 -5
  161. package/dist/docs/types/tools_callbacks.Callback.html +2 -2
  162. package/dist/docs/types/tools_integrations.ArchiveLinkFilter.html +5 -5
  163. package/dist/docs/types/tools_integrations.ArchiveNotesFilter.html +5 -0
  164. package/dist/docs/types/tools_integrations.AuthToken.html +6 -5
  165. package/dist/docs/types/tools_integrations.Authorization.html +4 -4
  166. package/dist/docs/types/tools_integrations.Channel.html +6 -6
  167. package/dist/docs/types/tools_integrations.ComposeConfig.html +4 -4
  168. package/dist/docs/types/tools_integrations.ContactRoleConfig.html +5 -5
  169. package/dist/docs/types/tools_integrations.LinkTypeConfig.html +21 -21
  170. package/dist/docs/types/tools_integrations.NewCustomEmoji.html +8 -8
  171. package/dist/docs/types/tools_integrations.StatusIcon.html +1 -1
  172. package/dist/docs/types/tools_integrations.SyncContext.html +4 -4
  173. package/dist/docs/types/tools_network.WebhookRequest.html +6 -6
  174. package/dist/docs/types/tools_plot.LinkFilter.html +5 -5
  175. package/dist/docs/types/tools_plot.LinkSearchResult.html +1 -1
  176. package/dist/docs/types/tools_plot.NoteIntentHandler.html +4 -4
  177. package/dist/docs/types/tools_plot.NoteSearchResult.html +1 -1
  178. package/dist/docs/types/tools_plot.SearchOptions.html +4 -4
  179. package/dist/docs/types/tools_plot.SearchResult.html +1 -1
  180. package/dist/docs/types/tools_twists.Log.html +2 -2
  181. package/dist/docs/types/tools_twists.TwistPermissions.html +1 -1
  182. package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
  183. package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
  184. package/dist/docs/types/utils_types.InferOptions.html +1 -1
  185. package/dist/docs/types/utils_types.InferTools.html +1 -1
  186. package/dist/docs/types/utils_types.JSONValue.html +1 -1
  187. package/dist/docs/types/utils_types.PromiseValues.html +1 -1
  188. package/dist/docs/types/utils_types.ToolBuilder.html +1 -1
  189. package/dist/docs/variables/tools_plot.SEARCH_DEFAULT_LIMIT.html +1 -1
  190. package/dist/docs/variables/tools_plot.SEARCH_MAX_LIMIT.html +1 -1
  191. package/dist/facets.d.ts +30 -0
  192. package/dist/facets.d.ts.map +1 -0
  193. package/dist/facets.js +16 -0
  194. package/dist/facets.js.map +1 -0
  195. package/dist/llm-docs/connector.d.ts +1 -1
  196. package/dist/llm-docs/connector.d.ts.map +1 -1
  197. package/dist/llm-docs/connector.js +1 -1
  198. package/dist/llm-docs/connector.js.map +1 -1
  199. package/dist/llm-docs/facets.d.ts +9 -0
  200. package/dist/llm-docs/facets.d.ts.map +1 -0
  201. package/dist/llm-docs/facets.js +8 -0
  202. package/dist/llm-docs/facets.js.map +1 -0
  203. package/dist/llm-docs/index.d.ts.map +1 -1
  204. package/dist/llm-docs/index.js +2 -0
  205. package/dist/llm-docs/index.js.map +1 -1
  206. package/dist/llm-docs/plot.d.ts +1 -1
  207. package/dist/llm-docs/plot.d.ts.map +1 -1
  208. package/dist/llm-docs/plot.js +1 -1
  209. package/dist/llm-docs/plot.js.map +1 -1
  210. package/dist/llm-docs/tool.d.ts +1 -1
  211. package/dist/llm-docs/tool.d.ts.map +1 -1
  212. package/dist/llm-docs/tool.js +1 -1
  213. package/dist/llm-docs/tool.js.map +1 -1
  214. package/dist/llm-docs/tools/ai.d.ts +1 -1
  215. package/dist/llm-docs/tools/ai.d.ts.map +1 -1
  216. package/dist/llm-docs/tools/ai.js +1 -1
  217. package/dist/llm-docs/tools/ai.js.map +1 -1
  218. package/dist/llm-docs/tools/callbacks.d.ts +1 -1
  219. package/dist/llm-docs/tools/callbacks.d.ts.map +1 -1
  220. package/dist/llm-docs/tools/callbacks.js +1 -1
  221. package/dist/llm-docs/tools/callbacks.js.map +1 -1
  222. package/dist/llm-docs/tools/files.d.ts +1 -1
  223. package/dist/llm-docs/tools/files.d.ts.map +1 -1
  224. package/dist/llm-docs/tools/files.js +1 -1
  225. package/dist/llm-docs/tools/files.js.map +1 -1
  226. package/dist/llm-docs/tools/imap.d.ts +1 -1
  227. package/dist/llm-docs/tools/imap.d.ts.map +1 -1
  228. package/dist/llm-docs/tools/imap.js +1 -1
  229. package/dist/llm-docs/tools/imap.js.map +1 -1
  230. package/dist/llm-docs/tools/integrations.d.ts +1 -1
  231. package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
  232. package/dist/llm-docs/tools/integrations.js +1 -1
  233. package/dist/llm-docs/tools/integrations.js.map +1 -1
  234. package/dist/llm-docs/tools/network.d.ts +1 -1
  235. package/dist/llm-docs/tools/network.d.ts.map +1 -1
  236. package/dist/llm-docs/tools/network.js +1 -1
  237. package/dist/llm-docs/tools/network.js.map +1 -1
  238. package/dist/llm-docs/tools/plot.d.ts +1 -1
  239. package/dist/llm-docs/tools/plot.d.ts.map +1 -1
  240. package/dist/llm-docs/tools/plot.js +1 -1
  241. package/dist/llm-docs/tools/plot.js.map +1 -1
  242. package/dist/llm-docs/tools/smtp.d.ts +1 -1
  243. package/dist/llm-docs/tools/smtp.d.ts.map +1 -1
  244. package/dist/llm-docs/tools/smtp.js +1 -1
  245. package/dist/llm-docs/tools/smtp.js.map +1 -1
  246. package/dist/llm-docs/tools/tasks.d.ts +1 -1
  247. package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
  248. package/dist/llm-docs/tools/tasks.js +1 -1
  249. package/dist/llm-docs/tools/tasks.js.map +1 -1
  250. package/dist/llm-docs/tools/twists.d.ts +1 -1
  251. package/dist/llm-docs/tools/twists.d.ts.map +1 -1
  252. package/dist/llm-docs/tools/twists.js +1 -1
  253. package/dist/llm-docs/tools/twists.js.map +1 -1
  254. package/dist/llm-docs/twist-guide-template.d.ts +1 -1
  255. package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
  256. package/dist/llm-docs/twist-guide-template.js +1 -1
  257. package/dist/llm-docs/twist-guide-template.js.map +1 -1
  258. package/dist/llm-docs/twist.d.ts +1 -1
  259. package/dist/llm-docs/twist.d.ts.map +1 -1
  260. package/dist/llm-docs/twist.js +1 -1
  261. package/dist/llm-docs/twist.js.map +1 -1
  262. package/dist/plot.d.ts +15 -8
  263. package/dist/plot.d.ts.map +1 -1
  264. package/dist/plot.js.map +1 -1
  265. package/dist/tool.d.ts +4 -4
  266. package/dist/tool.js +4 -4
  267. package/dist/tools/ai.d.ts +12 -13
  268. package/dist/tools/ai.d.ts.map +1 -1
  269. package/dist/tools/ai.js +8 -9
  270. package/dist/tools/ai.js.map +1 -1
  271. package/dist/tools/callbacks.d.ts +1 -1
  272. package/dist/tools/files.d.ts +2 -2
  273. package/dist/tools/imap.d.ts +1 -1
  274. package/dist/tools/imap.js +1 -1
  275. package/dist/tools/integrations.d.ts +29 -2
  276. package/dist/tools/integrations.d.ts.map +1 -1
  277. package/dist/tools/integrations.js.map +1 -1
  278. package/dist/tools/network.d.ts +5 -5
  279. package/dist/tools/plot.d.ts +42 -37
  280. package/dist/tools/plot.d.ts.map +1 -1
  281. package/dist/tools/plot.js +16 -12
  282. package/dist/tools/plot.js.map +1 -1
  283. package/dist/tools/smtp.d.ts +1 -1
  284. package/dist/tools/smtp.js +1 -1
  285. package/dist/tools/tasks.d.ts +6 -8
  286. package/dist/tools/tasks.d.ts.map +1 -1
  287. package/dist/tools/tasks.js +5 -7
  288. package/dist/tools/tasks.js.map +1 -1
  289. package/dist/tools/twists.d.ts +15 -14
  290. package/dist/tools/twists.d.ts.map +1 -1
  291. package/dist/tools/twists.js +2 -2
  292. package/dist/tools/twists.js.map +1 -1
  293. package/dist/twist-guide.d.ts +1 -1
  294. package/dist/twist-guide.d.ts.map +1 -1
  295. package/dist/twist.d.ts +2 -2
  296. package/dist/twist.js +2 -2
  297. package/package.json +6 -1
  298. package/src/connector.ts +23 -16
  299. package/src/facets.ts +40 -0
  300. package/src/llm-docs/connector.ts +1 -1
  301. package/src/llm-docs/facets.ts +8 -0
  302. package/src/llm-docs/index.ts +2 -0
  303. package/src/llm-docs/plot.ts +1 -1
  304. package/src/llm-docs/tool.ts +1 -1
  305. package/src/llm-docs/tools/ai.ts +1 -1
  306. package/src/llm-docs/tools/callbacks.ts +1 -1
  307. package/src/llm-docs/tools/files.ts +1 -1
  308. package/src/llm-docs/tools/imap.ts +1 -1
  309. package/src/llm-docs/tools/integrations.ts +1 -1
  310. package/src/llm-docs/tools/network.ts +1 -1
  311. package/src/llm-docs/tools/plot.ts +1 -1
  312. package/src/llm-docs/tools/smtp.ts +1 -1
  313. package/src/llm-docs/tools/tasks.ts +1 -1
  314. package/src/llm-docs/tools/twists.ts +1 -1
  315. package/src/llm-docs/twist-guide-template.ts +1 -1
  316. package/src/llm-docs/twist.ts +1 -1
  317. package/src/plot.ts +15 -8
  318. package/src/tool.ts +4 -4
  319. package/src/tools/ai.ts +12 -13
  320. package/src/tools/callbacks.ts +1 -1
  321. package/src/tools/files.ts +2 -2
  322. package/src/tools/imap.ts +1 -1
  323. package/src/tools/integrations.ts +34 -1
  324. package/src/tools/network.ts +5 -5
  325. package/src/tools/plot.ts +42 -37
  326. package/src/tools/smtp.ts +1 -1
  327. package/src/tools/tasks.ts +6 -8
  328. package/src/tools/twists.ts +15 -14
  329. package/src/twist.ts +2 -2
  330. package/dist/docs/media/MULTI_USER_AUTH.md +0 -116
  331. package/dist/docs/media/SYNC_STRATEGIES.md +0 -818
@@ -1,818 +0,0 @@
1
- # Sync Strategies
2
-
3
- This guide explains good ways to build connectors that sync other services with Plot. Choosing the right strategy depends on whether you need to update items, deduplicate them, or simply create them once.
4
-
5
- ## Table of Contents
6
-
7
- - [Overview](#overview)
8
- - [Strategy 1: Create Once (Fire and Forget)](#strategy-1-create-once-fire-and-forget)
9
- - [Strategy 2: Upsert via Source and Key (Recommended)](#strategy-2-upsert-via-source-and-key-recommended)
10
- - [Strategy 3: Generate and Store IDs (Advanced)](#strategy-3-generate-and-store-ids-advanced)
11
- - [Tag Management](#tag-management)
12
- - [Initial vs Incremental Sync](#initial-vs-incremental-sync)
13
- - [Choosing the Right Strategy](#choosing-the-right-strategy)
14
-
15
- ## Overview
16
-
17
- Plot provides three main strategies for managing activities and notes:
18
-
19
- | Strategy | Use Case | Complexity | Deduplication | Updates |
20
- | -------------------------- | -------------------------------------------------- | ---------- | ------------- | ------- |
21
- | **Create Once** | One-time notifications, transient events | Low | None | No |
22
- | **Upsert via Source/Key** | Most integrations (calendars, tasks, issues) | Low | Automatic | Yes |
23
- | **Generate and Store IDs** | Complex transformations, multiple items per source | High | Manual | Yes |
24
-
25
- **Recommended for most use cases**: Strategy 2 (Upsert via Source/Key)
26
-
27
- ## Strategy 1: Create Once (Fire and Forget)
28
-
29
- ### When to Use
30
-
31
- Use this strategy when:
32
-
33
- - Items are created once and never need updates
34
- - Duplicates are acceptable or expected
35
- - You're creating notifications, alerts, or transient events
36
- - The external system doesn't provide stable identifiers
37
-
38
- ### How It Works
39
-
40
- Simply create activities and notes without specifying `id` or `source` fields. Plot will generate unique IDs automatically.
41
-
42
- ### Example: Simple Notification
43
-
44
- ```typescript
45
- export default class NotificationTwist extends Twist {
46
- async sendAlert(title: string, message: string): Promise<void> {
47
- // Create a simple note-only activity
48
- await this.tools.plot.createActivity({
49
- type: ActivityType.Note,
50
- title: title,
51
- notes: [
52
- {
53
- content: message,
54
- },
55
- ],
56
- });
57
- }
58
- }
59
- ```
60
-
61
- ### Pros and Cons
62
-
63
- **Pros:**
64
-
65
- - Simplest approach
66
- - No storage overhead
67
- - No external API lookups needed
68
- - Fast execution
69
-
70
- **Cons:**
71
-
72
- - No deduplication
73
- - Cannot update existing items
74
- - Can create duplicates if called multiple times
75
-
76
- ## Strategy 2: Upsert via Source and Key (Recommended)
77
-
78
- ### When to Use
79
-
80
- Use this strategy when:
81
-
82
- - You're integrating with external systems that provide stable URLs or IDs
83
- - Items need to be updated when the external source changes
84
- - You want automatic deduplication without manual tracking
85
- - You're syncing calendars, tasks, issues, messages, or similar entities
86
-
87
- ### How It Works
88
-
89
- **For Activities:**
90
- Set the `source` field to a canonical URL or stable identifier. When you create an activity with a source that already exists in the priority tree, Plot will **update** the existing activity instead of creating a duplicate.
91
-
92
- **For Notes:**
93
- Use the `key` field combined with the activity's source to enable upserts. When you create a note with a key that already exists on the activity, Plot will **update** that note instead of creating a duplicate.
94
-
95
- ### Activity Upserts
96
-
97
- The `source` field should be:
98
-
99
- - A canonical URL from the external system (preferred)
100
- - A stable identifier in a namespaced format (e.g., `gmail:thread-id-123`)
101
- - **Globally unique for the logical external item** — see "Source identifier uniqueness" below
102
-
103
- > **Cross-user dedup:** Two instances of the same connector (run by two different Plot users) that emit the same `source` for the same external item will converge on a **single shared thread**. This is how two users on the same Gmail message, calendar event, or Linear issue see one thread rather than two.
104
- >
105
- > This means `source` must not merely be unique within one user's account — it must be globally unique for the item. If an external id is workspace- or tenant-scoped (Attio record ids, PostHog distinct_ids, Outlook event ids, Fellow note ids, etc.), include the workspace/tenant/mailbox id as a qualifier: `attio:<workspaceId>:person:<recordId>`, not `attio:person:<recordId>`. See `connectors/AGENTS.md` → "Source identifier uniqueness" for the full guidance.
106
-
107
- ```typescript
108
- // Activity.source field definition
109
- interface Activity {
110
- /**
111
- * Canonical URL for the item in an external system.
112
- * For example, https://acme.atlassian.net/browse/PROJ-42 could represent a Jira issue.
113
- * When set, it uniquely identifies the activity within a priority tree.
114
- */
115
- source: string | null;
116
- }
117
- ```
118
-
119
- ### Example: Calendar Event Sync
120
-
121
- ```typescript
122
- export default class GoogleCalendarConnector extends Connector<GoogleCalendarConnector> {
123
- async syncEvent(event: calendar_v3.Schema$Event): Promise<void> {
124
- const activity: NewActivityWithNotes = {
125
- // Use the event's canonical URL as the source
126
- source: event.htmlLink,
127
- type: ActivityType.Event,
128
- title: event.summary || "(No title)",
129
- start: event.start?.dateTime || event.start?.date || null,
130
- end: event.end?.dateTime || event.end?.date || null,
131
- notes: [],
132
- };
133
-
134
- // Add description as an upsertable note
135
- if (event.description) {
136
- activity.notes.push({
137
- // Reference the activity by its source
138
- activity: { source: event.htmlLink },
139
- // Use a key for this specific note type
140
- key: "description",
141
- content: event.description,
142
- });
143
- }
144
-
145
- // Add attendees as an upsertable note
146
- if (event.attendees?.length) {
147
- const attendeeList = event.attendees
148
- .map((a) => `- ${a.email}${a.displayName ? ` (${a.displayName})` : ""}`)
149
- .join("\n");
150
-
151
- activity.notes.push({
152
- activity: { source: event.htmlLink },
153
- key: "attendees",
154
- content: `## Attendees\n${attendeeList}`,
155
- });
156
- }
157
-
158
- // Create or update the activity
159
- await this.tools.plot.createActivity(activity);
160
- }
161
- }
162
- ```
163
-
164
- **How it works:**
165
-
166
- 1. First sync: Activity with `source: event.htmlLink` is created with two notes (description and attendees)
167
- 2. Event updated externally: Same `source` is used, so Plot updates the existing activity
168
- 3. Description changes: Note with `key: "description"` is updated
169
- 4. Attendees change: Note with `key: "attendees"` is updated
170
- 5. No duplicates created, no manual ID tracking needed
171
-
172
- ### Example: Task/Issue Sync
173
-
174
- ```typescript
175
- export default class LinearConnector extends Connector<LinearConnector> {
176
- async syncIssue(issue: LinearIssue): Promise<void> {
177
- const activity: NewActivityWithNotes = {
178
- source: issue.url, // Linear provides stable URLs
179
- type: ActivityType.Action,
180
- title: `${issue.identifier}: ${issue.title}`,
181
- done: issue.state.type === "completed" ? new Date() : null,
182
- notes: [],
183
- };
184
-
185
- // Description note with upsert
186
- if (issue.description) {
187
- activity.notes.push({
188
- activity: { source: issue.url },
189
- key: "description",
190
- content: issue.description,
191
- });
192
- }
193
-
194
- // Metadata note with upsert
195
- activity.notes.push({
196
- activity: { source: issue.url },
197
- key: "metadata",
198
- content: [
199
- `**Status**: ${issue.state.name}`,
200
- `**Priority**: ${issue.priority}`,
201
- `**Assignee**: ${issue.assignee?.name || "Unassigned"}`,
202
- ].join("\n"),
203
- });
204
-
205
- await this.tools.plot.createActivity(activity);
206
- }
207
- }
208
- ```
209
-
210
- ### Referencing Activities When Creating Notes
211
-
212
- When creating a note separately (not as part of `NewActivityWithNotes`), reference the activity by its source:
213
-
214
- ```typescript
215
- // Add a comment to an existing activity
216
- await this.tools.plot.createNote({
217
- activity: { source: "https://github.com/user/repo/issues/42" },
218
- key: `comment-${comment.id}`, // Unique key per comment
219
- content: comment.body,
220
- });
221
- ```
222
-
223
- ### Note Key Patterns
224
-
225
- The `key` field enables upsert behavior for notes. Choose keys based on your use case:
226
-
227
- **Single instance notes** (will be updated on each sync):
228
-
229
- - `key: "description"` - Main description/body
230
- - `key: "metadata"` - Status, assignee, etc.
231
- - `key: "attendees"` - Event attendees list
232
-
233
- **Multiple instance notes** (use unique keys):
234
-
235
- - `key: "comment-${commentId}"` - Each comment has unique ID
236
- - `key: "attachment-${filename}"` - Each attachment has unique name
237
- - `key: "change-${timestamp}"` - Each change log entry
238
-
239
- **No key** (creates new note every time):
240
-
241
- - Omit `key` field when you want new notes created on each sync
242
- - Useful for chat messages, activity logs, or append-only data
243
-
244
- ### Pros and Cons
245
-
246
- **Pros:**
247
-
248
- - Automatic deduplication
249
- - No storage overhead for ID mappings
250
- - No need to look up existing items before creating
251
- - Clean, maintainable code
252
- - Works with external URLs (user-friendly)
253
-
254
- **Cons:**
255
-
256
- - Requires stable identifiers from external system
257
- - One Plot activity per external source item
258
- - Cannot create multiple Plot items from single source item
259
-
260
- ## Strategy 3: Generate and Store IDs (Advanced)
261
-
262
- ### When to Use
263
-
264
- Use this strategy when:
265
-
266
- - You need to create multiple Plot activities from a single external item
267
- - External system doesn't provide stable identifiers
268
- - You need complex transformations or splitting
269
- - Source-based upserts aren't flexible enough for your use case
270
-
271
- ### How It Works
272
-
273
- 1. Generate a unique ID using `Uuid.Generate()`
274
- 2. Store the mapping between external ID and Plot ID
275
- 3. Look up existing IDs before creating items
276
- 4. Use stored IDs when updating
277
-
278
- ### Example: Multiple Activities from Single Source
279
-
280
- ```typescript
281
- export default class GmailConnector extends Connector<GmailConnector> {
282
- /**
283
- * Creates separate activities for email threads and individual messages.
284
- * One email thread can have multiple Plot activities.
285
- */
286
- async syncThread(thread: GmailThread): Promise<void> {
287
- // Check if we've seen this thread before
288
- const threadKey = `thread:${thread.id}`;
289
- let threadActivityId = await this.get<string>(threadKey);
290
-
291
- // Generate ID if this is a new thread
292
- if (!threadActivityId) {
293
- threadActivityId = Uuid.Generate();
294
- await this.set(threadKey, threadActivityId);
295
- }
296
-
297
- // Create/update the thread activity
298
- await this.tools.plot.createActivity({
299
- id: threadActivityId,
300
- type: ActivityType.Note,
301
- title: thread.snippet,
302
- // Note: we use `id` instead of `source` for manual control
303
- });
304
-
305
- // Create separate activities for each important message in thread
306
- for (const message of thread.messages) {
307
- if (this.isImportantMessage(message)) {
308
- const messageKey = `message:${message.id}`;
309
- let messageActivityId = await this.get<string>(messageKey);
310
-
311
- if (!messageActivityId) {
312
- messageActivityId = Uuid.Generate();
313
- await this.set(messageKey, messageActivityId);
314
- }
315
-
316
- await this.tools.plot.createActivity({
317
- id: messageActivityId,
318
- type: ActivityType.Action,
319
- title: `Reply to: ${message.subject}`,
320
- notes: [
321
- {
322
- content: message.body,
323
- },
324
- ],
325
- });
326
- }
327
- }
328
- }
329
-
330
- private isImportantMessage(message: GmailMessage): boolean {
331
- // Custom logic to determine if message needs separate activity
332
- return message.labelIds?.includes("IMPORTANT") || false;
333
- }
334
- }
335
- ```
336
-
337
- ### Storage Patterns
338
-
339
- **Simple mapping:**
340
-
341
- ```typescript
342
- // Store external ID → Plot ID
343
- await this.set(`external:${externalId}`, plotId);
344
-
345
- // Retrieve
346
- const plotId = await this.get<string>(`external:${externalId}`);
347
- ```
348
-
349
- **Structured mapping:**
350
-
351
- ```typescript
352
- interface Mapping {
353
- plotId: string;
354
- externalId: string;
355
- lastSynced: string;
356
- syncCount: number;
357
- }
358
-
359
- await this.set(`mapping:${externalId}`, mapping);
360
- ```
361
-
362
- ### Lookup and Update Pattern
363
-
364
- ```typescript
365
- async syncItem(externalItem: ExternalItem): Promise<void> {
366
- const key = `item:${externalItem.id}`;
367
-
368
- // Look up existing Plot ID
369
- let plotId = await this.get<string>(key);
370
-
371
- // Generate new ID if not found
372
- if (!plotId) {
373
- plotId = Uuid.Generate();
374
- await this.set(key, plotId);
375
- }
376
-
377
- // Create or update using the ID
378
- await this.tools.plot.createActivity({
379
- id: plotId,
380
- type: ActivityType.Action,
381
- title: externalItem.title,
382
- // ... other fields
383
- });
384
- }
385
- ```
386
-
387
- ### Pros and Cons
388
-
389
- **Pros:**
390
-
391
- - Maximum flexibility
392
- - Can create multiple Plot items per external item
393
- - Works without stable external identifiers
394
- - Full control over ID lifecycle
395
-
396
- **Cons:**
397
-
398
- - Requires storage for mappings
399
- - Needs lookup before each create/update
400
- - More code to maintain
401
- - Slower due to additional storage operations
402
- - Must manage cleanup of old mappings
403
-
404
- ## Tag Management
405
-
406
- Tags can be applied to both activities and notes. Plot provides helpers for managing tags during sync operations.
407
-
408
- ### Tag Helpers
409
-
410
- The Plot tool provides tag management methods:
411
-
412
- ```typescript
413
- // Activity tags
414
- await this.tools.plot.setActivityTags(activity, ["work", "urgent"]);
415
- await this.tools.plot.addActivityTag(activity, "client-meeting");
416
- await this.tools.plot.removeActivityTag(activity, "draft");
417
-
418
- // Note tags
419
- await this.tools.plot.setNoteTags(note, ["comment", "external"]);
420
- await this.tools.plot.addNoteTag(note, "resolved");
421
- await this.tools.plot.removeNoteTag(note, "pending");
422
- ```
423
-
424
- ### Tag Sync Patterns
425
-
426
- **Replace all tags (set):**
427
-
428
- ```typescript
429
- // Sync tags from external system, replacing existing tags
430
- await this.tools.plot.setActivityTags(
431
- { source: issue.url },
432
- issue.labels.map((l) => l.name)
433
- );
434
- ```
435
-
436
- **Additive tagging (add):**
437
-
438
- ```typescript
439
- // Add tags without removing existing ones
440
- if (event.isRecurring) {
441
- await this.tools.plot.addActivityTag({ source: event.htmlLink }, "recurring");
442
- }
443
- ```
444
-
445
- **Conditional tagging:**
446
-
447
- ```typescript
448
- // Tag based on external state
449
- if (task.priority === "high") {
450
- await this.tools.plot.addActivityTag({ source: task.url }, "urgent");
451
- } else {
452
- await this.tools.plot.removeActivityTag({ source: task.url }, "urgent");
453
- }
454
- ```
455
-
456
- ### Tag Namespacing
457
-
458
- To avoid tag collisions between different twists and tools, consider namespacing:
459
-
460
- ```typescript
461
- // Namespace tags with tool name
462
- const tags = externalLabels.map((label) => `jira:${label}`);
463
- await this.tools.plot.setActivityTags({ source: issue.url }, tags);
464
-
465
- // Or use a prefix constant
466
- const TAG_PREFIX = "linear-";
467
- await this.tools.plot.addActivityTag(
468
- { source: issue.url },
469
- `${TAG_PREFIX}${issue.state.name}`
470
- );
471
- ```
472
-
473
- ### Referencing Items for Tag Operations
474
-
475
- Like note creation, tag operations can reference activities by source:
476
-
477
- ```typescript
478
- // Using source
479
- await this.tools.plot.addActivityTag(
480
- { source: "https://app.asana.com/0/123/456" },
481
- "in-progress"
482
- );
483
-
484
- // Using ID (if you're using Strategy 3)
485
- await this.tools.plot.addActivityTag({ id: storedActivityId }, "synced");
486
- ```
487
-
488
- ### Tag Cleanup
489
-
490
- When an external item is deleted or tags are removed, clean up tags in Plot:
491
-
492
- ```typescript
493
- // Remove all tags from external system
494
- const externalTags = issue.labels.map((l) => `jira:${l}`);
495
- await this.tools.plot.setActivityTags({ source: issue.url }, externalTags);
496
-
497
- // Or remove specific tags
498
- for (const removedLabel of removedLabels) {
499
- await this.tools.plot.removeActivityTag(
500
- { source: issue.url },
501
- `jira:${removedLabel}`
502
- );
503
- }
504
- ```
505
-
506
- ## Initial vs Incremental Sync
507
-
508
- When syncing activities from external systems, it's critical to distinguish between initial sync (first import) and incremental sync (ongoing updates). This prevents notification spam and properly handles archived state.
509
-
510
- ### The `initialSync` Flag Pattern
511
-
512
- All sync-based connectors should track whether they're performing an initial sync or incremental sync:
513
-
514
- | Field | Initial Sync | Incremental Sync | Reason |
515
- |-------|--------------|------------------|---------|
516
- | `unread` | `false` | `true` | Avoid notification overload from historical items |
517
- | `archived` | `false` | *omit* | Unarchive on install, preserve user choice on updates |
518
-
519
- ### Example Implementation
520
-
521
- ```typescript
522
- async startSync(authToken: string, resourceId: string): Promise<void> {
523
- // Store initial sync state
524
- await this.set(`sync_state_${resourceId}`, {
525
- resourceId,
526
- sequence: 1,
527
- });
528
-
529
- // Start first batch with initialSync = true
530
- const callback = await this.callback("syncBatch", {
531
- authToken,
532
- resourceId,
533
- initialSync: true
534
- });
535
- // runTask creates NEW execution with fresh ~1000 request limit
536
- await this.runTask(callback);
537
- }
538
-
539
- async syncBatch(
540
- args: any,
541
- context: { authToken: string; resourceId: string; initialSync: boolean }
542
- ): Promise<void> {
543
- const { authToken, resourceId, initialSync } = context;
544
-
545
- // Fetch events from external API (keep batch size reasonable to stay under request limit)
546
- // If each event makes ~10 requests, fetch ~100 events per batch
547
- // 100 events × 10 requests = 1000 requests (at limit)
548
- const events = await this.fetchEvents(authToken, resourceId);
549
-
550
- // Create activities with proper flags
551
- for (const event of events) {
552
- const activity: NewActivity = {
553
- type: ActivityType.Event,
554
- source: event.url,
555
- title: event.title,
556
- unread: !initialSync, // false for initial, true for incremental
557
- ...(initialSync ? { archived: false } : {}), // unarchive on initial only
558
- notes: [
559
- {
560
- activity: { source: event.url },
561
- key: "description",
562
- content: event.description,
563
- },
564
- ],
565
- };
566
-
567
- await this.tools.plot.createActivity(activity);
568
- }
569
-
570
- // Queue next batch or switch to incremental mode
571
- if (hasMorePages) {
572
- const callback = await this.callback("syncBatch", {
573
- authToken,
574
- resourceId,
575
- initialSync, // Keep same mode for remaining pages
576
- });
577
- // Each runTask creates NEW execution with fresh request limit
578
- await this.runTask(callback);
579
- } else if (initialSync) {
580
- // Initial sync complete, switch to incremental mode
581
- await this.set(`sync_state_${resourceId}`, {
582
- resourceId,
583
- initialSync: false,
584
- lastSync: new Date().toISOString(),
585
- });
586
- }
587
- }
588
- ```
589
-
590
- ### Why This Matters
591
-
592
- **Initial sync (first import):**
593
- - Activities are **unarchived** (`archived: false`) - gives user a fresh start
594
- - Activities are marked as **read** (`unread: false`) - prevents notification spam from bulk historical imports
595
- - Use case: When user first installs the connector or reconnects after disconnection
596
-
597
- **Incremental sync (ongoing updates):**
598
- - New activities appear as **unread** (`unread: true`) - user gets notified of new items
599
- - Archived state is **preserved** (field omitted) - respects user's archiving decisions
600
- - Use case: Regular syncs after initial setup is complete
601
-
602
- **Reinstall behavior:**
603
- - Acts as initial sync - previously archived activities are unarchived for fresh start
604
- - User gets a clean slate without notification overload
605
-
606
- ### Tracking Sync State
607
-
608
- Store the `initialSync` flag in your sync state:
609
-
610
- ```typescript
611
- interface SyncState {
612
- resourceId: string;
613
- initialSync: boolean;
614
- lastSync: string | null;
615
- sequence: number;
616
- }
617
-
618
- // Check sync mode before each batch
619
- const state = await this.get<SyncState>(`sync_state_${resourceId}`);
620
- const initialSync = state?.initialSync ?? true; // Default to initial if not set
621
- ```
622
-
623
- ## Choosing the Right Strategy
624
-
625
- Use this decision tree to select the appropriate strategy:
626
-
627
- ```
628
- Do items need to be updated after creation?
629
- ├─ No
630
- │ └─ Use Strategy 1 (Create Once)
631
- │ Example: Alerts, one-time notifications
632
-
633
- └─ Yes
634
-
635
- Does the external system provide stable URLs or IDs?
636
- ├─ Yes
637
- │ │
638
- │ Do you need multiple Plot items per external item?
639
- │ ├─ No
640
- │ │ └─ Use Strategy 2 (Upsert via Source/Key) ⭐ RECOMMENDED
641
- │ │ Example: Calendar events, tasks, issues
642
- │ │
643
- │ └─ Yes
644
- │ └─ Use Strategy 3 (Generate and Store IDs)
645
- │ Example: Email thread → multiple activities
646
-
647
- └─ No
648
- └─ Use Strategy 3 (Generate and Store IDs)
649
- Example: Systems without stable identifiers
650
- ```
651
-
652
- ### Common Use Cases
653
-
654
- | Integration | Recommended Strategy | Rationale |
655
- | ---------------- | -------------------- | ------------------------------------------- |
656
- | Google Calendar | Strategy 2 | Events have stable `htmlLink` URLs |
657
- | Outlook Calendar | Strategy 2 | Events have `webLink` URLs |
658
- | Jira | Strategy 2 | Issues have stable URLs |
659
- | Linear | Strategy 2 | Issues have stable URLs |
660
- | Asana | Strategy 2 | Tasks have stable URLs |
661
- | GitHub Issues | Strategy 2 | Issues have stable URLs |
662
- | Gmail (threads) | Strategy 2 or 3 | Use 2 for thread-level, 3 for message-level |
663
- | Slack (threads) | Strategy 2 | Threads have stable channel:thread IDs |
664
- | RSS Feeds | Strategy 2 | Items usually have GUIDs or links |
665
- | Webhooks | Strategy 1 or 2 | Depends on whether updates are needed |
666
- | Notifications | Strategy 1 | Usually one-time, no updates needed |
667
-
668
- ### Migration Between Strategies
669
-
670
- If you need to change strategies for an existing tool:
671
-
672
- **From Strategy 1 to Strategy 2:**
673
-
674
- - Existing items will remain as duplicates
675
- - New syncs will use source-based deduplication
676
- - Consider adding migration logic to clean up duplicates
677
-
678
- **From Strategy 3 to Strategy 2:**
679
-
680
- ```typescript
681
- // Migration: lookup existing ID, add source, then clean up mapping
682
- const existingId = await this.get<string>(`external:${item.id}`);
683
- if (existingId) {
684
- await this.tools.plot.createActivity({
685
- id: existingId,
686
- source: item.url, // Add source to existing activity
687
- // ... other fields
688
- });
689
- await this.delete(`external:${item.id}`); // Clean up mapping
690
- }
691
- ```
692
-
693
- **From Strategy 2 to Strategy 3:**
694
-
695
- - Existing activities will remain with their sources
696
- - New items can use generated IDs
697
- - Both can coexist if needed
698
-
699
- ## Best Practices
700
-
701
- ### 1. Be Consistent Within a Connector
702
-
703
- Choose one strategy per connector and stick with it. Mixing strategies in the same connector can lead to confusion and bugs.
704
-
705
- ### 2. Use Descriptive Keys
706
-
707
- ```typescript
708
- // Good: descriptive, unique keys
709
- key: "description";
710
- key: "metadata";
711
- key: "comment-${commentId}";
712
- key: "attachment-${filename}";
713
-
714
- // Bad: generic, collision-prone keys
715
- key: "note";
716
- key: "data";
717
- key: "1";
718
- ```
719
-
720
- ### 3. Handle Missing Sources Gracefully
721
-
722
- ```typescript
723
- const source = event.htmlLink || event.id || `temp:${Uuid.Generate()}`;
724
- ```
725
-
726
- ### 4. Document Your Strategy
727
-
728
- Add comments explaining which strategy you're using and why:
729
-
730
- ```typescript
731
- /**
732
- * Syncs calendar events using Strategy 2 (Upsert via Source).
733
- * Each Google Calendar event has a stable htmlLink that serves as the source.
734
- * Event details and attendees are stored as upsertable notes using keys.
735
- */
736
- async syncEvents(): Promise<void> {
737
- // ...
738
- }
739
- ```
740
-
741
- ### 5. Clean Up When Needed
742
-
743
- For Strategy 3, implement cleanup for old mappings:
744
-
745
- ```typescript
746
- async cleanupOldMappings(): Promise<void> {
747
- // Remove mappings for items deleted externally
748
- const allKeys = await this.keys();
749
- for (const key of allKeys) {
750
- if (key.startsWith('external:')) {
751
- const externalId = key.replace('external:', '');
752
- const exists = await this.checkExternalItemExists(externalId);
753
- if (!exists) {
754
- await this.delete(key);
755
- }
756
- }
757
- }
758
- }
759
- ```
760
-
761
- ### 6. Avoid Race Conditions in Two-Way Sync
762
-
763
- When implementing two-way sync where items can be created in Plot and pushed to an external system (e.g. Notes becoming comments), update `Activity.source` / `Note.key` **after** creating the external item. If the external system supports setting custom metadata, include the `Activity.id` / `Note.id` in the metadata when creating the external item. Then, when processing an incoming webhook, check for the Plot ID in the metadata first and use it if present.
764
-
765
- This eliminates a race condition where a webhook for an item you're creating arrives before you've updated the Activity/Note with the external key. Without this pattern, the webhook handler won't find the item by external key and may create a duplicate.
766
-
767
- In a connector, return a `NoteWriteBackResult` from `onNoteCreated` — the runtime sets the key atomically and also records the external content as the sync baseline:
768
-
769
- ```typescript
770
- async onNoteCreated(note: Note, thread: Thread): Promise<NoteWriteBackResult | void> {
771
- const externalComment = await externalApi.createComment(thread.meta.externalItemId, {
772
- body: note.content ?? "",
773
- metadata: { plotNoteId: note.id }, // Embed Plot ID for webhook correlation
774
- });
775
- if (!externalComment?.id) return;
776
- return {
777
- key: `comment-${externalComment.id}`,
778
- // What the external system NOW STORES — must match what your sync-in
779
- // path emits as NewNote.content on re-ingest. The runtime hashes this
780
- // so the next sync re-listing unchanged content preserves Plot's
781
- // (possibly richer-markdown) version instead of clobbering it.
782
- externalContent: externalComment.body,
783
- };
784
- }
785
-
786
- async onWebhook(payload: WebhookPayload): Promise<void> {
787
- const comment = payload.comment;
788
-
789
- // Use the Plot ID from metadata if present (handles the race where the
790
- // webhook arrives before onNoteCreated's return has been applied),
791
- // otherwise fall back to upserting by activity source and key.
792
- await this.tools.plot.createNote({
793
- ...(comment.metadata?.plotNoteId
794
- ? { id: comment.metadata.plotNoteId }
795
- : { activity: { source: payload.itemUrl } }),
796
- key: `comment-${comment.id}`,
797
- content: comment.body,
798
- });
799
- }
800
- ```
801
-
802
- For twists that write notes outside the `onNoteCreated` dispatch path (explicit `pushNoteAsComment`-style methods), set `key` via `updateNote` after the external write — see the legacy pattern below. In that path the sync baseline is **not** established, so the next sync-in will overwrite Plot's content with the external version. Prefer the connector `onNoteCreated` flow when round-trip preservation matters.
803
-
804
- See `connectors/AGENTS.md` → "Sync baseline preservation" for the full contract on what `externalContent` must equal.
805
-
806
- ## Summary
807
-
808
- - **Strategy 1** (Create Once): Simplest, no deduplication, use for one-time items
809
- - **Strategy 2** (Upsert via Source/Key): Recommended for most integrations, automatic deduplication
810
- - **Strategy 3** (Generate and Store IDs): Advanced use cases, maximum flexibility, more complexity
811
-
812
- Start with Strategy 2 for most integrations. Only use Strategy 3 when you have specific requirements that Strategy 2 cannot fulfill.
813
-
814
- For more information:
815
-
816
- - [Core Concepts](CORE_CONCEPTS.md) - Understanding activities, notes, and priorities
817
- - [Tools Guide](TOOLS_GUIDE.md) - Complete reference for the Plot tool
818
- - [Building Connectors](BUILDING_CONNECTORS.md) - Creating external service integrations