Understanding these core concepts will help you build effective Plot Twists.
Twists are smart automations that connect, organize, and prioritize your work. They implement opinionated workflows and integrations.
A twist is a class that:
Twist<T> base classbuild() methodactivate, deactivate, upgrade)import { Twist, type Priority, type ToolBuilder } from "@plotday/twister";
import { Plot } from "@plotday/twister/tools/plot";
export default class MyTwist extends Twist<MyTwist> {
// 1. Declare dependencies
build(build: ToolBuilder) {
return {
plot: build(Plot),
};
}
// 2. Initialize on activation
async activate(priority: Pick<Priority, "id">) {
// Setup code - runs once when twist is added to a priority
}
// 3. Handle lifecycle events
async upgrade() {
// Runs when a new version is deployed
}
async deactivate() {
// Cleanup - runs when twist is removed
}
}
Use twists for:
Twist tools provide capabilities to twists. They are usually unopinionated and do nothing on their own. Tools encapsulate reusable capabilities and can be composed together.
Core Plot functionality provided by the Twist Creator:
See the Built-in Tools Guide for complete documentation.
Tools you create or install from npm packages:
See Building Custom Tools to create your own.
Use the build() method to declare which tools your twist needs:
build(build: ToolBuilder) {
return {
plot: build(Plot),
store: build(Store),
calendar: build(GoogleCalendar, {
// Tool-specific options
defaultCalendar: "primary"
}),
};
}
Access your tools via this.tools:
async activate(priority: Pick<Priority, "id">) {
// Tools are fully typed
await this.tools.plot.createActivity({
type: ActivityType.Note,
title: "Hello from my twist"
});
}
Some tool methods are available directly on the Twist class for convenience:
// Store
await this.get("key");
await this.set("key", value);
await this.clear("key");
// Tasks
await this.runTask(callback);
await this.cancelTask(token);
// Callbacks
await this.callback("methodName", ...args);
await this.run(callbackToken);
Priorities are contexts that organize activities. Think of them like projects or focus areas.
Priorities can be nested to create hierarchies:
Work
├── Project A
│ ├── Backend
│ └── Frontend
└── Project B
// Top-level priority
const work = await this.tools.plot.createPriority({
title: "Work",
});
// Nested priority
const projectA = await this.tools.plot.createPriority({
title: "Project A",
parentId: work.id,
});
Twists are activated within a specific priority. When activated, the twist has access to that priority and all its children.
async activate(priority: Pick<Priority, "id">) {
// This twist is now active for this priority
// It can create activities, set up webhooks, etc.
}
Activities are the core data type in Plot, representing tasks, events, and notes.
import { ActivityType } from "@plotday/twister";
// Note
await this.tools.plot.createActivity({
type: ActivityType.Note,
title: "Meeting notes from sync",
});
// Task
await this.tools.plot.createActivity({
type: ActivityType.Task,
title: "Review pull request",
doneAt: null, // null = not done
});
// Event
await this.tools.plot.createActivity({
type: ActivityType.Event,
title: "Team standup",
start: new Date("2025-02-01T10:00:00Z"),
end: new Date("2025-02-01T10:30:00Z"),
});
type Activity = {
id: string; // Unique identifier
type: ActivityType; // Note, Task, or Event
title: string | null; // Display title
note: string | null; // Additional details
start: Date | null; // Event start time
end: Date | null; // Event end time
doneAt: Date | null; // Task completion time
links: ActivityLink[]; // Action links
tags: Record<Tag, ActorId[]>; // Tag assignments
// ... and more
};
Links enable user interaction with activities:
import { ActivityLinkType } from "@plotday/twister";
await this.tools.plot.createActivity({
type: ActivityType.Task,
title: "Fix bug #123",
links: [
{
type: ActivityLinkType.external,
title: "View Issue",
url: "https://github.com/org/repo/issues/123",
},
{
type: ActivityLinkType.callback,
title: "Mark as Fixed",
callback: await this.callback("markAsFixed", "123"),
},
],
});
Link Types:
Twists have several lifecycle methods that are called at specific times.
Called when the twist is first activated for a priority.
Use for:
async activate(priority: Pick<Priority, "id">) {
// Create welcome message
await this.tools.plot.createActivity({
type: ActivityType.Note,
title: "Calendar sync is now active"
});
// Set up webhook
const webhookUrl = await this.tools.network.createWebhook("onUpdate");
await this.set("webhook_url", webhookUrl);
}
Called when a new version of your twist is deployed to an existing priority.
Use for:
async upgrade() {
// Check version and migrate
const version = await this.get<string>("version");
if (!version || version < "2.0.0") {
// Migrate old data format
const oldData = await this.get("old_key");
await this.set("new_key", transformData(oldData));
await this.clear("old_key");
}
await this.set("version", "2.0.0");
}
Called when the twist is removed from a priority.
Use for:
async deactivate() {
// Clean up webhook
const webhookUrl = await this.get<string>("webhook_url");
if (webhookUrl) {
await this.tools.network.deleteWebhook(webhookUrl);
}
// Clean up stored data
await this.clearAll();
}
Use the Store tool for persistent state, not instance variables:
// ❌ WRONG - Instance variables don't persist
class MyTwist extends Twist<MyTwist> {
private syncToken: string; // This will be lost!
}
// ✅ CORRECT - Use Store
class MyTwist extends Twist<MyTwist> {
async getSyncToken() {
return await this.get<string>("sync_token");
}
async setSyncToken(token: string) {
await this.set("sync_token", token);
}
}
Always handle errors gracefully:
async activate(priority: Pick<Priority, "id">) {
try {
await this.tools.plot.createActivity({
type: ActivityType.Note,
title: "Twist activated"
});
} catch (error) {
console.error("Failed to create activity:", error);
// Twist activation continues even if this fails
}
}
Break long-running operations into batches:
async startSync() {
const callback = await this.callback("syncBatch", { page: 1 });
await this.runTask(callback);
}
async syncBatch(args: any, context: { page: number }) {
// Process one page
const hasMore = await processPage(context.page);
if (hasMore) {
// Queue next batch
const callback = await this.callback("syncBatch", {
page: context.page + 1
});
await this.runTask(callback);
}
}
See Runtime Environment for more details.
Leverage TypeScript for type safety:
// Define interfaces for stored data
interface SyncState {
lastSync: string;
token: string;
status: "active" | "paused";
}
async getSyncState(): Promise<SyncState | null> {
return await this.get<SyncState>("sync_state");
}
Build complex functionality by composing tools:
build(build: ToolBuilder) {
return {
plot: build(Plot),
network: build(Network, {
urls: ["https://api.service.com/*"]
}),
auth: build(Integrations),
ai: build(AI)
};
}
Make activity titles clear and actionable:
// ❌ Vague
await this.tools.plot.createActivity({
type: ActivityType.Task,
title: "Thing",
});
// ✅ Clear
await this.tools.plot.createActivity({
type: ActivityType.Task,
title: "Review pull request #123 for authentication fix",
});