@plotday/twister 0.57.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.
- package/README.md +53 -44
- package/bin/commands/create.js +9 -14
- package/bin/commands/create.js.map +1 -1
- package/bin/commands/deploy.js +2 -0
- package/bin/commands/deploy.js.map +1 -1
- package/bin/commands/generate.js +8 -5
- package/bin/commands/generate.js.map +1 -1
- package/bin/index.js +2 -2
- package/bin/index.js.map +1 -1
- package/bin/templates/AGENTS.template.md +110 -94
- package/bin/templates/README.template.md +36 -33
- package/cli/templates/AGENTS.template.md +110 -94
- package/cli/templates/README.template.md +36 -33
- package/dist/connector.d.ts +24 -17
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js +19 -12
- package/dist/connector.js.map +1 -1
- package/dist/docs/assets/hierarchy.js +1 -1
- package/dist/docs/assets/navigation.js +1 -1
- package/dist/docs/assets/search.js +1 -1
- package/dist/docs/classes/index.Connector.html +66 -60
- package/dist/docs/classes/index.FileNotFoundError.html +2 -2
- package/dist/docs/classes/index.Files.html +4 -4
- package/dist/docs/classes/index.Imap.html +10 -10
- package/dist/docs/classes/index.Options.html +2 -2
- package/dist/docs/classes/index.Smtp.html +6 -6
- package/dist/docs/classes/tool.ITool.html +2 -2
- package/dist/docs/classes/tool.Tool.html +23 -23
- package/dist/docs/classes/tools_ai.AI.html +5 -5
- package/dist/docs/classes/tools_callbacks.Callbacks.html +8 -8
- package/dist/docs/classes/tools_integrations.Integrations.html +15 -15
- package/dist/docs/classes/tools_network.Network.html +9 -9
- package/dist/docs/classes/tools_plot.Plot.html +34 -33
- package/dist/docs/classes/tools_store.Store.html +8 -8
- package/dist/docs/classes/tools_tasks.Tasks.html +6 -6
- package/dist/docs/classes/tools_twists.Twists.html +12 -11
- package/dist/docs/classes/twist.Twist.html +28 -28
- package/dist/docs/documents/Building_Connectors.html +42 -28
- package/dist/docs/documents/Built-in_Tools.html +170 -67
- package/dist/docs/documents/CLI_Reference.html +68 -47
- package/dist/docs/documents/Core_Concepts.html +52 -81
- package/dist/docs/documents/Getting_Started.html +28 -31
- package/dist/docs/documents/MULTI_USER_AUTH.html +45 -0
- package/dist/docs/documents/Runtime_Environment.html +13 -12
- package/dist/docs/documents/SYNC_STRATEGIES.html +373 -0
- package/dist/docs/enums/plot.ActionType.html +9 -9
- package/dist/docs/enums/plot.ActorType.html +4 -4
- package/dist/docs/enums/plot.ConferencingProvider.html +6 -6
- package/dist/docs/enums/plot.ThemeColor.html +9 -9
- package/dist/docs/enums/tag.Tag.html +3 -3
- package/dist/docs/enums/tools_ai.AIModel.html +3 -3
- package/dist/docs/enums/tools_integrations.AuthProvider.html +13 -13
- package/dist/docs/enums/tools_plot.ContactAccess.html +2 -2
- package/dist/docs/enums/tools_plot.FocusAccess.html +3 -3
- package/dist/docs/enums/tools_plot.LinkAccess.html +3 -3
- package/dist/docs/enums/tools_plot.ThreadAccess.html +4 -4
- package/dist/docs/functions/index.Uuid.Generate.html +1 -1
- package/dist/docs/functions/utils_hash.quickHash.html +1 -1
- package/dist/docs/hierarchy.html +1 -1
- package/dist/docs/index.html +7 -8
- package/dist/docs/interfaces/tools_ai.AIRequest.html +13 -13
- package/dist/docs/interfaces/tools_ai.AIResponse.html +9 -9
- package/dist/docs/interfaces/tools_ai.FilePart.html +5 -5
- package/dist/docs/interfaces/tools_ai.ImagePart.html +4 -4
- package/dist/docs/interfaces/tools_ai.ReasoningPart.html +4 -4
- package/dist/docs/interfaces/tools_ai.RedactedReasoningPart.html +3 -3
- package/dist/docs/interfaces/tools_ai.TextPart.html +3 -3
- package/dist/docs/interfaces/tools_ai.ToolCallPart.html +5 -5
- package/dist/docs/interfaces/tools_ai.ToolExecutionOptions.html +4 -4
- package/dist/docs/interfaces/tools_ai.ToolResultPart.html +5 -5
- package/dist/docs/interfaces/tools_twists.TwistSource.html +3 -3
- package/dist/docs/interfaces/utils_types.ToolShed.html +5 -5
- package/dist/docs/media/AGENTS.md +101 -74
- package/dist/docs/modules.html +1 -1
- package/dist/docs/types/index.BooleanDef.html +2 -2
- package/dist/docs/types/index.CreateLinkDraft.html +9 -9
- package/dist/docs/types/index.ImapAddress.html +3 -3
- package/dist/docs/types/index.ImapConnectOptions.html +6 -6
- package/dist/docs/types/index.ImapFetchOptions.html +4 -4
- package/dist/docs/types/index.ImapFlagOperation.html +1 -1
- package/dist/docs/types/index.ImapMailbox.html +5 -5
- package/dist/docs/types/index.ImapMailboxStatus.html +7 -7
- package/dist/docs/types/index.ImapMessage.html +14 -14
- package/dist/docs/types/index.ImapSearchCriteria.html +9 -9
- package/dist/docs/types/index.ImapSession.html +1 -1
- package/dist/docs/types/index.NewSchedule.html +13 -13
- package/dist/docs/types/index.NewScheduleContact.html +2 -2
- package/dist/docs/types/index.NewScheduleOccurrence.html +1 -1
- package/dist/docs/types/index.NoteWriteBackResult.html +3 -3
- package/dist/docs/types/index.NumberDef.html +2 -2
- package/dist/docs/types/index.OptionDef.html +1 -1
- package/dist/docs/types/index.OptionalScopeGroup.html +6 -6
- package/dist/docs/types/index.OptionsSchema.html +1 -1
- package/dist/docs/types/index.ReactionCapabilities.html +1 -1
- package/dist/docs/types/index.ResolvedOptions.html +1 -1
- package/dist/docs/types/index.ResolvedRecipient.html +5 -5
- package/dist/docs/types/index.Schedule.html +12 -12
- package/dist/docs/types/index.ScheduleContact.html +2 -2
- package/dist/docs/types/index.ScheduleContactRole.html +1 -1
- package/dist/docs/types/index.ScheduleContactStatus.html +1 -1
- package/dist/docs/types/index.ScheduleOccurrence.html +6 -6
- package/dist/docs/types/index.ScheduleOccurrenceUpdate.html +1 -1
- package/dist/docs/types/index.ScopeConfig.html +3 -3
- package/dist/docs/types/index.SelectDef.html +2 -2
- package/dist/docs/types/index.Serializable.html +1 -1
- package/dist/docs/types/index.SmtpAddress.html +3 -3
- package/dist/docs/types/index.SmtpConnectOptions.html +7 -7
- package/dist/docs/types/index.SmtpMessage.html +12 -12
- package/dist/docs/types/index.SmtpSendResult.html +4 -4
- package/dist/docs/types/index.SmtpSession.html +1 -1
- package/dist/docs/types/index.TextDef.html +2 -2
- package/dist/docs/types/index.Uuid.html +1 -1
- package/dist/docs/types/plot.Action.html +1 -1
- package/dist/docs/types/plot.Actor.html +5 -5
- package/dist/docs/types/plot.ActorId.html +4 -4
- package/dist/docs/types/plot.Contact.html +4 -4
- package/dist/docs/types/plot.ContentType.html +1 -1
- package/dist/docs/types/plot.Focus.html +8 -8
- package/dist/docs/types/plot.FocusUpdate.html +1 -1
- package/dist/docs/types/plot.Link.html +17 -17
- package/dist/docs/types/plot.LinkUpdate.html +1 -1
- package/dist/docs/types/plot.NewActor.html +1 -1
- package/dist/docs/types/plot.NewContact.html +1 -1
- package/dist/docs/types/plot.NewFocus.html +1 -1
- package/dist/docs/types/plot.NewLink.html +5 -2
- package/dist/docs/types/plot.NewLinkWithNotes.html +1 -1
- package/dist/docs/types/plot.NewNote.html +1 -1
- package/dist/docs/types/plot.NewReactions.html +1 -1
- package/dist/docs/types/plot.NewTags.html +1 -1
- package/dist/docs/types/plot.NewThread.html +1 -1
- package/dist/docs/types/plot.NewThreadWithNotes.html +1 -1
- package/dist/docs/types/plot.Note.html +1 -1
- package/dist/docs/types/plot.NoteUpdate.html +1 -1
- package/dist/docs/types/plot.PlanOperation.html +1 -1
- package/dist/docs/types/plot.Reaction.html +3 -3
- package/dist/docs/types/plot.Reactions.html +1 -1
- package/dist/docs/types/plot.Tags.html +1 -1
- package/dist/docs/types/plot.Thread.html +1 -1
- package/dist/docs/types/plot.ThreadAccessLevel.html +1 -1
- package/dist/docs/types/plot.ThreadCommon.html +6 -6
- package/dist/docs/types/plot.ThreadFilter.html +2 -2
- package/dist/docs/types/plot.ThreadMeta.html +1 -1
- package/dist/docs/types/plot.ThreadType.html +1 -1
- package/dist/docs/types/plot.ThreadUpdate.html +1 -1
- package/dist/docs/types/plot.ThreadWithNotes.html +1 -1
- package/dist/docs/types/tools_ai.AIAssistantMessage.html +2 -2
- package/dist/docs/types/tools_ai.AICapabilities.html +4 -4
- package/dist/docs/types/tools_ai.AIMessage.html +1 -1
- package/dist/docs/types/tools_ai.AIOptions.html +2 -2
- package/dist/docs/types/tools_ai.AISource.html +1 -1
- package/dist/docs/types/tools_ai.AISystemMessage.html +2 -2
- package/dist/docs/types/tools_ai.AITool.html +1 -1
- package/dist/docs/types/tools_ai.AIToolMessage.html +2 -2
- package/dist/docs/types/tools_ai.AIToolSet.html +1 -1
- package/dist/docs/types/tools_ai.AIUsage.html +5 -5
- package/dist/docs/types/tools_ai.AIUserMessage.html +2 -2
- package/dist/docs/types/tools_ai.DataContent.html +1 -1
- package/dist/docs/types/tools_ai.ModelPreferences.html +5 -5
- package/dist/docs/types/tools_callbacks.Callback.html +2 -2
- package/dist/docs/types/tools_integrations.ArchiveLinkFilter.html +5 -5
- package/dist/docs/types/tools_integrations.ArchiveNotesFilter.html +2 -2
- package/dist/docs/types/tools_integrations.AuthToken.html +6 -5
- package/dist/docs/types/tools_integrations.Authorization.html +4 -4
- package/dist/docs/types/tools_integrations.Channel.html +6 -6
- package/dist/docs/types/tools_integrations.ComposeConfig.html +4 -4
- package/dist/docs/types/tools_integrations.ContactRoleConfig.html +5 -5
- package/dist/docs/types/tools_integrations.LinkTypeConfig.html +21 -21
- package/dist/docs/types/tools_integrations.NewCustomEmoji.html +8 -8
- package/dist/docs/types/tools_integrations.StatusIcon.html +1 -1
- package/dist/docs/types/tools_integrations.SyncContext.html +4 -4
- package/dist/docs/types/tools_network.WebhookRequest.html +6 -6
- package/dist/docs/types/tools_plot.LinkFilter.html +5 -5
- package/dist/docs/types/tools_plot.LinkSearchResult.html +1 -1
- package/dist/docs/types/tools_plot.NoteIntentHandler.html +4 -4
- package/dist/docs/types/tools_plot.NoteSearchResult.html +1 -1
- package/dist/docs/types/tools_plot.SearchOptions.html +4 -4
- package/dist/docs/types/tools_plot.SearchResult.html +1 -1
- package/dist/docs/types/tools_twists.Log.html +2 -2
- package/dist/docs/types/tools_twists.TwistPermissions.html +1 -1
- package/dist/docs/types/utils_types.BuiltInTools.html +2 -2
- package/dist/docs/types/utils_types.ExtractBuildReturn.html +1 -1
- package/dist/docs/types/utils_types.InferOptions.html +1 -1
- package/dist/docs/types/utils_types.InferTools.html +1 -1
- package/dist/docs/types/utils_types.JSONValue.html +1 -1
- package/dist/docs/types/utils_types.PromiseValues.html +1 -1
- package/dist/docs/types/utils_types.ToolBuilder.html +1 -1
- package/dist/docs/variables/tools_plot.SEARCH_DEFAULT_LIMIT.html +1 -1
- package/dist/docs/variables/tools_plot.SEARCH_MAX_LIMIT.html +1 -1
- package/dist/facets.d.ts +30 -0
- package/dist/facets.d.ts.map +1 -0
- package/dist/facets.js +16 -0
- package/dist/facets.js.map +1 -0
- package/dist/llm-docs/connector.d.ts +1 -1
- package/dist/llm-docs/connector.d.ts.map +1 -1
- package/dist/llm-docs/connector.js +1 -1
- package/dist/llm-docs/connector.js.map +1 -1
- package/dist/llm-docs/facets.d.ts +9 -0
- package/dist/llm-docs/facets.d.ts.map +1 -0
- package/dist/llm-docs/facets.js +8 -0
- package/dist/llm-docs/facets.js.map +1 -0
- package/dist/llm-docs/index.d.ts.map +1 -1
- package/dist/llm-docs/index.js +2 -0
- package/dist/llm-docs/index.js.map +1 -1
- package/dist/llm-docs/plot.d.ts +1 -1
- package/dist/llm-docs/plot.d.ts.map +1 -1
- package/dist/llm-docs/plot.js +1 -1
- package/dist/llm-docs/plot.js.map +1 -1
- package/dist/llm-docs/tool.d.ts +1 -1
- package/dist/llm-docs/tool.d.ts.map +1 -1
- package/dist/llm-docs/tool.js +1 -1
- package/dist/llm-docs/tool.js.map +1 -1
- package/dist/llm-docs/tools/ai.d.ts +1 -1
- package/dist/llm-docs/tools/ai.d.ts.map +1 -1
- package/dist/llm-docs/tools/ai.js +1 -1
- package/dist/llm-docs/tools/ai.js.map +1 -1
- package/dist/llm-docs/tools/callbacks.d.ts +1 -1
- package/dist/llm-docs/tools/callbacks.d.ts.map +1 -1
- package/dist/llm-docs/tools/callbacks.js +1 -1
- package/dist/llm-docs/tools/callbacks.js.map +1 -1
- package/dist/llm-docs/tools/files.d.ts +1 -1
- package/dist/llm-docs/tools/files.d.ts.map +1 -1
- package/dist/llm-docs/tools/files.js +1 -1
- package/dist/llm-docs/tools/files.js.map +1 -1
- package/dist/llm-docs/tools/imap.d.ts +1 -1
- package/dist/llm-docs/tools/imap.d.ts.map +1 -1
- package/dist/llm-docs/tools/imap.js +1 -1
- package/dist/llm-docs/tools/imap.js.map +1 -1
- package/dist/llm-docs/tools/integrations.d.ts +1 -1
- package/dist/llm-docs/tools/integrations.d.ts.map +1 -1
- package/dist/llm-docs/tools/integrations.js +1 -1
- package/dist/llm-docs/tools/integrations.js.map +1 -1
- package/dist/llm-docs/tools/network.d.ts +1 -1
- package/dist/llm-docs/tools/network.d.ts.map +1 -1
- package/dist/llm-docs/tools/network.js +1 -1
- package/dist/llm-docs/tools/network.js.map +1 -1
- package/dist/llm-docs/tools/plot.d.ts +1 -1
- package/dist/llm-docs/tools/plot.d.ts.map +1 -1
- package/dist/llm-docs/tools/plot.js +1 -1
- package/dist/llm-docs/tools/plot.js.map +1 -1
- package/dist/llm-docs/tools/smtp.d.ts +1 -1
- package/dist/llm-docs/tools/smtp.d.ts.map +1 -1
- package/dist/llm-docs/tools/smtp.js +1 -1
- package/dist/llm-docs/tools/smtp.js.map +1 -1
- package/dist/llm-docs/tools/tasks.d.ts +1 -1
- package/dist/llm-docs/tools/tasks.d.ts.map +1 -1
- package/dist/llm-docs/tools/tasks.js +1 -1
- package/dist/llm-docs/tools/tasks.js.map +1 -1
- package/dist/llm-docs/tools/twists.d.ts +1 -1
- package/dist/llm-docs/tools/twists.d.ts.map +1 -1
- package/dist/llm-docs/tools/twists.js +1 -1
- package/dist/llm-docs/tools/twists.js.map +1 -1
- package/dist/llm-docs/twist-guide-template.d.ts +1 -1
- package/dist/llm-docs/twist-guide-template.d.ts.map +1 -1
- package/dist/llm-docs/twist-guide-template.js +1 -1
- package/dist/llm-docs/twist-guide-template.js.map +1 -1
- package/dist/llm-docs/twist.d.ts +1 -1
- package/dist/llm-docs/twist.d.ts.map +1 -1
- package/dist/llm-docs/twist.js +1 -1
- package/dist/llm-docs/twist.js.map +1 -1
- package/dist/plot.d.ts +15 -8
- package/dist/plot.d.ts.map +1 -1
- package/dist/plot.js.map +1 -1
- package/dist/tool.d.ts +4 -4
- package/dist/tool.js +4 -4
- package/dist/tools/ai.d.ts +12 -13
- package/dist/tools/ai.d.ts.map +1 -1
- package/dist/tools/ai.js +8 -9
- package/dist/tools/ai.js.map +1 -1
- package/dist/tools/callbacks.d.ts +1 -1
- package/dist/tools/files.d.ts +2 -2
- package/dist/tools/imap.d.ts +1 -1
- package/dist/tools/imap.js +1 -1
- package/dist/tools/integrations.d.ts +2 -1
- package/dist/tools/integrations.d.ts.map +1 -1
- package/dist/tools/network.d.ts +5 -5
- package/dist/tools/plot.d.ts +42 -37
- package/dist/tools/plot.d.ts.map +1 -1
- package/dist/tools/plot.js +16 -12
- package/dist/tools/plot.js.map +1 -1
- package/dist/tools/smtp.d.ts +1 -1
- package/dist/tools/smtp.js +1 -1
- package/dist/tools/tasks.d.ts +6 -8
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +5 -7
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/twists.d.ts +15 -14
- package/dist/tools/twists.d.ts.map +1 -1
- package/dist/tools/twists.js +2 -2
- package/dist/tools/twists.js.map +1 -1
- package/dist/twist-guide.d.ts +1 -1
- package/dist/twist-guide.d.ts.map +1 -1
- package/dist/twist.d.ts +2 -2
- package/dist/twist.js +2 -2
- package/package.json +6 -1
- package/src/connector.ts +23 -16
- package/src/facets.ts +40 -0
- package/src/llm-docs/connector.ts +1 -1
- package/src/llm-docs/facets.ts +8 -0
- package/src/llm-docs/index.ts +2 -0
- package/src/llm-docs/plot.ts +1 -1
- package/src/llm-docs/tool.ts +1 -1
- package/src/llm-docs/tools/ai.ts +1 -1
- package/src/llm-docs/tools/callbacks.ts +1 -1
- package/src/llm-docs/tools/files.ts +1 -1
- package/src/llm-docs/tools/imap.ts +1 -1
- package/src/llm-docs/tools/integrations.ts +1 -1
- package/src/llm-docs/tools/network.ts +1 -1
- package/src/llm-docs/tools/plot.ts +1 -1
- package/src/llm-docs/tools/smtp.ts +1 -1
- package/src/llm-docs/tools/tasks.ts +1 -1
- package/src/llm-docs/tools/twists.ts +1 -1
- package/src/llm-docs/twist-guide-template.ts +1 -1
- package/src/llm-docs/twist.ts +1 -1
- package/src/plot.ts +15 -8
- package/src/tool.ts +4 -4
- package/src/tools/ai.ts +12 -13
- package/src/tools/callbacks.ts +1 -1
- package/src/tools/files.ts +2 -2
- package/src/tools/imap.ts +1 -1
- package/src/tools/integrations.ts +2 -1
- package/src/tools/network.ts +5 -5
- package/src/tools/plot.ts +42 -37
- package/src/tools/smtp.ts +1 -1
- package/src/tools/tasks.ts +6 -8
- package/src/tools/twists.ts +15 -14
- package/src/twist.ts +2 -2
- package/dist/docs/media/MULTI_USER_AUTH.md +0 -116
- 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
|