You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
This file (also AGENTS.md) holds the repo-wide rules. 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/.
- Package manager:
bunandbunx, nevernpmandnpx. - Logging:
createLoggerfrom@sim/logger;logger.info/logger.warn/logger.error, neverconsole.log. InsidewithRouteHandlerthe logger already carries the request ID — no manualwithMetadata({ requestId }). - Comments: TSDoc for documentation. An inline
//only for a terse, non-obvious why, or for a script-enforced// <tag>: <reason>annotation (boundary-raw-fetch,double-cast-allowed,boundary-raw-json,untyped-response,rq-lint-allow,client-boundary-allow, …). No====separators. - ID generation:
generateId()(UUID v4, the default) orgenerateShortId(size?)(URL-safe, 21 chars by default) from@sim/utils/id— nevercrypto.randomUUID(),nanoid, oruuid. Both usecrypto.getRandomValues(), so they also work in non-secure (HTTP) browsers. - Common utilities: use the shared helpers from
@sim/utilsinstead of inline implementations:sleep(ms)from@sim/utils/helpers— nevernew Promise(resolve => setTimeout(resolve, ms))toError(e)from@sim/utils/errors— normalize caught values toError; nevere instanceof Error ? e : new Error(String(e))getErrorMessage(e, fallback?)from@sim/utils/errors— nevere instanceof Error ? e.message : 'fallback'structuredClone(value)— built-in deep clone; neverJSON.parse(JSON.stringify(...))omit(obj, keys)/filterUndefined(obj)from@sim/utils/object— neverObject.fromEntries(Object.entries(...).filter(...))isRecordLike(value)from@sim/utils/object— never redeclaretypeof value === 'object' && value !== null && !Array.isArray(value)toRecord(value)/toRecordOrNull(value)/toArray(value)from@sim/utils/object— coerce an untyped payload value; never inlineisRecordLike(v) ? v : {}orArray.isArray(v) ? v : []. Where the source is already typed, keep the inlineArray.isArraycheck: it narrows, whiletoArrayassertstoStringOrNull(value)/toNumberOrNull(value)/toBooleanOrNull(value)from@sim/utils/coerce— read one scalar out of an untyped payload; never declare a local one-liner byte-identical to one of these. Keep a local helper that differs:undefinedinstead ofnullchanges the wire shape, and aNumber.isFiniteor string-parse variant is a stricter check these omittruncate(str, maxLength, suffix?)from@sim/utils/string— never inline slice + ellipsisescapeRegExp(value)from@sim/utils/string— never inlinereplace(/[.*+?^${}()|[\]\\]/g, '\\$&')compareStrings(left, right)from@sim/utils/string— code-unit ordering for hashes, fingerprints, and cross-process comparisons; neverlocaleComparetherebackoffWithJitter(attempt, retryAfterMs, options?)/parseRetryAfter(header)from@sim/utils/retry— never reimplement exponential backoff inline
- Deployment flags in the browser: client code inside a workspace, organization, or standalone settings surface reads
hosted,billingEnabled,chatEnabled, and the enterprise feature set throughuseDeploymentShape()(components) orgetDeploymentShape()(block conditions, stores, helpers) from@/lib/core/config/deployment-shape, neverisHosted/isBillingEnabled/... fromenv-flags. Those constants freeze at module init from the root layout'sNEXT_PUBLIC_*transport, which Next's bare 404 shell andglobal-errornever emit, so a recovered tab would render Sim Cloud as self-hosted; the reader is seeded from the server-resolved workspace host context, organization layout, or standalone settings layout instead. Server code keeps readingenv-flags. - Type-checking:
bun run type-check(per workspace) orbunx turbo run type-check(all). Never remove the@typescript/nativealias from the rootdevDependencies. Nothing imports it; it exists so a baretscresolves to the native TypeScript 7 compiler.apps/simneeds@typescript/typescript6, whose@typescript/olddependency (an alias oftypescript@6) ships its owntscbin, and bin winners are picked by lexical sort, so without the aliastscsilently becomes the ~10x slower JavaScript compiler.bun run check:native-typecheckenforces this, and also fails when a newly added dependency that sorts ahead of@typescript/nativeships atscbin (microsoft/typescript-go#4567). - Checks:
bun run lintautofixes formatting;bun run check:auditsruns everycheck:*audit CI enforces.
apps/
├── sim/ # Next.js app: UI, API routes, workflow builder, executor
│ ├── app/ # App router — pages and API routes (app/api/**)
│ ├── blocks/ # Block definitions and registry
│ ├── tools/ # Tool definitions and registry
│ ├── triggers/ # Trigger definitions and registry
│ ├── connectors/ # Knowledge base connectors
│ ├── executor/ # Workflow execution engine
│ ├── providers/ # LLM provider integrations
│ ├── components/ # Shared app UI (ui/, icons, …)
│ ├── hooks/ # Shared hooks (queries/, selectors/)
│ ├── stores/ # Zustand stores
│ ├── lib/ # App-wide modules, incl. lib/api/contracts and lib/<domain>/application
│ └── ee/ # Enterprise features
├── realtime/ # Bun Socket.IO server (collaborative workflow builder)
├── desktop/ # Electron shell around the hosted web app
├── docs/ # Documentation site
└── pii/ # Python PII detection service (not in the JS/turbo build)
packages/
├── emcn/ # @sim/emcn — design system (chip family, tokens, icons)
├── db/ # @sim/db — Drizzle schema, migrations, client
├── auth/ # @sim/auth — shared Better Auth verifier
├── platform-authz/ # @sim/platform-authz — workspace + workflow authz (subpath exports)
├── audit/ logger/ security/ utils/ runtime-secrets/ deployment-config/
├── realtime-protocol/ browser-protocol/ terminal-protocol/ desktop-bridge/
├── workflow-types/ workflow-persistence/ workflow-renderer/
├── testing/ # @sim/testing — test factories and mocks
├── tsconfig/ # shared tsconfig presets
└── cli/ sim-cli/ sim-setup/ ts-sdk/ python-sdk/ # published CLIs and SDKs
apps/* → packages/*only. Packages never import fromapps/*.apps/realtimeavoids Next.js, React, the block/tool registry, provider SDKs, and the executor. Never add imports from@/lib/webhooks/providers/*,@/executor/*,@/blocks/*, or@/tools/*to any package it consumes; it calls back intoapps/simonly over internal HTTP withINTERNAL_API_SECRET. CI enforces this viascripts/check-monorepo-boundaries.tsandscripts/check-realtime-prune-graph.ts.- Auth is shared across both apps via the Better Auth "Shared Database Session" pattern (same
BETTER_AUTH_SECRET, same DB via@sim/db).
- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case.
- Define one stable semantic operation with its minimum role, workspace-key policy, allowed principal kinds, and delegated services. Internal APIs, v2 APIs, Copilot, and trusted tools call the same use case when the domain behavior is the same.
- Surface adapters authenticate and construct a
Principal, apply request-rate policy, parse contracts, map input, and present their own result. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit. - Application use cases load canonical context, compare asserted scope, authorize current access, execute managers/repositories, project semantic audit, and trigger shared domain effects. Managers accept canonical IDs and scope, never credentials or principals. Application code stays surface-neutral: it never imports
app/api/**,next/server, route contracts/presenters, or Copilot handlers. - Copilot is a surface adapter. Use
createCopilotApplicationAdapterand the domain's registered operation object; never a Copilot-only authorization or business implementation. - Protected compound mutations belong in one top-level semantic application operation, never a sequence of independently committing mutations in a route or tool adapter.
- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal. Fail fast when the identity model or operation policy cannot express the caller.
- Use the
migrate-application-operationskill whenever creating or migrating a protected endpoint, tool command, or resource method.
The 'use client' server boundary, the app/worker runtime env split, and feature folder layout are in .claude/rules/sim-architecture.md.
- Naming: components PascalCase (
WorkflowList); hooksuse*; files kebab-case (workflow-list.tsx); constants SCREAMING_SNAKE_CASE; interfaces PascalCase with a suffix (WorkflowListProps); storesstores/<feature>/store.ts. - Imports: absolute (
@/...) only, never relative. A folder with 3+ exports gets anindex.tsbarrel; never re-export from a non-barrel file.import typefor type-only imports. Order and lazy-loading through barrels:.claude/rules/sim-imports.md. - TypeScript: no
any(use precise types orunknownwith guards); a props interface for every component;as constfor constant objects/arrays; explicit ref types (useRef<HTMLDivElement>(null)). - Components:
'use client'only for hooks or browser APIs. Structure order, extraction thresholds, and list-render rules:.claude/rules/sim-components.md. Render-performance idioms (lazy-init refs, hoisting,Mappre-indexing,[...arr].sort()nevertoSorted()on client paths):.claude/rules/sim-react-performance.md. For effect/state/memo/callback anti-patterns use the/you-might-not-need-*skills and verify against the running UI. - State ownership: React Query owns server data — never
useState+fetch; shareable client view-state (tabs, filters, search, pagination, selected id) lives in the URL vianuqs; Zustand owns global client state;useStateowns UI-only state. Hooks:.claude/rules/sim-hooks.md. Stores (devtools,persistonly with an explicitpartializewhitelist, workflow value invariants):.claude/rules/sim-stores.md. URL state:.claude/rules/sim-url-state.md. - Utils: inline a helper with one consumer; create
utils.tswhen 2+ files share it — inlib/(app-wide) orfeature/utils/(feature-scoped). Checklib/before writing a new one. - Lists and menus mirror the order the user already reads elsewhere (sidebar, toolbar), encoded in one exported order constant; a separator marks only a change in what the action acts on (typically one, before the destructive action):
.claude/rules/sim-list-ordering.md. - Caching:
lru-cachewith amaxceiling, never a hand-rolled TTLMap; a lifecycle map is not a cache; cache the gate, never the credential:.claude/rules/sim-caching.md.
- Request/response shapes for every route under
apps/sim/app/api/**live inapps/sim/lib/api/contracts/**, built withdefineRouteContractand exporting named schemas plus named type aliases. Routes never importzodor define route-local boundary schemas; clients never write ad-hoc wire types orz.input/z.output. - Every route handler runs inside
withRouteHandler. Ordinary internal and v2 routes use the shared builders (defineInternalJsonRoute,defineV2JsonRoute, binary/stream variants), which already apply it — never double-wrap. RawwithRouteHandleris only for documented protocol or lifecycle exceptions. Never export a bareasync function GET/POST/.... - Same-origin JSON calls go through
requestJson(contract, ...)from@/lib/api/client/request. A rawfetchis only for streaming, binary downloads, multipart uploads, signed URLs, OAuth redirects, or external origins, and carries// boundary-raw-fetch: <reason>. - The other script-enforced exceptions are
// double-cast-allowed:,// boundary-raw-json:, and// untyped-response:. Never add one to silence a fixable finding. bun run check:api-validation:strictmust pass. Full contract rules, route pattern, annotation placement, the end-to-end order, and the schema review checklist:.claude/rules/sim-api-contracts.md. React Query hooks (key factories, namedstaleTimeconstants,signal, invalidation, server prefetch):.claude/rules/sim-queries.md.
- Tailwind only. Inline
styleonly for a genuinely dynamic value or a CSS variable. Never update global styles; keep styling local to the component.cn()from@sim/emcnfor conditional classes.size-*for equal height and width (icons defaultsize-[14px]), neverh-N w-N. - Import components,
cn, and tokens from the@sim/emcnbarrel; icons from@sim/emcn/icons; CSS modules by file path. Never deep-import other component subpaths. - The chip family is the canonical chrome:
ChipInput,ChipTextarea,ChipModal/ChipModalField,ChipSelect/ChipCombobox/ChipDropdown,ChipSwitch,ChipDatePicker,Chip/ChipLink,ChipTag;DropdownMenufor context/action menus. Components own their chrome: consumers pass props (error,icon,endAdornment,inputClassName) andclassNamecarries only layout/sizing. Every labeled field inside aChipModalBodyis aChipModalField. - Consumer rules, tokens, text scale, and modal rhythm:
.claude/rules/sim-styling.md. Authoring components inpackages/emcn:.claude/rules/emcn-components.md. Product UI copy:.claude/rules/sim-ui-copy.md. Marketing copy and positioning:.claude/rules/constitution.md.
Most unit tests in a codebase like this restate the code they test. They pass on the first run, break on every refactor, and catch nothing that type-check, next build, bun run check:audits, or a real end-to-end run would miss. Test for confidence, not coverage.
- Never write unit tests after you write code. A test written to describe code that already exists restates the implementation and proves nothing. If the change needs proof, prove it end to end.
- Highly prefer E2E tests. Use them to verify complex features work, against the real boundary: real Postgres/Redis (
*.integration.ts), the running app over real HTTP (apps/sim/scripts/test-*-e2e.ts), or the packaged desktop app (apps/desktop/e2e, Playwright). At the end of an E2E test, produce a verifiable and repeatable artifact — a JSON report of each check with status and duration, an HTTP status log, a trace, or a screenshot — written to a caller-supplied<SUITE>_REPORT_PATHand uploaded by CI on failure.apps/sim/scripts/test-scim-e2e.tsis the reference. - If you must test a system in isolation, first write down all the ways it could fail, then write the code. Each failure mode (bad input, boundary, concurrency, partial failure, permission denial, resource cap) becomes one test 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 before you trust it.
- Never write tests that restate declarations (block/tool/provider config, registries, constants, schemas accepting valid input), assert that mocks were called, check rendered text or class names, or test mocks and factories themselves.
- Never hand-roll a mock or test helper that
apps/sim/vitest.setup.tsor@sim/testingalready provides; a module mocked in a third file gets one central mock.bun run check:test-patternsenforces this.
Use the test-audit skill whenever you write, change, review, or sweep tests — it holds the authoring gate, the junk patterns, and the retention bar. Test layers, file naming, and Vitest mechanics (global mocks, @sim/testing, performance rules) are in .claude/rules/sim-testing.md.
Build order: Tools → Block → Icon → optional Trigger, starting from the service's API docs. Use the skills: /add-integration (end-to-end), /add-tools, /add-block, /add-trigger. Two rules the skills assume:
- Tool IDs are
snake_case(service_action), registered intools/registry.ts; blocks register inblocks/registry-maps.ts(BLOCK_REGISTRY+BLOCK_META_REGISTRY, alphabetically). - Type coercions go in
tools.config.params(runs at execution, after variable resolution), never intools.config.tool(runs at serialization, whereNumber()destroys dynamic<Block.output>references).
Remaining block/tool/trigger rules: .claude/rules/sim-integrations.md. Canvas sentences: apps/sim/blocks/AGENTS.md.
Table column types are registry entries in apps/sim/lib/table/column-types/ — one file per type owning its label, icon, storage cast, coercion, validation, conversion compatibility, formatting, and editor. Record<ColumnType, …> on registry.ts and registry.server.ts is a compile-time completeness gate: adding a type to the union errors until both entries exist.
Never add a case 'sometype': outside column-types/ — a missing arm fails silently (a wrong jsonbCast breaks every filter on the column). If a consumer needs per-type knowledge, add a registry field. Use /add-column-type for the full procedure.