Add Block Preview Skill
You manage block visibility gating in Sim — hiding blocks from every discovery surface (toolbar, cmd+K search, copilot @-mentions, agent tool picker, mothership VFS/metadata/tools, Access Control list, public docs/catalog) while never gating execution of already-placed instances.
The model
Three levers, evaluated in apps/sim/lib/core/config/block-visibility.ts and folded into the registry accessors (apps/sim/blocks/registry.ts):
preview: true on the BlockConfig (static, in code) — the block is default-hidden EVERYWHERE (hosted, self-hosted, dev, SSR) until revealed. Fail-closed.
The hosted block-visibility AppConfig document — per-block rule keyed by the existing block type:
{
"<block-type>": {
"enabled": false, // required. true = GA (visible to everyone)
"orgIds": ["org_..."], // optional allowlist clauses (any match reveals)
"userIds": ["user_..."],
"adminEnabled": true // platform admins (user.role === 'admin')
}
}
PREVIEW_BLOCKS env (comma-separated block types) — the off-AppConfig reveal path for self-hosters and local dev.
A revealed block that is not globally GA (enabled !== true, or env-revealed) renders with a " (Preview)" name suffix on discovery surfaces. getBlock() stays pure, so placed instances keep their canonical name and always execute.
Lifecycle of a preview block
Author the block normally (/add-block etc.) and set preview: true on its BlockConfig. Ship no BlockMeta and no docs until GA — check-block-registry deliberately skips preview blocks in meta coverage, and generate-docs skips them at every gate.
Local dev: set PREVIEW_BLOCKS=<block-type> in your env to see it (with the suffix).
Merge/deploy. The block's code is live everywhere but visible nowhere — no AppConfig rule exists and self-hosters have no env entry.
Hosted preview: add a rule to the block-visibility AppConfig document and start a deployment (no code deploy):
- Admins only:
{ "enabled": false, "adminEnabled": true }
- Design-partner org:
{ "enabled": false, "orgIds": ["org_123"] }
- GA via config (code cleanup pending):
{ "enabled": true } — suffix disappears everywhere within ~30s (AppConfig TTL) + client refetch.
Same runbook as feature-flags: edit the hosted document, aws appconfig start-deployment with the sim-<env>-fast strategy (see the infra README).
GA cleanup: delete preview: true from the block (now visible to self-hosters on their next upgrade), add its BlockMeta + regen docs, and drop the AppConfig entry. For a v2 upgrade, this is also when v1 gets hideFromToolbar: true and sunset: { status: 'legacy', replacedBy: '<v2-type>' } (the superseded-version paradigm). Both edits must land in the same commit as the preview: true removal — check-block-registry fails a sunset block whose replacedBy is still preview, so splitting them breaks the build in between. Also move the block's BLOCK_DISPLAY_WORKFLOWS entry (apps/docs/components/workflow-preview/block-display-workflows.ts) to the new type, or BlockPreview silently renders nothing on the docs page.
Kill switch (shipped blocks)
To pull an already-GA block from discovery surfaces on hosted (incident, deprecation): add { "<block-type>": { "enabled": false } } to the document. Allowlist clauses can carve out exceptions. Execution is NOT stopped — workflows already using the block keep running; the kill switch only prevents new placement/discovery.
Invariants (do not violate)
- Execution is never gated. The executor, serializer, drop-naming, and
isBlockTypeAccessControlExempt resolve via pure getBlock. Do not add visibility checks to execution paths.
- Clone-not-remove: gated blocks stay in
getAllBlocks() output as clones with hideFromToolbar: true — .find-by-type consumers rely on this. Never filter them out.
- Keys are registry block types. Never
custom_block_* (parse drops them — custom blocks have their own enabled/disabled lifecycle).
- The shared hidden-predicate is
isHiddenUnder (apps/sim/blocks/visibility/context.ts). Never restate the preview/disabled rule inline at a new consumer.
- Process-global caches stay ungated.
getStaticComponentFiles (VFS) and getExposedIntegrationTools build the ungated universe; per-viewer filtering happens at stamp/consumer time. Never move gating into a shared builder.
- Gating is surface hiding, not secrecy — the full config ships in the client JS bundle. Anything truly secret cannot be a registered block.
Tests
Evaluation semantics: apps/sim/lib/core/config/block-visibility.test.ts. Registry projection: apps/sim/blocks/visibility/visibility.test.ts. When gating behavior changes, extend those — mock isPlatformAdmin for the admin clause; use the local withAppConfig harness.
1---2name: add-block-preview3description: Gate a block's visibility — ship an unreleased block as a preview (hidden until revealed via AppConfig/env), reveal it to admins/orgs, GA it, or kill-switch a shipped block4---5
6# Add Block Preview Skill
7
8You manage **block visibility gating** in Sim — hiding blocks from every discovery surface (toolbar, cmd+K search, copilot @-mentions, agent tool picker, mothership VFS/metadata/tools, Access Control list, public docs/catalog) while **never** gating execution of already-placed instances.
9
10## The model
11
12Three levers, evaluated in `apps/sim/lib/core/config/block-visibility.ts` and folded into the registry accessors (`apps/sim/blocks/registry.ts`):
13
141. **`preview: true`** on the `BlockConfig` (static, in code) — the block is default-hidden EVERYWHERE (hosted, self-hosted, dev, SSR) until revealed. Fail-closed.
152. **The hosted `block-visibility` AppConfig document** — per-block rule keyed by the existing block type:
16
17 ```jsonc
18 {
19 "<block-type>": {
20 "enabled": false, // required. true = GA (visible to everyone)
21 "orgIds": ["org_..."], // optional allowlist clauses (any match reveals)
22 "userIds": ["user_..."],
23 "adminEnabled": true // platform admins (user.role === 'admin')
24 }
25 }
26 ```
27
283. **`PREVIEW_BLOCKS` env** (comma-separated block types) — the off-AppConfig reveal path for self-hosters and local dev.
29
30A revealed block that is not globally GA (`enabled !== true`, or env-revealed) renders with a **" (Preview)"** name suffix on discovery surfaces. `getBlock()` stays pure, so placed instances keep their canonical name and always execute.
31
32## Lifecycle of a preview block
33
341. **Author** the block normally (`/add-block` etc.) and set `preview: true` on its `BlockConfig`. **Ship no `BlockMeta` and no docs until GA** — `check-block-registry` deliberately skips preview blocks in meta coverage, and `generate-docs` skips them at every gate.
352. **Local dev:** set `PREVIEW_BLOCKS=<block-type>` in your env to see it (with the suffix).
363. **Merge/deploy.** The block's code is live everywhere but visible nowhere — no AppConfig rule exists and self-hosters have no env entry.
374. **Hosted preview:** add a rule to the `block-visibility` AppConfig document and start a deployment (no code deploy):
38 - Admins only: `{ "enabled": false, "adminEnabled": true }`
39 - Design-partner org: `{ "enabled": false, "orgIds": ["org_123"] }`
40 - GA via config (code cleanup pending): `{ "enabled": true }` — suffix disappears everywhere within ~30s (AppConfig TTL) + client refetch.
41
42 Same runbook as `feature-flags`: edit the hosted document, `aws appconfig start-deployment` with the `sim-<env>-fast` strategy (see the infra README).
435. **GA cleanup:** delete `preview: true` from the block (now visible to self-hosters on their next upgrade), add its `BlockMeta` + regen docs, and drop the AppConfig entry. For a v2 upgrade, this is also when v1 gets `hideFromToolbar: true` **and** `sunset: { status: 'legacy', replacedBy: '<v2-type>' }` (the superseded-version paradigm). Both edits must land in the **same commit** as the `preview: true` removal — `check-block-registry` fails a sunset block whose `replacedBy` is still `preview`, so splitting them breaks the build in between. Also move the block's `BLOCK_DISPLAY_WORKFLOWS` entry (`apps/docs/components/workflow-preview/block-display-workflows.ts`) to the new type, or `BlockPreview` silently renders nothing on the docs page.
44
45## Kill switch (shipped blocks)
46
47To pull an already-GA block from discovery surfaces on hosted (incident, deprecation): add `{ "<block-type>": { "enabled": false } }` to the document. Allowlist clauses can carve out exceptions. **Execution is NOT stopped** — workflows already using the block keep running; the kill switch only prevents new placement/discovery.
48
49## Invariants (do not violate)
50
51- **Execution is never gated.** The executor, serializer, drop-naming, and `isBlockTypeAccessControlExempt` resolve via pure `getBlock`. Do not add visibility checks to execution paths.
52- **Clone-not-remove:** gated blocks stay in `getAllBlocks()` output as clones with `hideFromToolbar: true` — `.find`-by-type consumers rely on this. Never filter them out.
53- **Keys are registry block types.** Never `custom_block_*` (parse drops them — custom blocks have their own enabled/disabled lifecycle).
54- **The shared hidden-predicate is `isHiddenUnder`** (`apps/sim/blocks/visibility/context.ts`). Never restate the preview/disabled rule inline at a new consumer.
55- **Process-global caches stay ungated.** `getStaticComponentFiles` (VFS) and `getExposedIntegrationTools` build the ungated universe; per-viewer filtering happens at stamp/consumer time. Never move gating into a shared builder.
56- Gating is **surface hiding, not secrecy** — the full config ships in the client JS bundle. Anything truly secret cannot be a registered block.
57
58## Tests
59
60Evaluation semantics: `apps/sim/lib/core/config/block-visibility.test.ts`. Registry projection: `apps/sim/blocks/visibility/visibility.test.ts`. When gating behavior changes, extend those — mock `isPlatformAdmin` for the admin clause; use the local `withAppConfig` harness.