> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chatblocks.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduled runs

> Cron-triggered agent loops that decrypt a workspace BYOK key, connect over MCP, and write back to your blocks.

A **scheduled run** is a Trigger.dev cron job that wakes up on its schedule, runs the AI SDK `generateText` loop with your BYOK key, and writes back to your blocks via the same MCP tools your coding agent uses. Think of it as a worker that lives in your workspace and runs a prompt on a clock.

<Note>
  Scheduled runs are a **Builder beta** feature. Free workspaces can read this page but the `/settings/schedule/new` route shows an upgrade panel instead of the form, and MCP schedule tools return `PRO_REQUIRED`.
</Note>

## What a run actually does

Every tick runs the same sequence:

<Steps>
  <Step title="Trigger.dev fires the cron">
    Each scheduled run row is registered with Trigger.dev via `schedules.create`. Pause / resume / delete sync through the same SDK. Cron expressions are interpreted as **UTC**.
  </Step>

  <Step title="Decrypt the BYOK key">
    The runner action looks up the configured BYOK row, calls `_decryptForRunner({ actor: "system:scheduledRunner" })`, and gets the provider plaintext just-in-time. Every decrypt writes an `auditLog` row.
  </Step>

  <Step title="Connect to your workspace MCP">
    A hidden, system-flagged `cb_live_*` key is generated per scheduled-run row. The runner uses it to call `tools/list` against `${RUNNER_MCP_BASE_URL}/api/mcp/v1`, then wraps each discovered tool as an AI SDK `tool()` definition.
  </Step>

  <Step title="Run the agent loop">
    `generateText({ model, prompt, tools, stopWhen: stepCountIs(25) })` with the BYOK plaintext as the provider API key. The model sees the same MCP tool surface your coding agent does — `blocks.setWidgetData`, `dataSources.triggerRefresh`, etc.
  </Step>

  <Step title="Record history">
    The runner writes a `scheduledRunHistory` row whether the run succeeded or failed (try/finally). Config-level failures (BYOK decrypt, MCP connect, provider 401/403) also flip the row's `status` to `error` with an `errorMessage`.
  </Step>
</Steps>

## The per-row hidden API key

Each scheduled-run row has its own system-flagged `cb_live_*` key, encrypted and stored on the row (`internalApiKeyId` + `internalApiKeyCiphertext`). It doesn't appear in your `/settings/api-keys` list (filtered out via `apiKeys.system: true`).

This matters because:

* **Revoking a schedule revokes only its own key.** Deleting the row cascades the key.
* **Audit log granularity.** Tool calls from a scheduled run show up as `actor: "mcp:<apiKeyId>"` with a workspace-unique id per row.
* **You can't accidentally reuse a runner key elsewhere.** It's not user-visible.

## Cron presets

Five presets ship in `packages/shared/src/cronPresets.ts`:

| Preset         | Cron           | Description                           |
| -------------- | -------------- | ------------------------------------- |
| `every-15-min` | `*/15 * * * *` | Every 15 minutes                      |
| `every-hour`   | `0 * * * *`    | Every hour                            |
| `daily-9am`    | `0 9 * * *`    | Every day at 9am UTC                  |
| `weekdays-9am` | `0 9 * * 1-5`  | Weekdays at 9am UTC                   |
| `custom`       | (user input)   | Anything else — standard 5-field cron |

The form's cron field accepts custom expressions for anything beyond the preset list. Validation accepts 5- to 7-field cron strings; semantics are whatever Trigger.dev's parser does with them.

## Provider routing

Per-workspace BYOK uses `@ai-sdk/anthropic` or `@ai-sdk/openai` directly, **not** the AI Gateway. Gateway BYOK is project-scoped, so per-workspace billing only works when the runner instantiates the provider SDK with the customer's key.

The model dropdown is filtered against `KNOWN_MODELS[provider]` in `packages/shared/src/byokProviders.ts`:

* **Anthropic** — `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5`
* **OpenAI** — `gpt-5.5`, `gpt-5.4-mini`, `gpt-5`

## How runs differ from preview and run-now

| Surface            | Trigger          | Persistence                             | Plan gate |
| ------------------ | ---------------- | --------------------------------------- | --------- |
| Scheduled run      | Cron tick        | Saved row, persistent                   | Builder   |
| `schedule.preview` | One-off MCP call | Ephemeral key, no row                   | Builder   |
| Run-now button     | Manual click     | Uses saved row, one extra history entry | Builder   |

See [Preview and run-now](/scheduled-runs/preview-and-run-now) for the two manual paths.

## Failure modes

A few categories of failure surface differently:

<AccordionGroup>
  <Accordion title="Provider auth (401/403)">
    The BYOK key was rejected by Anthropic or OpenAI — usually rotated or deleted on the provider side. The row flips to `status: "error"` with a clear `errorMessage`. Fix by rotating the BYOK key in `/settings/api-keys`.
  </Accordion>

  <Accordion title="MCP connection failure">
    The runner couldn't reach `${RUNNER_MCP_BASE_URL}`. Usually means the env var is misconfigured. The row flips to `status: "error"`.
  </Accordion>

  <Accordion title="BYOK decrypt failure">
    The workspace DEK couldn't be unwrapped — typically a KEK rotation issue. Surfaces with a `KEK_V1 is not set` or "unable to unwrap" message. Contact us.
  </Accordion>

  <Accordion title="Per-tool errors">
    Individual tool calls (`blocks.setWidgetData`, `dataSources.triggerRefresh`, etc.) can fail inside the agent loop without failing the whole run. The model sees the JSON-RPC error in its `tool_result` and decides what to do.
  </Accordion>
</AccordionGroup>

## What's next

<CardGroup cols={2}>
  <Card title="Creating a schedule" icon="plus" href="/scheduled-runs/creating">
    The `/settings/schedule/new` form, field-by-field, plus history and edit.
  </Card>

  <Card title="Preview and run-now" icon="play" href="/scheduled-runs/preview-and-run-now">
    Two manual paths for testing a prompt without waiting for the cron.
  </Card>
</CardGroup>
