Skip to content

Commit 780523b

Browse files
committed
Clarify Activity and Note relationship in docs
1 parent fe78a0f commit 780523b

6 files changed

Lines changed: 244 additions & 89 deletions

File tree

twister/README.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export default class MyTwist extends Twist<MyTwist> {
7979
await this.tools.plot.createActivity({
8080
type: ActivityType.Note,
8181
title: "Welcome! Your twist is now active.",
82+
notes: [
83+
{
84+
content: "Your twist is ready to use. Check out the [documentation](https://twist.plot.day) to learn more.",
85+
},
86+
],
8287
});
8388
}
8489
}
@@ -124,22 +129,38 @@ Twist tools provide capabilities to twists. They are usually unopinionated and d
124129

125130
[View all tools →](https://twist.plot.day/documents/Built-in_Tools.html)
126131

127-
### Activities
132+
### Activities and Notes
128133

129-
The core data type representing tasks, events, and notes.
134+
**Activity** represents something done or to be done (a task, event, or conversation).
135+
**Notes** represent updates and details on that activity.
136+
137+
Think of an **Activity as a thread** and **Notes as messages in that thread**. Always create activities with an initial note, and add notes for updates rather than creating new activities.
130138

131139
```typescript
140+
// Create an activity with an initial note (thread with first message)
132141
await this.tools.plot.createActivity({
133142
type: ActivityType.Action,
134143
title: "Review pull request",
135-
links: [
144+
source: "github:pr:123", // For deduplication
145+
notes: [
136146
{
137-
type: ActivityLinkType.external,
138-
title: "View PR",
139-
url: "https://github.com/org/repo/pull/123",
147+
content: "New PR ready for review",
148+
links: [
149+
{
150+
type: ActivityLinkType.external,
151+
title: "View PR",
152+
url: "https://github.com/org/repo/pull/123",
153+
},
154+
],
140155
},
141156
],
142157
});
158+
159+
// Add a note to existing activity (add message to thread)
160+
await this.tools.plot.createNote({
161+
activity: { id: activityId },
162+
content: "LGTM! Approved ✅",
163+
});
143164
```
144165

145166
[Learn more →](https://twist.plot.day/documents/Core_Concepts.html)
@@ -201,6 +222,11 @@ export default class WelcomeTwist extends Twist<WelcomeTwist> {
201222
await this.tools.plot.createActivity({
202223
type: ActivityType.Note,
203224
title: "Welcome to Plot! 👋",
225+
notes: [
226+
{
227+
content: "This twist will help you get started with Plot.",
228+
},
229+
],
204230
});
205231
}
206232
}

twister/cli/templates/AGENTS.template.md

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,39 @@ Plot Twists are TypeScript classes that extend the `Twist` base class. Twists in
1717
- **Store intermediate state**: Use the Store tool to persist state between batches
1818
- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations
1919

20-
## twist Structure Pattern
20+
## Understanding Activities and Notes
21+
22+
**CRITICAL CONCEPT**: An **Activity** represents something done or to be done (a task, event, or conversation), while **Notes** represent the updates and details on that activity.
23+
24+
**Think of an Activity as a thread** on a messaging platform, and **Notes as the messages in that thread**.
25+
26+
### Key Guidelines
27+
28+
1. **Always create Activities with an initial Note** - The title is just a summary; detailed content goes in Notes
29+
2. **Add Notes to existing Activities for updates** - Don't create a new Activity for each related message
30+
3. **Use `source` field for deduplication** - Enables safe, idempotent sync from external systems
31+
4. **Most Activities should be `ActivityType.Note`** - Use `Action` only for tasks with `doneAt`, use `Event` only for items with `start`/`end`
32+
33+
### Decision Tree
34+
35+
```
36+
New event/task/conversation?
37+
├─ Yes → Create new Activity with initial Note
38+
│ Include `source` field for deduplication
39+
40+
└─ No (update/reply/comment) → Check for existing Activity
41+
├─ Found → Add Note to existing Activity
42+
└─ Not found → Create new Activity with initial Note
43+
```
44+
45+
## Twist Structure Pattern
2146

2247
```typescript
2348
import {
2449
type Activity,
25-
twist,
2650
type Priority,
2751
type ToolBuilder,
52+
twist,
2853
} from "@plotday/twister";
2954
import { Plot } from "@plotday/twister/tools/plot";
3055

@@ -118,9 +143,14 @@ async activate(_priority: Pick<Priority, "id">) {
118143
);
119144

120145
await this.tools.plot.createActivity({
121-
type: ActivityType.Action,
146+
type: ActivityType.Note,
122147
title: "Connect your account",
123-
links: [authLink],
148+
notes: [
149+
{
150+
content: "Click the link below to connect your account and start syncing.",
151+
links: [authLink],
152+
},
153+
],
124154
});
125155
}
126156
```
@@ -129,8 +159,13 @@ async activate(_priority: Pick<Priority, "id">) {
129159

130160
```typescript
131161
const activity = await this.tools.plot.createActivity({
132-
type: ActivityType.Action,
162+
type: ActivityType.Note,
133163
title: "Setup",
164+
notes: [
165+
{
166+
content: "Your twist is being set up. Configuration steps will appear here.",
167+
},
168+
],
134169
});
135170

136171
await this.set("setup_activity_id", activity.id);
@@ -180,11 +215,16 @@ const callbackLink: ActivityLink = {
180215
token: token,
181216
};
182217

183-
// Add to activity
218+
// Add to activity note
184219
await this.tools.plot.createActivity({
185-
type: ActivityType.Action,
220+
type: ActivityType.Note,
186221
title: "Task with links",
187-
links: [urlLink, callbackLink],
222+
notes: [
223+
{
224+
content: "Click the links below to take action.",
225+
links: [urlLink, callbackLink],
226+
},
227+
],
188228
});
189229
```
190230

@@ -202,9 +242,14 @@ async activate(_priority: Pick<Priority, "id">) {
202242

203243
// Create activity with auth link
204244
const activity = await this.tools.plot.createActivity({
205-
type: ActivityType.Action,
245+
type: ActivityType.Note,
206246
title: "Connect Google account",
207-
links: [authLink],
247+
notes: [
248+
{
249+
content: "Click below to connect your Google account and start syncing.",
250+
links: [authLink],
251+
},
252+
],
208253
});
209254

210255
// Store for later use
@@ -222,7 +267,7 @@ async onAuthComplete(authResult: { authToken: string }, provider: string) {
222267

223268
## Sync Pattern
224269

225-
Pattern for syncing external data with callbacks:
270+
Pattern for syncing external data - demonstrates adding Notes to existing Activities:
226271

227272
```typescript
228273
async startSync(calendarId: string): Promise<void> {
@@ -236,9 +281,30 @@ async startSync(calendarId: string): Promise<void> {
236281
);
237282
}
238283

239-
async handleEvent(activity: Activity, calendarId: string): Promise<void> {
240-
// Process incoming event from external service
241-
await this.tools.plot.createActivity(activity);
284+
async handleEvent(
285+
incomingActivity: NewActivityWithNotes,
286+
calendarId: string
287+
): Promise<void> {
288+
// Check if this activity already exists (using source for deduplication)
289+
if (incomingActivity.source) {
290+
const existing = await this.tools.plot.getActivityBySource(
291+
incomingActivity.source
292+
);
293+
294+
if (existing) {
295+
// Add update as a Note to existing Activity (add message to thread)
296+
if (incomingActivity.notes?.[0]?.content) {
297+
await this.tools.plot.createNote({
298+
activity: { id: existing.id },
299+
content: incomingActivity.notes[0].content,
300+
});
301+
}
302+
return;
303+
}
304+
}
305+
306+
// Create new Activity with initial Note (new thread with first message)
307+
await this.tools.plot.createActivity(incomingActivity);
242308
}
243309

244310
async stopSync(calendarId: string): Promise<void> {
@@ -278,7 +344,12 @@ private async createCalendarSelectionActivity(
278344
await this.tools.plot.createActivity({
279345
type: ActivityType.Note,
280346
title: "Which calendars would you like to connect?",
281-
links,
347+
notes: [
348+
{
349+
content: "Select the calendars you want to sync:",
350+
links,
351+
},
352+
],
282353
});
283354
}
284355

@@ -334,9 +405,14 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
334405
// Process one batch (keep under time limit)
335406
const result = await this.fetchBatch(state.nextPageToken);
336407

337-
// Process results
408+
// Process results (create activities with Notes)
338409
for (const item of result.items) {
339-
await this.tools.plot.createActivity(item);
410+
await this.tools.plot.createActivity({
411+
type: ActivityType.Note,
412+
title: item.title,
413+
source: `external:${item.id}`, // For deduplication
414+
notes: [{ content: item.description }],
415+
});
340416
}
341417

342418
if (result.nextPageToken) {
@@ -357,7 +433,12 @@ async syncBatch(args: any, resourceId: string): Promise<void> {
357433
// Optionally notify user of completion
358434
await this.tools.plot.createActivity({
359435
type: ActivityType.Note,
360-
note: `Sync complete: ${state.itemsProcessed + result.items.length} items processed`,
436+
title: "Sync complete",
437+
notes: [
438+
{
439+
content: `Successfully processed ${state.itemsProcessed + result.items.length} items.`,
440+
},
441+
],
361442
});
362443
}
363444
}
@@ -375,7 +456,12 @@ try {
375456

376457
await this.tools.plot.createActivity({
377458
type: ActivityType.Note,
378-
note: `Failed to complete operation: ${error.message}`,
459+
title: "Operation failed",
460+
notes: [
461+
{
462+
content: `Failed to complete operation: ${error.message}`,
463+
},
464+
],
379465
});
380466
}
381467
```
@@ -384,7 +470,10 @@ try {
384470

385471
- **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist.
386472
- **Processing self-created activities** - Other users may change an Activity created by the twist, resulting in an \`activity\` call. Be sure to check the \`changes === null\` and/or \`activity.author.id !== this.id\` to avoid re-processing.
387-
- Most activity should be `type = ActivityType.Note` with a `title` and `note`, and no `start` or `end`. This represents a typical message. `start` and `end` should only be used for a note if it should be displayed for a specific date or time, such as a birthday.
473+
- **Always create Activities with Notes** - See "Understanding Activities and Notes" section above for the thread/message pattern and decision tree.
474+
- **Use correct Activity types** - Most should be `ActivityType.Note`. Only use `Action` for tasks with `doneAt`, and `Event` for items with `start`/`end`.
475+
- **Use `source` field for deduplication** - Always include `source` when syncing external data to enable safe, idempotent operations.
476+
- **Add Notes to existing Activities** - Check for existing Activities with `getActivityBySource()` before creating new ones. Think thread replies, not new threads.
388477
- Tools are declared in the `build` method and accessed via `this.tools.toolName` in twist methods.
389478
- **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Tasks tool. Process enough items per batch to be efficient, but few enough to stay under time limits.
390479
- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts.

0 commit comments

Comments
 (0)