AbstractProtectedtoolsGets the initialized tools for this twist.
AbstractbuildDeclares tool dependencies for this twist. Return an object mapping tool names to build() promises.
The build function to use for declaring dependencies
Object mapping tool names to tool promises
ProtectedcallbackCreates a persistent callback to a method on this twist.
ExtraArgs are strongly typed to match the method's signature. They must be serializable.
Promise resolving to a persistent callback token
Creates a persistent callback to a method on this twist.
ExtraArgs are strongly typed to match the method's signature. They must be serializable.
Promise resolving to a persistent callback token
ProtectedactionLike callback(), but for an Action, which receives the action as the first argument.
Promise resolving to a persistent callback token
ProtecteddeleteDeletes a specific callback by its token.
The callback token to delete
Promise that resolves when the callback is deleted
ProtecteddeleteDeletes all callbacks for this twist.
Promise that resolves when all callbacks are deleted
ProtectedrunExecutes 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().
The callback token to execute
Optional arguments to pass to the callback
Promise resolving to the callback result
ProtectedgetRetrieves a value from persistent storage by key.
Values are automatically deserialized using SuperJSON, which properly restores Date objects, Maps, Sets, and other complex types.
The expected type of the stored value (must be Serializable)
The storage key to retrieve
Promise resolving to the stored value or null
ProtectedsetStores 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.
The type of value being stored (must be Serializable)
The storage key to use
The value to store (must be SuperJSON-serializable)
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);
ProtectedsetStores 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.
The type of values being stored (must be Serializable)
Array of [key, value] pairs to store
Promise that resolves when all values are stored
ProtectedgetReads 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.
The type of the stored values
The storage keys to read
Promise resolving to one value (or null) per requested key
ProtectedlistLists 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.
The type of the stored values
The prefix to match keys against
Promise resolving to [key, value] pairs, key-ascending
ProtectedclearRemoves a specific key from persistent storage.
The storage key to remove
Promise that resolves when the key is removed
ProtectedclearRemoves many keys in one round-trip. Pair with listEntries so a drain costs two round-trips regardless of key count. Atomic. See Store.clearMany.
The storage keys to remove
Promise that resolves when all keys are removed
ProtectedclearRemoves all keys from this twist's storage.
Promise that resolves when all keys are removed
ProtectedrunQueues a callback to execute in a separate worker context.
The callback token created with this.callback()
Optionaloptions: { runAt?: Date }Optional configuration for the execution
OptionalrunAt?: DateIf provided, schedules execution at this time; otherwise runs immediately
Promise resolving to a cancellation token (only for scheduled executions)
ProtectedcancelCancels a previously scheduled execution.
The cancellation token returned by runTask() with runAt option
Promise that resolves when the cancellation is processed
ProtectedcancelCancels all scheduled executions for this twist.
Promise that resolves when all cancellations are processed
ProtectedscheduleSchedules 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.
Stable identifier scoped to what the task renews
The callback token created with this.callback()
When to run (required)
Optionalcoalesce?: booleanKeep an existing pending task (earliest wins)
Promise resolving to the scheduled task's cancellation token
ProtectedcancelCancels the singleton task previously scheduled under key (if any).
No-op if none exists or it already ran. See Tasks.cancelScheduledTask.
The same key passed to scheduleTask
Promise that resolves when the cancellation is processed
ProtectedscheduleSchedules 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.
Stable identifier, e.g. "mailbox-self-heal"
Callback token created with this.callback()
Safety-ceiling cadence in milliseconds
OptionalfirstRunAt?: DateOptional precise time for the next fire
ProtectedscheduleRecord 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.
Optionaloptions: DrainOptionsasync 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).
ProtectedcancelCancel the pending drain pass for key and discard its recorded ids.
Use in teardown paths. See scheduleDrain.
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.
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.
Optionalcontext: { actor: Actor }Optional context containing the actor who triggered activation
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.
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.
The previously resolved options
The newly resolved options
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.
Promise that resolves when deactivation is complete
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.
The newly created note
Optional note key for external deduplication
Static Optional ReadonlymultipleWhen 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).
ProtecteduserThe user ID (twist_instance.owner_id) that installed this twist.
Populated by the runtime before any lifecycle method runs.
Protectedid
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.Example