Creating Plot Twists
    Preparing search index...

    Class TasksAbstract

    Run background tasks and scheduled jobs.

    The Tasks tool enables twists and tools to queue callbacks for execution in separate worker contexts. This is critical for staying under request limits: each execution has a limit of ~1000 requests (HTTP requests, tool calls, database operations), and running a task creates a NEW execution with a fresh request limit.

    Key distinction:

    • Calling a callback (via this.run()) continues the same execution and shares the request count
    • Running a task (via this.runTask()) creates a NEW execution with fresh ~1000 request limit

    When to use tasks:

    • Processing large datasets that would exceed 1000 requests
    • Breaking loops into chunks where each chunk stays under the request limit
    • Scheduling operations for future execution

    Note: Tasks tool methods are also available directly on Twist and Tool classes via this.runTask(), this.cancelTask(), and this.cancelAllTasks(). This is the recommended approach for most use cases.

    Best Practices:

    • Size batches to stay under ~1000 requests per execution
    • Calculate requests per item to determine safe batch size
    • Create callbacks first using this.callback()
    • Store intermediate state using the Store tool
    class SyncTool extends Tool<SyncTool> {
    async startBatchSync(totalItems: number) {
    // Store initial state using built-in set method
    await this.set("sync_progress", { processed: 0, total: totalItems });

    // Create callback and queue first batch
    const callback = await this.callback(this.processBatch, 1);
    // runTask creates NEW execution with fresh ~1000 request limit
    await this.runTask(callback);
    }

    async processBatch(batchNumber: number) {
    // Process one batch of items (sized to stay under request limit)
    const progress = await this.get("sync_progress");

    // If each item makes ~10 requests, process ~100 items per batch
    // 100 items × 10 requests = 1000 requests (at limit)
    const batchSize = 100;
    const items = await this.fetchItems(progress.processed, batchSize);

    for (const item of items) {
    await this.processItem(item); // Makes ~10 requests per item
    }

    await this.set("sync_progress", {
    processed: progress.processed + batchSize,
    total: progress.total
    });

    if (progress.processed < progress.total) {
    // Queue next batch - creates NEW execution with fresh request limit
    const callback = await this.callback(this.processBatch, batchNumber + 1);
    await this.runTask(callback);
    }
    }

    async scheduleCleanup() {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);

    const callback = await this.callback(this.cleanupOldData);
    // Schedule for future execution
    return await this.runTask(callback, { runAt: tomorrow });
    }
    }

    Hierarchy (View Summary)

    Index

    Constructors

    Methods

    • Queues a callback to execute in a separate worker context with a fresh request limit.

      Creates a NEW execution with its own request limit of ~1000 requests (HTTP requests, tool calls, database operations). This is the primary way to stay under request limits when processing large datasets or making many API calls.

      The callback will be invoked either immediately or at a scheduled time in an isolated execution environment. Each execution has ~1000 requests and ~60 seconds CPU time. Use this for breaking loops into chunks that stay under the request limit.

      Key distinction:

      • this.run(callback) - Continues same execution, shares request count
      • this.runTask(callback) - NEW execution, fresh request limit

      Parameters

      • callback: Callback

        Callback 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)

      // Break large loop into batches to stay under request limit
      const callback = await this.callback(this.syncBatch, 1);
      await this.runTask(callback); // Fresh execution with ~1000 requests
    • Cancels a previously scheduled execution.

      Prevents a scheduled function from executing. No error is thrown if the token is invalid or the execution has already completed.

      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 tool/twist.

      Cancels all pending scheduled executions created by this tool or twist instance. Immediate executions cannot be cancelled.

      Returns Promise<void>

      Promise that resolves when all cancellations are processed

    • Schedules a one-shot singleton task identified by key: scheduling under a key that already has a pending task atomically cancels the existing one and replaces it. At most one scheduled task per key is ever live.

      Use this for one-shot keyed deferred work — a single future task whose pending occurrence should be atomically replaced if re-scheduled (e.g. a deferred cleanup, a one-time expiry action, a single future send).

      For recurring/self-renewing jobs (watch renewals, polling loops, periodic syncs, self-heal checks), use scheduleRecurring instead. It owns the cadence on the platform side, so the chain survives dropped runs, suspensions, and deploys without the callback needing to reschedule itself.

      Replacement is atomic on the server, so concurrent executions racing to schedule the same key converge on a single task rather than leaking.

      Coalescing (coalesce: true): instead of replacing the pending task, an existing one is KEPT — its fire time is pulled earlier when the new runAt is sooner, but never pushed later. Use this for high-frequency triggers (e.g. scheduling a sync pass from a provider webhook): a burst of N calls collapses into a single pending task that fires at the earliest requested time, where plain replacement would reset the timer on every call and could starve under a continuous stream. With coalesce: true the callback you pass may be discarded (when an existing task is kept), so create a fresh callback for each call and do NOT store or reuse its token elsewhere.

      Parameters

      • key: string

        Stable identifier for this logical task. Scope it to what it renews, e.g. `watch-renewal:${folderId}`.

      • callback: Callback

        Callback created with this.callback()

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

          When to run. Required: keying only applies to scheduled tasks (immediate tasks go straight to the queue).

        • Optionalcoalesce?: boolean

          Keep an existing pending task for this key (earliest fire time wins) instead of replacing it.

      Returns Promise<string | void>

      Promise resolving to the cancellation token for the scheduled task

      const cb = await this.callback(this.renewWatch, folderId);
      await this.scheduleTask(`watch-renewal:${folderId}`, cb, { runAt });
      // ...later, on disable:
      await this.cancelScheduledTask(`watch-renewal:${folderId}`);

      // Webhook-driven sync: collapse a notification burst into one pass
      // that runs a few seconds from now.
      const sync = await this.callback(this.incrementalSync);
      await this.scheduleTask("incremental-sync", sync, {
      runAt: new Date(Date.now() + 10_000),
      coalesce: true,
      });
    • Cancels the singleton task previously scheduled under key (if any).

      No error is thrown if no task exists for the key or it has already run. Pair this with scheduleTask in teardown paths (e.g. onChannelDisabled, stopSync).

      Parameters

      Returns Promise<void>

      Promise that resolves when the cancellation is processed

    • Schedules a durable recurring task identified by key. Unlike scheduleTask (one-shot, deleted when it fires), a recurring task's next occurrence is owned by the platform: the runtime re-arms it every intervalMs automatically, so the chain survives a dropped queue message, a suspension, a deploy/eviction, or a callback that throws before it could reschedule. The callback just does the work, idempotently — it does NOT need to reschedule itself.

      intervalMs is a safety ceiling (the maximum gap between fires). For data-dependent cadence (e.g. renew 24h before a provider-returned expiry), pass firstRunAt for the precise next fire and re-call scheduleRecurring with the same key on each run to keep tightening it; the ceiling guarantees the chain still fires if a run is lost. firstRunAt can pull the next fire earlier than the ceiling but never later.

      Recurring tasks are keyed/singleton: re-scheduling under the same key atomically replaces the pending occurrence (one live task per key). Tear down with cancelScheduledTask.

      Parameters

      • key: string

        Stable identifier, scoped to what it maintains, e.g. `watch-renewal:${folderId}` or "mailbox-self-heal".

      • callback: Callback

        Callback 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 (clamped to no later than now + intervalMs).

      Returns Promise<void>

      // Fixed cadence (self-heal, polling): register once, never reschedule.
      const cb = await this.callback(this.selfHealCheck);
      await this.scheduleRecurring("mailbox-self-heal", cb, { intervalMs: 60 * 60 * 1000 });

      // Variable cadence (watch renewal): precise firstRunAt + safety ceiling.
      const renew = await this.callback(this.renewWatch, folderId);
      await this.scheduleRecurring(`watch-renewal:${folderId}`, renew, {
      intervalMs: 3.5 * 24 * 60 * 60 * 1000, // ceiling: half the 7-day watch
      firstRunAt: new Date(expiry.getTime() - 24 * 60 * 60 * 1000),
      });