This file (also AGENTS.md) holds the repo-wide rules: Global Standards, Architecture (incl. the Application Operation Boundary), Code Conventions, API Contracts and Routes, Testing, Integrations, and How your work is checked (the local gate to run before calling a change done). Area detail lives in .claude/rules/*.md, indexed in apps/sim/AGENTS.md: Claude loads each one by path, and any other agent reads the file a section points to before editing in that area. Skills live in .agents/skills/.
Most rules here are enforced by a check:* audit or biome. When one fails, its message and its script's header say what the rule protects and how to fix it: fix the code, never the check, and never add an annotation (// boundary-raw-fetch:, // double-cast-allowed:, …) to silence a fixable finding.
- Package manager:
bunandbunx, nevernpmandnpx. - Logging:
createLoggerfrom@sim/logger, neverconsole.log. Messages are static text and ids go in metadata (logger.info('Sync started', { documentId })); insidewithRouteHandlerevery line already carries the request ID. - Comments: name things so the code explains itself. TSDoc on exported APIs and non-obvious modules; an inline
//only for a terse, non-obvious why or a script-enforced// <tag>: <reason>annotation. History goes in the commit message. No====separators, commented-out code, or ESLint directives (check:comment-hygiene). - Common utilities: use the
@sim/utils/<module>helpers (id,random,errors,object,coerce,string,helpers,retry) over inline versions. IDs aregenerateId()(default) orgenerateShortId()from@sim/utils/id, neverrandomUUID,nanoid, oruuid: the helpers also work in non-secure (HTTP) browsers.check:utilsbans the inline forms and names the replacement. It cannot catch these, so reach for them yourself:toArrayandtoStringOrNull/toNumberOrNull/toBooleanOrNullfor untyped payloads,compareStrings(neverlocaleCompare) for hashes and cross-process ordering, andbackoffWithJitter/parseRetryAfterfor retries. Where the source is already typed, keep an inlineArray.isArraycheck (it narrows;toArrayasserts), and keep a local helper that deliberately differs (returnsundefinedinstead ofnull, or a stricterNumber.isFinitecheck). - Deployment flags in the browser: client code reads
hosted,billingEnabled,chatEnabled, and enterprise flags throughuseDeploymentShape()/getDeploymentShape()from@/lib/core/config/deployment-shape, neverenv-flags(its values freeze wrong on 404 and error shells). Server code keepsenv-flags(check:client-boundary). - Type-checking:
bun run type-checkat the root checks every workspace (inside a workspace it checks just that one).apps/simexcludes*.test.ts(x)from it; CI'scheck:test-typesratchets their errors per file instead. Keep the unused@typescript/nativeroot devDependency (check:native-typecheck).
- Layout:
apps/simis the Next.js app (UI, API routes, executor; use cases live inlib/<domain>/application),apps/realtimethe collaborative Socket.IO server,apps/desktopan Electron shell around the hosted web app,apps/docsthe docs site, andapps/piia Python service outside the JS/turbo build. Shared code is inpackages/*(@sim/*). - Package boundaries:
apps/* → packages/*only; packages never import fromapps/*.apps/realtime(a Bun Socket.IO server) avoids Next.js, React, the block/tool registry, provider SDKs, and the executor, and callsapps/simonly over internal HTTP withINTERNAL_API_SECRET(check:boundaries,check:realtime-prune). Both apps share Better Auth sessions through@sim/dband the sameBETTER_AUTH_SECRET. 'use client'boundary, app/worker env split, and feature folder layout:.claude/rules/sim-architecture.md.
- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through one authorized application use case with its minimum role, workspace-key policy, and allowed principal kinds. Internal APIs, v2 APIs, Copilot (via
createCopilotApplicationAdapter), and trusted tools call the same use case for the same domain behavior. - Surface adapters authenticate, build a
Principal, apply rate policy, parse contracts, and present results. They never query protected data, decide authorization, run business transactions, or record semantic audit (check:source-conventionsratchets value imports of@sim/dbunderapp/api/). Managers accept canonical IDs and scope, never credentials or principals. Application code stays surface-neutral (check:boundaries): nonext/server,app/api/**, or runtime import of a route contract object, presenter, or Copilot handler (contract types, schemas, and constants are fine). - A protected compound mutation is one top-level operation, never a sequence of independently committing mutations in an adapter.
- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal; fail fast when the identity model cannot express the caller.
- Use the
migrate-application-operationskill for any new or migrated protected endpoint, tool command, or resource method.
- Naming: components PascalCase, hooks
use*, files kebab-case, constants SCREAMING_SNAKE_CASE, interfaces with a suffix (WorkflowListProps), stores atstores/<feature>/store.ts. Insidelib/,executor/,providers/,stores/,hooks/,serializer/, and a package'ssrc/, a file never starts with its folder's name (logs/views.ts, notlogs/log-views.ts;feature/feature.tsxstays the component convention), and a file inutils/orhelpers/never repeats that role (date.ts, notdate-utils.ts) (check:file-names). - Imports: absolute (
@/...) only; a folder with 3+ exports gets anindex.tsbarrel, and nothing re-exports from a non-barrel file;import typefor type-only imports (.claude/rules/sim-imports.md). - TypeScript: no
any(use precise types orunknownwith guards), no non-null!, no export nothing imports (check:explicit-any,check:unused-exports). Unused bindings fail biome with no autofix: delete them and update callers, and writecatch {}when the error is unused._only for a parameter that must hold its position or a type parameter kept for API shape;const { a, ...rest } = objis fine. - Utils: check
lib/and the@sim/utils/<module>helpers before writing a helper. Inline a helper with one consumer; createutils.ts(inlib/orfeature/utils/) only when 2+ files share it. - Caching:
lru-cachewith amaxceiling, never a hand-rolled TTLMap; cache the gate, never the credential (.claude/rules/sim-caching.md). - State ownership: React Query owns server data (never
useState+fetch); shareable view-state (tabs, filters, search, pagination, selected id) lives in the URL vianuqs; Zustand owns global client state;useStateowns UI-only state. Hooks, queries, stores, URL state, components, lists and menus, and render performance each have a rule file in.claude/rules/. - UI: Tailwind and the
@sim/emcnchip family (.claude/rules/sim-styling.md); product copy.claude/rules/sim-ui-copy.md; marketing copy.claude/rules/constitution.md.
- Request/response shapes for every route under
apps/sim/app/api/**live inapps/sim/lib/api/contracts/**viadefineRouteContract. Routes never importzod; clients never hand-write wire types. - Handlers use the shared builders (
defineInternalJsonRoute,defineV2JsonRoute, binary/stream variants), which already applywithRouteHandler; never double-wrap or export a bare handler. - Same-origin JSON calls use
requestJson(contract, ...)from@/lib/api/client/request. A rawfetchis only for streaming, binary, multipart, signed URLs, OAuth redirects, or external origins, and carries// boundary-raw-fetch: <reason>. bun run check:api-validation:strictmust pass. Full rules:.claude/rules/sim-api-contracts.md.
Most unit tests restate the code they test: they pass first time, break on every refactor, and catch nothing type-check, the build, the audits, or a real run would miss. Test for confidence, not coverage.
- Never write unit tests after the code: a test written to describe existing code restates it and proves nothing. When a change needs proof, prove it end to end against the real boundary (real Postgres/Redis
*.integration.ts, the running app over HTTP (apps/sim/scripts/test-*-e2e.ts), or the packaged desktop app), ending in a report artifact written to a caller-supplied<SUITE>_REPORT_PATH(apps/sim/scripts/test-scim-e2e.tsis the reference). - Testing in isolation: first list the failure modes (bad input, boundary, concurrency, partial failure, permission denial, resource cap), then write one test per mode that fails before the code exists. A regression test must fail on the pre-fix code: revert each guard of the fix and watch its test go red.
- Never test declarations, mock every collaborator and assert on the mocks, check rendered text or class names, or hand-roll a mock that
apps/sim/vitest.setup.tsor@sim/testingprovides (check:test-patterns).
Use the test-audit skill whenever you write, change, or review tests; mechanics are in .claude/rules/sim-testing.md.
Build order Tools → Block → Icon → optional Trigger, from the service's API docs, using /add-integration (or /add-tools, /add-block, /add-trigger).
- Tool IDs are
snake_case(service_action), registered intools/registry.ts; blocks register alphabetically inblocks/registry-maps.ts. - Type coercions go in
tools.config.params(runs at execution), nevertools.config.tool(runs at serialization, whereNumber()destroys<Block.output>references). - Requests made for a workspace use
outboundFetch(@/lib/core/network/outbound-fetch.server), never the globalfetchor another importedfetch, so an organization with a dedicated egress gateway keeps its static IPs;check:organization-egressenforces this, and a request that rightly goes direct (Sim's own infrastructure, an operator-local server) takes// egress-direct: <reason>. AWS SDK clients signing with customer credentials takeorganizationAwsRequestHandler(): no audit catches a missing one, so reviewers check it. - A surface listing integrations to a viewer reads
useIntegrationAccess, never a raw catalog (check:integration-access). - Table column types are registry entries in
apps/sim/lib/table/column-types/; never add a per-typecaseelsewhere except the one documented import-path switch (coerceValueinlib/table/import.ts), which every new type extends: a missed arm fails silently. Use/add-column-type.
Before declaring a change done, run the local gate from the repo root; CI fails on any of them:
bun run lint # biome format + lint, autofixes (CI runs lint:check)
bun run type-check # every workspace
bun run check:audits # every check:* audit, generated-artifact checks included
bun run test # script tests, then every workspace's Vitest suite
git fetch origin staging # the block-registry check diffs against it
bun run apps/sim/scripts/check-block-registry.ts origin/stagingAuthor a packages/db schema change or migration with the db-migrate skill and run bun run check:migrations (not in check:audits). Ratchet baselines (scripts/*baseline.json) only shrink: regenerate one with --update after removing violations. The one exception is check:tool-registry-boundary, whose module-count baseline is re-recorded with --update-baseline when growth is deliberate (see the tool-registry-boundary skill).