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.
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.What a run actually does
Every tick runs the same sequence:1
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.2
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.3
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.4
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.5
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.The per-row hidden API key
Each scheduled-run row has its own system-flaggedcb_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 inpackages/shared/src/cronPresets.ts:
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
See Preview and run-now for the two manual paths.
Failure modes
A few categories of failure surface differently:Provider auth (401/403)
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.MCP connection failure
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".BYOK decrypt failure
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.Per-tool errors
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.What’s next
Creating a schedule
The
/settings/schedule/new form, field-by-field, plus history and edit.Preview and run-now
Two manual paths for testing a prompt without waiting for the cron.