Creating Plot Twists
    Preparing search index...

    Core Concepts

    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:

    • Extends the Twist<T> base class
    • Declares tool dependencies in the build() method
    • Responds to lifecycle events (activate, deactivate, upgrade)
    • Can process activities and create new ones
    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:

    • Integrations - Connecting external services (Google Calendar, GitHub, Slack)
    • Automations - Automatic task creation, reminders, status updates
    • Data Processing - Analyzing and organizing activities
    • Notifications - Sending alerts based on conditions

    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:

    • Plot - Create and manage activities and priorities
    • Store - Persistent key-value storage
    • Integrations - OAuth authentication
    • Tasks - Background task execution
    • Network - HTTP access and webhooks
    • Callbacks - Persistent function references
    • AI - Language model integration

    See the Built-in Tools Guide for complete documentation.

    Tools you create or install from npm packages:

    • External Service Integrations - Google Calendar, Slack, GitHub
    • Data Processors - Text analysis, image processing
    • Utilities - Date formatting, validation

    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.

    • Note - Information without actionable requirements
    • Task - Actionable items that can be completed
    • Event - Scheduled occurrences with start/end times
    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:

    • external - Opens URL in browser
    • auth - Initiates OAuth flow
    • callback - Triggers twist method when clicked

    Twists have several lifecycle methods that are called at specific times.

    Called when the twist is first activated for a priority.

    Use for:

    • Creating initial activities
    • Setting up webhooks
    • Initializing state
    • Requesting authentication
    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:

    • Migrating data structures
    • Updating webhook configurations
    • Adding new features to existing installations
    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:

    • Removing webhooks
    • Cleanup of external resources
    • Final data operations
    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",
    });