Files
sim/scripts
Waleed bdda083f93 refactor: consolidate three drifted copies, close a gate blind spot, fix stale docs (#7062)
* refactor(workflows): one owner for new-workflow sort order

The same ~35-line query — parent condition for workflows and folders, two
parallel min(sortOrder) reads, fold to a min, subtract one, fall back to 0 —
existed three times:

  lib/workflows/utils.ts               inline in createWorkflowRecord
  lib/workflows/orchestration/...      as a file-private nextWorkflowSortOrder
  lib/workflows/persistence/duplicate  inline, inside the duplicate transaction

The first two are character-identical modulo the table alias. The third had
drifted: it omits isNull(workflow.archivedAt), which the other two apply, so a
folder whose lowest-sortOrder workflow is soft-deleted positioned a *duplicate*
differently from a *create*. The folder-side query agrees in all three, which
marks it as a copy-paste slip rather than intent.

Promotes the helper to lib/workflows/sort-order.ts, taking an optional DbOrTx
so the duplicate path can keep reading inside its transaction. Its own module
rather than utils.ts because duplicate.test.ts and workflow-lifecycle.test.ts
both mock '@/lib/workflows/utils' wholesale — from a separate module the real
query still runs under those suites, so their existing sort-order assertions
keep their meaning and needed no edits.

Note the archived-row behavior itself is not unit-testable here: the shared
dbChainMock does not evaluate WHERE predicates. The guarantee is structural —
one query builder instead of three means the predicate can no longer drift.

* fix(ee): case-fold the stored integration allowlist

ee/access-control re-implemented the allowlist intersection instead of calling
intersectIntegrationAllowlists, and lost the case-folding: normalization only
happened on the envAllowlist !== null branch, so with ALLOWED_INTEGRATIONS
unset a stored config went through untouched. Callers compare against
blockType.toLowerCase(), so a stored 'Slack' failed to match 'slack' and the
block was denied.

The access-control UI writes block.type directly and block types are lowercase,
so this is not reachable from the UI — but allowedIntegrations is a bare
z.array(z.string()) on the wire, so any API client can store mixed case.

Replaces the fork with the shared helper. Adds two tests; the first fails
against the old code.

* fix(queries): forward the abort signal to getFullOrganization

useOrganization destructured `signal` from the queryFn and passed it to
fetchOrganization, which named the parameter `_signal` and never used it — so
the org detail fetch could not be cancelled. Switching orgs rapidly left every
prior request in flight, free to resolve out of order.

Better Auth takes cancellation two ways and both are already used here:
fetchOptions on the params object (session.ts:21) and a second argument
(admin-users.ts:129). Uses the former. This was the only `_signal` under
apps/sim/hooks.

The existing transition test asserted the exact call shape, so it now asserts
intent via objectContaining. Adds a test for the signal itself; it fails
against the old code.

* fix(canvas): select from useWorkflowRegistry instead of subscribing whole

check-zustand-v5-selectors matched /use[A-Z]\w*Store\(/, and exactly one
Zustand store in the repo is not named with a Store suffix —
useWorkflowRegistry. So the store behind the canvas went unchecked, and two
bare whole-store subscriptions had accumulated in the action bar, re-rendering
it on every registry mutation (clipboard, hydration, pendingSelection,
activeWorkflowId). Every other call site in the repo already uses a selector.

Widens the pattern to (?:Store|Registry) and fixes both call sites. The
widened gate reports these two and nothing else, so there is no cleanup tail.

* docs: correct comments that name symbols which no longer exist

Each of these points a reader at an identifier that is not in the repo:

- table/import-data.ts, table/service.ts — `acquireTablePositionLock` and
  `nextAutoPosition` were removed with the service.ts split; the surviving lock
  is `acquireRowOrderLock`, which import-data.ts already imports and calls.
  service.ts's mention is load-bearing: it exists to tell the reader which
  other lock this one mirrors, for lock-ordering.
- resources/orchestration/restore-resource.ts — named `performRestoreFolder`
  (the callee is `restoreFolder`) and described a `'workflow'` default it
  falls back to. There is no such default: resourceType is required and the
  config lookup is a bare index. Describing a fiction is how a future reader
  talks themselves into relaxing the total Record to a Partial.
- knowledge/search/queries.ts — cited apps/docs/app/api/chat/route.ts, deleted
  with Ask AI. The k=60 it pins against now lives in the docs search route.
- rate-limiter/hosted-key/queue.ts — documented a `waitForHead` method the
  class does not have; the queue exposes `checkHead` and the polling loop is
  private to the consumer.
- logs/log-views.ts — a "Level 1.5 / 2 / 3" scheme that appears nowhere else;
  the real contract is the five named views. Dropped, and the three banner
  rules with it (CLAUDE.md bans banner separators).

Comment-only apart from the log-views banners.

* refactor(ui): derive three values instead of storing or memoizing them

- import-modal: browserId and profileId were state corrected by two effects
  when a reload dropped the selection. That commits and paints one frame in
  which the profile still belongs to the previously selected browser — and
  Import is enabled during it, submitting via a `profiles.find` that searches
  every browser's profiles. Both now fall back during render, and `selected`
  searches only the current browser's profiles. Covered by the existing
  'never leaves a profile selected that belongs to another browser' test.

- workflow.tsx: isWorkflowEmpty was a second useMemo over the same [blocks]
  dep computing exactly !hasBlocks, allocating its own Object.keys array.
  Both feed primitives, so neither memo bought identity stability.

- thinking-loader: an effect seeding cycleVariant whenever variant is defined,
  which `shown = variant ?? cycleVariant` can never read. On the one
  transition where cycleVariant becomes visible (variant going undefined) the
  cycling effect assigns it in every branch — settle, reduced-motion, and
  tick — in the same flush, so the seed was never observable.

* fix(review): exclude deleted folders from sort order; make the allowlist tests real

Three findings from cubic on #7062, all valid.

1. nextWorkflowSortOrder consulted the folder minimum without excluding
   soft-deleted folders. Because the helper returns min - 1, a deleted folder
   holding the lowest slot ratchets the floor down permanently — the same class
   of bug as the archived-workflow one this PR set out to fix, on the other half
   of the query. lib/folders/orchestration.ts already documents this exact
   rationale for the folder-creation side of the same algorithm, and the uploads
   folder manager filters it too; this was the outlier.

2. The two new allowlist tests did not call setEnterpriseOrgWorkspace(), so
   resolution never reached the group queries and validateBlockType returned
   early. They passed against the unfixed code when run in isolation and only
   appeared to fail in a full-file run, where mock state leaked from earlier
   tests. Verified with 'vitest -t': both now fail without the case-folding fix
   and pass with it.

3. The restore-resource comment this PR rewrote was itself wrong. A Partial map
   does not defer the failure into the cascade — the lookup widens to
   FolderResourceType | undefined and the error lands on the restoreFolder call
   site. Reworded to say that, and why keeping the check at the mapping matters.
2026-08-24 22:53:32 -07:00
..

Integration documentation generator

generate-docs.ts compiles the per-service integration pages under apps/docs/content/docs/en/integrations/ from the block/tool/trigger registry in apps/sim. The ontology it encodes: everything is a block, and an integration is one block that has Actions and, optionally, a Trigger.

Golden rule: the generated .mdx files are derived artifacts, not the source of truth. Do not hand-edit them — your changes are overwritten on the next run. The only editable region is the MANUAL-CONTENT block (see below). To change what a page says, edit the TypeScript in apps/sim and regenerate.

Where an integration lives canonically

For a service like Gmail, three TS sources define it:

Source What it is What it feeds in the page
apps/sim/blocks/blocks/<service>.ts The block: type, name, category (tools for integrations), bgColor, config sub-blocks, tools.access (which actions it exposes), an optional triggers capability, outputs Header / BlockInfoCard, Usage Instructions, and which actions + trigger appear
apps/sim/tools/<service>/*.ts Each action's params + outputs Every ### <action> → #### Input / #### Output under ## Actions
apps/sim/triggers/<provider>/ The trigger's config fields + outputs The ## Triggers section
apps/sim/components/icons.tsx The brand glyph The page icon

The block references actions by id in tools.access; the generator looks each one up in apps/sim/tools/.

What the generator does

Run with cd apps/sim && bun run generate-docs (or bun run scripts/generate-docs.ts from the repo root). One pass (generateAllBlockDocs):

  1. Copies icons apps/sim/components/icons.tsx → apps/docs/components/icons.tsx and builds apps/docs/components/ui/icon-mapping.ts.
  2. Block pass — for each integration block (category: 'tools', plus the memory / knowledge / table exceptions), writes integrations/<service>.mdx: BlockInfoCard + Usage Instructions + ## Actions.
  3. Trigger pass (generateAllTriggerDocs) — reads apps/sim/triggers/<provider>/ and appends a ## Triggers section to that service's page, or writes a standalone page for trigger-only services.
  4. Writes integrations/meta.json and regenerates the landing page's integrations.json.

Hand-written pages it never touches

Core block pages (blocks/*), the native trigger pages (triggers/{start,schedule,webhook,rss,table}), the integrations overview (integrations/index.mdx), and the service-account pages are fully hand-written. The generator skips them via HANDWRITTEN_INTEGRATION_DOCS, HANDWRITTEN_TRIGGER_DOCS, and SKIP_TRIGGER_PROVIDERS. Add a page name to those sets if you hand-author a page the generator would otherwise produce.

Manual content (the one editable region)

Each generated page may carry hand-written prose inside marker comments. The generator preserves anything between the markers and overwrites everything else, so this survives every regeneration:

{/* MANUAL-CONTENT-START:intro */}
[AgentMail](https://agentmail.to/) is an API-first email platform…
{/* MANUAL-CONTENT-END */}

Supported section names: intro (after the BlockInfoCard — the most common), usage, configuration, outputs, notes. The merge is by marker name (extractManualContent + mergeWithManualContent), so a section is re-inserted at the matching spot in the freshly generated structure.

If you move the output folder, reseed manual content from the old location first — the generator only preserves markers it finds in the existing output file, so a fresh folder starts with none.

Practical: to change…

  • An action's params/outputs, a trigger, or to add a service → edit apps/sim/{blocks,tools,triggers} and re-run the generator.
  • A page's prose intro → edit its MANUAL-CONTENT:intro block directly; it survives regen.
  • The overview / service-account / core-block / native-trigger pages → hand-edit freely.

Gotchas

  • Never hand-edit apps/docs/components/icons.tsx — step 1 overwrites it from the sim app. Components that need an icon the sim app lacks should define it locally or use @sim/emcn/icons (see components/workflow-preview/block-icons.tsx).
  • The generator is the source of truth for integrations/ and its meta.json; manual edits there are transient.

CI

The generator runs in CI on pushes to the main branch and commits the regenerated docs back. Keep block/tool/trigger metadata accurate in apps/sim and the docs follow.