Creating Plot Twists
    Preparing search index...

    Class Twist<TSelf>Abstract

    Base class for all twists.

    A twist is installed at the workspace level and is owned by a single user (see this.userId). It has no inherent focus scope: threads, notes, and links it creates are filed against the owner's focuses, with automatic focus matching when no explicit target is provided.

    Override build() to declare tool dependencies and lifecycle methods to handle events.

    class FlatteringTwist extends Twist<FlatteringTwist> {
    build(build: ToolBuilder) {
    return {
    plot: build(Plot),
    };
    }

    async activate() {
    await this.tools.plot.createThread({
    title: "Hello, good looking!",
    });
    }
    }

    Type Parameters

    • TSelf

    Hierarchy (View Summary)

    Index

    Accessors

    Constructors

    Methods

    • Declares tool dependencies for this twist. Return an object mapping tool names to build() promises.

      Parameters

      • build: ToolBuilder

        The build function to use for declaring dependencies

      Returns Record<string, Promise<ITool>>

      Object mapping tool names to tool promises

      build(build: ToolBuilder) {
      return {
      plot: build(Plot),
      calendar: build(GoogleCalendar, { apiKey: "..." }),
      };
      }
    • Creates a persistent callback to a method on this twist.

      ExtraArgs are strongly typed to match the method's signature. They must be serializable.

      Type Parameters

      Parameters

      • fn: Fn

        The method to callback

      • ...extraArgs: TArgs

        Additional arguments to pass (type-checked, must be serializable)

      Returns Promise<Callback>

      Promise resolving to a persistent callback token

      const callback = await this.callback(this.onWebhook, "calendar", 123);
      
    • Creates a persistent callback to a method on this twist.

      ExtraArgs are strongly typed to match the method's signature. They must be serializable.

      Type Parameters

      Parameters

      • fn: Fn

        The method to callback

      • ...extraArgs: TArgs

        Additional arguments to pass (type-checked, must be serializable)

      Returns Promise<Callback>

      Promise resolving to a persistent callback token

      const callback = await this.callback(this.onWebhook, "calendar", 123);
      
    • Like callback(), but for an Action, which receives the action as the first argument.

      Type Parameters

      Parameters

      • fn: Fn

        The method to callback

      • ...extraArgs: TArgs

        Additional arguments to pass after the action

      Returns Promise<Callback>

      Promise resolving to a persistent callback token

      const callback = await this.actionCallback(this.doSomething, 123);
      const action: Action = {
      type: ActionType.callback,
      title: "Do Something",
      callback,
      };
    • Deletes a specific callback by its token.

      Parameters

      • token: Callback

        The callback token to delete

      Returns Promise<void>

      Promise that resolves when the callback is deleted

    • Deletes all callbacks for this twist.

      Returns Promise<void>

      Promise that resolves when all callbacks are deleted

    • Executes a callback by its token inline in the current execution.

      Use this.runTask() instead for batch continuations and long-running work. this.run() executes inline, sharing the current request count (~1000 limit) and blocking the HTTP response. This causes timeouts when used in lifecycle methods like onChannelEnabled or syncBatch continuations.

      this.run() is appropriate when you need the callback's return value — e.g., running a parent callback token that returns data. For fire-and-forget work, always prefer this.runTask().

      Parameters

      • token: Callback

        The callback token to execute

      • ...args: []

        Optional arguments to pass to the callback

      Returns Promise<any>

      Promise resolving to the callback result

    • Retrieves a value from persistent storage by key.

      Values are automatically deserialized using SuperJSON, which properly restores Date objects, Maps, Sets, and other complex types.

      Type Parameters

      • T extends Serializable

        The expected type of the stored value (must be Serializable)

      Parameters

      • key: string

        The storage key to retrieve

      Returns Promise<T | null>

      Promise resolving to the stored value or null

    • Stores a value in persistent storage.

      The value will be serialized using SuperJSON and stored persistently. SuperJSON automatically handles Date objects, Maps, Sets, undefined values, and other complex types that standard JSON doesn't support.

      Important: Functions and Symbols cannot be stored. For function references: Use callbacks instead of storing functions directly.

      Type Parameters

      • T extends Serializable

        The type of value being stored (must be Serializable)

      Parameters

      • key: string

        The storage key to use

      • value: T

        The value to store (must be SuperJSON-serializable)

      Returns Promise<void>

      Promise that resolves when the value is stored

      // ✅ Date objects are preserved
      await this.set("sync_state", {
      lastSync: new Date(),
      minDate: new Date(2024, 0, 1)
      });

      // ✅ undefined is now supported
      await this.set("data", { name: "test", optional: undefined });

      // ❌ WRONG: Cannot store functions directly
      await this.set("handler", this.myHandler);

      // ✅ CORRECT: Create a callback token first
      const token = await this.callback(this.myHandler, "arg1", "arg2");
      await this.set("handler_token", token);

      // Later, execute the callback
      const token = await this.get<Callback>("handler_token");
      await this.run(token);
    • Stores many key/value pairs in one round-trip. Always prefer this over looping set() for batch writes — each set() is a network round-trip. Atomic: either every entry lands or none do. See Store.setMany.

      Type Parameters

      • T extends Serializable

        The type of values being stored (must be Serializable)

      Parameters

      • entries: [key: string, value: T][]

        Array of [key, value] pairs to store

      Returns Promise<void>

      Promise that resolves when all values are stored

    • Reads many keys in one round-trip. Always prefer this over looping get(). Results are positionally aligned with keys; missing keys are null. See Store.getMany.

      Type Parameters

      Parameters

      • keys: string[]

        The storage keys to read

      Returns Promise<(T | null)[]>

      Promise resolving to one value (or null) per requested key

    • Lists matching keys with their values in one round-trip — the read counterpart of setMany. Replaces list() + a get() per key, which costs 1 + N round-trips. See Store.listEntries.

      Type Parameters

      Parameters

      • prefix: string

        The prefix to match keys against

      Returns Promise<[key: string, value: T][]>

      Promise resolving to [key, value] pairs, key-ascending

    • Removes a specific key from persistent storage.

      Parameters

      • key: string

        The storage key to remove

      Returns Promise<void>

      Promise that resolves when the key is removed

    • Removes many keys in one round-trip. Pair with listEntries so a drain costs two round-trips regardless of key count. Atomic. See Store.clearMany.

      Parameters

      • keys: string[]

        The storage keys to remove

      Returns Promise<void>

      Promise that resolves when all keys are removed

    • Removes all keys from this twist's storage.

      Returns Promise<void>

      Promise that resolves when all keys are removed

    • Queues a callback to execute in a separate worker context.

      Parameters

      • callback: Callback

        The callback token created with this.callback()

      • Optionaloptions: { runAt?: Date }

        Optional configuration for the execution

        • OptionalrunAt?: Date

          If provided, schedules execution at this time; otherwise runs immediately

      Returns Promise<string | void>

      Promise resolving to a cancellation token (only for scheduled executions)

    • Cancels a previously scheduled execution.

      Parameters

      • token: string

        The cancellation token returned by runTask() with runAt option

      Returns Promise<void>

      Promise that resolves when the cancellation is processed

    • Cancels all scheduled executions for this twist.

      Returns Promise<void>

      Promise that resolves when all cancellations are processed

    • Schedules a singleton task keyed by key: re-scheduling under the same key atomically replaces any pending task, so at most one is ever live.

      Prefer this over runTask({ runAt }) for recurring/self-renewing jobs (watch renewals, polling, deferred cleanup) — it removes the error-prone "store token, cancel before re-scheduling" bookkeeping that otherwise leaks parallel task chains. See Tasks.scheduleTask.

      With coalesce: true, an existing pending task is kept instead of replaced (its fire time is pulled earlier, never pushed later) — use for high-frequency triggers like webhook-driven sync scheduling; the passed callback may be discarded, so don't reuse its token.

      Parameters

      • key: string

        Stable identifier scoped to what the task renews

      • callback: Callback

        The callback token created with this.callback()

      • options: { runAt: Date; coalesce?: boolean }
        • runAt: Date

          When to run (required)

        • Optionalcoalesce?: boolean

          Keep an existing pending task (earliest wins)

      Returns Promise<string | void>

      Promise resolving to the scheduled task's cancellation token

    • Cancels the singleton task previously scheduled under key (if any). No-op if none exists or it already ran. See Tasks.cancelScheduledTask.

      Parameters

      Returns Promise<void>

      Promise that resolves when the cancellation is processed

    • Schedules a durable recurring task under a stable key. The platform re-arms the task every intervalMs automatically — the callback does NOT need to reschedule itself. Re-scheduling under the same key atomically replaces the pending occurrence (at most one live task per key). Tear down with cancelScheduledTask. See Tasks.scheduleRecurring.

      Parameters

      • key: string

        Stable identifier, e.g. "mailbox-self-heal"

      • callback: Callback

        Callback token created with this.callback()

      • options: { intervalMs: number; firstRunAt?: Date }
        • intervalMs: number

          Safety-ceiling cadence in milliseconds

        • OptionalfirstRunAt?: Date

          Optional precise time for the next fire

      Returns Promise<void>

    • Record dirty items and ensure a bounded drain pass runs soon — THE pattern for webhook-driven sync and any other high-frequency "something changed" trigger.

      A burst of calls under the same key collapses into ONE pending pass (never one queued task per notification); ids are persisted durably and released only after the handler processes them (at-least-once, race-free under concurrent deliveries); each pass hands the handler at most batchSize ids, with the platform scheduling continuations while a backlog remains; ids that keep failing are dropped after maxAttempts passes so one poison item can't wedge the drain.

      The handler must be a named method on this class (like this.callback targets). It receives the ids slice — or [] for signal-only drains (omit ids) where it derives its own work from a cursor or time window.

      Parameters

      Returns Promise<void>

      async onWebhook(request: WebhookRequest): Promise<void> {
      const ids = parseChangedIds(request);
      await this.scheduleDrain("incremental-sync", this.drainChanges, { ids });
      }

      async drainChanges(ids: string[]): Promise<void> {
      for (const id of ids) await this.syncItem(id); // ≤ batchSize items
      }

      Tear down with cancelDrain (e.g. in onChannelDisabled).

    • Cancel the pending drain pass for key and discard its recorded ids. Use in teardown paths. See scheduleDrain.

      Parameters

      • key: string

      Returns Promise<void>

    • SDK-internal: executes one bounded drain pass (the scheduled-task target behind scheduleDrain). Public only so the task runtime can dispatch to it by name — do not call or override.

      Parameters

      • key: string
      • handlerName: string
      • options: { batchSize: number; delayMs: number; maxAttempts: number }

      Returns Promise<void>

    • Called when the twist is installed by a user.

      This method should contain initialization logic such as seeding initial threads, configuring webhooks, or establishing external connections. When it runs, this.userId is already populated with the installing user's ID.

      Parameters

      • Optionalcontext: { actor: Actor }

        Optional context containing the actor who triggered activation

      Returns Promise<void>

      Promise that resolves when activation is complete

    • Called when a new version of the twist is deployed.

      This method should contain migration logic for updating old data structures or setting up new resources that weren't needed by the previous version. It is called once per active twist_instance with the new version.

      Returns Promise<void>

      Promise that resolves when upgrade is complete

    • Called when the twist's options configuration changes.

      Override to react to option changes, e.g. archiving items when a sync type is toggled off, or starting sync when a type is toggled on.

      Parameters

      • oldOptions: Record<string, any>

        The previously resolved options

      • newOptions: Record<string, any>

        The newly resolved options

      Returns Promise<void>

      Promise that resolves when the change is handled

    • Called when the twist is uninstalled.

      This method should contain cleanup logic such as removing webhooks, cleaning up external resources, or performing final data operations.

      Returns Promise<void>

      Promise that resolves when deactivation is complete

    • Called when a thread created by this twist is updated. Override to implement two-way sync with an external system.

      Parameters

      • thread: ThreadFields

        The updated thread

      • changes: { tagsAdded: Record<Tag, ActorId[]>; tagsRemoved: Record<Tag, ActorId[]> }

        Tag additions and removals on the thread

      Returns Promise<void>

    • Called when a note is created on a thread created by this twist. Override to implement two-way sync (e.g. syncing notes as comments).

      Notes created by the twist itself are filtered out to prevent loops.

      Returning a string sets the note's key for future upsert matching, linking the Plot note to its external counterpart so that subsequent syncs (reactions, edits) update the existing note instead of creating duplicates.

      Parameters

      • note: Note

        The newly created note

      • ...args: any[]

      Returns Promise<string | void | NoteWriteBackResult>

      Optional note key for external deduplication

    • Called when a link is created in a connected source channel. Requires link: true in Plot options.

      Parameters

      • link: Link

        The newly created link

      • notes: Note[]

        Notes on the link's thread

      Returns Promise<void>

    • Called when a link in a connected source channel is updated. Requires link: true in Plot options.

      Parameters

      • link: Link

        The updated link

      • Optionalnotes: Note[]

        Notes on the link's thread (optional)

      Returns Promise<void>

    • Called when a note is created on a thread with a link from a connected channel. Requires link: true in Plot options.

      Parameters

      • note: Note

        The newly created note

      • link: Link

        The link associated with the thread

      Returns Promise<void>

    Properties

    multipleInstances?: boolean

    When true, users may install multiple instances of this twist within the same scope (personal workspace or team). Each instance must have a distinct name.

    Defaults to false (single instance per scope).

    class WorkflowTwist extends Twist<WorkflowTwist> {
    static readonly multipleInstances = true;
    // ...
    }
    userId: Uuid

    The user ID (twist_instance.owner_id) that installed this twist. Populated by the runtime before any lifecycle method runs.

    id: Uuid