This guide will walk you through creating your first Plot Twist. There are two ways to build twists: with natural language (no code) or with TypeScript code for maximum flexibility.
Create twists using natural language descriptions - no programming required!
Create a file named plot-twist.md in your project directory and describe what you want your twist to do:
# My Calendar Twist
I want a twist that:
- Syncs my Google Calendar events into Plot as activities
- Creates tasks for upcoming meetings
- Sends me a reminder 10 minutes before each meeting
- Updates activity status when meetings are completed
Be specific about:
You'll need a Plot account to deploy twists.
# Login to Plot
npx @plotday/twister login
# Deploy directly from your spec
npx @plotday/twister deploy
That's it! Your twist is now live in Plot.
If you want to review or customize the generated code before deploying:
# Generate TypeScript code from your spec
npx @plotday/twister generate
# Review and edit the generated src/index.ts
# Then deploy
npx @plotday/twister deploy
The generate command creates a complete TypeScript twist that you can modify and extend.
Build twists with full control using TypeScript.
Use the Plot CLI to scaffold a new twist:
npx @plotday/twister create
# or
yarn dlx @plotday/twister create
# or
pnpm dlx @plotday/twister create
You'll be prompted for:
my-calendar-twist)This creates a new directory with:
my-calendar-twist/
├── src/
│ └── index.ts # Your twist code
├── package.json
├── tsconfig.json
└── plot-twist.json # Twist configuration
Edit src/index.ts to add your twist logic:
import {
type Activity,
ActivityType,
type Priority,
type ToolBuilder,
Twist,
} from "@plotday/twister";
import { Plot } from "@plotday/twister/tools/plot";
export default class MyTwist extends Twist<MyTwist> {
// Declare tool dependencies
build(build: ToolBuilder) {
return {
plot: build(Plot),
};
}
// Called when the twist is activated for a priority
async activate(priority: Pick<Priority, "id">) {
await this.tools.plot.createActivity({
type: ActivityType.Note,
title: "Welcome! Your twist is now active.",
notes: [
{
content: "Your twist is ready to use. You can now start creating activities and automating your workflow.",
},
],
});
}
// Called when an activity is routed to this twist
async activity(activity: Activity) {
console.log("Processing activity:", activity.title);
}
}
Build and check for errors:
npm run build
# or
pnpm build
You'll need a Plot account to deploy twists.
# Login to Plot
npm run plot login
# Deploy your twist
npm run deploy
Your twist is now deployed and ready to activate in Plot!
Your twist extends the Twist class and implements:
build() - Declares tool dependenciesactivate() - Initialization when added to a prioritydeactivate() - Cleanup when removed from a priorityupgrade() - Migration when deploying a new versionContains twist metadata:
{
"name": "my-calendar-twist",
"displayName": "My Calendar Twist",
"version": "1.0.0",
"description": "Syncs calendar events to Plot"
}
Extends the Twist Creator's base configuration:
{
"extends": "@plotday/twister/tsconfig.base.json",
"include": ["src/*.ts"]
}
Now that you have a basic twist running, explore:
Activity represents something done or to be done (a task, event, or conversation), while Notes represent the updates and details on that activity.
Think of an Activity as a thread on a messaging platform, and Notes as the messages in that thread. Always create activities with an initial note, and add notes for updates rather than creating new activities.
Always create activities with an initial note. The notes array can contain multiple notes (messages in the thread).
Data Sync Tip: When syncing from external systems, use Activity.source for automatic deduplication and Note.key for upsertable notes. See the Sync Strategies guide for detailed patterns.
await this.tools.plot.createActivity({
source: "https://github.com/org/repo/pull/123", // Enables automatic upserts
type: ActivityType.Action,
title: "Review pull request",
notes: [
{
activity: { source: "https://github.com/org/repo/pull/123" },
key: "description", // Using key enables upserts
content: "Please review the authentication changes and ensure they follow security best practices.",
links: [
{
type: ActivityLinkType.external,
title: "View PR",
url: "https://github.com/org/repo/pull/123",
},
],
},
],
});
Important: When creating Actions (tasks), the start field determines how they appear in Plot. By default, omitting start creates a "Do Now" task. For most integrations, you should explicitly set start: null to create backlog items.
// "Do Now" - Actionable today (DEFAULT when start is omitted)
// Use for urgent tasks or items actively in progress
await this.tools.plot.createActivity({
type: ActivityType.Action,
title: "Fix critical bug in production",
notes: [{ content: "Users reporting login failures" }],
// Omitting start defaults to current time
});
// "Do Someday" - Backlog item (RECOMMENDED for most synced tasks)
// Use for task backlog, future ideas, non-urgent items
await this.tools.plot.createActivity({
type: ActivityType.Action,
title: "Refactor authentication module",
start: null, // Explicitly set to null for backlog
notes: [{ content: "Technical debt item to address later" }],
});
// "Do Later" - Scheduled for specific date
// Use when task has a specific due date
await this.tools.plot.createActivity({
type: ActivityType.Action,
title: "Submit expense report",
start: new Date("2025-01-31"), // Due date
notes: [{ content: "December expenses need to be submitted by end of month" }],
});
// Save
await this.set("last_sync", new Date().toISOString());
// Retrieve
const lastSync = await this.get<string>("last_sync");
// Run immediately
const callback = await this.callback("processData");
await this.runTask(callback);
// Schedule for later
await this.runTask(callback, {
runAt: new Date("2025-02-01T10:00:00Z"),
});
Important: Always create Activities with at least one initial Note. The title and preview are brief summaries that may be truncated in the UI. Detailed information should go in Notes.
// ✅ Good - Activity with detailed Note
await this.tools.plot.createActivity({
type: ActivityType.Action,
title: "Deploy v2.0",
notes: [
{
note: "Deployment checklist:\n- Run database migrations\n- Update environment variables\n- Deploy backend services\n- Deploy frontend\n- Run smoke tests",
links: [
{
type: ActivityLinkType.external,
title: "Deployment Guide",
url: "https://docs.example.com/deploy",
},
],
},
],
});
// ❌ Bad - No detailed information
await this.tools.plot.createActivity({
type: ActivityType.Action,
title: "Deploy v2.0",
// Missing Notes with context and steps
});
For conversations, email threads, or workflows, add Notes to the existing Activity instead of creating new Activities.
Recommended Pattern: Use Activity.source and Note.key for automatic upserts - no need to check if the activity exists first:
// Simply create - Plot handles deduplication automatically via source
const conversationUrl = `https://app.example.com/conversations/${conversationId}`;
await this.tools.plot.createNote({
activity: { source: conversationUrl }, // References activity by source
key: `message-${messageId}`, // Use unique key per message for upserts
content: newMessage.text,
});
// If the activity doesn't exist yet, create it with source
await this.tools.plot.createActivity({
source: conversationUrl, // Same source for deduplication
type: ActivityType.Note,
title: "New conversation",
notes: [{
activity: { source: conversationUrl },
key: `message-${messageId}`,
content: newMessage.text,
}],
});
Alternative Pattern (for advanced cases): Use getActivityBySource to check existence:
const existing = await this.tools.plot.getActivityBySource({
conversation_id: conversationId,
});
if (existing) {
await this.tools.plot.createNote({
activity: { id: existing.id },
content: newMessage.text,
});
} else {
await this.tools.plot.createActivity({
type: ActivityType.Note,
title: "New conversation",
meta: { conversation_id: conversationId },
notes: [{ content: newMessage.text }],
});
}
See Sync Strategies for more patterns and guidance on choosing the right approach.
See Core Concepts - Best Practices for more details.