Store run artifacts
Let runs move large files — a 1 GiB attachment out of a run, a 300 MB video dropped on a thread into a run's workspace — through one private R2 bucket instead of the inline caps (5 MiB per image, 10 MiB per document). The bot signs URLs and records keys; the container and the bot's Worker move the bytes.
You need:
- A Cloudflare account with R2 enabled, and
CLOUDFLARE_API_TOKENfor it in your shell with Workers R2 Storage: Edit (thedeploycommands already use this token;deploycreates the bucket with it). - A second, bucket-scoped R2 token for the bot: in the dashboard, R2 → Manage API tokens → Object Read & Write on the one bucket. Save its two halves as
ARTIFACTS_R2_ACCESS_KEY_IDandARTIFACTS_R2_SECRET_ACCESS_KEYwhere the profile'ssecretsSourcereads them (~/.secrets/switchboard/<NAME>by default). - A bearer you mint yourself for the bot's copy route, saved as
ARTIFACTS_COPY_TOKENbeside them (openssl rand -hex 32). PUBLIC_BASE_URLset on the bot: the run page's file links are built from it.
Run the commands below from the operator directory with the released CLI version you operate.
Name the bucket in the deployment profile
"artifacts": { "bucket": "switchboard-artifacts" }With this in deploy/profile.json, the bot Worker's rendered wrangler.jsonc binds the bucket, and the bot's npm run deploy creates it before the upload — idempotently, so an existing bucket reports "already exists".
Put the secrets, then turn the store on
npx --yes @coreplane/switchboard@<version> deploy secrets bot --only ARTIFACTS_R2_ACCESS_KEY_ID,ARTIFACTS_R2_SECRET_ACCESS_KEY,ARTIFACTS_COPY_TOKENSecrets first, always: a configured store with a missing secret fails the bot's startup by name. Then add the section to the bot's config.yaml, push it and restart:
artifacts:
r2:
accountId: <your Cloudflare account id>
bucket: switchboard-artifacts # the profile's name, exactly
retentionDays: 30 # optional; the defaultnpx --yes @coreplane/switchboard@<version> deploy config
npx --yes @coreplane/switchboard@<version> deploy restart/healthz now carries "artifacts": { "bucket": "switchboard-artifacts" }. A bucket name that differs between the profile and the config refuses the bot at startup naming both.
Apply retention
npx --yes @coreplane/switchboard@<version> artifacts lifecycle --dry-run
npx --yes @coreplane/switchboard@<version> artifacts lifecycleThe first prints the two rules — every object deleted retentionDays after it was written, an incomplete multipart upload aborted after one day — and touches nothing. The second applies them through Cloudflare's API with your token and reads them back; it refuses to claim success when the read-back differs. Run it again after changing retentionDays. The run page shows a file whose object has expired as expired after N days.
Check the bucket is private
npx --yes @coreplane/switchboard@<version> artifacts check… is private: the managed domain pub-….r2.dev is disabled and no custom domain is attached is the answer you want. Anything else names the setting to turn off in the dashboard (R2 → the bucket → Settings → Public access). The check reads the bucket's domain settings; nothing unsigned can reach an R2 object either way, which is why the bot does not probe at runtime.
Turn it off
Remove the artifacts: section, deploy config, deploy restart. Every path falls back to the inline caps; the bucket and its objects stay until the lifecycle rules expire them.
Next
- Rotate a secret: the three
ARTIFACTS_*secrets rotate like any other bot secret. - execution.md item 20: what moves where, and every proof.